EST. 2026 — Notes from real server work SYSADMIN · SECURITY · AI · DIRECTADMIN Issue 01
THE SYSADMIN ALMANAC
Home / Linux / Inferring Cold Room Defrost Timing from Temperature Slopes
Linux

Inferring Cold Room Defrost Timing from Temperature Slopes

by Ton-Or ·22 Jul 2026·10 min read
Inferring Cold Room Defrost Timing from Temperature Slopes

There are no humidity sensors in the room, but the temperature graph reveals when ice starts clogging the coil, because thicker frost slows cooling through physics, not coincidence. Reading the slope lets me trigger defrost when needed, without setting a timer based on guesswork.

I had a cold room where wet glass noodles were loaded to remove water from them (freeze-dehydration), with the temperature held around -11 to -14°C, but there were no humidity sensors anywhere in the room, just 4 temperature measurement points and current readings for the compressor/fans/defrost heater taken over RS485 every few seconds. The question was how to tell when ice was starting to clog the coil and defrost should be triggered. I started looking into what the slope of the temperature graph could tell me, and it turned out to be almost everything.

The factory name and register numbers in this article are examples, but the equations, constants, and code are real and can be used to reproduce the calculations.

Why frost on the coil flattens the temperature graph

The core is the room's lumped-capacitance energy balance equation, treating the room, its contents, and its structure as a single thermal mass:

# สมดุลพลังงานของห้อง (lumped-capacitance)
#   (m*cp)_eff * dT/dt = Q_load - Q_evap
#
# (m*cp)_eff : thermal mass ของอากาศ + วุ้นเส้น + โครง (J/K)
# Q_load     : โหลดเข้า (แทรกซึม + ของที่ใส่ + พัดลม + ความร้อน defrost ตกค้าง)
# Q_evap     : ความเย็นที่คอยล์ดึงออก (W)
#
# ตอนคอมเดินดึงอุณหภูมิลง จัดรูปได้เป็น
#   dT/dt = -(Q_evap - Q_load) / (m*cp)_eff
# แปลว่า |dT/dt| (ความชันการลด) แปรผันตรงกับ Q_evap

Q_evap at the coil can then be written as Q_evap = U * A * ΔT_lm, where U is the overall heat-transfer coefficient, A is the heat exchange area, and ΔT_lm is the log-mean temperature difference between the refrigerant and air.

Frost on the coil hurts Q_evap in two ways at once: it acts as insulation (k around 0.1 W/m·K), adding thermal resistance so U decreases, and it blocks the air passages between the fins, reducing airflow through the coil so A and h_air decrease as well. The combined result is that U·A keeps falling over time, reducing Q_evap, and as Q_evap decreases, the room's temperature drop slope flattens according to the equation above. This is the observable relationship: "slower room cooling = water accumulating on the coil."

What happens after frost builds up on the coilThicker frostInsulation k~0.1 W/m·K→Lower U·AThermal resistance + blocked airflow→Lower Q_evapCoil removes less heat→|dT/dt| flattensRoom cools more slowly

Published measurements put the energy penalty at 5-30% once frost exceeds 5mm, with capacity falling by as much as ~32% at around 25mm. So a flattening slope is more than a rough signal: it is a direct proxy for declining U·A.

Detecting frost with time constant τ instead of raw slope

The problem with comparing raw slopes is that as the room approaches the setpoint, the slope naturally flattens because of physics (exponential approach), rather than frost. Instead of measuring the slope as a single number, I fit the entire pull-down curve to Newton's cooling equation and extract the time constant τ, where τ = (m·cp)_eff / (U·A) — frost reduces U·A, so τ increases; straightforward.

One pull-down cycle: gap from T_inf (fraction of initial gap)0102030405060Time since compressor start (minutes)37% remains at time = τClean coil τ ~ 18 minutesFrosted τ ~ 26 minutes (44% worse)τ 18τ 26τ at least 30% worse than baseline triggers defrost
The curves are sketches based on Newton's cooling equation. The curve with the larger τ drops more slowly, and this difference is what determines when defrost is due.
import numpy as np
from scipy.optimize import curve_fit

