canbus: expose water heater + awning; block water pump
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>
This commit is contained in:
@@ -1,17 +1,23 @@
|
||||
#pragma once
|
||||
// Command-path safety policy for the OneControl integration.
|
||||
//
|
||||
// POLICY: slides and jacks are operated from the physical control panel ONLY,
|
||||
// POLICY: some loads are operated from the physical control panel / OEM app ONLY,
|
||||
// never over CAN. command_blocked() returns true for any node this firmware must
|
||||
// never send a command to. It is the single source of truth for that rule and is
|
||||
// checked in two independent places (the command-entry script and the actual
|
||||
// transmit point), so loosening one does not open the other.
|
||||
//
|
||||
// What it blocks:
|
||||
// * an explicit denylist of this rig's known slide / jack nodes (6A/7F/9C) —
|
||||
// always blocked, even before we've heard their identity broadcast;
|
||||
// * slides / jacks — never automate. Explicit denylist of this rig's nodes
|
||||
// (6A/7F/9C), always blocked, even before we've heard their identity;
|
||||
// * the water pump (61) — winterizing-only; operate it from the panel/app so it
|
||||
// can't be left running (e.g. dry) by accident;
|
||||
// * any movement/motor-class node (device class 0x21) other than the awning
|
||||
// (0x75) — generalizes the rule to "any motor output that isn't the awning".
|
||||
// (0x75) — generalizes the slide/jack rule to "any motor that isn't the awning".
|
||||
//
|
||||
// Permitted (NOT blocked here): lights, water heater, and the awning. Whether a
|
||||
// permitted node is actually exposed as an entity is a separate allowlist in the
|
||||
// command-entry script — this gate only enforces the never-allowed set.
|
||||
//
|
||||
// node_type is the page-2 identity byte (device class) for the node, or 0 if not
|
||||
// yet observed. The static denylist covers the not-yet-observed window.
|
||||
@@ -21,8 +27,10 @@
|
||||
namespace onecontrol {
|
||||
|
||||
inline bool command_blocked(uint8_t node, uint8_t node_type) {
|
||||
// Known slide / jack / movement-class nodes on this rig — always refused.
|
||||
// Slide / jack / movement-class nodes on this rig — always refused.
|
||||
if (node == 0x6A || node == 0x7F || node == 0x9C) return true;
|
||||
// Water pump — panel/app only (winterizing).
|
||||
if (node == 0x61) return true;
|
||||
// Any movement-class (0x21) node except the awning (0x75).
|
||||
if (node_type == 0x21 && node != 0x75) return true;
|
||||
return false;
|
||||
|
||||
@@ -157,8 +157,18 @@ canbus:
|
||||
// water heater (node 95): b0 bit0 = on, bit5 (0x20) = DSI/gas
|
||||
// ignition fault/lockout (healthy: 0x80 off / 0x81 running;
|
||||
// fault: 0xA0). Confirmed by a forced lockout 2026-06-12.
|
||||
case 0x95: id(dsi_fault).publish_state(x[0] & 0x20); break;
|
||||
// TODO: water pump (61) on/off once exposed below.
|
||||
case 0x95: id(water_heater).publish_state(x[0] & 0x01);
|
||||
id(dsi_fault).publish_state(x[0] & 0x20); break;
|
||||
// awning H-bridge (node 75): b0 C0 idle / C2 extending (opening)
|
||||
// / C3 retracting (closing). Reflect motion onto the cover so HA
|
||||
// shows open/opening/closing; assumed_state fills the resting pos.
|
||||
case 0x75:
|
||||
if (x[0] == 0xC2) id(awning).current_operation = COVER_OPERATION_IS_OPENING;
|
||||
else if (x[0] == 0xC3) id(awning).current_operation = COVER_OPERATION_IS_CLOSING;
|
||||
else id(awning).current_operation = COVER_OPERATION_IDLE;
|
||||
id(awning).publish_state();
|
||||
break;
|
||||
// (water pump 61 is command-blocked — read-only, not exposed.)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,12 +226,11 @@ script:
|
||||
id(g_cmd_pending) = false;
|
||||
return;
|
||||
}
|
||||
// SAFETY, layer 2 — current scope is lights only. Only the allowlisted
|
||||
// switched loads (2A ext lights, F8 int lights) may be commanded. To add
|
||||
// the awning later, add 0x75 here; the motor gate above already permits
|
||||
// it. Do NOT add slide/jack nodes — the gate would refuse them anyway.
|
||||
if (node != 0x2A && node != 0xF8) {
|
||||
ESP_LOGW("idscan", "refusing command to non-allowlisted node %02X (lights only)", node);
|
||||
// SAFETY, layer 2 — exposed-entity allowlist. Nodes wired to entities:
|
||||
// 2A ext lights, F8 int lights, 95 water heater, 75 awning (cover). Do
|
||||
// NOT add slide/jack/pump nodes — the gate above refuses them regardless.
|
||||
if (node != 0x2A && node != 0xF8 && node != 0x95 && node != 0x75) {
|
||||
ESP_LOGW("idscan", "refusing command to non-allowlisted node %02X", node);
|
||||
id(g_cmd_pending) = false;
|
||||
return;
|
||||
}
|
||||
@@ -303,14 +312,13 @@ binary_sensor:
|
||||
# Each turn_on/off queues send_load_command for the load's node; the on_frame
|
||||
# handler completes the page-42/43 challenge/response and sends the opcode.
|
||||
#
|
||||
# SAFETY: lights only. Only the allowlisted switched-load nodes (2A = exterior
|
||||
# lights, F8 = interior lights) may be wired here.
|
||||
# Exposed switched loads: exterior lights (2A), interior lights (F8), water
|
||||
# heater (95). Each must be in the layer-2 allowlist in send_load_command.
|
||||
#
|
||||
# SLIDES AND JACKS ARE CONTROL-PANEL ONLY — never over CAN. This is a hard policy
|
||||
# (command_guard.h): the command path refuses any motor-class (0x21) node other
|
||||
# than the awning, enforced both here and at the transmit point, so even adding a
|
||||
# slide/jack switch below cannot actuate it. The awning is technically permitted
|
||||
# by the gate but is intentionally left unwired (operate it from the panel too).
|
||||
# SLIDES, JACKS, and the WATER PUMP ARE PANEL/APP ONLY — never over CAN. Hard
|
||||
# policy in command_guard.h, enforced both here and at the transmit point, so even
|
||||
# adding a switch for one below cannot actuate it. The awning is permitted by the
|
||||
# gate but needs a proper cover entity + an attended first test before it's wired.
|
||||
# optimistic:true for now — the page-3 (b0 bit0) read-back above already publishes
|
||||
# true module state, so these can switch to optimistic:false once verified.
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -333,6 +341,41 @@ switch:
|
||||
turn_off_action:
|
||||
- script.execute: { id: send_load_command, node: 0xF8, op: 0 }
|
||||
|
||||
# water pump (61) / water heater (95) are also type-0x1E switched loads and use
|
||||
# the same authentication — add them here once you want them in HA. Movement
|
||||
# nodes stay off this list until tested.
|
||||
- platform: template
|
||||
name: "Water Heater"
|
||||
id: water_heater
|
||||
optimistic: true
|
||||
turn_on_action:
|
||||
- script.execute: { id: send_load_command, node: 0x95, op: 1 }
|
||||
turn_off_action:
|
||||
- script.execute: { id: send_load_command, node: 0x95, op: 0 }
|
||||
|
||||
# Slides/jacks/pump are command-blocked and must never be wired here.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Awning (node 75) — open / close / stop cover.
|
||||
#
|
||||
# H-bridge motor: open = extend (op 01), close = retract (op 02), stop (op 00),
|
||||
# per the captures. assumed_state -> HA shows discrete open/close/stop controls
|
||||
# (no position slider); current_operation is published from the page-3 motion
|
||||
# byte in the dispatcher above.
|
||||
#
|
||||
# ⚠️ FIRST ACTUATION MUST BE ATTENDED. We haven't confirmed whether one command
|
||||
# latches the motor (runs to the travel limit) or is hold-to-run (the OEM app
|
||||
# streamed repeats, which hints at hold-to-run). The single-shot command here is
|
||||
# the safe default: if hold-to-run, the awning just moves a little and stops on
|
||||
# its own — it can't run away. If it under-travels, stream the command (repeat
|
||||
# send_load_command until stop) — do that change only after watching it move.
|
||||
# ---------------------------------------------------------------------------
|
||||
cover:
|
||||
- platform: template
|
||||
name: "Awning"
|
||||
id: awning
|
||||
device_class: awning
|
||||
assumed_state: true
|
||||
open_action:
|
||||
- script.execute: { id: send_load_command, node: 0x75, op: 1 }
|
||||
close_action:
|
||||
- script.execute: { id: send_load_command, node: 0x75, op: 2 }
|
||||
stop_action:
|
||||
- script.execute: { id: send_load_command, node: 0x75, op: 0 }
|
||||
|
||||
Reference in New Issue
Block a user