Build

Mismatched Sensors, on One Screen

By makemind · May 14, 2026

A greenhouse has several sensors on it. Temperature, humidity, CO2. Then moving things get added — vents and valves.

At install time this is not a problem. Three of them, so write three into the code. The problem arrives three years later. The thermometer fails and is replaced with a different product. That product measures in Fahrenheit. Another CO2 sensor goes in. A valve is added.

If the control code has to be edited every time, that greenhouse becomes a thing that cannot be fixed without the person who installed it.

The node says what it is

In this sample each node declares itself in six fields.

printf("%s{\"id\":\"%s\",\"role\":\"%s\",\"kind\":\"%s\","
       "\"model\":\"%s\",\"unit\":\"%s\",\"value\":%.1f}",
       i ? "," : "", n->id, n->role, n->kind, n->model,
       n->unit, n->value);

id · role · kind · model · unit · value.

unit existing as a field is the core of it. A thermometer that measures in Fahrenheit says Fahrenheit. It does not helpfully pre-convert to Celsius. Hardware says honestly only what it knows, and the aligning happens above.

The aligning happens in one place

/// The value moved into the unit the rules use.
///
/// This is the only place in this sample that knows about units, and it
/// decides from the unit a node declared, not from a model number. A sensor
/// nobody has ever heard of still states its unit, so it lands here exactly.
double get canonicalValue {
  switch (unit) {
    case 'F':
      return (value - 32) * 5 / 9;
    default:
      return value;
  }
}

Branch on the model number and this place becomes a list. It grows a line with every new product bought, and a product missing from the list quietly gets used with the wrong value.

Look at unit and a product you've never seen aligns itself.

The rules don't know part numbers

One growing rule is seven lines.

Rule(
  name: 'vent above 26C',
  whenKind: 'temperature',
  above: 26.0,
  thenKind: 'vent',
  setTo: 80.0,
  elseSetTo: 0.0,
),

No sensor is named. No relay is named either. Only the kind of value read and the kind of thing to move.

TH-100 or FX-200 — if its kind is temperature, this rule sees it.

The swap was actually done

Install A — Celsius thermometer, three nodes.

A: discovered 3 nodes — t1:temperature:TH-100(C), h1:humidity:HM-20(pct), v1:vent:VT-9(pct)
A: vent above 26C: 26.1 -> v1=80.0
3 nodes — TH-100 Celsius, HM-20, VT-9
3 nodes — TH-100 Celsius, HM-20, VT-9
after the rules — vent 80%
after the rules — vent 80%

Now swap the thermometer for a Fahrenheit model and add a CO2 sensor and a valve.

=== pass B: probe swapped for a Fahrenheit model, two nodes added ===
    server code: unchanged   rules: unchanged   rebuild: none
B: discovered 5 nodes — t1:temperature:FX-200(F), h1:humidity:HM-20(pct),
   c1:co2:CO-5(ppm), v1:vent:VT-9(pct), w1:valve:WV-3(pct)
B: vent above 26C: 26.1 -> v1=80.0   ·   water below 60% humidity: 62.8 -> w1=0.0
5 nodes — FX-200 Fahrenheit, CO-5 ppm, WV-3 added
5 nodes — FX-200 Fahrenheit, CO-5 ppm, WV-3 added
two rules applied — vent 80%, valve 0%
two rules applied — vent 80%, valve 0%

Server code unchanged. Rules unchanged. No rebuild.

And vent above 26C fired at exactly 26.1 in both installs. Because the value the Fahrenheit sensor sent was moved to Celsius in one place. Without that one place, 79 degrees Fahrenheit would have sailed past a 26-degree threshold and the vent would have stayed open.

This content requires Developer or above

Sign in and upgrade your plan to continue reading.

View Plans
Twitter