From 6733a793907c2a931ce36eda77e482585bf2cae5 Mon Sep 17 00:00:00 2001 From: Wesley Ray Date: Sat, 13 Jun 2026 00:19:46 -0400 Subject: [PATCH] canbus: furnace + water-heater status sensors (running + DSI fault) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- canbus/README.md | 2 +- canbus/esphome/onecontrol-canbus.yaml | 39 ++++++++++++++++++- canbus/ha/dsi_fault_alert.yaml | 41 ++++++++++++++++++++ canbus/ha/mqtt_bridge_onecontrol.yaml | 55 +++++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 2 deletions(-) diff --git a/canbus/README.md b/canbus/README.md index 21776fe..ef1a60a 100644 --- a/canbus/README.md +++ b/canbus/README.md @@ -65,7 +65,7 @@ mapped**, and the **command path is implemented and confirmed by live actuation | `F8` | **interior lights** | type 0x1E; toggle test t≈51–61 s; **operated live 2026-06-12** ✓ | | `95` | **water heater** | type 0x1E; toggle test t≈85–94 s | | `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 | | `6A`, `7F`, `9C` | slide / jacks / movement class | type 0x21, untested | | `AE` | unknown (type 0x27, page3=0x00) | LP gas sensor? | diff --git a/canbus/esphome/onecontrol-canbus.yaml b/canbus/esphome/onecontrol-canbus.yaml index ef4028a..d8ed441 100644 --- a/canbus/esphome/onecontrol-canbus.yaml +++ b/canbus/esphome/onecontrol-canbus.yaml @@ -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: diff --git a/canbus/ha/dsi_fault_alert.yaml b/canbus/ha/dsi_fault_alert.yaml index a568181..1ae88cd 100644 --- a/canbus/ha/dsi_fault_alert.yaml +++ b/canbus/ha/dsi_fault_alert.yaml @@ -83,3 +83,44 @@ automation: averted. Try to remember this feeling next time you eyeball the tank gauge and say "eh, it's fine." 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 diff --git a/canbus/ha/mqtt_bridge_onecontrol.yaml b/canbus/ha/mqtt_bridge_onecontrol.yaml index ab5de29..7f0c147 100644 --- a/canbus/ha/mqtt_bridge_onecontrol.yaml +++ b/canbus/ha/mqtt_bridge_onecontrol.yaml @@ -165,6 +165,43 @@ automation: "device_class": "problem", "availability_topic": "campsite/onecontrol/availability", "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 - action: mqtt.publish data: @@ -230,6 +267,21 @@ automation: topic: "campsite/binary_sensor/onecontrol_system_fault/state" retain: true 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 data: topic: "campsite/cover/awning/state" @@ -300,6 +352,9 @@ automation: entity_id: - binary_sensor.onecontrol_can_water_heater_dsi_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: - condition: template value_template: "{{ trigger.to_state.state in ['on', 'off'] }}"