canbus: presence layer + departure failsafes
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>
This commit is contained in:
@@ -154,6 +154,40 @@ The DSI fault is already decoded and wired in (see below) — no capture needed.
|
|||||||
- One transceiver = one bus-end terminator. Never add a terminated node in the
|
- One transceiver = one bus-end terminator. Never add a terminated node in the
|
||||||
middle of the bus (would make three terminators).
|
middle of the bus (would make three terminators).
|
||||||
|
|
||||||
|
## Presence & departure failsafes (2026-06-12)
|
||||||
|
|
||||||
|
Occupancy truth = the **water heater switch** (always on while camping, off on
|
||||||
|
departure). Architecture: GPS persons/zones live on home HA; the Pi computes
|
||||||
|
occupancy + (pending) WiFi presence and bridges them home; failsafes run on
|
||||||
|
home HA against the bridged controls; all alerts go through the Pi's Octavia
|
||||||
|
webhook via the MQTT relay topic `campsite/octavia/say`.
|
||||||
|
|
||||||
|
- `canbus/ha/campsite_presence.yaml` (Pi package): `binary_sensor.rv_occupied`
|
||||||
|
(heater on), `binary_sensor.phones_on_campsite_wifi` (unavailable until the
|
||||||
|
MikroTik step below), MQTT bridging for both, a local notify-only dead-man
|
||||||
|
(heater on + no phones on WiFi 3 h → Octavia), and **disabled pre-staged lot-
|
||||||
|
light automations** for the future shed Shelly (`switch.lot_lights`
|
||||||
|
placeholder — fix the entity id, remove `initial_state: false`, and reload
|
||||||
|
when the relay is installed; on-at-sunset is occupancy-gated, off-at-sunrise
|
||||||
|
unconditional).
|
||||||
|
- Home HA (UI-managed, via API): `automation.campsite_departure_failsafe_quick`
|
||||||
|
(both out of zone 25 min + RV occupied → gazebo fans/lights + interior lights
|
||||||
|
off, Octavia + push, only pings if something was actually on) and
|
||||||
|
`automation.campsite_water_heater_failsafe_2h` (2 h + heater on → heater off
|
||||||
|
with 60 s bridge round-trip confirmation; distinct failure message if
|
||||||
|
unconfirmed). Both fired and verified live 2026-06-12 (fans physically cycled,
|
||||||
|
states restored after).
|
||||||
|
- `zone.our_campsite` radius enlarged 50→150 m (GPS wobble safety).
|
||||||
|
- **PENDING — MikroTik WiFi presence:** needs Chateau admin once: enable
|
||||||
|
`/ip service api address=192.168.69.10`, add read-only `homeassistant` user,
|
||||||
|
then add the `mikrotik` integration on the Pi (hub 192.168.69.1) and rename
|
||||||
|
the phone trackers to `device_tracker.wes_phone_wifi` /
|
||||||
|
`device_tracker.lindsey_iphone_wifi`. Everything downstream lights up by
|
||||||
|
itself (sensor availability flips on).
|
||||||
|
- **Phone GPS checklist (manual, Wes's phone):** companion app background
|
||||||
|
location unrestricted, high-accuracy on, battery optimization off — his
|
||||||
|
tracker was >1 h stale at survey time; Lindsey's iPhone reports fine.
|
||||||
|
|
||||||
## DSI fault — DECODED (2026-06-12)
|
## DSI fault — DECODED (2026-06-12)
|
||||||
|
|
||||||
Forced a real lockout and captured it (`captures/dsi-fault-*.log`). Already wired
|
Forced a real lockout and captured it (`captures/dsi-fault-*.log`). Already wired
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
# 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
|
||||||
@@ -28,6 +28,25 @@ rest_command:
|
|||||||
"allowed_mentions": {"users": ["321798967669030912"]}}
|
"allowed_mentions": {"users": ["321798967669030912"]}}
|
||||||
|
|
||||||
automation:
|
automation:
|
||||||
|
# --- Octavia relay: anything on the broker can speak through the one
|
||||||
|
# verified webhook path. Home HA's failsafe automations publish here
|
||||||
|
# (campsite/octavia/say, payload = the message) since home has no
|
||||||
|
# file/packages access for its own rest_command. ---
|
||||||
|
- id: campsite_octavia_relay
|
||||||
|
alias: "Octavia: MQTT relay"
|
||||||
|
triggers:
|
||||||
|
- trigger: mqtt
|
||||||
|
topic: "campsite/octavia/say"
|
||||||
|
conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ trigger.payload | length > 0 and trigger.payload | length < 1900 }}"
|
||||||
|
actions:
|
||||||
|
- action: rest_command.discord_server_alerts
|
||||||
|
data:
|
||||||
|
message: "{{ trigger.payload }}"
|
||||||
|
mode: queued
|
||||||
|
max: 5
|
||||||
|
|
||||||
- id: camper_dsi_fault_discord
|
- id: camper_dsi_fault_discord
|
||||||
alias: "Camper: DSI fault → Discord"
|
alias: "Camper: DSI fault → Discord"
|
||||||
description: >-
|
description: >-
|
||||||
|
|||||||
Reference in New Issue
Block a user