def newton_cooling(t_min, T_inf, T0, tau):
    # T(t) = T_inf + (T0 - T_inf) * exp(-t/tau)
    return T_inf + (T0 - T_inf) * np.exp(-t_min / tau)

def fit_tau(t_min, temp_c):
    """fit หนึ่งรอบ pull-down -> คืน tau (นาที). tau ยิ่งมาก = คอยล์ยิ่งตัน"""
    p0 = (float(np.min(temp_c)), float(temp_c[0]), 20.0)   # เดา T_inf, T0, tau เริ่มต้น
    (T_inf, T0, tau), _ = curve_fit(newton_cooling, t_min, temp_c, p0=p0, maxfev=10000)
    return tau

# ตัวอย่างจากรอบจริง: คอยล์เพิ่งล้าง fit ได้ tau ~ 18 นาที
# อีกไม่กี่รอบต่อมาน้ำเกาะจน fit ได้ tau ~ 26 นาที = แย่ลง +44%

Before fitting, I need to isolate clean "pull-down cycles," using only periods when the compressor runs continuously and defrost is inactive; otherwise, the τ values will be distorted:

import pandas as pd

def pulldown_cycles(df, comp_thr=5.0):
    """
    df: คอลัมน์ ts(unix), temp(°C กลางห้อง), i_comp(A), i_defrost(A)
    คืน list ของ DataFrame แต่ละรอบ pull-down (comp ON, defrost OFF)
    """
    on = (df["i_comp"] > comp_thr) & (df["i_defrost"] < 1.0)   # กรอง comp ON + defrost OFF
    grp = (on != on.shift()).cumsum()                          # นับ segment ต่อเนื่อง
    cycles = []
    for _, seg in df[on].groupby(grp[on]):
        if len(seg) < 30:                 # รอบสั้นเกินไป ตัดทิ้ง
            continue
        seg = seg.copy()
        seg["t_min"] = (seg["ts"] - seg["ts"].iloc[0]) / 60.0
        cycles.append(seg)
    return cycles
Do not compare raw slopes across temperature ranges

As the room approaches the setpoint, the slope naturally flattens because of physics; it is not always frost. I therefore use τ from the whole curve instead of comparing raw slopes directly, or, when comparing slopes, time the same temperature interval every time, such as how many minutes it takes to go from -5 → -10°C, filtering for comp ON and defrost OFF. Accidentally including periods when the compressor is stopped immediately ruins the result.

The demand-defrost rule: τ exceeds the baseline threshold = trigger defrost

Once I have τ for each cycle, the remaining step is to compare it against the baseline for a freshly cleaned coil. If τ increases beyond the threshold (I start at 30%), enough frost has accumulated to make defrost worthwhile; the entire logic is this short:

def decide_defrost(tau_now, tau_baseline, threshold=0.30):
    """คืน (should_defrost, degrade) — degrade = τ แย่ลงกี่ %"""
    degrade = (tau_now - tau_baseline) / tau_baseline
    return degrade >= threshold, degrade

# ต่อกับ pipeline จริง
cycles = pulldown_cycles(df)
tau_series = [fit_tau(c["t_min"].to_numpy(), c["temp"].to_numpy()) for c in cycles]

tau_baseline = tau_series[0]          # รอบแรกหลังล้างคอยล์ = คอยล์สะอาด
tau_now = tau_series[-1]
go, degrade = decide_defrost(tau_now, tau_baseline)
print(f"tau baseline={tau_baseline:.1f} min  now={tau_now:.1f} min  "
      f"degrade={degrade*100:.0f}%  -> {'DEFROST' if go else 'hold'}")

The clear advantage is that it adapts to the product automatically. Early on, when the glass noodles are still very wet, water evaporates and accumulates on the coil quickly, τ deteriorates quickly, and the system defrosts frequently to drain the water promptly. As the noodles dry, frost builds up more slowly, τ deteriorates more slowly, and defrost intervals lengthen automatically, without having to tune a timer for the season or the weight of the load.

Timer defrost

