Files
wesandClaude Fable 5 5b9198878c canbus: WiFi presence live — Wes's phone tracked, DHCP+ARP mode
Registration-table mode missed roof-AP clients (same SSID, bridged);
switched the mikrotik integration to force_dhcp + arp_ping so both APs
are covered. wes_phone_wifi verified end-to-end (tracker home → Pi
sensor on → bridged home on). Lindsey's tracker pending her next
association.

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

13 KiB
Raw Permalink Blame History

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/RGPIO4 (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_ids 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 switches, 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).
  • MikroTik WiFi presence — router + integration DONE (2026-06-12): Chateau ("manifold-002", 192.168.69.1) API service locked to 192.168.69.10/32, read-only user homeassistant (random password; it lives only in the Pi's mikrotik config entry — to rotate, reset via Chateau admin and re-add the integration). Runs in force_dhcp + arp_ping mode (detection 120 s) — the registration-table default missed clients on the rvlink roof AP (same SSID, bridged); DHCP+ARP covers both APs. New clients register as disabled-by-default device_trackers keyed by MAC. Wes's phone WIRED AND VERIFIED: device_tracker.wes_phone_wifi (MAC 22:1E:E1:E0:1E:3B, the ".155 iPhone" lease — identified by join-time correlation) → phones_on_campsite_wifi reads on, bridged home on. Remaining: when Lindsey's iPhone next associates, a new disabled tracker appears — enable + rename to device_tracker.lindsey_iphone_wifi (the sensor template already references that id). Ignore the other auto-registered disabled trackers (TVs/cameras/laptop).
  • 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_sensors; 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).