Files
lippert-onecontrol/canbus/HANDOFF.md
T
wesandClaude Fable 5 2dd884af3c canbus: presence layer + departure failsafes
Water heater = occupancy truth. New Pi package campsite_presence.yaml:
rv_occupied + phones_on_campsite_wifi template sensors (wifi one stays
unavailable until the MikroTik integration lands — load-bearing for the
dead-man trigger), MQTT bridging home, a local notify-only dead-man
(heater on, no phones on WiFi 3h), and disabled pre-staged lot-light
automations for the future shed Shelly.

dsi_fault_alert.yaml gains the Octavia MQTT relay (campsite/octavia/say)
so home HA — which has no file access — can speak through the verified
webhook path.

Home-side (UI-managed, not in repo): quick failsafe (both out of the
enlarged 150m campsite zone 25 min → fans/lights off + summary ping) and
heater failsafe (2h → heater off with bridge round-trip confirmation).
Both verified live; fan states snapshot-restored after the test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 16:56:30 -04:00

204 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 56):
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).
## Presence & departure failsafes (2026-06-12)
Occupancy truth = the **water heater switch** (always on while camping, off on
departure). Architecture: GPS persons/zones live on home HA; the Pi computes
occupancy + (pending) WiFi presence and bridges them home; failsafes run on
home HA against the bridged controls; all alerts go through the Pi's Octavia
webhook via the MQTT relay topic `campsite/octavia/say`.
- `canbus/ha/campsite_presence.yaml` (Pi package): `binary_sensor.rv_occupied`
(heater on), `binary_sensor.phones_on_campsite_wifi` (unavailable until the
MikroTik step below), MQTT bridging for both, a local notify-only dead-man
(heater on + no phones on WiFi 3 h → Octavia), and **disabled pre-staged lot-
light automations** for the future shed Shelly (`switch.lot_lights`
placeholder — fix the entity id, remove `initial_state: false`, and reload
when the relay is installed; on-at-sunset is occupancy-gated, off-at-sunrise
unconditional).
- Home HA (UI-managed, via API): `automation.campsite_departure_failsafe_quick`
(both out of zone 25 min + RV occupied → gazebo fans/lights + interior lights
off, Octavia + push, only pings if something was actually on) and
`automation.campsite_water_heater_failsafe_2h` (2 h + heater on → heater off
with 60 s bridge round-trip confirmation; distinct failure message if
unconfirmed). Both fired and verified live 2026-06-12 (fans physically cycled,
states restored after).
- `zone.our_campsite` radius enlarged 50→150 m (GPS wobble safety).
- **PENDING — MikroTik WiFi presence:** needs Chateau admin once: enable
`/ip service api address=192.168.69.10`, add read-only `homeassistant` user,
then add the `mikrotik` integration on the Pi (hub 192.168.69.1) and rename
the phone trackers to `device_tracker.wes_phone_wifi` /
`device_tracker.lindsey_iphone_wifi`. Everything downstream lights up by
itself (sensor availability flips on).
- **Phone GPS checklist (manual, Wes's phone):** companion app background
location unrestricted, high-accuracy on, battery optimization off — his
tracker was >1 h stale at survey time; Lindsey's iPhone reports fine.
## 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).