Fixed timing, based on guesswork

When the product is very wet, frost builds up quickly, but the timer is not due yet, so the coil is already clogged, water removal slows, and power consumption rises. Once the noodles dry and frost builds up more slowly, the timer triggers defrost more often than necessary, wasting heater energy and adding unnecessary re-cooling load.

Demand defrost

Triggered by measured τ

Defrost starts when τ deteriorates to the threshold, automatically adjusting frequency to the water leaving the noodles. Very wet noodles mean frequent defrost; dry noodles mean longer intervals, with no timer values to change.

COP and electricity costs: why frost costs more than expected

Frost does more than slow cooling; it also raises the electricity cost per unit of cooling, because when Q_evap drops, the compressor has to run longer to remove the same amount of heat. This requires knowing the system's actual COP first, rather than guessing. At low temperatures like these, the actual COP of R404A is only around 0.4-0.6 of Carnot. I calculate it from actual enthalpy values using CoolProp at the measured Te/Tc:

import math
from CoolProp.CoolProp import PropsSI

def cop_r404a(Te_C, Tc_C, superheat=5.0, subcool=3.0, eta_isen=0.65):
    Te, Tc = Te_C + 273.15, Tc_C + 273.15
    p_low  = PropsSI('P', 'T', Te, 'Q', 1, 'R404A')   # ความดันด้าน evaporator
    p_high = PropsSI('P', 'T', Tc, 'Q', 0, 'R404A')   # ความดันด้าน condenser

    # state 1: ทางออก evaporator (superheated)
    h1 = PropsSI('H', 'T', Te + superheat, 'P', p_low, 'R404A')
    s1 = PropsSI('S', 'T', Te + superheat, 'P', p_low, 'R404A')
    # state 2: อัดแบบ isentropic ไป p_high แล้วหารด้วยประสิทธิภาพคอมจริง
    h2s = PropsSI('H', 'P', p_high, 'S', s1, 'R404A')
    h2  = h1 + (h2s - h1) / eta_isen
    # state 3: ทางออก condenser (subcooled), state 4: หลัง expansion (h4 = h3)
    h3 = PropsSI('H', 'T', Tc - subcool, 'P', p_high, 'R404A')
    h4 = h3

    cop = (h1 - h4) / (h2 - h1)            # COP = Q_evap / W_comp
    cop_carnot = Te / (Tc - Te)            # เพดาน Carnot
    return cop, cop_carnot

cop, ceil = cop_r404a(Te_C=-25.0, Tc_C=40.0)
print(f"COP(R404A) = {cop:.2f}   Carnot ceiling = {ceil:.2f}   ratio = {cop/ceil:.2f}")
# ~ COP 1.7, Carnot 3.8, ratio ~0.45  <- ตรงกับที่คาดไว้สำหรับ low-temp R404A

The point to remember is that the lower Te gets (a very cold room) and the higher Tc gets (a hot day when the condenser cannot reject heat fast enough), the lower COP falls and the higher the electricity cost per unit of cooling becomes. Electrical power from measured current on a 3-phase supply is straightforward to calculate with P = √3·V·I·PF:

def power_3ph_kw(current_a, v_line=380.0, pf=0.85):
    return math.sqrt(3) * v_line * current_a * pf / 1000.0

p_comp    = power_3ph_kw(33.0)              # กระแสคอมจาก register
p_defrost = power_3ph_kw(28.0, pf=1.0)      # ฮีตเตอร์เป็น resistive -> PF = 1
print(f"comp ~ {p_comp:.1f} kW   defrost heater ~ {p_defrost:.1f} kW")
2838kJ/kg h_sublimation ice→vapor
334kJ/kg h_fusion melting frost
~0.1W/m·K frost k (insulation)
0.45xActual COP relative to Carnot, R404A

How many kilograms of water are removed per defrost cycle

When defrost melts the frost, the water flows out of the system through the drain pipe. This is the step that actually removes the collected water from the room (frost on the coil is water that left the noodles and accumulated at the coldest point). For a rough estimate of how many kilograms of water are removed per cycle, divide the defrost energy by the latent heat of fusion, subtracting the portion used to warm the metal and air:

