canbus: decode the water-heater DSI fault + add fault binary_sensors

Forced a real lockout (captures/dsi-fault-*.log) and diffed vs the healthy
baseline:
- Water-heater DSI fault = node 95 page-3 b0 bit5 (0x20). Healthy 0x80/0x81,
  fault 0xA0. (Earlier suspects b1=FF and node AE were both wrong.)
- Bus-wide "system fault present" = page-0 b0 bit0 (every node flips 02->03).

Both wired into esphome/onecontrol-canbus.yaml as binary_sensor (device_class:
problem). README + HANDOFF updated; DSI item closed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wes
2026-06-12 11:29:05 -04:00
co-authored by Claude Opus 4.8
parent f3073f180f
commit d57c2b21d2
3 changed files with 51 additions and 27 deletions
+11 -16
View File
@@ -60,10 +60,11 @@ state + what's left.
opcode. The node allowlist is **lights only** — keep movement nodes opcode. The node allowlist is **lights only** — keep movement nodes
(awning/slides/jacks, type `0x21`) off the switch list until a careful (awning/slides/jacks, type `0x21`) off the switch list until a careful
attended first test. attended first test.
6. **DSI fault `binary_sensor`** — see the section below (pending a capture). 6. **(Optional) Surface at the campsite HA + bridge home** like the gazebo fans /
7. **(Optional) Surface at the campsite HA + bridge home** like the gazebo fans /
OneControl BLE devices, if you want these in the home dashboard too. OneControl BLE devices, if you want these in the home dashboard too.
The DSI fault is already decoded and wired in (see below) — no capture needed.
## File map ## File map
| File | What it is | | File | What it is |
@@ -86,18 +87,12 @@ state + what's left.
- One transceiver = one bus-end terminator. Never add a terminated node in the - One transceiver = one bus-end terminator. Never add a terminated node in the
middle of the bus (would make three terminators). middle of the bus (would make three terminators).
## DSI fault — PENDING a fault capture ## DSI fault — DECODED (2026-06-12)
> The water-heater DSI (gas ignition) fault is almost certainly on the bus, but Forced a real lockout and captured it (`captures/dsi-fault-*.log`). Already wired
> every capture so far is of a healthy heater, so the exact byte is unknown. into the YAML as two `binary_sensor`s; nothing left to do here.
>
> **Capture plan (this session):** close the propane valve, run the water heater - **Water Heater DSI Fault** = node `95` page-3 `b0` bit5 (`0x20`). Healthy heater
> on gas until it locks out (DSI fault light on the panel), capture ~20 s, and = `0x80` off / `0x81` running; lockout = `0xA0`.
> diff against a healthy baseline (`captures/baseline-2026-06-11_223823.log`). - **OneControl System Fault** = page-0 `b0` bit0 (`0x01`), a bus-wide
> Prime suspects (both sit at a constant "all-clear" value today): "fault exists somewhere" flag (read from node `95`'s page 0 in the lambda).
> - node `95` (heater) page-3 `b1` — always `0xFF`; expect a bit to drop on fault.
> - node `AE` (type 0x27, ?LP-gas/diagnostics) page-3 — always `0x00`; expect non-zero on fault.
>
> **TODO once decoded:** record the node/page/byte/bit here, then add a
> `binary_sensor` to the YAML (`device_class: problem`) that reads it — the DSI
> fault the Bluetooth app never exposed.
+15 -10
View File
@@ -187,18 +187,23 @@ the app uses to build its UI.
`6A`/`7F`/`9C` (movement — slide?), and find the battery SoC / "4 green lights" `6A`/`7F`/`9C` (movement — slide?), and find the battery SoC / "4 green lights"
source. source.
### Planned: capture the DSI fault (2026-06-12) ### DSI fault — decoded (2026-06-12, `captures/dsi-fault-*.log`)
The water-heater DSI fault is almost certainly on the bus, but every capture so Forced a real lockout (propane valve closed, heater run on gas until it gave up)
far is of a *healthy* heater, so the fault encoding is unknown. **Plan:** close and diffed against the healthy baseline. Two signals:
the propane tank valve, run the water heater on gas until it locks out (DSI fault
light on the panel), capture ~20 s, then diff against a healthy baseline. Prime
suspects (both sit at a constant "all-clear" value in current captures):
- **node `95` (heater) page-3 `b1`** — always `0xFF`; expect it to drop a bit on fault.
- **node `AE` (type 0x27, ?LP-gas/diagnostics) page-3** — always `0x00`; expect non-zero on fault.
Whichever flips → becomes a `binary_sensor` in the ESPHome node (the DSI fault - **Water-heater DSI fault = node `95` page-3 `b0` bit5 (`0x20`).** Healthy heater
the Bluetooth app never exposed). Reset = reopen valve, re-light. reads `0x80` (off) or `0x81` (running); during the lockout it read **`0xA0`**
(bit0 cleared, bit5 set) for every sample. Bit5 never appears healthy → it's
the gas-ignition lockout flag. (`b1` stays `0xFF` and `node AE` stays `0x00`
the two earlier suspects were both wrong.)
- **Bus-wide "system fault present" = page-0 `b0` bit0 (`0x01`).** *Every* node's
page-0 `b0` flipped `0x02``0x03` during the fault, so any node carries a
generic "a fault exists somewhere" flag.
Both are wired into the ESPHome node as `binary_sensor`s (`device_class:
problem`) — the DSI fault the Bluetooth app never exposed. Reset = reopen valve,
re-light.
--- ---
+25 -1
View File
@@ -132,10 +132,21 @@ canbus:
// switched loads // switched loads
case 0xF8: id(interior_lights).publish_state(x[0] & 0x01); break; case 0xF8: id(interior_lights).publish_state(x[0] & 0x01); break;
case 0x2A: id(exterior_lights).publish_state(x[0] & 0x01); break; case 0x2A: id(exterior_lights).publish_state(x[0] & 0x01); break;
// TODO: water pump (61), water heater (95) once exposed below. // 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.
case 0x95: id(dsi_fault).publish_state(x[0] & 0x20); break;
// TODO: water pump (61) on/off once exposed below.
} }
} }
// page 0 b0 bit0 = a bus-wide "system fault present" flag (every
// node mirrors it; healthy 0x02, fault 0x03). Read it from one node
// (the heater) so we publish a single source, not 14 duplicates.
if (page == 0 && node == 0x95 && x.size() >= 1) {
id(system_fault).publish_state(x[0] & 0x01);
}
// Battery voltage rides 29-bit telemetry frames (src 7D/AE, page // Battery voltage rides 29-bit telemetry frames (src 7D/AE, page
// 0x11), payload 00 2B 0D 4x ..; b2..b3 (BE) / 256 = volts. // 0x11), payload 00 2B 0D 4x ..; b2..b3 (BE) / 256 = volts.
// TODO: match the exact source frame and publish battery_voltage. // TODO: match the exact source frame and publish battery_voltage.
@@ -236,6 +247,19 @@ sensor:
unit_of_measurement: "%" unit_of_measurement: "%"
accuracy_decimals: 0 accuracy_decimals: 0
# ---------------------------------------------------------------------------
# Fault indicators (published by the dispatcher above)
# ---------------------------------------------------------------------------
binary_sensor:
- platform: template
name: "Water Heater DSI Fault"
id: dsi_fault
device_class: problem # node 95 page-3 b0 bit5 — gas ignition lockout
- platform: template
name: "OneControl System Fault"
id: system_fault
device_class: problem # page-0 b0 bit0 — bus-wide "a fault exists"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Switches — authenticated command path (see send_load_command above). # Switches — authenticated command path (see send_load_command above).
# Each turn_on/off queues send_load_command for the load's node; the on_frame # Each turn_on/off queues send_load_command for the load's node; the on_frame