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>
202 lines
7.7 KiB
YAML
202 lines
7.7 KiB
YAML
# Campsite presence / occupancy package.
|
|
#
|
|
# Occupancy truth: the gas water heater — always on while camping, always
|
|
# turned off when leaving (propane). WiFi presence (MikroTik Chateau
|
|
# device_trackers) is the second, internet-independent signal; until that
|
|
# integration is set up the wifi sensor reads `unavailable` and everything
|
|
# downstream treats it as "no veto" (failsafes key on GPS, dead-man stays
|
|
# quiet).
|
|
#
|
|
# Deploy to Pi: /config/packages/campsite_presence.yaml
|
|
# Repo copy: canbus/ha/campsite_presence.yaml
|
|
#
|
|
# ⚠️ FIRST DEPLOY: `template:` may be a new domain on this instance — full
|
|
# core restart, then confirm the sensors exist (see dsi_fault_alert.yaml for
|
|
# the reload_all trap).
|
|
|
|
template:
|
|
- binary_sensor:
|
|
- name: "RV Occupied"
|
|
unique_id: campsite_rv_occupied
|
|
device_class: occupancy
|
|
icon: mdi:campfire
|
|
# unavailable/unknown heater (node down) -> sensor unavailable, not a
|
|
# false "left": availability gates it.
|
|
availability: >-
|
|
{{ states('switch.onecontrol_can_water_heater')
|
|
not in ['unavailable', 'unknown'] }}
|
|
state: "{{ is_state('switch.onecontrol_can_water_heater', 'on') }}"
|
|
|
|
- name: "Phones On Campsite WiFi"
|
|
unique_id: campsite_phones_on_wifi
|
|
device_class: presence
|
|
icon: mdi:wifi-marker
|
|
# MikroTik trackers (renamed to these ids after the integration is
|
|
# added). While they don't exist the sensor is unavailable — that is
|
|
# load-bearing: the dead-man automation below triggers on a sustained
|
|
# 'off', which unavailable never satisfies.
|
|
availability: >-
|
|
{{ states('device_tracker.wes_phone_wifi')
|
|
not in ['unavailable', 'unknown']
|
|
or states('device_tracker.lindsey_iphone_wifi')
|
|
not in ['unavailable', 'unknown'] }}
|
|
state: >-
|
|
{{ is_state('device_tracker.wes_phone_wifi', 'home')
|
|
or is_state('device_tracker.lindsey_iphone_wifi', 'home') }}
|
|
|
|
automation:
|
|
# --- Bridge both presence sensors home (discovery + state) -------------
|
|
- id: campsite_presence_mqtt_discovery
|
|
alias: "Presence: Publish MQTT Discovery"
|
|
triggers:
|
|
- trigger: homeassistant
|
|
event: start
|
|
- trigger: event
|
|
event_type: automation_reloaded
|
|
- trigger: time_pattern
|
|
hours: "/6"
|
|
actions:
|
|
- delay: "00:00:07"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "homeassistant/binary_sensor/campsite/rv_occupied/config"
|
|
retain: true
|
|
payload: >-
|
|
{"name": "RV Occupied",
|
|
"unique_id": "campsite_rv_occupied",
|
|
"state_topic": "campsite/binary_sensor/rv_occupied/state",
|
|
"payload_on": "ON", "payload_off": "OFF",
|
|
"device_class": "occupancy",
|
|
"availability_topic": "campsite/onecontrol/availability",
|
|
"device": {"identifiers": ["campsite_onecontrol"]}}
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "homeassistant/binary_sensor/campsite/phones_on_campsite_wifi/config"
|
|
retain: true
|
|
payload: >-
|
|
{"name": "Phones On Campsite WiFi",
|
|
"unique_id": "campsite_phones_on_wifi",
|
|
"state_topic": "campsite/binary_sensor/phones_on_campsite_wifi/state",
|
|
"payload_on": "ON", "payload_off": "OFF",
|
|
"payload_available": "online", "payload_not_available": "offline",
|
|
"availability_topic": "campsite/presence/wifi_availability",
|
|
"device_class": "presence",
|
|
"device": {"identifiers": ["campsite_onecontrol"]}}
|
|
- delay: "00:00:02"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/binary_sensor/rv_occupied/state"
|
|
retain: true
|
|
payload: "{{ 'ON' if is_state('binary_sensor.rv_occupied', 'on') else 'OFF' }}"
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/presence/wifi_availability"
|
|
retain: true
|
|
payload: >-
|
|
{{ 'offline' if states('binary_sensor.phones_on_campsite_wifi')
|
|
in ['unavailable', 'unknown'] else 'online' }}
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/binary_sensor/phones_on_campsite_wifi/state"
|
|
retain: true
|
|
payload: "{{ 'ON' if is_state('binary_sensor.phones_on_campsite_wifi', 'on') else 'OFF' }}"
|
|
|
|
- id: campsite_presence_forward_states
|
|
alias: "Presence: Forward States"
|
|
triggers:
|
|
- trigger: state
|
|
entity_id:
|
|
- binary_sensor.rv_occupied
|
|
- binary_sensor.phones_on_campsite_wifi
|
|
actions:
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/binary_sensor/{{ trigger.to_state.object_id }}/state"
|
|
retain: true
|
|
payload: "{{ 'ON' if trigger.to_state.state == 'on' else 'OFF' }}"
|
|
- if:
|
|
- condition: template
|
|
value_template: "{{ trigger.to_state.object_id == 'phones_on_campsite_wifi' }}"
|
|
then:
|
|
- action: mqtt.publish
|
|
data:
|
|
topic: "campsite/presence/wifi_availability"
|
|
retain: true
|
|
payload: >-
|
|
{{ 'offline' if trigger.to_state.state
|
|
in ['unavailable', 'unknown'] else 'online' }}
|
|
|
|
# --- Local dead-man notify (works with the WG tunnel down) -------------
|
|
# Heater on but neither phone seen on campsite WiFi for 3 h. Notify-only:
|
|
# WiFi absence alone is not proof of departure (pool afternoon, dead
|
|
# battery), so the auto-off lives home-side where GPS can confirm.
|
|
- id: campsite_deadman_heater_notify
|
|
alias: "Presence: Dead-man heater notify (local)"
|
|
triggers:
|
|
- trigger: state
|
|
entity_id: binary_sensor.phones_on_campsite_wifi
|
|
to: "off"
|
|
for: "03:00:00"
|
|
conditions:
|
|
- condition: state
|
|
entity_id: binary_sensor.rv_occupied
|
|
state: "on"
|
|
actions:
|
|
- action: rest_command.discord_server_alerts
|
|
data:
|
|
message: >-
|
|
<@321798967669030912> 🤨 The water heater has been on for 3+ hours
|
|
with neither of your phones on the campsite WiFi. If you actually
|
|
left, you forgot the shutdown ritual again — kill the heater from
|
|
the dashboard. If you're at the pool, carry on.
|
|
mode: single
|
|
|
|
# --- Lot lights (Shelly) — PRE-STAGED, DISABLED until the relay exists --
|
|
# Enable + fix the entity id once the Shelly is installed in the shed
|
|
# outlet box. Mirrors the camper exterior-lights sunset/sunrise pattern
|
|
# (retry because nothing should be trusted fire-and-forget), gated on
|
|
# occupancy so the lot doesn't light up for an empty site.
|
|
- id: campsite_lot_lights_sunset
|
|
alias: "Lot lights on at sunset (occupied only)"
|
|
initial_state: false
|
|
triggers:
|
|
- trigger: sun
|
|
event: sunset
|
|
offset: 0
|
|
conditions:
|
|
- condition: state
|
|
entity_id: binary_sensor.rv_occupied
|
|
state: "on"
|
|
actions:
|
|
- repeat:
|
|
sequence:
|
|
- action: switch.turn_on
|
|
target:
|
|
entity_id: switch.lot_lights
|
|
- delay: "00:00:05"
|
|
until:
|
|
- condition: template
|
|
value_template: >-
|
|
{{ is_state('switch.lot_lights', 'on') or repeat.index >= 3 }}
|
|
mode: single
|
|
|
|
- id: campsite_lot_lights_sunrise
|
|
alias: "Lot lights off at sunrise"
|
|
initial_state: false
|
|
triggers:
|
|
- trigger: sun
|
|
event: sunrise
|
|
offset: 0
|
|
actions:
|
|
- repeat:
|
|
sequence:
|
|
- action: switch.turn_off
|
|
target:
|
|
entity_id: switch.lot_lights
|
|
- delay: "00:00:05"
|
|
until:
|
|
- condition: template
|
|
value_template: >-
|
|
{{ is_state('switch.lot_lights', 'off') or repeat.index >= 3 }}
|
|
mode: single
|