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
+1 -1
View File
@@ -65,7 +65,7 @@ mapped**, and the **command path is implemented and confirmed by live actuation
| `F8` | **interior lights** | type 0x1E; toggle test t≈5161 s; **operated live 2026-06-12** ✓ | | `F8` | **interior lights** | type 0x1E; toggle test t≈5161 s; **operated live 2026-06-12** ✓ |
| `95` | **water heater** | type 0x1E; toggle test t≈8594 s | | `95` | **water heater** | type 0x1E; toggle test t≈8594 s |
| `61` | **water pump** | type 0x1E; toggle test 2026-06-11 (on 13.5s / off 23.8s) ✓ | | `61` | **water pump** | type 0x1E; toggle test 2026-06-11 (on 13.5s / off 23.8s) ✓ |
| `89` | unknown switched load | type 0x1E, never toggled (furnace? DSI?) | | `89` | **furnace** (read-only) | type 0x1E; thermostat-controlled, not a Lippert load — present only to report DSI state. Same page-3 encoding as the heater: b0 bit0 = running, bit5 = DSI lockout. Stayed 0x80 (off) all bench captures; running/fault bytes inferred from node 95, confirm on first burn |
| `75` | **awning** | type 0x21; jog test 2026-06-11 — b0 C0→C3 (in?) →C0→C2 (out?) with motor current on b2-3 | | `75` | **awning** | type 0x21; jog test 2026-06-11 — b0 C0→C3 (in?) →C0→C2 (out?) with motor current on b2-3 |
| `6A`, `7F`, `9C` | slide / jacks / movement class | type 0x21, untested | | `6A`, `7F`, `9C` | slide / jacks / movement class | type 0x21, untested |
| `AE` | unknown (type 0x27, page3=0x00) | LP gas sensor? | | `AE` | unknown (type 0x27, page3=0x00) | LP gas sensor? |
+38 -1
View File
@@ -172,8 +172,24 @@ canbus:
// water heater (node 95): b0 bit0 = on, bit5 (0x20) = DSI/gas // water heater (node 95): b0 bit0 = on, bit5 (0x20) = DSI/gas
// ignition fault/lockout (healthy: 0x80 off / 0x81 running; // ignition fault/lockout (healthy: 0x80 off / 0x81 running;
// fault: 0xA0). Confirmed by a forced lockout 2026-06-12. // 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); 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) // awning H-bridge (node 75): b0 C0 idle / C2 extending (opening)
// / C3 retracting (closing). Reflect motion onto the cover so HA // / C3 retracting (closing). Reflect motion onto the cover so HA
// shows open/opening/closing; assumed_state fills the resting pos. // shows open/opening/closing; assumed_state fills the resting pos.
@@ -347,6 +363,18 @@ binary_sensor:
name: "Water Heater DSI Fault" name: "Water Heater DSI Fault"
id: dsi_fault id: dsi_fault
device_class: problem # node 95 page-3 b0 bit5 — gas ignition lockout 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 - platform: template
name: "OneControl System Fault" name: "OneControl System Fault"
id: 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 # 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. # 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: switch:
- platform: template - platform: template
name: "Exterior Lights" name: "Exterior Lights"
id: exterior_lights id: exterior_lights
optimistic: false optimistic: false
restore_mode: DISABLED
turn_on_action: turn_on_action:
- script.execute: { id: send_load_command, node: 0x2A, op: 1 } - script.execute: { id: send_load_command, node: 0x2A, op: 1 }
turn_off_action: turn_off_action:
@@ -382,6 +417,7 @@ switch:
name: "Interior Lights" name: "Interior Lights"
id: interior_lights id: interior_lights
optimistic: false optimistic: false
restore_mode: DISABLED
turn_on_action: turn_on_action:
- script.execute: { id: send_load_command, node: 0xF8, op: 1 } - script.execute: { id: send_load_command, node: 0xF8, op: 1 }
turn_off_action: turn_off_action:
@@ -391,6 +427,7 @@ switch:
name: "Water Heater" name: "Water Heater"
id: water_heater id: water_heater
optimistic: false optimistic: false
restore_mode: DISABLED
turn_on_action: turn_on_action:
- script.execute: { id: send_load_command, node: 0x95, op: 1 } - script.execute: { id: send_load_command, node: 0x95, op: 1 }
turn_off_action: turn_off_action:
+41
View File
@@ -83,3 +83,44 @@ automation:
averted. Try to remember this feeling next time you eyeball the averted. Try to remember this feeling next time you eyeball the
tank gauge and say "eh, it's fine." tank gauge and say "eh, it's fine."
mode: single mode: single
# --- Furnace DSI fault (node 89). Same lockout scheme as the water heater
# (page-3 b0 bit5); see canbus/README.md. A furnace lockout means no forced-air
# heat — more urgent than the water heater on a cold night. ---
- id: camper_furnace_dsi_fault_discord
alias: "Camper: Furnace DSI fault → Discord"
description: >-
10s 'for' debounces a single glitched broadcast frame; a real lockout
is latched by the module so it easily survives the wait.
triggers:
- trigger: state
entity_id: binary_sensor.onecontrol_can_furnace_dsi_fault
to: "on"
for: "00:00:10"
actions:
- action: rest_command.discord_server_alerts
data:
message: >-
<@321798967669030912> 🥶 **FURNACE DSI FAULT** — the furnace tried to
light and gave up. That means no heat. Same usual suspect: you're out
of propane (or the igniter died, but c'mon). It's in lockout — swap the
tank, then cycle the thermostat to relight before everything inside
hits ambient.
mode: single
- id: camper_furnace_dsi_fault_cleared_discord
alias: "Camper: Furnace DSI fault cleared → Discord"
triggers:
- trigger: state
entity_id: binary_sensor.onecontrol_can_furnace_dsi_fault
from: "on"
to: "off"
for: "00:00:10"
actions:
- action: rest_command.discord_server_alerts
data:
message: >-
✅ Furnace fault cleared — it's lighting again and making heat. Enjoy
the warmth, and maybe glance at the propane gauge once in a while like
a functioning adult.
mode: single
+55
View File
@@ -165,6 +165,43 @@ automation:
"device_class": "problem", "device_class": "problem",
"availability_topic": "campsite/onecontrol/availability", "availability_topic": "campsite/onecontrol/availability",
"device": {"identifiers": ["campsite_onecontrol"]}} "device": {"identifiers": ["campsite_onecontrol"]}}
- action: mqtt.publish
data:
topic: "homeassistant/binary_sensor/campsite/furnace_dsi_fault/config"
retain: true
payload: >-
{"name": "Furnace DSI Fault",
"unique_id": "campsite_furnace_dsi_fault",
"state_topic": "campsite/binary_sensor/furnace_dsi_fault/state",
"payload_on": "ON", "payload_off": "OFF",
"device_class": "problem",
"availability_topic": "campsite/onecontrol/availability",
"device": {"identifiers": ["campsite_onecontrol"]}}
# Running/heating status binary sensors
- action: mqtt.publish
data:
topic: "homeassistant/binary_sensor/campsite/water_heater_heating/config"
retain: true
payload: >-
{"name": "Water Heater Heating",
"unique_id": "campsite_water_heater_heating",
"state_topic": "campsite/binary_sensor/water_heater_heating/state",
"payload_on": "ON", "payload_off": "OFF",
"device_class": "running",
"availability_topic": "campsite/onecontrol/availability",
"device": {"identifiers": ["campsite_onecontrol"]}}
- action: mqtt.publish
data:
topic: "homeassistant/binary_sensor/campsite/furnace_running/config"
retain: true
payload: >-
{"name": "Furnace Running",
"unique_id": "campsite_furnace_running",
"state_topic": "campsite/binary_sensor/furnace_running/state",
"payload_on": "ON", "payload_off": "OFF",
"device_class": "running",
"availability_topic": "campsite/onecontrol/availability",
"device": {"identifiers": ["campsite_onecontrol"]}}
# Awning # Awning
- action: mqtt.publish - action: mqtt.publish
data: data:
@@ -230,6 +267,21 @@ automation:
topic: "campsite/binary_sensor/onecontrol_system_fault/state" topic: "campsite/binary_sensor/onecontrol_system_fault/state"
retain: true retain: true
payload: "{{ 'ON' if is_state('binary_sensor.onecontrol_can_onecontrol_system_fault', 'on') else 'OFF' }}" payload: "{{ 'ON' if is_state('binary_sensor.onecontrol_can_onecontrol_system_fault', 'on') else 'OFF' }}"
- action: mqtt.publish
data:
topic: "campsite/binary_sensor/furnace_dsi_fault/state"
retain: true
payload: "{{ 'ON' if is_state('binary_sensor.onecontrol_can_furnace_dsi_fault', 'on') else 'OFF' }}"
- action: mqtt.publish
data:
topic: "campsite/binary_sensor/water_heater_heating/state"
retain: true
payload: "{{ 'ON' if is_state('binary_sensor.onecontrol_can_water_heater_heating', 'on') else 'OFF' }}"
- action: mqtt.publish
data:
topic: "campsite/binary_sensor/furnace_running/state"
retain: true
payload: "{{ 'ON' if is_state('binary_sensor.onecontrol_can_furnace_running', 'on') else 'OFF' }}"
- action: mqtt.publish - action: mqtt.publish
data: data:
topic: "campsite/cover/awning/state" topic: "campsite/cover/awning/state"
@@ -300,6 +352,9 @@ automation:
entity_id: entity_id:
- binary_sensor.onecontrol_can_water_heater_dsi_fault - binary_sensor.onecontrol_can_water_heater_dsi_fault
- binary_sensor.onecontrol_can_onecontrol_system_fault - binary_sensor.onecontrol_can_onecontrol_system_fault
- binary_sensor.onecontrol_can_furnace_dsi_fault
- binary_sensor.onecontrol_can_water_heater_heating
- binary_sensor.onecontrol_can_furnace_running
conditions: conditions:
- condition: template - condition: template
value_template: "{{ trigger.to_state.state in ['on', 'off'] }}" value_template: "{{ trigger.to_state.state in ['on', 'off'] }}"