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>
The alert package looked deployed but the rest_command domain was never
set up — reload_all only reloads already-loaded integrations, so all
four "verified" test fires died with Action-not-found while
check_config said valid. Needs one full core restart on first deploy;
header now says so and gives the verification command.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Gatus webhook delivered to a channel nobody reads; the secret on the
Pi now holds the confirmed server-alerts webhook (URL stays out of the
repo as before).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
The campsite HAOS Pi now runs the ESPHome CAN node natively ("OneControl
CAN", *.onecontrol_can_*); the BLE integration is fully torn out (config
entry, custom_components on the Pi, stale MQTT registry orphans) and the
camper dashboard rewritten — water-pump tile dropped on purpose (pump is
panel-only), awning + fault sensors added.
New canbus/ha/mqtt_bridge_onecontrol.yaml (deployed to the Pi as
packages/mqtt_bridge.yaml) bridges the CAN entities to the home broker
via MQTT Discovery, same pattern as the gazebo bridge. Kept the old
unique_ids so home entity ids and recorder history carried over; added
an availability topic and explicit command-topic allowlists (3 switches
+ awning — a future debug entity must not become remotely controllable
for free). Round-trip verified from home HA.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Flash-session firmware work, live on the node since 2026-06-12:
- battery voltage decode (29-bit page-0x11 telemetry, b2..b3 BE / 256),
gated to extended frames so an 11-bit node 0x11 can never spoof it,
with a delta/throttle filter to stop 1/256-V jitter churning both
HA recorders and the MQTT bridge
- second on_frame trigger (use_extended_id: false) sharing the decode
lambda via YAML anchor — this esp32_can build filters triggers by
frame type, so a single trigger silently dropped the 11-bit reads
- switches optimistic:false now the page-3 read-back is verified live
- arm retry widened to 8x150ms; module-side ~2s post-success cooldown
documented
- canbus component logs to INFO (per-frame DEBUG dump saturated serial)
- toolchain fixes: named std::array initializer, namespaced cover enums
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Per policy, the firmware must never command a slide or jack. command_guard.h adds
command_blocked(node, type): refuses any motor-class (0x21) node except the
awning (0x75), with an explicit slide/jack denylist (6A/7F/9C) that holds even
before a node's page-2 identity is observed.
Enforced in two independent places — the command-entry script (send_load_command)
and the actual transmit point (on_frame, right before TX) — so loosening one
can't open the other. Adding a slide/jack switch entity cannot actuate it. Node
device class is learned from page-2 broadcasts (g_node_type). Predicate
unit-tested 9/9 (host g++). Switch comments + HANDOFF safety notes updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Command path proven end to end on the bus (node F8 interior lights, on/off/on),
each answering a distinct fresh challenge; bare opcodes without the exchange are
ignored. ids_can_auth.h verified bit-exact against ids_can_auth.py and the
captured/live pairs.
- idscan_cmd.py: stdlib socketcan tool running the full page-42/43 exchange
- esphome/onecontrol-canbus.yaml: correct IDS-CAN read dispatch (was stale RV-C
DGN code) + command path wired to the auth header
- README/memory: document the read map + command authentication; rename
sniff/ -> captures/; neutral device-integration framing throughout
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CAN write gate (page-42/43 challenge/response) is a 32-round TEA/XTEA-family
Feistel keyed by a per-session 32-bit key; REMOTE_CONTROL = 0xB16B00B5. Verified
51/51 against captured challenge/response pairs across nodes 2A/61/75/F8 (one
global key, not per-node), so the CAN path can now actuate, not just sense.
- ids_can_auth.py Python reference + self-test (51/51)
- esphome/ids_can_auth.h C++ port for the ESP32 node (host-tested 8/8)
- sniff/analyze_auth.py structural analysis (rules out affine; confirms keyed cipher)
- sniff/auth-pairs-multinode-2026-06-11.txt +9 pairs across 4 nodes
- README document the cipher, session keys, unlock sequence
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Close propane valve, force a water-heater lockout, capture + diff against
healthy baseline. Prime suspects: node 95 b1 (always FF) or node AE page-3
(always 00). Whichever flips becomes the DSI binary_sensor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tapped the X180T's CAN bus via CANable 2.0 at the monitor panel's terminator
port. The bus is NOT RV-C — it's Lippert's proprietary IDS-CAN (250k, 11-bit
IDs, (page<<8)|node, 1 Hz broadcasts).
Read side fully mapped from live captures:
- device classes (page-2 type byte: 0x0A tank, 0x1E switched load, 0x21 motor)
- node map for this rig (Catalina 263BHSCK): tanks 27/E2/7D/FE, lights 2A/F8,
heater 95, pump 61, awning 75 (+ direction & live motor current)
- battery voltage on 29-bit extended frames
Write side: commands are DLC-0 ext frames 0006<node><op>, but auth-gated by a
rolling challenge-response (page 42/43). Replay confirmed dead (spoofed cansend
did not actuate). Not the BLE TEA cypher. response=f(challenge) is deterministic
(no session state) so crackable offline later — seeded 42 pairs in
sniff/2A-auth-pairs.txt.
Includes raw captures (sniff/*.log, force-added past *.log ignore), a read-only
esp32_can ESPHome skeleton, and the log-can.sh sniff helper. Full writeup in
canbus/README.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The OneControl panel's command characteristic is a streaming (Write Without
Response) endpoint. Bleak 3.x changed write_gatt_char to default to
write-with-response when the char advertises the "write" property, so every
command (incl. switch turn_on/off) got rejected by the panel with ATT 0x0E
(Unlikely Error), surfaced as BleakGATTProtocolError. Force response=False on
the command write (matching the auth key write) to restore control.
Also commits the productionized custom_components integration (config flow,
coordinator, switch/sensor/cover entities, key-seed TEA auth, COBS codec) and
the matching src/ RE client/COBS fixes (big-endian framing, table-driven CRC8,
status-event decoding) that were developed but never tracked.
Verified live on the campsite HAOS Pi: switch.exterior_lights / interior_lights
toggle the physical panel with no GATT error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of immediate disconnect after write: panel requires auth on
service 0010 before accepting commands on 0030. Protocol found by
decompiling Plugin.BLE.dll (BleDeviceUnlockManager.PerformKeySeedExchange).
Auth flow:
1. Read 4-byte seed from char 0012 (00000012-0200-a58e-e411-afe28044e62c)
2. Apply modified TEA with RV-specific cypher (612643285 / 0x248431D5)
3. Write 4-byte little-endian result to char 0013
4. Retries up to 3x if seed < 4 bytes (panel not yet ready)
connect() now calls perform_auth() before enabling 0034 notifications.
Also cleans up experimental auth attempts from campsite debugging session.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix scan_for_onecontrol() to use return_adv=True and adv.service_uuids
(device.metadata removed in bleak 3.x)
- Add missing _parse_get_devices_response() — was called but never defined,
would crash on first real device response
- main() now auto-connects when exactly one device is found via scan
- Remove premature turn_on/off_light(5) calls from main() — device IDs
unknown until get_devices() has been run against real hardware
- Guard send_command() and disconnect() against unconnected state
- control_awning() now takes MovementState enum instead of raw int
- Bump post-get_devices sleep from 2s to 5s for real device latency
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The decompiled code is not included in the repository (excluded via .gitignore).
Mentioning it in the README could create legal concerns and is misleading.
- Added Apache License 2.0 (full text)
- Strengthened README disclaimer with interoperability language
- Added references to DMCA 1201(f) and EU Software Directive Article 6
- Clarified what IS and IS NOT included in repo
- Added license badge to README
- Explicitly states purpose: legally purchased hardware integration
Compliance checklist:
✅ Clean room implementation (no decompiled code in repo)
✅ .gitignore excludes all binaries and extracted code
✅ Interoperability defense clearly stated
✅ Apache 2.0 license with patent grant clause
✅ Disclaimer for Lippert Components Inc.