New Pi package (repo copy canbus/ha/dsi_fault_alert.yaml): when the CAN node's DSI-lockout binary sensor holds 'on' for 10s, fire the server- alerts Discord webhook (@here, appropriately rude), with a cleared message when it recovers. Webhook URL lives in the Pi's HA secrets.yaml (!secret discord_server_alerts_webhook), not the repo. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
170 lines
10 KiB
Markdown
170 lines
10 KiB
Markdown
# Handoff — finish & flash the OneControl IDS-CAN node
|
||
|
||
**Goal:** complete and flash an ESP32 firmware that puts our RV's Lippert
|
||
OneControl system (tanks, lights, switches) into Home Assistant as native
|
||
entities, talking directly to the OneControl CAN network. This replaces the
|
||
slower Bluetooth integration in `../src/` + `../custom_components/`.
|
||
|
||
Read `README.md` first — it has the full message-format documentation, the node
|
||
map for this coach, and the wiring procedure. This file is just the current
|
||
state + what's left.
|
||
|
||
## FLASHED & READ-VERIFIED (2026-06-12, afternoon)
|
||
|
||
The node is built, flashed over USB, on the bus, and on WiFi
|
||
(`192.168.69.18`, hostname `onecontrol-canbus`, adoptable via the API key in
|
||
`secrets.yaml`). **All 11 read entities verified live over the native API**:
|
||
battery 13.27 V, tanks fresh 0 / black 33 / grey1 33 / grey2 100 %, interior
|
||
lights on, exterior/heater off, both fault flags clear, awning idle.
|
||
|
||
What changed this session:
|
||
- `secrets.yaml` filled (WiFi + fallback AP from the shared gazebo proxy creds).
|
||
- **Battery voltage decode finished** (remaining-work item 4): matches the
|
||
page-`0x11` `00 2B …` telemetry frame, `b2..b3` BE / 256 — reads 13.27 V.
|
||
- **Toolchain fixes** (ESPHome moved since the last build; it no longer compiled):
|
||
the `g_node_type` array global's bare `{}` initializer was ambiguous (named the
|
||
type), and the awning cover enum is `COVER_OPERATION_OPENING/CLOSING` (no `IS_`).
|
||
- **Dual `on_frame` trigger** — this build filters `on_frame` by frame type, so a
|
||
single `use_extended_id: true` trigger only saw the 29-bit frames; the 11-bit
|
||
read broadcasts (tanks/lights/heater/awning) were silently dropped. Added a
|
||
second `use_extended_id: false` trigger sharing the same lambda via a YAML
|
||
anchor. (This was the exact risk the bus-config note called out.)
|
||
- **Logger** — raised the `canbus` component to INFO and silenced the per-frame
|
||
dump; at DEBUG it logged ~50 frames/s and saturated the 115200 serial link.
|
||
|
||
Still TODO (needs the operator in the loop — see "Remaining work" items 5–6):
|
||
command-path verification (toggle a light) and the attended awning test.
|
||
|
||
## Current status (2026-06-12)
|
||
|
||
- **Message format documented and confirmed against live captures.** The bus is
|
||
Lippert **IDS-CAN** (250 kbit/s, 11-bit IDs), not RV-C. Every tank, light,
|
||
pump, heater, the awning, and battery voltage is decoded — see the node map in
|
||
`README.md`.
|
||
- **Read path:** modules broadcast their state continuously, no authentication.
|
||
- **Command path:** each command is preceded by a short challenge/response
|
||
authentication exchange (the same one the OEM app uses). Implemented in
|
||
`ids_can_auth.py` (reference) and `esphome/ids_can_auth.h` (the firmware copy,
|
||
verified **bit-exact** against the Python and against captured/live values).
|
||
- **Proven working end to end.** `idscan_cmd.py` ran the full exchange over a USB
|
||
CAN adapter and operated the interior lights (node `F8`) on/off/on, each
|
||
answering a distinct fresh challenge, with the module's own status broadcast
|
||
confirming the result. A bare command with no exchange is ignored.
|
||
- **Firmware compiled successfully** last night (`esphome/.esphome/build/…`).
|
||
|
||
## Hardware (assembly-ready)
|
||
|
||
- ESP32 WROOM devboard (`esp32dev`) + Waveshare **SN65HVD230** transceiver
|
||
(3.3 V logic, onboard 120 Ω terminator → this node is the bus-END node).
|
||
- **Power:** buck converter dialed to **5 V**, tapped from the panel's 12 V
|
||
supply (common ground with the bus — good). Feed the ESP32 5 V pin.
|
||
- **Connection:** Molex Mini-Fit Jr. pigtail into the panel's CAN **data** port
|
||
(the one with the terminator), per `README.md` → "Physical connection".
|
||
⚠️ Meter the port first: data idles ~2.5 V, power reads ~12 V; 12 V on CAN-H/L
|
||
destroys the transceiver.
|
||
- **Pigtail wiring (as crimped):** green = CAN-L, blue = CAN-H.
|
||
- **GPIO:** transceiver `CTX/D` ← ESP32 `GPIO5` (tx_pin), `CRX/R` → `GPIO4`
|
||
(rx_pin). Adjust `substitutions:` in the YAML if you wire differently.
|
||
|
||
## Remaining work (in order)
|
||
|
||
1. ~~**`secrets.yaml`**~~ — DONE. WiFi + fallback-AP creds filled from the shared
|
||
gazebo proxy; the `api_key` was already set.
|
||
2. ~~**First flash over USB**~~ — DONE (`/dev/ttyUSB0`). OTA from here on.
|
||
Note: this laptop's user isn't in the `uucp` group, so the serial node needed
|
||
`chmod 666 /dev/ttyUSB0` (resets on replug) — irrelevant for OTA.
|
||
3. ~~**Confirm the read entities populate**~~ — DONE. All 11 verified live over the
|
||
native API (see status block above). NB: ESPHome 2026.5 doesn't echo
|
||
`publish_state` at DEBUG over serial, so a quiet serial log is normal — read the
|
||
states over the API, not the console.
|
||
4. **Open read items in the YAML lambda:**
|
||
- ~~**Battery voltage**~~ — DONE; reads 13.27 V.
|
||
- Optionally add **water pump (`61`)** and **water heater (`95`)** — both are
|
||
ordinary switched loads, same decode + command path as the lights. (Heater is
|
||
already exposed; pump stays command-blocked.)
|
||
5. ~~**Verify the command path**~~ — DONE (Interior + Exterior lights, on & off,
|
||
each a confirmed challenge→response→opcode exchange on the serial log, with the
|
||
read-back flipping to match). Switches are now `optimistic:false` so HA shows
|
||
ground truth. **Confirmed module behaviour:** after a successful session the
|
||
module enforces a **~2 s cooldown** — a second command to the same load sooner
|
||
gets no challenge and is dropped (opcode-independent, module-side, not a bug).
|
||
The arm retry was widened to 8×150 ms to absorb the odd dropped frame on the
|
||
busy bus. Heater is wired the same way but wasn't actuated (avoid cycling gas
|
||
ignition casually); it should behave identically.
|
||
6. **Attended awning test (motor — watch it move).** The Awning `cover` is wired
|
||
(open/close/stop). On the first run, confirm: (a) which direction open/close
|
||
actually go, (b) whether one command runs to the travel limit or only moves
|
||
while commands stream. If it under-travels, change `send_load_command` to
|
||
stream the opcode (repeat until Stop) — only after watching it. The single-shot
|
||
default can't run the motor away.
|
||
7. ~~**Surface at the campsite HA**~~ — DONE (2026-06-12). Added to the campsite
|
||
HAOS Pi (`192.168.69.10`) as ESPHome config entry "OneControl CAN"; all 11
|
||
entities (`*.onecontrol_can_*`) live. The old BLE integration is **fully torn
|
||
out**: config entry deleted, `custom_components/lippert_onecontrol` removed
|
||
from the Pi, 11 stale `campsite_onecontrol_*` MQTT registry orphans purged
|
||
(home broker had no retained discovery topics — they were registry-only). The
|
||
camper dashboard (`lovelace.dashboard_camper`) was rewritten to the new
|
||
entities; the water-pump tile was dropped (pump is panel-only by design) and
|
||
awning + both fault sensors added.
|
||
8. ~~**Bridge home via MQTT**~~ — DONE (2026-06-12 evening). The Pi package
|
||
`/config/packages/mqtt_bridge.yaml` was rewritten against the CAN entities
|
||
(repo copy: `canbus/ha/mqtt_bridge_onecontrol.yaml`); it publishes MQTT
|
||
Discovery + state to cyrion's Mosquitto (192.168.88.69) and relays commands
|
||
back, same pattern as the gazebo bridge. Same `unique_id`s kept for surviving
|
||
loads, so home HA entity ids + history carried over (`*.campsite_onecontrol_*`);
|
||
water pump / cover_2 / cover_3 retained topics cleared (entities auto-removed
|
||
on home); awning + both fault sensors added, plus an availability topic
|
||
(`campsite/onecontrol/availability`) the old bridge lacked. Home Overview
|
||
"Camper" view updated to match. Command round-trip verified from home HA.
|
||
|
||
The DSI fault is already decoded and wired in (see below) — no capture needed.
|
||
|
||
## File map
|
||
|
||
| File | What it is |
|
||
|------|-----------|
|
||
| `esphome/onecontrol-canbus.yaml` | the ESP32 firmware (read dispatch + command path) — the thing to finish & flash |
|
||
| `esphome/ids_can_auth.h` | command-authentication response, used by the YAML lambda |
|
||
| `esphome/secrets.yaml.example` | template for the git-ignored secrets |
|
||
| `ids_can_auth.py` | Python reference for the same authentication + 51/51 self-test |
|
||
| `idscan_cmd.py` | desktop tool that proved the command path over a USB CAN adapter |
|
||
| `captures/` | raw bus logs + the challenge/response pairs + `analyze_auth.py` |
|
||
| `captures/log-can.sh` | bring up the USB CAN adapter and log frames |
|
||
| `README.md` | full message-format documentation + node map + wiring |
|
||
|
||
## Safety notes
|
||
|
||
- **SLIDES, JACKS, and the WATER PUMP ARE PANEL/APP ONLY — never over CAN. This
|
||
is a hard rule, do not weaken it.** `esphome/command_guard.h`
|
||
(`command_blocked()`) is the single source of truth: it refuses the slide/jack
|
||
nodes (6A/7F/9C, effective even before a node's identity is heard), the water
|
||
pump (61, winterizing-only), and — generally — any motor-class (0x21) node other
|
||
than the awning. It's enforced in two independent places (the command-entry
|
||
script and the actual transmit point), so loosening one does not open the other.
|
||
Wiring a switch for a blocked node cannot actuate it; the gate drops the command
|
||
before any frame goes out.
|
||
- **Exposed (controllable) loads:** exterior lights (2A), interior lights (F8),
|
||
water heater (95) as `switch`es, and the awning (75) as a `cover`. To expose
|
||
another *permitted* switched load, add its node to the layer-2 allowlist in
|
||
`send_load_command` and add a `switch` entity; never add a slide/jack/pump.
|
||
- **The awning is a motor — its first actuation must be attended** (see
|
||
remaining-work item 6). The wired commands are single-shot, which can't run the
|
||
motor away, but direction and latch-vs-stream behavior need a live check.
|
||
- The physical connection is fully reversible: unplug, re-seat the terminator.
|
||
- One transceiver = one bus-end terminator. Never add a terminated node in the
|
||
middle of the bus (would make three terminators).
|
||
|
||
## DSI fault — DECODED (2026-06-12)
|
||
|
||
Forced a real lockout and captured it (`captures/dsi-fault-*.log`). Already wired
|
||
into the YAML as two `binary_sensor`s; nothing left to do here.
|
||
|
||
- **Water Heater DSI Fault** = node `95` page-3 `b0` bit5 (`0x20`). Healthy heater
|
||
= `0x80` off / `0x81` running; lockout = `0xA0`.
|
||
- **Discord alert wired (2026-06-12):** `canbus/ha/dsi_fault_alert.yaml` (on the
|
||
Pi as `packages/dsi_fault_alert.yaml`) pings the server-alerts channel via
|
||
webhook (`!secret discord_server_alerts_webhook`, same one Gatus uses) when
|
||
the fault latches for 10 s, plus a cleared message. Test-fired OK.
|
||
- **OneControl System Fault** = page-0 `b0` bit0 (`0x01`), a bus-wide
|
||
"fault exists somewhere" flag (read from node `95`'s page 0 in the lambda).
|