# 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] }}"