Per updated policy: - Water heater (node 95): new switch entity + on/off state read-back. - Awning (node 75): new cover entity (open=op01 / close=op02 / stop=op00), with current_operation published from the page-3 motion byte (C2/C3/C0). First actuation must be attended; single-shot commands can't run the motor away. - Water pump (node 61): added to command_guard denylist (winterizing-only, panel/app only) alongside slides/jacks. Guard re-tested 8/8 (host g++). Switch/cover comments + HANDOFF safety notes and remaining-work updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
116 lines
6.7 KiB
Markdown
116 lines
6.7 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.
|
||
|
||
## 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.
|
||
- **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`** — copy `esphome/secrets.yaml.example`, fill in WiFi, the
|
||
ESPHome `api_key`, and the fallback-AP password. It's git-ignored.
|
||
2. **First flash over USB:** `esphome run esphome/onecontrol-canbus.yaml` (pick
|
||
the serial port). OTA after that. Mirrors the gazebo-fan-proxy workflow.
|
||
3. **Confirm the read entities populate** in HA once it's on the bus: the four
|
||
tanks and the two light switches publish from the page-3 broadcasts. Watch the
|
||
DEBUG frame dump (`logger: level: DEBUG`) to confirm frames are decoded; drop
|
||
to INFO when happy.
|
||
4. **Finish the two open read items in the YAML lambda:**
|
||
- **Battery voltage** — rides a 29-bit telemetry frame (src `7D`/`AE`,
|
||
page `0x11`), bytes 2–3 big-endian / 256 = volts. Match that frame and
|
||
publish `battery_voltage`. (See README "29-bit extended frames".)
|
||
- Optionally add **water pump (`61`)** and **water heater (`95`)** — both are
|
||
ordinary switched loads, same decode + command path as the lights.
|
||
5. **Verify the command path** from HA: toggle Interior/Exterior Lights and the
|
||
Water Heater. The `send_load_command` script + `on_frame` handler do the
|
||
exchange and send the opcode.
|
||
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. **(Optional) Surface at the campsite HA + bridge home** like the gazebo fans /
|
||
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 | 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`.
|
||
- **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).
|