feat(doorman): unlock dispatch — arm switch + rest_command to doorctl [#345]

Founder-approved: valid badge + armed input_boolean -> rest_command
fires the pfvsvrpi doorctl endpoint (Tailscale-bound, token+source
allowlisted, self-securing relay). Disarm = boolean off = log-only.
URL secret on-box only (doorman_unlock_url).

https://projects.knownelement.com/issues/345
This commit is contained in:
2026-09-03 07:08:06 -05:00
parent b7f376b6b3
commit 66a5c45b7e
+43 -9
View File
@@ -8,10 +8,11 @@
# mechanics, NO submodule (git_pull add-on lacks submodule support), # mechanics, NO submodule (git_pull add-on lacks submodule support),
# NO local copy: access-roster remains the single source of truth. # NO local copy: access-roster remains the single source of truth.
# #
# UNLOCK IS DELIBERATELY NOT WIRED. Known badges are logged and # UNLOCK PATH (founder-approved 2026-09-03): valid badge + the
# notified; nothing actuates. The HA->Pi relay control path is an open # doorman_unlock_enabled input_boolean ON -> rest_command fires the
# design decision (#345 sketch, GLPI #741). Fail-safe: this automation # doorctl endpoint on pfvsvrpi (Tailscale-bound, token+source
# can never open the door. # allowlisted, self-securing relay — KNEL/doorman README). Disarm = the
# boolean OFF; unknown/disabled badges never reach this path.
# #
# Change control (founder ruling 2026-09-02/03): this file deploys via # Change control (founder ruling 2026-09-02/03): this file deploys via
# the pfv-bms master pipeline (webhook fast path + sha-watch safety # the pfv-bms master pipeline (webhook fast path + sha-watch safety
@@ -72,6 +73,23 @@ input_datetime:
has_date: true has_date: true
has_time: true has_time: true
input_boolean:
# Arming switch (founder-level): ON = a valid badge DISPATCHES the
# unlock to the pfvsvrpi relay. OFF = valid badges log + notify but
# the door stays shut.
doorman_unlock_enabled:
name: Doorman unlock armed
icon: mdi:lock-open-variant
rest_command:
# Fires the doorctl endpoint on pfvsvrpi (systemd socket-activated,
# Tailscale-bound, token+source allowlisted; self-securing relay).
# URL contains the token -> secret. KNEL/doorman README documents it.
doorman_unlock:
url: !secret doorman_unlock_url
method: get
timeout: 20
automation: automation:
- id: doorman_badge_scan - id: doorman_badge_scan
alias: Doorman - badge scan ingest + decision alias: Doorman - badge scan ingest + decision
@@ -137,8 +155,8 @@ automation:
checked: roster sensors are not populated. Logged checked: roster sensors are not populated. Logged
only; whitelist NOT evaluated. Check gitea only; whitelist NOT evaluated. Check gitea
connectivity / KNEL/access-roster. connectivity / KNEL/access-roster.
# Known + enabled badge: log + quiet notification. Unlock not # Known + enabled badge: log, then dispatch unlock IF armed
# wired by design (see header). # (founder-level input_boolean). Disarmed = log + notice only.
- conditions: "{{ is_known and is_enabled }}" - conditions: "{{ is_known and is_enabled }}"
sequence: sequence:
- action: logbook.log - action: logbook.log
@@ -146,11 +164,27 @@ automation:
name: Doorman name: Doorman
message: >- message: >-
Badge {{ badge_id }} ({{ holder }}) accepted at Badge {{ badge_id }} ({{ holder }}) accepted at
{{ reader }} — logged, unlock not wired (#741). {{ reader }}.
- if:
- condition: state
entity_id: input_boolean.doorman_unlock_enabled
state: 'on'
then:
- action: rest_command.doorman_unlock
continue_on_error: true
- action: logbook.log
data:
name: Doorman
message: "UNLOCK dispatched to the {{ reader }} relay (armed)."
- action: notify.persistent_notification - action: notify.persistent_notification
data: data:
title: Doorman — badge accepted title: Doorman — badge accepted, UNLOCK dispatched
message: "{{ holder }} scanned at {{ reader }} ({{ scanned }}). Logged. Unlock not wired (#741)." message: "{{ holder }} scanned at {{ reader }} ({{ scanned }}). Door released for the hold window."
else:
- action: persistent_notification.create
data:
title: Doorman — badge accepted, unlock NOT armed
message: "{{ holder }} scanned at {{ reader }} ({{ scanned }}). Unlock is disarmed — door stayed shut."
# In roster but disabled: flag it loudly (stolen/suspended # In roster but disabled: flag it loudly (stolen/suspended
# badge use is exactly what we want to know about). # badge use is exactly what we want to know about).
- conditions: "{{ is_known and not is_enabled }}" - conditions: "{{ is_known and not is_enabled }}"