h_sf = 334.0      # kJ/kg  latent heat of fusion (ละลายน้ำแข็ง->น้ำ)
h_sg = 2838.0     # kJ/kg  latent heat of sublimation (น้ำแข็ง->ไอ)

def water_harvested_kg(p_heater_kw, t_defrost_s, melt_fraction=0.6):
    """melt_fraction = สัดส่วนพลังงานฮีตเตอร์ที่ใช้ละลายจริง (ที่เหลืออุ่นโลหะ/ห้อง)"""
    e_kj = p_heater_kw * t_defrost_s       # kW * s = kJ
    e_melt = e_kj * melt_fraction
    return e_melt / h_sf

m = water_harvested_kg(p_heater_kw=18.4, t_defrost_s=600)
print(f"น้ำที่รีดออกราว {m:.1f} kg/รอบ defrost")

This is only an estimate. A more accurate approach is to weigh the glass noodles before and after, or measure the drain water directly; with no humidity sensors, that is the best ground truth available. The energy that must be supplied to remove water from the noodles themselves is substantial: sublimating 1 kg of water takes around 2838 kJ, a load the coil must absorb as the water deposits as frost and releases that heat again. This explains why frost on the coil slows water removal: as the frost-covered coil surface warms, its surface vapor pressure rises, reducing the driving force that draws water from the noodles (the vapor pressure difference). That is another reason to defrost to maintain the drying rate.

The trade-off to optimize

Defrost too often and the coil stays clean with high Q, but at the cost of heater energy + re-cooling + compressor downtime · Defrost too rarely and thick frost lowers Q, slowing cooling and water removal. The right balance is demand-defrost based on τ, rather than a fixed timer. My target is the lowest kWh per kg of water, not the prettiest coil.

Connecting the pipeline to cron

What remains is to make it run automatically. I split it into two parts: one reads values from the controller over RS485 (Modbus RTU) and appends them to a csv file; the other runs periodically to make the decision and trigger defrost through a relay:

  1. The poller reads registers (room-center temp, compressor current, defrost heater current) every 1-5 seconds and writes them to csv
  2. The decision process reads recent csv data, separates pull-down cycles, and filters for comp ON + defrost OFF
  3. Fit τ for each cycle and compare it against the baseline; if deterioration exceeds the threshold, trigger defrost, then reset the baseline after defrost finishes
#!/usr/bin/env bash
# coldroom-poll.sh — อ่าน RS485 แล้วต่อท้าย csv (ตัวอย่าง register/พอร์ต)
PORT=/dev/ttyUSB0          # USB-RS485 adapter
UNIT=1                     # Modbus unit id
OUT=/var/log/coldroom/telemetry.csv
mkdir -p "$(dirname "$OUT")"
while true; do
  ts=$(date +%s)
  # reg4=temp*10(°C), reg8=กระแสคอม*10(A), reg11=กระแสฮีตเตอร์*10(A)
  read t i_comp i_def < <(python3 /opt/coldroom/read_regs.py "$PORT" "$UNIT" 4 8 11)
  echo "$ts,$t,$i_comp,$i_def" >> "$OUT"
  sleep 5
done

The decision process does not need to stay running; calling it from cron every 5 minutes is enough, reading enough csv history to cover the latest pull-down cycle:

# /etc/cron.d/coldroom-defrost
*/5 * * * * coldroom /opt/coldroom/venv/bin/python /opt/coldroom/defrost_decide.py >> /var/log/coldroom/defrost.log 2>&1

At this point, I have concrete numbers instead of guesswork. A single τ tells me how clogged the coil is, when to defrost, and how much water is removed per cycle, all from the temperature and current readings already available, without buying any humidity sensors. Anyone adapting this setup should tune the 30% threshold and melt_fraction for their own equipment; the values I use are starting points. If I have left anything out or made a typo, I will come back and correct it later.

Hit something like this? Stuck somewhere?

Come trade notes on the LinuxThai board

→ Continue on the board

Comments 0

No comments yet — be the first