Motion is hold-to-run (one authed opcode runs the motor ~1s). Reproduce the OEM movement session -- auth once, then stream the opcode @110ms + a page-44 keepalive @510ms -- to sustain continuous retract, and watch node-75 page-3 motor current at 20Hz to stop at the fully-closed stall (~4200 vs ~<1550 running), then mark the cover CLOSED. Backstops: 70s timeout, motion-lost detector, hold-to-run stop-on-silence. Proven live at the camper 2026-07-01. - esphome: awning_auto_retract script + 100ms streamer interval + case-0x75 stall gate + "Awning Motor Current" sensor. Cover CLOSE=auto-retract, STOP=abort. Removed open_action (1s jog is useless, no safe timed auto-open). - bridge: optimistic:true keeps the home HA cover assumed-state so retract is always pressable (was greyed when closed); payload_open:null drops OPEN on the home (primary) dashboard. Also synced the 6h->15min discovery-cadence drift. - captures + README: full-retract stall profile and the live auto-retract test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
426 lines
18 KiB
YAML
426 lines
18 KiB
YAML
# Campsite MQTT Bridge — publishes the OneControl CAN entities to home
|
|
# Mosquitto (cyrion, 192.168.88.69) via MQTT Discovery so they auto-appear
|
|
# on the home HA (192.168.88.40) as device "Campsite OneControl".
|
|
#
|
|
# Source side is the ESPHome IDS-CAN node ("OneControl CAN", entities
|
|
# *.onecontrol_can_*) — NOT the retired BLE integration.
|
|
# No water pump and no slide covers on purpose: those are panel-only
|
|
# (see canbus/HANDOFF.md safety notes).
|
|
#
|
|
# Deploy to Pi: /config/packages/mqtt_bridge.yaml
|
|
# Repo copy: canbus/ha/mqtt_bridge_onecontrol.yaml
|
|
|
|
automation:
|
|
# --- MQTT Discovery + state republish on startup / reload / every 15 min ---
|
|
# The 15-min cadence (was 6h) heals stale retained switch states when an
|
|
# on-change forward publish is lost during a roof-AP/MQTT link flap — the
|
|
# weak-link failure that left home showing Exterior off while the camper
|
|
# was on (2026-06-14). Config republishes are idempotent (HA dedupes).
|
|
- id: campsite_mqtt_discovery
|
|
alias: "MQTT Bridge: Publish Discovery"
|
|
triggers:
|
|
- trigger: homeassistant
|
|
event: start
|
|
- trigger: event
|
|
event_type: automation_reloaded
|
|
- trigger: time_pattern
|
|
minutes: "/15"
|
|
actions:
|
|
- delay: "00:00:05"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/onecontrol/availability"
|
|
retain: true
|
|
payload: >-
|
|
{{ 'offline' if states('sensor.onecontrol_can_battery_voltage')
|
|
in ['unavailable', 'unknown'] else 'online' }}
|
|
# Switches
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "homeassistant/switch/campsite/gas_water_heater/config"
|
|
retain: true
|
|
payload: >-
|
|
{"name": "Water Heater",
|
|
"unique_id": "campsite_gas_water_heater",
|
|
"state_topic": "campsite/switch/water_heater/state",
|
|
"command_topic": "campsite/switch/water_heater/set",
|
|
"payload_on": "ON", "payload_off": "OFF",
|
|
"state_on": "ON", "state_off": "OFF",
|
|
"availability_topic": "campsite/onecontrol/availability",
|
|
"device": {"identifiers": ["campsite_onecontrol"],
|
|
"name": "Campsite OneControl",
|
|
"manufacturer": "Lippert",
|
|
"model": "Chateau 22QB (IDS-CAN)"}}
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "homeassistant/switch/campsite/exterior_lights/config"
|
|
retain: true
|
|
payload: >-
|
|
{"name": "Exterior Lights",
|
|
"unique_id": "campsite_exterior_lights",
|
|
"state_topic": "campsite/switch/exterior_lights/state",
|
|
"command_topic": "campsite/switch/exterior_lights/set",
|
|
"payload_on": "ON", "payload_off": "OFF",
|
|
"state_on": "ON", "state_off": "OFF",
|
|
"availability_topic": "campsite/onecontrol/availability",
|
|
"device": {"identifiers": ["campsite_onecontrol"]}}
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "homeassistant/switch/campsite/interior_lights/config"
|
|
retain: true
|
|
payload: >-
|
|
{"name": "Interior Lights",
|
|
"unique_id": "campsite_interior_lights",
|
|
"state_topic": "campsite/switch/interior_lights/state",
|
|
"command_topic": "campsite/switch/interior_lights/set",
|
|
"payload_on": "ON", "payload_off": "OFF",
|
|
"state_on": "ON", "state_off": "OFF",
|
|
"availability_topic": "campsite/onecontrol/availability",
|
|
"device": {"identifiers": ["campsite_onecontrol"]}}
|
|
# Sensors
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "homeassistant/sensor/campsite/battery_voltage/config"
|
|
retain: true
|
|
payload: >-
|
|
{"name": "Battery Voltage",
|
|
"unique_id": "campsite_battery_voltage",
|
|
"state_topic": "campsite/sensor/battery_voltage/state",
|
|
"unit_of_measurement": "V",
|
|
"device_class": "voltage",
|
|
"state_class": "measurement",
|
|
"suggested_display_precision": 2,
|
|
"availability_topic": "campsite/onecontrol/availability",
|
|
"device": {"identifiers": ["campsite_onecontrol"]}}
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "homeassistant/sensor/campsite/fresh_water_tank/config"
|
|
retain: true
|
|
payload: >-
|
|
{"name": "Fresh Water Tank",
|
|
"unique_id": "campsite_fresh_water_tank",
|
|
"state_topic": "campsite/sensor/fresh_water_tank/state",
|
|
"unit_of_measurement": "%",
|
|
"state_class": "measurement",
|
|
"icon": "mdi:water",
|
|
"availability_topic": "campsite/onecontrol/availability",
|
|
"device": {"identifiers": ["campsite_onecontrol"]}}
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "homeassistant/sensor/campsite/grey_tank_1/config"
|
|
retain: true
|
|
payload: >-
|
|
{"name": "Grey Tank 1",
|
|
"unique_id": "campsite_grey_tank_1",
|
|
"state_topic": "campsite/sensor/grey_tank_1/state",
|
|
"unit_of_measurement": "%",
|
|
"state_class": "measurement",
|
|
"icon": "mdi:water-opacity",
|
|
"availability_topic": "campsite/onecontrol/availability",
|
|
"device": {"identifiers": ["campsite_onecontrol"]}}
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "homeassistant/sensor/campsite/grey_tank_2/config"
|
|
retain: true
|
|
payload: >-
|
|
{"name": "Grey Tank 2",
|
|
"unique_id": "campsite_grey_tank_2",
|
|
"state_topic": "campsite/sensor/grey_tank_2/state",
|
|
"unit_of_measurement": "%",
|
|
"state_class": "measurement",
|
|
"icon": "mdi:water-opacity",
|
|
"availability_topic": "campsite/onecontrol/availability",
|
|
"device": {"identifiers": ["campsite_onecontrol"]}}
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "homeassistant/sensor/campsite/black_tank/config"
|
|
retain: true
|
|
payload: >-
|
|
{"name": "Black Tank",
|
|
"unique_id": "campsite_black_tank",
|
|
"state_topic": "campsite/sensor/black_tank/state",
|
|
"unit_of_measurement": "%",
|
|
"state_class": "measurement",
|
|
"icon": "mdi:water-alert",
|
|
"availability_topic": "campsite/onecontrol/availability",
|
|
"device": {"identifiers": ["campsite_onecontrol"]}}
|
|
# Fault binary sensors
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "homeassistant/binary_sensor/campsite/water_heater_dsi_fault/config"
|
|
retain: true
|
|
payload: >-
|
|
{"name": "Water Heater DSI Fault",
|
|
"unique_id": "campsite_water_heater_dsi_fault",
|
|
"state_topic": "campsite/binary_sensor/water_heater_dsi_fault/state",
|
|
"payload_on": "ON", "payload_off": "OFF",
|
|
"device_class": "problem",
|
|
"availability_topic": "campsite/onecontrol/availability",
|
|
"device": {"identifiers": ["campsite_onecontrol"]}}
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "homeassistant/binary_sensor/campsite/onecontrol_system_fault/config"
|
|
retain: true
|
|
payload: >-
|
|
{"name": "System Fault",
|
|
"unique_id": "campsite_onecontrol_system_fault",
|
|
"state_topic": "campsite/binary_sensor/onecontrol_system_fault/state",
|
|
"payload_on": "ON", "payload_off": "OFF",
|
|
"device_class": "problem",
|
|
"availability_topic": "campsite/onecontrol/availability",
|
|
"device": {"identifiers": ["campsite_onecontrol"]}}
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "homeassistant/binary_sensor/campsite/furnace_dsi_fault/config"
|
|
retain: true
|
|
payload: >-
|
|
{"name": "Furnace DSI Fault",
|
|
"unique_id": "campsite_furnace_dsi_fault",
|
|
"state_topic": "campsite/binary_sensor/furnace_dsi_fault/state",
|
|
"payload_on": "ON", "payload_off": "OFF",
|
|
"device_class": "problem",
|
|
"availability_topic": "campsite/onecontrol/availability",
|
|
"device": {"identifiers": ["campsite_onecontrol"]}}
|
|
# Running/heating status binary sensors
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "homeassistant/binary_sensor/campsite/water_heater_heating/config"
|
|
retain: true
|
|
payload: >-
|
|
{"name": "Water Heater Heating",
|
|
"unique_id": "campsite_water_heater_heating",
|
|
"state_topic": "campsite/binary_sensor/water_heater_heating/state",
|
|
"payload_on": "ON", "payload_off": "OFF",
|
|
"device_class": "running",
|
|
"availability_topic": "campsite/onecontrol/availability",
|
|
"device": {"identifiers": ["campsite_onecontrol"]}}
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "homeassistant/binary_sensor/campsite/furnace_running/config"
|
|
retain: true
|
|
payload: >-
|
|
{"name": "Furnace Running",
|
|
"unique_id": "campsite_furnace_running",
|
|
"state_topic": "campsite/binary_sensor/furnace_running/state",
|
|
"payload_on": "ON", "payload_off": "OFF",
|
|
"device_class": "running",
|
|
"availability_topic": "campsite/onecontrol/availability",
|
|
"device": {"identifiers": ["campsite_onecontrol"]}}
|
|
# Awning
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "homeassistant/cover/campsite/awning/config"
|
|
retain: true
|
|
payload: >-
|
|
{"name": "Awning",
|
|
"unique_id": "campsite_awning",
|
|
"state_topic": "campsite/cover/awning/state",
|
|
"command_topic": "campsite/cover/awning/set",
|
|
"optimistic": true,
|
|
"payload_open": null,
|
|
"device_class": "awning",
|
|
"availability_topic": "campsite/onecontrol/availability",
|
|
"device": {"identifiers": ["campsite_onecontrol"]}}
|
|
# Publish current states immediately after discovery
|
|
- delay: "00:00:02"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/switch/water_heater/state"
|
|
retain: true
|
|
payload: "{{ 'ON' if is_state('switch.onecontrol_can_water_heater', 'on') else 'OFF' }}"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/switch/exterior_lights/state"
|
|
retain: true
|
|
payload: "{{ 'ON' if is_state('switch.onecontrol_can_exterior_lights', 'on') else 'OFF' }}"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/switch/interior_lights/state"
|
|
retain: true
|
|
payload: "{{ 'ON' if is_state('switch.onecontrol_can_interior_lights', 'on') else 'OFF' }}"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/sensor/battery_voltage/state"
|
|
retain: true
|
|
payload: "{{ states('sensor.onecontrol_can_battery_voltage') }}"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/sensor/fresh_water_tank/state"
|
|
retain: true
|
|
payload: "{{ states('sensor.onecontrol_can_fresh_water_tank') }}"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/sensor/grey_tank_1/state"
|
|
retain: true
|
|
payload: "{{ states('sensor.onecontrol_can_grey_tank_1') }}"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/sensor/grey_tank_2/state"
|
|
retain: true
|
|
payload: "{{ states('sensor.onecontrol_can_grey_tank_2') }}"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/sensor/black_tank/state"
|
|
retain: true
|
|
payload: "{{ states('sensor.onecontrol_can_black_tank') }}"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/binary_sensor/water_heater_dsi_fault/state"
|
|
retain: true
|
|
payload: "{{ 'ON' if is_state('binary_sensor.onecontrol_can_water_heater_dsi_fault', 'on') else 'OFF' }}"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/binary_sensor/onecontrol_system_fault/state"
|
|
retain: true
|
|
payload: "{{ 'ON' if is_state('binary_sensor.onecontrol_can_onecontrol_system_fault', 'on') else 'OFF' }}"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/binary_sensor/furnace_dsi_fault/state"
|
|
retain: true
|
|
payload: "{{ 'ON' if is_state('binary_sensor.onecontrol_can_furnace_dsi_fault', 'on') else 'OFF' }}"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/binary_sensor/water_heater_heating/state"
|
|
retain: true
|
|
payload: "{{ 'ON' if is_state('binary_sensor.onecontrol_can_water_heater_heating', 'on') else 'OFF' }}"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/binary_sensor/furnace_running/state"
|
|
retain: true
|
|
payload: "{{ 'ON' if is_state('binary_sensor.onecontrol_can_furnace_running', 'on') else 'OFF' }}"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/cover/awning/state"
|
|
retain: true
|
|
payload: "{{ states('cover.onecontrol_can_awning') }}"
|
|
|
|
# --- Availability tracking (CAN node up/down) ---
|
|
- id: campsite_forward_availability
|
|
alias: "MQTT Bridge: Forward Availability"
|
|
triggers:
|
|
- trigger: state
|
|
entity_id: sensor.onecontrol_can_battery_voltage
|
|
to: "unavailable"
|
|
- trigger: state
|
|
entity_id: sensor.onecontrol_can_battery_voltage
|
|
from: "unavailable"
|
|
actions:
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/onecontrol/availability"
|
|
retain: true
|
|
payload: >-
|
|
{{ 'offline' if trigger.to_state.state == 'unavailable' else 'online' }}
|
|
|
|
# --- State change forwarding ---
|
|
- id: campsite_forward_switch_states
|
|
alias: "MQTT Bridge: Forward Switch States"
|
|
triggers:
|
|
- trigger: state
|
|
entity_id:
|
|
- switch.onecontrol_can_water_heater
|
|
- switch.onecontrol_can_exterior_lights
|
|
- switch.onecontrol_can_interior_lights
|
|
conditions:
|
|
- condition: template
|
|
value_template: "{{ trigger.to_state.state in ['on', 'off'] }}"
|
|
actions:
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/switch/{{ trigger.to_state.object_id | replace('onecontrol_can_', '') }}/state"
|
|
retain: true
|
|
payload: "{{ trigger.to_state.state | upper }}"
|
|
|
|
- id: campsite_forward_sensor_states
|
|
alias: "MQTT Bridge: Forward Sensor States"
|
|
triggers:
|
|
- trigger: state
|
|
entity_id:
|
|
- sensor.onecontrol_can_battery_voltage
|
|
- sensor.onecontrol_can_fresh_water_tank
|
|
- sensor.onecontrol_can_grey_tank_1
|
|
- sensor.onecontrol_can_grey_tank_2
|
|
- sensor.onecontrol_can_black_tank
|
|
conditions:
|
|
- condition: template
|
|
value_template: "{{ trigger.to_state.state not in ['unavailable', 'unknown'] }}"
|
|
actions:
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/sensor/{{ trigger.to_state.object_id | replace('onecontrol_can_', '') }}/state"
|
|
retain: true
|
|
payload: "{{ trigger.to_state.state }}"
|
|
|
|
- id: campsite_forward_binary_sensor_states
|
|
alias: "MQTT Bridge: Forward Fault States"
|
|
triggers:
|
|
- trigger: state
|
|
entity_id:
|
|
- binary_sensor.onecontrol_can_water_heater_dsi_fault
|
|
- binary_sensor.onecontrol_can_onecontrol_system_fault
|
|
- binary_sensor.onecontrol_can_furnace_dsi_fault
|
|
- binary_sensor.onecontrol_can_water_heater_heating
|
|
- binary_sensor.onecontrol_can_furnace_running
|
|
conditions:
|
|
- condition: template
|
|
value_template: "{{ trigger.to_state.state in ['on', 'off'] }}"
|
|
actions:
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/binary_sensor/{{ trigger.to_state.object_id | replace('onecontrol_can_', '') }}/state"
|
|
retain: true
|
|
payload: "{{ trigger.to_state.state | upper }}"
|
|
|
|
- id: campsite_forward_cover_states
|
|
alias: "MQTT Bridge: Forward Cover States"
|
|
triggers:
|
|
- trigger: state
|
|
entity_id:
|
|
- cover.onecontrol_can_awning
|
|
conditions:
|
|
- condition: template
|
|
value_template: "{{ trigger.to_state.state not in ['unavailable', 'unknown'] }}"
|
|
actions:
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/cover/{{ trigger.to_state.object_id | replace('onecontrol_can_', '') }}/state"
|
|
retain: true
|
|
payload: "{{ trigger.to_state.state }}"
|
|
|
|
# --- Incoming command handling ---
|
|
# Explicit allowlists — the wildcard subscription must never widen the bridge
|
|
# beyond the 3 switches + awning it intends to expose. A future debug/test
|
|
# entity on the ESPHome node must NOT become remotely controllable for free
|
|
# (the firmware command_guard is the backstop, not the only gate).
|
|
- id: campsite_handle_switch_commands
|
|
alias: "MQTT Bridge: Handle Switch Commands"
|
|
triggers:
|
|
- trigger: mqtt
|
|
topic: "campsite/switch/+/set"
|
|
conditions:
|
|
- condition: template
|
|
value_template: >-
|
|
{{ trigger.topic.split('/')[2] in
|
|
['water_heater', 'exterior_lights', 'interior_lights'] }}
|
|
- condition: template
|
|
value_template: "{{ trigger.payload | lower in ['on', 'off'] }}"
|
|
actions:
|
|
- action: "switch.turn_{{ trigger.payload | lower }}"
|
|
target:
|
|
entity_id: "switch.onecontrol_can_{{ trigger.topic.split('/')[2] }}"
|
|
|
|
- id: campsite_handle_cover_commands
|
|
alias: "MQTT Bridge: Handle Cover Commands"
|
|
triggers:
|
|
- trigger: mqtt
|
|
topic: "campsite/cover/+/set"
|
|
conditions:
|
|
- condition: template
|
|
value_template: "{{ trigger.topic.split('/')[2] == 'awning' }}"
|
|
- condition: template
|
|
value_template: "{{ trigger.payload | lower in ['open', 'close', 'stop'] }}"
|
|
actions:
|
|
- action: "cover.{{ trigger.payload | lower }}_cover"
|
|
target:
|
|
entity_id: "cover.onecontrol_can_{{ trigger.topic.split('/')[2] }}"
|