The Node-RED flows + config behind the automations in colfin22/ha-config. Home Assistant handles the simple, single-trigger stuff; the multi-input, stateful logic lives here β deciding the house's mode, arming the alarm, making heating calls from presence + forecast + tariff, and turning camera detections into one smart notification. Runs in Docker in a Proxmox LXC (also backed up nightly by PBS); this repo adds flow versioning + quick recovery.
Seven flows, each documented below with a screenshot: House Mode Β· House & Shed Alarm Β· Alarm NFC Tags Β· Camera Concierge Β· Heating Control Β· Infra Watchdog Β· Infra Health & Alerts.
docker-compose.ymlβ container definitiondata/flows.jsonβ flows: House Mode, House Alarm, Alarm NFC Tags, Camera Concierge, Infra Watchdog, Infra Health & Alerts, Heating Controldata/settings.jsβ config; editor adminAuth reads its credentials from the gitignored.envdata/package.json+data/package-lock.jsonβ installed palette (reinstalls on first start)git-backup.shβ the daily backup script
data/flows_cred.json(encrypted credentials) +data/.config.runtime.json(their decrypt secret)- On restore, re-add two secrets in the editor: the Home Assistant long-lived token on the
ha-serverconfig node, and the MQTT password on themqtt-frigatebroker node. .env(gitignored) β recreate it next todocker-compose.ymlbefore starting:PVE_API_TOKEN=PVEAPIToken=<user>@pve!<tokenid>=<secret> # read-only audit token for the backup watchdog NABU_URL=https://<your-id>.ui.nabu.casa # remote base URL for notification images NR_ADMIN_USER=<editor username> NR_ADMIN_HASH=<bcrypt hash, with every $ escaped as $$> # editor login (adminAuth reads these from the environment)
The flows/ directory holds an importable JSON per tab, regenerated automatically on
every nightly backup β grab a file and paste it into Node-RED via Menu β Import. Each
file includes the config nodes its flow references (a Home Assistant server node, and the
MQTT broker for the camera flow) β after importing, point those at your own HA/MQTT and add
your credentials; they are exported without secrets. Entity ids, camera names and notify
targets are this house's β expect to search-and-replace.
- PBS-restore or rebuild the LXC (Debian + Docker + compose).
git clone https://github.com/colfin22/node-red-config.git /opt/node-red(or via the repo's SSH deploy-key alias)- Recreate
.env(see above), thencd /opt/node-red && docker compose up -d - Open the editor, re-enter the HA token (
ha-server) + MQTT password (mqtt-frigate), Deploy.
node-red-backup.timer (systemd) runs git-backup.sh daily at 02:30 β regenerates flows/, then commits + pushes if anything changed. The repo is public, so the script refuses to push and raises an alert if anything committed matches a secret pattern (API tokens, bcrypt hashes, JWTs, private keys, remote-access URLs).
The House Mode tab (tab-hm) is the household state orchestrator. It computes and maintains input_select.house_mode β the single source of truth that other flows (alarm, heating and infrastructure alerts) consume so each doesn't have to do its own presence tracking.
- Home β at least one resident is in (or just arrived).
- Away β all residents out, confirmed by a 20-minute quiet period (see below).
- Sleeping β everyone in bed; auto-detected overnight.
- Overlays (independent of the above):
storm_mode(auto from Met Γireann warnings),maintenance_mode(blocks Away, silences all infra alerting while planned maintenance is under way, auto-expires after 4 hours).guest_modeexists as a helper but is intentionally ignored by the engine β it is consumed directly by other automations.
All residents out β 20-minute timer β Away. At the fire point the engine checks the indoor sensors (sitting-room mmWave, office, landing, hallway) for motion in the last 15 minutes β if anyone is still moving around, it holds off and retries every 20 minutes. maintenance_mode also blocks the transition. Anyone arriving during the countdown cancels it immediately.
Dead/low-phone safeguards live here: if a resident's phone is off Wi-Fi and its battery sensor is unavailable (genuinely dead/off), the engine treats that person as unconfirmed-away and won't set Away β it pushes a notification instead. A low-but-alive phone still reports location and is trusted. A nudge fires when a phone drops below 15 % so presence keeps working. These alerts go to Colm and Olivia only.
Requires: mode is Home, a resident is home, maintenance_mode off, and no activity for 20 minutes β where activity means any of the 25 tracked lights, either TV, any dimmer press, or any indoor sensor. A 2-hour re-entry block prevents flipping back to Sleeping straight after waking. Nothing wakes before 06:00; first activity from 06:00 returns to Home.
Residents (Colm, Olivia) enable Sleeping and trigger Away. Guests (Daire) only contribute to anyoneHome() β they prevent Away when physically present but don't enable Sleeping. Cian (no phone, 12) is excluded from both; indoor sensors are his safety net.
- All Away evaluations use a fresh live HA snapshot via
ha-get-entitiesβ no stale flow context. server-state-changednodes always useoutputInitially: false; startup state is seeded via an inject βha-get-entitiesβ function chain (global context is not populated reliably at 1 s after start).- On every state change: sets
input_select.house_modeviahm-set-mode, logs to/data/house_mode.logwith a local timestamp + reason, pushes a notification, and publishes the same reason toinput_text.house_mode_reason(hm-reason). - Why the reason is published, not just notified: the engine always knew why it changed mode β
morning activity (light.sitting_room),resident returned,all residents left (20m),quiet 20m, no activityβ but that string only ever reached a push notification. Other flows (the heating controller, below) can now name the actual cause instead of guessing at it. A manual mode change publisheschanged by hand, so a consumer can't attribute it to whatever the engine last decided.
Two tabs automate the Alarmo alarm β alarm_control_panel.house and .shed (two independent panels); none needs a code. The house alarm is driven by House Mode state; the shed is NFC-driven. All notifications and voice announcements are state-driven, so they fire however the alarm changes β phone, Alarmo card, NFC, or automatic.
Node-RED signs in to Home Assistant as its own dedicated "Node-RED" user, so Alarmo's activity log attributes automatic arm/disarm to Node-RED instead of a person. Manual arm/disarm still shows whoever did it.
The alarm follows input_select.house_mode directly:
- Away β
alarm_arm_away(house) - Sleeping β
alarm_arm_night(house) β silent, no announcement; people are in bed - Home β
alarm_disarm(house). When returning from Away the spoken welcome is not fired on arrival β it is held until the front door opens (then a short delay), so you are inside to hear it, then a single merged line names whoever is back: "Welcome home, [name]. The house has been disarmed." on the home audio group. It stays silent if the door never opens within a few minutes, and says nothing when Home comes from Sleeping. (Someone whose phone never leaves the house can't flip AwayβHome, so is never named.)
All presence tracking, timing, dead-phone safeguards, and battery nudges live in the House Mode tab β the alarm tab just reacts to the resulting state. On Node-RED restart, a startup inject reads the current house_mode and guest_mode via ha-get-entities and syncs both into flow context before the first evaluation.
Two robustness details: a 60-second reconcile compares the live house_mode against the last mode the flow processed and re-applies any change missed during a websocket drop (a manual disarm is respected β it only reacts to missed mode changes, never to alarm state). And every arm/disarm call is idempotent β if the panel is already in the target state the call is skipped, so nothing spams the Alarmo log with "cannot go to state X from X".
When input_boolean.guest_mode is on, only Away arming is suspended (guests moving around would trip an away-armed alarm):
- House mode going Away β alarm stays as-is (arm away skipped silently)
- House mode going Sleeping β still arms night β guest mode does NOT block night arming (changed 02-07-2026), so the perimeter stays armed overnight with guests in
- Home always disarms regardless of guest mode
When guest mode is turned off, the flow immediately re-evaluates the current house mode and arms accordingly β if the house is already Away it arms away, if Sleeping it arms night, if Home it does nothing.
Derived from the state of .house + .shed (Alarmo's own events are internal, not on the HA bus). Five events β push to all people + a spoken announcement on the home audio group: armed Β· disarmed Β· triggered Β· no-longer-triggered Β· failed-to-arm. Trigger/failure messages carry the cause (open sensors); a failure is recognised both as an instant refusal and as an exit-delay arm that aborts back to disarmed with sensors open (a cancelled exit delay with nothing open stays a plain disarm). A visitor is only pushed while at home. On a return from Away the spoken disarmed line is suppressed in favour of the door-gated welcome above (the phone push still fires); the shed's disarm announcement is unaffected.
- House Mode β Home β automatic on resident arrival (handled by House Mode tab).
- Front-door NFC tag β instant and deterministic.
- Manual β the Alarmo app or panel.
- Touchpad (planned) β a physical keypad for the one case software can't cover: a fully-dead phone on arrival.
- Listens for the HA
tag_scannedevent. - Front-door tag β disarm the house (no-op if already disarmed).
- Back-door tag β disarm the shed for up to 2 hours. It re-arms when either the shed door has been closed for 15 minutes (after being opened) or the 2-hour cap is reached with the door closed (watching
binary_sensor.shed_door_contact). - Shed left open at the 2-hour cap β it does not arm; instead it notifies everyone + announces that the shed has been left open and unarmed, then waits to re-arm when the door is finally closed for 15 minutes.
- Nightly 22:00 auto-arm β arms the shed only if it is currently disarmed and the door is closed β a catch-all for a shed left disarmed during the day.
Two separate HA automations β one on alarm_control_panel.house, one on .shed β trigger on each panel's triggered state and run script.strobe_lights on light.downstairs until that panel is disarmed. A siren is planned.
server-state-changedtriggers use an explicit entity list β thesubstring/regexfilter throwsa?.some is not a functionon this palette version.- All
api-call-servicenodes use theactionproperty β the old separatedomain/servicefields are deprecated in v1.0 of the palette. Static calls: setaction: 'domain.service'. Dynamic calls (action from message): setaction: '{{payload.action}}'(mustache). Do not useactionType/dataTypeβ the palette ignores them; the only wayisDynamicValue()returns true is mustache or a Node-RED env var. - Notify dispatch uses the
actionform ({action:'notify.x', data:β¦}); thepeople confignode publishes toglobalcontext so both alarm tabs share one source of truth. - TTS for alarm state changes is centralised in the notification engine (one announce per event, any source); the NFC shed-open alert announces from the NFC tab as it is not an alarm state change.
Runs off House Mode + time of day, driving the local HomeKit thermostat (climate.netatmo_smart_thermostat). The Netatmo holds a flat eco 19Β°C baseline; this flow only ever raises above it and re-asserts the target every 30 minutes so a manual override never lapses back to the baseline.
eco 19 Β· night 19.5 Β· comfort 20 Β· hot 20.5 Β· frost 12
- Home β comfort 20; hot 20.5 between 19:00β22:00
- Sleeping β night 19.5 overnight; comfort 20 from 07:00
- Away β eco 19; drops to frost 12 after 24 h empty (gated by the
Away 24h+dashboard toggle,input_boolean.heating_extended_away) - The same 24 h-empty state also stops the hot-water solar diverter (no point heating water for an empty house); it goes back to normal when anyone returns, when a pre-warm boost is started, or when someone is heading home β and the flow only ever writes on those transitions, so a manually-stopped diverter is left alone
Nightly at 21:30 it reads the Met Γireann hourly forecast for tomorrow's 05:00β07:00 low and starts the 07:00 warm-up earlier β the colder it is, the earlier: 4β8Β°C β 15 min, 0β4Β°C β 30 min, β3β0Β°C β 45 min, below β3Β°C β 60 min. A phone push to Colm + Olivia the night before, only when the low is sub-zero.
When the house is empty and someone is driving home (within 10 km and getting closer, via the Proximity integration), it warms toward comfort so it's ready on arrival. The pre-heat latches once triggered β a GPS wobble flipping "towards" to "away from" for a moment can't bounce the setpoint mid-approach; it releases only when they arrive (house leaves Away) or genuinely leave the area again (beyond 12 km).
Boost from the dashboard (pick a temperature, tap Boost) or by nudging the thermostat above the scheduled target β either way it holds for 2 hours before the schedule resumes, and re-boosting restarts the clock. Turning the thermostat down to or below the schedule (or tapping Cancel on the dashboard) cancels the boost β a turn-down is never treated as a "boost" (this also absorbs the Netatmo app's boost-delete, which reverts the device to its 19 baseline). A boost still cancels the moment everyone leaves β but you can start one from the dashboard while the house is Away to pre-warm it before arriving home; it holds the usual 2 hours, so if nobody makes it back it lapses to the Away setback.
The controller writes a plain-English status to input_text.heating_status β used both by the heating card and by a Recent activity logbook card β and it names what triggered the change, not just what the heating is doing:
| Status line | What happened |
|---|---|
Home β comfort 20Β° Β· house woke up (sitting room light) |
someone turned a light on in the morning; the house switched out of Sleeping |
Evening warm-up 20.5Β° Β· evening schedule (19:00) |
the clock, not you |
Sleeping β overnight 19.5Β° Β· quiet for 20 min β bed |
the house settled |
Away β setback 19Β° Β· everyone left |
the last person left |
Boost 22Β° until 15:30 Β· you asked |
dashboard or thermostat dial |
Morning warm-up 20Β° Β· cold morning β started early |
forecast pre-heat |
Home β comfort 20Β° Β· heading home |
proximity pre-heat |
The cause is taken from input_text.house_mode_reason when a mode change drove it (entity ids resolved to friendly names), from the boost/pre-heat state when one of those did, and otherwise from the clock.
While input_boolean.guest_mode is on the heating never drops to the Away setback (visitors stay warm); the normal overnight and morning behaviour still applies.
- Tab
Heating Control; controllerheat-fn, fed byheat-getβ anha-get-entitiesversion 3 node. A version-1 node returns an empty list, which silently broke this flow (it fell back to "Home" and never saw Away) until fixed 02-07-2026. When adding a get-entities node, clone a v3 one. - Triggers:
input_select.house_modechange + a 60 s heartbeat; output de-duped with a 30-min re-assert. - Forecast sub-flow:
heat-fc-cron(21:30) βheat-fc-get(weather.get_forecasts, hourly,weather.forecast_home; response viaoutputPropertiesvalueTyperesults) βheat-fc-fnβ notify (sub-zero only). The pre-heat decision lives inflow.preHeat(in-memory β lost on a restart between 21:30 and morning, fails safe to the normal 07:00). - Boost detection is poll-lag-proof: a manual setpoint is only treated as a boost once the flow's own last write has been confirmed by the thermostat.
- The status line is capped at 100 characters β that is the
input_textlimit, and Home Assistant rejects an over-long value, which would silently stop the ticker updating.
The Camera Concierge (tab Camera Concierge) is the sole handler of Frigate camera notifications. It turns Frigate detections into smart, consolidated phone alerts. It replaced six separate HA automations and the old package concierge.
- Primary: MQTT topic
frigate/reviews(broker10.0.0.229). Frigate only publishes a review atseverity: alertwhen an object enters an alert zone β so masks/zones (e.g. the driveway car-mask) are honoured upstream, and the concierge only acts on real, zone-qualified events. - Secondary branches: HA websocket for the courier count sensors + the postman sensor, and the patio-door switch state.
For each frigate/reviews message the flow decides three things:
- Relevant? Only
severity: alert. Rear cameras are dropped entirely while the patio-door NFC switch is on (input_boolean.patio_door_open). - Kind (first match, in order): Person β Vehicle (car/truck) β Motorcycle β Bicycle β Package β Umbrella β the six Frigate alert labels. Each kind is tracked separately.
- Zone: Front (
Front_Car+Front_Van), Doorbell (on its own), or Rear (Rear_Door+Rear_Shed).
Each unique (zone + kind) opens an "incident" (120-second window). So a person out front and at the doorbell = two notifications ("Person β Front", "Person β Doorbell"); a person and a vehicle out front = two more.
All stages share the same notification tag, so they update in place rather than stacking:
- Immediate text β fires the instant the review starts, no image, so the alert lands without waiting on a download (e.g. "π· Person β Front").
- Snapshot β a still frame attached as soon as Frigate has it (fast).
- GIF β the animated preview replaces the snapshot when the review ends.
If more cameras in the same zone+kind see it, the notification updates its camera list instead of firing new ones.
Within a zone the snapshot, GIF and tap-link all come from the camera that detected first β the one that opened the 120-second incident (shed-first β shed's view, door-first β door's view). The notification text still lists every camera that saw it.
The notification's tap action (clickAction/url) opens the event clip (clip.mp4) of that first-detecting camera β straight to the footage.
- Phone push (text β snapshot β GIF) β both phones: Colm (
mobile_app_np3) + Olivia (mobile_app_pixel_6a). - Doorbell + person also casts a snapshot to the kitchen display (20 s) and an overlay on the Shield TV.
- Couriers (DPD/GLS/Amazon/UPS/FedEx/DHL/An Post on the front cameras) β spoken "<courier> has landed" on the home audio group β suppressed when
house_mode = Sleeping. - Postman at the doorbell β spoken "the postman has been detected" β suppressed when
house_mode = Sleeping. - The two TTS branches share a 2-minute cooldown so one An Post delivery never announces twice.
- Phone pushes are unaffected by Sleeping β that suppression is speaker-only. When
house_mode = Away, camera pushes are escalated: delivered immediately at high priority on a dedicated high-importance βCameras Awayβ channel with aβ οΈ title, so a person at the house while everyone is out cuts through.
When house_mode = Sleeping, a person detected in the front car or van focus zone triggers a deterrent: sitting-room and hallway lights strobe and a warning is announced on the bedroom and sitting-room speakers. A 120-second cooldown prevents repeat triggers from the same event. Tied to Sleeping mode rather than a fixed clock window so it responds to when the household actually goes to bed.
- Rear silenced while the patio-door switch is on.
- Courier/postman TTS silenced when
house_mode = Sleeping(push still goes to phones). - Per-incident grouping (multiple cameras = one notification).
- 120 s incident window + courier/postman cooldowns to avoid repeats.
- Every action is logged to
data/camera_concierge.log(tagspush-grp/push-grp-snap/push-grp-gif:<cam>, etc.). - Whole flow version-controlled in this repo (
colfin22/node-red-config, nightly git backup); the LXC is also in the PBS nightly backup.
- Real Frigate camera names are title-case:
Doorbell,Front_Van,Front_Car,Rear_Door,Rear_Shed. - Notify
datamust be built with explicit JSONata ({"title":β¦, "message":β¦, "data":β¦}) β otherwise the nesteddata(image/clickAction) gets flattened. house_modeis mirrored into flow context (flow.house_mode) so the courier/postman TTS suppression and the person-at-the-car deterrent can read it cheaply. Aserver-state-changedwatcher updates it on every change (withfor:0, forType:numβ a missing/emptyforthrowsConfigError: Invalid config value for 'for'on every change), and a startup inject βha-get-entitiesβ function seeds the current mode on restart. The watcher usesoutputInitially:false, so the seed is what keeps Sleeping-based behaviour correct after a Node-RED restart mid-Sleeping/Away β the seed'sha-get-entitiesmust output tomsg.payload(outputLocationType:msg, notnone) or it silently falls back to a default.- Context keys derived from review ids are sanitised (dots β
_) because Frigate review ids contain a dot (which Node-RED would otherwise treat as a nested-context path).
Uptime Kuma posts every monitor up/down event to a webhook in this tab (/uk-event). The watchdog turns those raw events into escalating alerts rather than one-ping-per-flap:
- Tier 1 β phone push on first confirmed down.
- Tier 2 β still down after the escalation window β repeat push + a spoken announcement (voice only while someone is home).
- Tier 3 β long outages re-alert on a slow repeat so a dead service can't be silently forgotten.
- Recovery sends an "up again" push and resets the monitor's state machine.
- Quiet hours (22:00β07:00) hold non-urgent noise; anything still outstanding is delivered in an 07:00 overnight summary.
- While
maintenance_modeis on, notifications are suppressed but the state machines keep running β anything still down when maintenance ends re-alerts on its next repeat. When maintenance switches off, the watchdog also re-polls the uptime monitor's status page and injects a synthetic up-event for every monitor currently up β this clears any down-state whose recovery happened during the window (the monitor's own maintenance window pauses its webhooks, so those recoveries would otherwise be missed and cause false "still down" alerts).
Uptime Kuma stays the source of truth for reachability; the flow below handles health.
One tab consolidates what used to be eleven separate HA infrastructure-alert automations. Every alert is titled [Category] Subsystem: detail (categories: Health / Backup / Monitoring / Service) and lands on a phone with a matching Android notification channel + group, plus a line in a log file.
Inputs:
- Sensor-driven rules β a
server-state-changedwatcher over ~40 entities feeds a rule engine (disk usage, pool health, service states, rsync failuresβ¦) with per-rule thresholds, sustain times and de-duplication. - Webhooks β backup scripts (TrueNAS config, MikroTik config, restic) and Zabbix post their outcomes straight to HTTP-in endpoints here.
- Backup watchdog β each morning it queries the Proxmox API on both nodes for last night's vzdump task list and alerts only on failures (the read-only API token comes from the gitignored
.env). - Data-freshness checks β e.g. an hourly check that the ESB Networks smart-meter add-on has polled recently; a stale poll timestamp means the add-on is wedged even though its sensors still show plausible values.
Delivery: shared quiet-hours gate β alerts between 22:00 and 07:00 are held and flushed at 07:00 with their original trigger time in the title; maintenance_mode drops alerts entirely (logged, not pushed) while planned work is under way.
Built by Colm Finn β MIT licensed.



