canbus: furnace + water-heater status sensors (running + DSI fault)

Decode node 89 (furnace) read-only: Furnace Running (page-3 b0 bit0) and
Furnace DSI Fault (b0 bit5), same type-0x1E DSI encoding as the water
heater. The furnace is thermostat-controlled, not a Lippert load — it
sits on the bus only to report ignition state. Captures only ever showed
0x80 (off), so the 0x81 running / 0xA0 fault bytes are inferred by
parallel to node 95; confirm on the first real burn.

Add Water Heater Heating (node 95 page-3 x[3] bit7 = burner actively
firing) — distinct from the on/off switch, which only reflects "enabled".

Carry all three through the MQTT bridge to home HA, add the furnace
DSI-fault Octavia alert mirroring the water heater, and update the README
node map (89 was "unknown switched load"). Campsite + home Camper
dashboards updated live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wes
2026-06-13 00:19:46 -04:00
co-authored by Claude Opus 4.8
parent 5b9198878c
commit 6733a79390
4 changed files with 135 additions and 2 deletions
+38 -1
View File
@@ -172,8 +172,24 @@ canbus:
// water heater (node 95): b0 bit0 = on, bit5 (0x20) = DSI/gas
// ignition fault/lockout (healthy: 0x80 off / 0x81 running;
// fault: 0xA0). Confirmed by a forced lockout 2026-06-12.
// x[3] bit7 = burner ACTUALLY FIRING (vs merely enabled): idle
// reads ...00 01, a live burn reads ...00 9X with the low nibble
// climbing as it heats (captured 2026-06-11). The switch state
// above is "enabled"; wh_heating is "actually making heat".
case 0x95: id(water_heater).publish_state(x[0] & 0x01);
id(dsi_fault).publish_state(x[0] & 0x20); break;
id(dsi_fault).publish_state(x[0] & 0x20);
if (x.size() >= 4) id(wh_heating).publish_state(x[3] & 0x80);
break;
// furnace (node 89): propane forced-air, THERMOSTAT-controlled
// (not a Lippert load) — on the bus only to report DSI state, in
// the same type-0x1E encoding as the water heater. b0 bit0 =
// running, bit5 (0x20) = DSI ignition lockout. A lockout also
// trips the bus-wide page-0 fault flag (system_fault). Only idle
// 0x80 was captured (summer bench); 0x81 running / 0xA0 fault are
// inferred by parallel to node 95 — confirm on the first real
// burn cycle (or a forced lockout, propane off).
case 0x89: id(furnace_running).publish_state(x[0] & 0x01);
id(furnace_dsi_fault).publish_state(x[0] & 0x20); break;
// awning H-bridge (node 75): b0 C0 idle / C2 extending (opening)
// / C3 retracting (closing). Reflect motion onto the cover so HA
// shows open/opening/closing; assumed_state fills the resting pos.
@@ -347,6 +363,18 @@ binary_sensor:
name: "Water Heater DSI Fault"
id: dsi_fault
device_class: problem # node 95 page-3 b0 bit5 — gas ignition lockout
- platform: template
name: "Water Heater Heating"
id: wh_heating
device_class: running # node 95 page-3 x[3] bit7 — burner actively firing
- platform: template
name: "Furnace Running"
id: furnace_running
device_class: running # node 89 page-3 b0 bit0 — thermostat-driven burn
- platform: template
name: "Furnace DSI Fault"
id: furnace_dsi_fault
device_class: problem # node 89 page-3 b0 bit5 — gas ignition lockout
- platform: template
name: "OneControl System Fault"
id: system_fault
@@ -368,11 +396,18 @@ binary_sensor:
# state (verified live 2026-06-12), so HA shows ground truth — a dropped command
# self-corrects within ~1 s instead of an optimistic echo falsely reporting success.
# ---------------------------------------------------------------------------
# restore_mode: DISABLED on all three is load-bearing: ESPHome's default
# restore "applies" the boot state BY EXECUTING THE SWITCH ACTION, which sent
# a real authenticated OFF to the water heater on the 2026-06-12 OTA reboot
# (send_load_command is mode:restart, so the last switch in setup order — the
# heater — won the race). Boot must send nothing: the broadcasts repopulate
# every state within ~1 s and are the only source of truth.
switch:
- platform: template
name: "Exterior Lights"
id: exterior_lights
optimistic: false
restore_mode: DISABLED
turn_on_action:
- script.execute: { id: send_load_command, node: 0x2A, op: 1 }
turn_off_action:
@@ -382,6 +417,7 @@ switch:
name: "Interior Lights"
id: interior_lights
optimistic: false
restore_mode: DISABLED
turn_on_action:
- script.execute: { id: send_load_command, node: 0xF8, op: 1 }
turn_off_action:
@@ -391,6 +427,7 @@ switch:
name: "Water Heater"
id: water_heater
optimistic: false
restore_mode: DISABLED
turn_on_action:
- script.execute: { id: send_load_command, node: 0x95, op: 1 }
turn_off_action: