Compare commits

..
70 Commits
Author SHA1 Message Date
zorruno d1fea67cfe various tidyups 2026-09-13 15:32:16 +12:00
ESPHome Device Builder 3ead85ef29 Edit esp-officeduallights.yaml 2026-09-13 11:19:34 +12:00
ESPHome Device Builder 2c22e6c07f Edit esp-officeduallights.yaml 2026-09-13 11:18:37 +12:00
ESPHome Device Builder 25e73750dc Edit esp-officelights.yaml 2026-09-13 11:18:12 +12:00
ESPHome Device Builder e205b0db67 Edit esp-bedside-panel.yaml 2026-09-04 17:09:47 +12:00
zorruno d81deae3be various tidyups 2026-08-30 15:35:24 +12:00
zorruno bce8abe48c various tidyups 2026-08-30 00:25:42 +12:00
ESPHome Device Builder fd7aab565e Edit esp-bedside-panel.yaml 2026-08-16 23:31:12 +12:00
ESPHome Device Builder 47c551694f Edit esp-officeduallights.yaml 2026-08-16 17:24:31 +12:00
ESPHome Device Builder a37e414299 Edit esp-officelights.yaml 2026-08-16 17:24:31 +12:00
ESPHome Device Builder 6f6054ad7b Edit esp-officeduallights.yaml 2026-08-16 17:19:31 +12:00
ESPHome Device Builder 85a177b3b7 Edit esp-officelights.yaml 2026-08-16 17:19:31 +12:00
ESPHome Device Builder b3345cf9f6 Edit esp-bedside-panel.yaml 2026-08-16 00:46:29 +12:00
zorruno dbb0a6a89c various tidyups 2026-08-15 14:58:19 +12:00
ESPHome Device Builder c85ab28c10 Edit esp-bedside-panel.yaml 2026-08-15 14:02:25 +12:00
ESPHome Device Builder ed59a3c62c Edit esp-reterminal-e1001.yaml 2026-08-02 16:27:18 +12:00
ESPHome Device Builder ed4758f1c8 Edit esp-reterminal-e1001.yaml 2026-08-02 16:24:46 +12:00
ESPHome Device Builder 04ed7ea4ad Edit esp-bedside-panel.yaml 2026-07-31 13:30:57 +12:00
ESPHome Device Builder f167f003b5 Edit esp-bedside-panel.yaml 2026-07-31 13:30:04 +12:00
ESPHome Device Builder 46a777951f Edit esp-bedside-panel.yaml 2026-07-31 13:29:47 +12:00
ESPHome Device Builder 00469e41c5 Edit esp-bedside-panel.yaml 2026-07-31 13:28:16 +12:00
ESPHome Device Builder f1caca4eea Edit esp-bedside-panel.yaml 2026-07-31 13:28:04 +12:00
ESPHome Device Builder 55764eba96 Edit esp-bedside-panel.yaml 2026-07-31 13:27:48 +12:00
ESPHome Device Builder 56bf8d9c8d Edit esp-reterminal-e1001.yaml 2026-07-31 11:40:13 +12:00
ESPHome Device Builder c2f7fe40fe Edit esp-reterminal-e1001.yaml 2026-07-30 22:38:20 +12:00
ESPHome Device Builder d39a2fff20 Edit esp-reterminal-e1001.yaml 2026-07-30 22:38:04 +12:00
ESPHome Device Builder 061ed306ff Edit esp-reterminal-e1001.yaml 2026-07-30 22:24:58 +12:00
ESPHome Device Builder 241002e111 Edit esp-reterminal-e1001.yaml 2026-07-30 22:24:52 +12:00
ESPHome Device Builder 52e9e49b51 Edit esp-reterminal-e1001.yaml 2026-07-30 22:24:41 +12:00
ESPHome Device Builder e29b3406c6 Edit esp-reterminal-e1001.yaml 2026-07-30 22:24:25 +12:00
ESPHome Device Builder 57b1203c7b Edit esp-reterminal-e1001.yaml 2026-07-30 22:24:04 +12:00
ESPHome Device Builder 64713687fd Edit esp-reterminal-e1001.yaml 2026-07-30 22:04:25 +12:00
ESPHome Device Builder 84451d9410 Edit esp-reterminal-e1001.yaml 2026-07-30 22:03:54 +12:00
ESPHome Device Builder f12cff30c2 Edit esp-reterminal-e1001.yaml 2026-07-30 22:01:11 +12:00
ESPHome Device Builder 2d03882fc2 Edit esp-reterminal-e1001.yaml 2026-07-30 21:59:20 +12:00
ESPHome Device Builder 204ee00563 Edit esp-reterminal-e1001.yaml 2026-07-30 21:59:15 +12:00
ESPHome Device Builder 41c9aa0fdc Edit esp-reterminal-e1001.yaml 2026-07-30 21:39:05 +12:00
ESPHome Device Builder 3fee307861 Edit esp-reterminal-e1001.yaml 2026-07-30 21:38:49 +12:00
ESPHome Device Builder 589c4f0501 Edit esp-reterminal-e1001.yaml 2026-07-30 21:38:43 +12:00
ESPHome Device Builder 9be9e3eba8 Edit esp-reterminal-e1001.yaml 2026-07-30 21:38:32 +12:00
ESPHome Device Builder 4dee77e9c3 Edit esp-reterminal-e1001.yaml 2026-07-30 21:25:32 +12:00
ESPHome Device Builder 884393688f Edit esp-reterminal-e1001.yaml 2026-07-30 21:25:26 +12:00
ESPHome Device Builder beb900e32e Edit esp-reterminal-e1001.yaml 2026-07-30 20:16:53 +12:00
ESPHome Device Builder d4d0faf1f4 Edit esp-reterminal-e1001.yaml 2026-07-30 20:16:42 +12:00
ESPHome Device Builder 6899fe7114 Edit esp-reterminal-e1001.yaml 2026-07-30 20:16:36 +12:00
ESPHome Device Builder a60730f153 Edit esp-reterminal-e1001.yaml 2026-07-30 20:16:30 +12:00
ESPHome Device Builder 58012234af Edit esp-reterminal-e1001.yaml 2026-07-30 20:16:25 +12:00
ESPHome Device Builder 5a484686dc Edit esp-reterminal-e1001.yaml 2026-07-30 20:16:19 +12:00
ESPHome Device Builder c47f1ff06b Edit esp-reterminal-e1001.yaml 2026-07-30 20:16:03 +12:00
ESPHome Device Builder bd5a2dc1a9 Edit esp-reterminal-e1001.yaml 2026-07-30 20:15:46 +12:00
ESPHome Device Builder b6cab50d8d Edit esp-reterminal-e1001.yaml 2026-07-30 20:15:41 +12:00
ESPHome Device Builder 02a485b028 Edit esp-reterminal-e1001.yaml 2026-07-30 20:15:36 +12:00
ESPHome Device Builder fe344ba875 Edit esp-reterminal-e1001.yaml 2026-07-30 20:15:30 +12:00
ESPHome Device Builder 0c74c73ed7 Edit esp-reterminal-e1001.yaml 2026-07-30 18:14:20 +12:00
ESPHome Device Builder 08d595b5db Edit esp-reterminal-e1001.yaml 2026-07-30 18:13:59 +12:00
ESPHome Device Builder e026008a46 Edit esp-reterminal-e1001.yaml 2026-07-30 18:13:22 +12:00
ESPHome Device Builder 871b64c6a2 Edit esp-reterminal-e1001.yaml 2026-07-30 18:13:01 +12:00
ESPHome Device Builder 23c189c9a3 Edit esp-reterminal-e1001.yaml 2026-07-30 18:12:19 +12:00
ESPHome Device Builder b4bbde9fcd Edit esp-reterminal-e1001.yaml 2026-07-30 18:11:21 +12:00
ESPHome Device Builder 9d3bc14d9c Edit esp-reterminal-e1001.yaml 2026-07-30 18:10:18 +12:00
ESPHome Device Builder 26160873f0 Edit esp-bedside-panel.yaml 2026-07-30 17:34:26 +12:00
ESPHome Device Builder 834904d749 Edit esp-reterminal-e1001.yaml 2026-07-30 00:02:23 +12:00
ESPHome Device Builder c7ed5462c1 Edit esp-reterminal-e1001.yaml 2026-07-30 00:02:18 +12:00
ESPHome Device Builder 5fa7dfd8c6 Edit esp-reterminal-e1001.yaml 2026-07-30 00:02:12 +12:00
ESPHome Device Builder aa37bf0577 Edit esp-reterminal-e1001.yaml 2026-07-30 00:02:07 +12:00
ESPHome Device Builder d5f8cadcc5 Edit esp-reterminal-e1001.yaml 2026-07-30 00:01:56 +12:00
ESPHome Device Builder 7aa03b072f Edit esp-reterminal-e1001.yaml 2026-07-30 00:01:50 +12:00
ESPHome Device Builder 9ac61dc90f Edit esp-reterminal-e1001.yaml 2026-07-30 00:01:45 +12:00
ESPHome Device Builder 67b148685c Edit esp-reterminal-e1001.yaml 2026-07-30 00:01:39 +12:00
ESPHome Device Builder 0d9e1ab4ac Edit esp-reterminal-e1001.yaml 2026-07-30 00:01:34 +12:00
76 changed files with 12970 additions and 5802 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

+1 -1
View File
@@ -1 +1 @@
{"pid": 70, "version": 1, "ha_version": "2026.7.4", "start_ts": 1785317114.2667625} {"pid": 71, "version": 1, "ha_version": "2026.9.2", "start_ts": 1789211351.1286738}
+1
View File
@@ -0,0 +1 @@
a5cfb18d66d8671cf91955eaea5ad94dcf80eaef22aad2e8374ac1223680d6aa
View File
+15 -11
View File
@@ -11,17 +11,20 @@ homeassistant:
customize: !include customize.yaml customize: !include customize.yaml
packages: !include_dir_named packages packages: !include_dir_named packages
template: !include_dir_merge_list templates # Unused directory includes retained for future YAML-based configuration.
group: !include_dir_merge_named group/ # template: !include_dir_merge_list templates
automation: !include_dir_merge_list automations/ # group: !include_dir_merge_named group/
script: !include_dir_merge_named scripts/ # automation: !include_dir_merge_list automations/
# script: !include_dir_merge_named scripts/
scene: !include scenes.yaml scene: !include scenes.yaml
sensor: !include_dir_merge_list sensor/ # sensor: !include_dir_merge_list sensor/
conversation: # Loaded by default_config.
# conversation:
logger: # Home Assistant's default logging level is info.
default: info # logger:
# default: info
zha: zha:
enable_quirks: true enable_quirks: true
@@ -35,7 +38,8 @@ zha:
ota: ota:
otau_directory: /config/zigpy_ota otau_directory: /config/zigpy_ota
ikea_provider: true ikea_provider: true
ledvance_provider: true
salus_provider: true salus_provider: true
inovelli_provider: true # Enabled by default.
thirdreality_provider: true # ledvance_provider: true
# inovelli_provider: true
# thirdreality_provider: true
+67
View File
@@ -0,0 +1,67 @@
TP-Link Deco Guest Wi-Fi Recovery Archive
==========================================
Version: 1.0.0
Created: 2026-09-02
Project: P026 Flounder downstairs guest-area controls
Purpose
-------
This archive preserves the locally maintained TP-Link Deco Guest Wi-Fi
customisation. The upstream tplink_deco integration does not provide this
service or sensor. Reinstalling or overwriting custom_components/tplink_deco
can remove them.
Archive
-------
File: docs/tplink_deco_guest_wifi_recovery_2026-09-02.tgz
SHA-256: 452ed7b7be5e8c469dc7740d5297959179204d24593a22c965f1915ca727c48d
Contents
--------
- custom_components/tplink_deco/api.py
- custom_components/tplink_deco/__init__.py
- custom_components/tplink_deco/const.py
- custom_components/tplink_deco/services.yaml
- custom_components/tplink_deco/coordinator.py
- custom_components/tplink_deco/binary_sensor.py
- packages/tplink_deco_guest_wifi.yaml
Behaviour Preserved
-------------------
- Service: tplink_deco.set_guest_network
- Service field: enable (boolean)
- Router state: binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network
- Requested state: input_boolean.wifi_guest_network
- Flounder control: the requested-state helper; the package reconciles it to
the router-confirmed sensor.
- Write protocol: read each returned wireless band, retain its host, guest and
backhaul settings, set only guest.enable, add radio.channel from host.channel,
write each band separately, then read back and verify every written band.
- Deco firmware can return a malformed successful response containing
{"error_code": 0}; the integration accepts that response only for writes and
relies on the read-back verification as the final confirmation.
Recovery Procedure
------------------
1. Stop before updating, reinstalling, or deleting custom_components/tplink_deco.
2. Verify the archive checksum above.
3. Extract the archive from /homeassistant, preserving the listed paths.
4. Run Home Assistant configuration validation and Python compilation for the
restored integration files.
5. Restart Home Assistant. A restart is required for custom integration code.
6. Confirm tplink_deco.set_guest_network is registered and the Guest Network
binary sensor is available before operating Guest Wi-Fi.
Update Policy
-------------
Do not replace custom_components/tplink_deco with an upstream or HACS update
without first comparing it to this archive and deliberately reapplying the
Guest Wi-Fi customisation. The archive contains no credentials, tokens, session
cookies, or router address.
Binary file not shown.
+3992 -3899
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+47 -55
View File
@@ -6,6 +6,10 @@
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-officeduallights.yaml # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-officeduallights.yaml
#:########################################################################################:# #:########################################################################################:#
# VERSIONS: # VERSIONS:
# V3.0 2026-09-12 Makes HA the primary Office synchronizer. Direct MQTT peer synchronization
# runs only while HA's native API state subscription is disconnected.
# V2.9 2026-08-16 Makes this physical relay the sole overhead status authority. Removes the
# bidirectional shared retained-status topic that could chatter during reconnects.
# V2.8 2026-07-25 Added 2-way MQTT sync with esp-officelights (office main lightswitch). # V2.8 2026-07-25 Added 2-way MQTT sync with esp-officelights (office main lightswitch).
# on_connect republishes retained status on MQTT reconnect. on_message # on_connect republishes retained status on MQTT reconnect. on_message
# subscribes to main lightswitch command topics for offline HA-independent # subscribes to main lightswitch command topics for offline HA-independent
@@ -91,7 +95,7 @@ substitutions:
# Project Naming # Project Naming
project_name: "Sonoff Technologies.Sonoff Dual R1" project_name: "Sonoff Technologies.Sonoff Dual R1"
project_version: "v2.8" project_version: "v3.0"
# Passwords and Secrets # Passwords and Secrets
api_key: !secret esp-api_key api_key: !secret esp-api_key
@@ -124,12 +128,6 @@ substitutions:
mqtt_mainlight_function_overhead: "office-main-lights-right" mqtt_mainlight_function_overhead: "office-main-lights-right"
mqtt_mainlight_command_overhead: "${mqtt_command_main_topic}/${mqtt_mainlight_function_overhead}" mqtt_mainlight_command_overhead: "${mqtt_command_main_topic}/${mqtt_mainlight_function_overhead}"
# MQTT Overhead Status - shared topic for overhead state sync
# Both this device and esp-officelights publish and subscribe to this topic so
# whichever device's timer finishes first keeps the other in sync.
mqtt_shared_overhead_status_function: "office-overhead-status"
mqtt_shared_overhead_status_topic: "${mqtt_status_main_topic}/${mqtt_shared_overhead_status_function}"
# MQTT Status/Command payloads # MQTT Status/Command payloads
mqtt_command_on: "On" mqtt_command_on: "On"
mqtt_command_off: "Off" mqtt_command_off: "Off"
@@ -237,24 +235,27 @@ mqtt:
payload: !lambda |- payload: !lambda |-
return id(relay_2).state ? "${mqtt_command_on}" : "${mqtt_command_off}"; return id(relay_2).state ? "${mqtt_command_on}" : "${mqtt_command_off}";
retain: true retain: true
- mqtt.publish:
topic: "${mqtt_shared_overhead_status_topic}"
payload: !lambda |-
return id(relay_2).state ? "${mqtt_command_on}" : "${mqtt_command_off}";
retain: true
on_message: on_message:
- topic: "${mqtt_local_command1_topic}" - topic: "${mqtt_local_command1_topic}"
payload: "On" payload: "On"
then: then:
- script.stop: relay_1_timer_script - script.stop: relay_1_timer_script
- light.turn_on: light_1 - if:
condition:
lambda: "return !id(relay_1).state;"
then:
- light.turn_on: light_1
- topic: "${mqtt_local_command1_topic}" - topic: "${mqtt_local_command1_topic}"
payload: "Off" payload: "Off"
then: then:
- script.stop: relay_1_timer_script - script.stop: relay_1_timer_script
- light.turn_off: light_1 - if:
condition:
lambda: "return id(relay_1).state;"
then:
- light.turn_off: light_1
- topic: "${mqtt_local_command1_topic}" - topic: "${mqtt_local_command1_topic}"
then: then:
@@ -286,13 +287,21 @@ mqtt:
payload: "On" payload: "On"
then: then:
- script.stop: relay_2_timer_script - script.stop: relay_2_timer_script
- light.turn_on: light_2 - if:
condition:
lambda: "return !id(relay_2).state;"
then:
- light.turn_on: light_2
- topic: "${mqtt_local_command2_topic}" - topic: "${mqtt_local_command2_topic}"
payload: "Off" payload: "Off"
then: then:
- script.stop: relay_2_timer_script - script.stop: relay_2_timer_script
- light.turn_off: light_2 - if:
condition:
lambda: "return id(relay_2).state;"
then:
- light.turn_off: light_2
- topic: "${mqtt_local_command2_topic}" - topic: "${mqtt_local_command2_topic}"
then: then:
@@ -320,16 +329,19 @@ mqtt:
id(relay_2_timer_script)->execute(timer_seconds * 1000); id(relay_2_timer_script)->execute(timer_seconds * 1000);
# Mirror remote bunker command from esp-officelights onto light_1, but only if a real # MQTT peer synchronization is the HA-down fallback. When HA is connected it is the
# state change is needed. Do not republish the command from here. Relay status is # sole synchronizer, so remote MQTT commands must not change either physical relay.
# published by the relay callback below.
- topic: "${mqtt_mainlight_command_bunker}" - topic: "${mqtt_mainlight_command_bunker}"
payload: "${mqtt_command_on}" payload: "${mqtt_command_on}"
then: then:
- script.stop: relay_1_timer_script - script.stop: relay_1_timer_script
- if: - if:
condition: condition:
lambda: "return !id(relay_1).state;" and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return !id(relay_1).state;"
then: then:
- light.turn_on: light_1 - light.turn_on: light_1
@@ -339,19 +351,25 @@ mqtt:
- script.stop: relay_1_timer_script - script.stop: relay_1_timer_script
- if: - if:
condition: condition:
lambda: "return id(relay_1).state;" and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return id(relay_1).state;"
then: then:
- light.turn_off: light_1 - light.turn_off: light_1
# Mirror remote overhead command from esp-officelights onto light_2, but only if a real
# state change is needed.
- topic: "${mqtt_mainlight_command_overhead}" - topic: "${mqtt_mainlight_command_overhead}"
payload: "${mqtt_command_on}" payload: "${mqtt_command_on}"
then: then:
- script.stop: relay_2_timer_script - script.stop: relay_2_timer_script
- if: - if:
condition: condition:
lambda: "return !id(relay_2).state;" and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return !id(relay_2).state;"
then: then:
- light.turn_on: light_2 - light.turn_on: light_2
@@ -361,29 +379,11 @@ mqtt:
- script.stop: relay_2_timer_script - script.stop: relay_2_timer_script
- if: - if:
condition: condition:
lambda: "return id(relay_2).state;" and:
then: - not:
- light.turn_off: light_2 api.connected:
state_subscription_only: true
# Mirror shared overhead status from esp-officelights onto light_2. This handles the - lambda: "return id(relay_2).state;"
# case where the main lightswitch's overhead timer finishes and turns off the overhead.
# State guards prevent ping-pong with the command topic handlers above.
- topic: "${mqtt_shared_overhead_status_topic}"
payload: "${mqtt_command_on}"
then:
- if:
condition:
lambda: "return !id(relay_2).state;"
then:
- light.turn_on: light_2
- topic: "${mqtt_shared_overhead_status_topic}"
payload: "${mqtt_command_off}"
then:
- script.stop: relay_2_timer_script
- if:
condition:
lambda: "return id(relay_2).state;"
then: then:
- light.turn_off: light_2 - light.turn_off: light_2
@@ -498,10 +498,6 @@ switch:
topic: "${mqtt_local_status2_topic}" topic: "${mqtt_local_status2_topic}"
payload: "On" payload: "On"
retain: true retain: true
- mqtt.publish:
topic: "${mqtt_shared_overhead_status_topic}"
payload: "On"
retain: true
- if: - if:
condition: condition:
switch.is_off: relay_1 switch.is_off: relay_1
@@ -514,10 +510,6 @@ switch:
topic: "${mqtt_local_status2_topic}" topic: "${mqtt_local_status2_topic}"
payload: "Off" payload: "Off"
retain: true retain: true
- mqtt.publish:
topic: "${mqtt_shared_overhead_status_topic}"
payload: "Off"
retain: true
- if: - if:
condition: condition:
switch.is_off: relay_1 switch.is_off: relay_1
+91 -113
View File
@@ -6,6 +6,10 @@
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-officelights.yaml # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-officelights.yaml
#:########################################################################################:# #:########################################################################################:#
# VERSIONS: # VERSIONS:
# v3.2 2026-09-12 Makes HA the primary Office synchronizer. Direct MQTT peer synchronization
# runs only while HA's native API state subscription is disconnected.
# v3.1 2026-08-16 Makes the Sonoff Dual the sole overhead status authority. Removes the
# bidirectional shared retained-status topic that could chatter during reconnects.
# v3.0 2026-07-25 Refactored MQTT sync to use template HA switch actions; physical button and # v3.0 2026-07-25 Refactored MQTT sync to use template HA switch actions; physical button and
# HA control now publish command topics to officeduallights; relay callbacks # HA control now publish command topics to officeduallights; relay callbacks
# publish status only to prevent command ping-pong/chattering. 2-way sync with # publish status only to prevent command ping-pong/chattering. 2-way sync with
@@ -41,11 +45,11 @@
# Overhead Lights (Button 2): # Overhead Lights (Button 2):
# - Command topic: ${mqtt_this_device_command_topic_overhead} # - Command topic: ${mqtt_this_device_command_topic_overhead}
# Payloads: ${mqtt_command_on}, ${mqtt_command_off}, or 1 to 3600 (timer seconds) # Payloads: ${mqtt_command_on}, ${mqtt_command_off}, or 1 to 3600 (timer seconds)
# - Status topic: ${mqtt_this_device_status_topic_overhead} # - Status topic: ${mqtt_dual_status_overhead_topic}
# Payloads mirror the overhead virtual relay state and are published retained. # Payloads are published by the Sonoff Dual physical relay and mirrored locally.
# #
# This device subscribes to status topics from esp-officeduallights to mirror remote state. # This device subscribes to status topics from esp-officeduallights to mirror remote state.
# Timer values from esp-occupancyoffice are handled locally (countdown runs on this device). # Timer values from esp-occupancyoffice are handled by the Sonoff Dual physical relay.
# Physical button presses cancel any active timer before toggling. # Physical button presses cancel any active timer before toggling.
#:########################################################################################:# #:########################################################################################:#
# OFFLINE NOTES: # OFFLINE NOTES:
@@ -79,7 +83,7 @@ substitutions:
# Project Naming # Project Naming
project_name: "Zemismart Technologies.KS-811 Triple" project_name: "Zemismart Technologies.KS-811 Triple"
project_version: "v3.0" project_version: "v3.2"
# Passwords & Secrets # Passwords & Secrets
api_key: !secret esp-api_key api_key: !secret esp-api_key
@@ -109,23 +113,17 @@ substitutions:
mqtt_this_device_status_topic_bunker: "${mqtt_status_main_topic}/${mqtt_this_device_function_bunker}" mqtt_this_device_status_topic_bunker: "${mqtt_status_main_topic}/${mqtt_this_device_function_bunker}"
# MQTT LOCAL Controls - Overhead Lights # MQTT LOCAL Controls - Overhead Lights
# Command topics: this device publishes here when Button 2 or HA toggles overhead. # Commands from Button 2 or HA are sent to the Sonoff Dual, which is the
# Status topics: this device publishes retained status here on virtual relay state changes. # sole physical relay and status authority for this circuit.
# esp-officeduallights subscribes to mirror remote overhead state.
mqtt_this_device_function_overhead: "office-main-lights-right" mqtt_this_device_function_overhead: "office-main-lights-right"
mqtt_this_device_command_topic_overhead: "${mqtt_command_main_topic}/${mqtt_this_device_function_overhead}" mqtt_this_device_command_topic_overhead: "${mqtt_command_main_topic}/${mqtt_this_device_function_overhead}"
mqtt_this_device_status_topic_overhead: "${mqtt_status_main_topic}/${mqtt_this_device_function_overhead}" mqtt_dual_status_overhead_function: "office-dual-lights-left"
mqtt_dual_status_overhead_topic: "${mqtt_status_main_topic}/${mqtt_dual_status_overhead_function}"
# MQTT Status/Command payloads # MQTT Status/Command payloads
mqtt_command_on: "On" mqtt_command_on: "On"
mqtt_command_off: "Off" mqtt_command_off: "Off"
# MQTT Overhead Status - shared topic for overhead state sync
# Both this device and esp-officeduallights publish and subscribe to this topic so
# whichever device's timer finishes first keeps the other in sync.
mqtt_shared_overhead_status_function: "office-overhead-status"
mqtt_shared_overhead_status_topic: "${mqtt_status_main_topic}/${mqtt_shared_overhead_status_function}"
# Switch/Relay/Button Naming # Switch/Relay/Button Naming
switch_1_name: "Bunker Lights" # Physical relay (GPIO13) connected to bunker light load switch_1_name: "Bunker Lights" # Physical relay (GPIO13) connected to bunker light load
switch_2_name: "Overhead Lights" # Virtual mirror of esp-officeduallights overhead state switch_2_name: "Overhead Lights" # Virtual mirror of esp-officeduallights overhead state
@@ -217,86 +215,62 @@ mqtt:
payload: !lambda |- payload: !lambda |-
return id(Relay_1).state ? "${mqtt_command_on}" : "${mqtt_command_off}"; return id(Relay_1).state ? "${mqtt_command_on}" : "${mqtt_command_off}";
retain: true retain: true
- mqtt.publish:
topic: "${mqtt_this_device_status_topic_overhead}"
payload: !lambda |-
return id(Relay_2).state ? "${mqtt_command_on}" : "${mqtt_command_off}";
retain: true
- mqtt.publish:
topic: "${mqtt_shared_overhead_status_topic}"
payload: !lambda |-
return id(Relay_2).state ? "${mqtt_command_on}" : "${mqtt_command_off}";
retain: true
on_message: on_message:
# Mirror remote overhead command onto virtual Relay_2, but only if a real state change # MQTT peer synchronization is the HA-down fallback. When HA is connected it is the
# is needed. Do not republish the command from here. Relay status is published by the # sole synchronizer, so remote MQTT state must not change this mirror.
# relay callback below.
- topic: "${mqtt_this_device_command_topic_overhead}" - topic: "${mqtt_this_device_command_topic_overhead}"
payload: "${mqtt_command_on}" payload: "${mqtt_command_on}"
then: then:
- script.stop: overhead_timer_script
- if: - if:
condition: condition:
lambda: "return !id(Relay_2).state;" and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return !id(Relay_2).state;"
then: then:
- switch.turn_on: Relay_2 - switch.turn_on: Relay_2
- topic: "${mqtt_this_device_command_topic_overhead}" - topic: "${mqtt_this_device_command_topic_overhead}"
payload: "${mqtt_command_off}" payload: "${mqtt_command_off}"
then: then:
- script.stop: overhead_timer_script
- if: - if:
condition: condition:
lambda: "return id(Relay_2).state;" and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return id(Relay_2).state;"
then: then:
- switch.turn_off: Relay_2 - switch.turn_off: Relay_2
- topic: "${mqtt_this_device_command_topic_overhead}" # Mirror the Sonoff Dual's physical overhead relay status onto virtual Relay_2.
then: # The main lightswitch never publishes this status, so a reconnect cannot create
- lambda: |- # a feedback loop.
if (x.empty()) { - topic: "${mqtt_dual_status_overhead_topic}"
return;
}
bool numeric_payload = true;
for (char c : x) {
if (c < '0' || c > '9') {
numeric_payload = false;
break;
}
}
if (!numeric_payload) {
return;
}
const int timer_seconds = atoi(x.c_str());
if (timer_seconds < 1 || timer_seconds > 3600) {
return;
}
id(overhead_timer_script)->execute(timer_seconds * 1000);
# Mirror shared overhead status from esp-officeduallights onto virtual Relay_2.
# This handles the case where the dual lights' overhead timer finishes and turns off
# the overhead — the main lightswitch mirrors the state so its virtual relay stays
# in sync. State guards prevent ping-pong with the command topic handlers above.
- topic: "${mqtt_shared_overhead_status_topic}"
payload: "${mqtt_command_on}" payload: "${mqtt_command_on}"
then: then:
- if: - if:
condition: condition:
lambda: "return !id(Relay_2).state;" and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return !id(Relay_2).state;"
then: then:
- switch.turn_on: Relay_2 - switch.turn_on: Relay_2
- topic: "${mqtt_shared_overhead_status_topic}" - topic: "${mqtt_dual_status_overhead_topic}"
payload: "${mqtt_command_off}" payload: "${mqtt_command_off}"
then: then:
- if: - if:
condition: condition:
lambda: "return id(Relay_2).state;" and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return id(Relay_2).state;"
then: then:
- switch.turn_off: Relay_2 - switch.turn_off: Relay_2
@@ -308,7 +282,11 @@ mqtt:
then: then:
- if: - if:
condition: condition:
lambda: "return !id(Relay_1).state;" and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return !id(Relay_1).state;"
then: then:
- switch.turn_on: Relay_1 - switch.turn_on: Relay_1
@@ -317,7 +295,11 @@ mqtt:
then: then:
- if: - if:
condition: condition:
lambda: "return id(Relay_1).state;" and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return id(Relay_1).state;"
then: then:
- switch.turn_off: Relay_1 - switch.turn_off: Relay_1
@@ -384,10 +366,16 @@ script:
lambda: "return !id(Relay_1).state;" lambda: "return !id(Relay_1).state;"
then: then:
- switch.turn_on: Relay_1 - switch.turn_on: Relay_1
- mqtt.publish: - if:
topic: "${mqtt_this_device_command_topic_bunker}" condition:
payload: "${mqtt_command_on}" not:
retain: false api.connected:
state_subscription_only: true
then:
- mqtt.publish:
topic: "${mqtt_this_device_command_topic_bunker}"
payload: "${mqtt_command_on}"
retain: false
# Local/HA requested OFF for Bunker Lights. # Local/HA requested OFF for Bunker Lights.
- id: office_bunker_off - id: office_bunker_off
@@ -398,10 +386,16 @@ script:
lambda: "return id(Relay_1).state;" lambda: "return id(Relay_1).state;"
then: then:
- switch.turn_off: Relay_1 - switch.turn_off: Relay_1
- mqtt.publish: - if:
topic: "${mqtt_this_device_command_topic_bunker}" condition:
payload: "${mqtt_command_off}" not:
retain: false api.connected:
state_subscription_only: true
then:
- mqtt.publish:
topic: "${mqtt_this_device_command_topic_bunker}"
payload: "${mqtt_command_off}"
retain: false
# Physical Button 1 toggle logic for Bunker Lights. This is the only place that decides # Physical Button 1 toggle logic for Bunker Lights. This is the only place that decides
# ON vs OFF for the bunker. # ON vs OFF for the bunker.
@@ -426,10 +420,16 @@ script:
lambda: "return !id(Relay_2).state;" lambda: "return !id(Relay_2).state;"
then: then:
- switch.turn_on: Relay_2 - switch.turn_on: Relay_2
- mqtt.publish: - if:
topic: "${mqtt_this_device_command_topic_overhead}" condition:
payload: "${mqtt_command_on}" not:
retain: false api.connected:
state_subscription_only: true
then:
- mqtt.publish:
topic: "${mqtt_this_device_command_topic_overhead}"
payload: "${mqtt_command_on}"
retain: false
# Local/HA requested OFF for Overhead Lights. # Local/HA requested OFF for Overhead Lights.
- id: office_overhead_off - id: office_overhead_off
@@ -440,10 +440,16 @@ script:
lambda: "return id(Relay_2).state;" lambda: "return id(Relay_2).state;"
then: then:
- switch.turn_off: Relay_2 - switch.turn_off: Relay_2
- mqtt.publish: - if:
topic: "${mqtt_this_device_command_topic_overhead}" condition:
payload: "${mqtt_command_off}" not:
retain: false api.connected:
state_subscription_only: true
then:
- mqtt.publish:
topic: "${mqtt_this_device_command_topic_overhead}"
payload: "${mqtt_command_off}"
retain: false
# Physical Button 2 toggle logic for Overhead Lights. # Physical Button 2 toggle logic for Overhead Lights.
- id: office_overhead_toggle - id: office_overhead_toggle
@@ -468,16 +474,6 @@ script:
- delay: !lambda "return timer_duration_ms;" - delay: !lambda "return timer_duration_ms;"
- switch.turn_off: Relay_1 - switch.turn_off: Relay_1
# Overhead timer script. Turns on immediately, waits for the duration, then turns off.
- id: overhead_timer_script
mode: restart
parameters:
timer_duration_ms: int
then:
- switch.turn_on: Relay_2
- delay: !lambda "return timer_duration_ms;"
- switch.turn_off: Relay_2
#:########################################################################################:# #:########################################################################################:#
# BINARY SENSORS # BINARY SENSORS
# https://esphome.io/components/binary_sensor/ # https://esphome.io/components/binary_sensor/
@@ -566,32 +562,14 @@ switch:
payload: "${mqtt_command_off}" payload: "${mqtt_command_off}"
retain: true retain: true
# Virtual Relay 2 (Overhead Lights mirror). No physical load connected. Mirrors the state # Virtual Relay 2 (Overhead Lights mirror). No physical load connected. Mirrors the
# of esp-officeduallights overhead light via MQTT status subscription. # Sonoff Dual's physical overhead relay via its one-way MQTT status topic.
- platform: gpio - platform: gpio
name: "Internal Relay 2: ${switch_2_name}" name: "Internal Relay 2: ${switch_2_name}"
internal: true internal: true
pin: GPIO12 pin: GPIO12
id: Relay_2 id: Relay_2
restore_mode: ${relay_restore_mode} restore_mode: ${relay_restore_mode}
on_turn_on:
- mqtt.publish:
topic: "${mqtt_this_device_status_topic_overhead}"
payload: "${mqtt_command_on}"
retain: true
- mqtt.publish:
topic: "${mqtt_shared_overhead_status_topic}"
payload: "${mqtt_command_on}"
retain: true
on_turn_off:
- mqtt.publish:
topic: "${mqtt_this_device_status_topic_overhead}"
payload: "${mqtt_command_off}"
retain: true
- mqtt.publish:
topic: "${mqtt_shared_overhead_status_topic}"
payload: "${mqtt_command_off}"
retain: true
# Relay 3 (Spare) - no function assigned # Relay 3 (Spare) - no function assigned
- platform: gpio - platform: gpio
File diff suppressed because it is too large Load Diff
+223
View File
@@ -0,0 +1,223 @@
--- a/esp-bedside-panel.yaml
+++ b/esp-bedside-panel.yaml
@@ -5,6 +5,7 @@
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-bedside-panel.yaml
#:########################################################################################:#
# VERSIONS:
+# V1.56 2026-09-04 Added latched blackout after 10 seconds below 1 lx with immediate exit
# V1.55 2026-08-16 Show only Clock time and date in Blackout ambient-light mode
# V1.54 2026-08-16 Dimmed Clock time and date palette in Blackout ambient-light mode
@@ -248,7 +249,6 @@
# Bright light: approximately 642-643 lx
ambient_lux_multiplier_default: "200"
-
- display_blackout_lux_threshold_default: "2"
display_dark_lux_threshold_default: "20"
display_bright_lux_threshold_default: "250"
@@ -921,21 +921,6 @@
step: 5
mode: box
- - platform: template
- id: display_blackout_lux_threshold
- name: "${friendly_name} Blackout Lux Threshold"
- icon: mdi:brightness-1
- entity_category: config
- optimistic: true
- restore_value: true
- initial_value: "${display_blackout_lux_threshold_default}"
- unit_of_measurement: "lx"
- min_value: 0
- max_value: 20
- step: 0.5
- mode: box
-
- platform: template
id: display_dark_lux_threshold
@@ -1228,6 +1213,45 @@
update_interval: 3s
lambda: |-
const float voltage = id(ambient_light_voltage).state;
+
+ if (isnan(voltage)) {
+ return NAN;
+ }
+
+ float multiplier = id(ambient_lux_multiplier).state;
+
+ if (isnan(multiplier) || multiplier <= 0.0f) {
+ multiplier = 200.0f;
+ }
+
+ return voltage * multiplier;
+ on_value:
+ then:
+ - if:
+ condition:
+ lambda: |-
+ return !isnan(x) && x >= 1.0f;
+ then:
+ - script.stop: latch_display_blackout
+ - lambda: |-
+ id(display_blackout_active) = false;
+ - if:
+ condition:
+ lambda: |-
+ return id(lvgl_ready) && id(display_is_dimmed);
+ then:
+ - script.execute: apply_dim_backlight
+ else:
+ - if:
+ condition:
+ lambda: |-
+ return id(lvgl_ready);
+ then:
+ - script.execute: apply_active_backlight
+ else:
+ - if:
+ condition:
+ lambda: |-
+ return !isnan(x) && x < 1.0f && !id(display_blackout_active);
+ then:
+ - script.execute: latch_display_blackout
- float multiplier = id(ambient_lux_multiplier).state;
-
- if (isnan(voltage)) {
- return NAN;
- }
-
- if (isnan(multiplier) || multiplier <= 0.0f) {
- multiplier = 200.0f;
- }
-
- return voltage * multiplier;
- platform: homeassistant
@@ -1918,6 +1942,12 @@
restore_value: false
initial_value: "false"
+ # True after ambient light remains below 1 lx for 10 continuous seconds.
+ - id: display_blackout_active
+ type: bool
+ restore_value: false
+ initial_value: "false"
+
# True only while the Security page is visible.
@@ -3133,31 +3163,52 @@
- # Four ambient ranges are used:
- # Blackout: lux < Blackout Lux Threshold
- # Dark: Blackout threshold to Dark Lux Threshold
- # Normal: between Dark and Bright thresholds
- # Bright: lux >= Bright Lux Threshold
+ # Blackout latches after ambient light remains below 1 lx for 10 seconds and
+ # exits immediately when ambient light reaches 1 lx. The remaining ranges are:
+ # Dark: lux <= Dark Lux Threshold
+ # Normal: between Dark and Bright thresholds
+ # Bright: lux >= Bright Lux Threshold
#
# The active and clock/idle brightness levels are independently adjustable
# and restored after reboot.
+ - id: latch_display_blackout
+ mode: single
+ then:
+ - delay: 10s
+ - if:
+ condition:
+ lambda: |-
+ const float lux = id(ambient_light_illuminance).state;
+ return !isnan(lux) && lux < 1.0f;
+ then:
+ - lambda: |-
+ id(display_blackout_active) = true;
+ - if:
+ condition:
+ lambda: |-
+ return id(lvgl_ready) && id(display_is_dimmed);
+ then:
+ - script.execute: apply_dim_backlight
+ else:
+ - if:
+ condition:
+ lambda: |-
+ return id(lvgl_ready);
+ then:
+ - script.execute: apply_active_backlight
+
# Keep the idle Clock unobtrusive at the minimum usable backlight level.
- id: apply_clock_blackout_palette
@@ -3146,15 +3197,7 @@
- if:
condition:
lambda: |-
- const float lux = id(ambient_light_illuminance).state;
- float blackout_threshold =
- id(display_blackout_lux_threshold).state;
-
- if (isnan(blackout_threshold)) {
- blackout_threshold = 2.0f;
- }
-
- return !isnan(lux) && lux < blackout_threshold;
+ return id(display_blackout_active);
@@ -3212,22 +3255,12 @@
brightness: !lambda |-
const float lux = id(ambient_light_illuminance).state;
- float blackout_threshold =
- id(display_blackout_lux_threshold).state;
float dark_threshold =
id(display_dark_lux_threshold).state;
float bright_threshold =
id(display_bright_lux_threshold).state;
- if (isnan(blackout_threshold)) {
- blackout_threshold = 2.0f;
- }
-
if (
isnan(dark_threshold) ||
- dark_threshold <= blackout_threshold
+ dark_threshold <= 0.0f
) {
dark_threshold = 20.0f;
}
@@ -3239,7 +3272,7 @@
float brightness_percent;
- if (!isnan(lux) && lux < blackout_threshold) {
+ if (id(display_blackout_active)) {
brightness_percent =
id(display_blackout_brightness).state;
@@ -3280,22 +3313,12 @@
brightness: !lambda |-
const float lux = id(ambient_light_illuminance).state;
- float blackout_threshold =
- id(display_blackout_lux_threshold).state;
float dark_threshold =
id(display_dark_lux_threshold).state;
float bright_threshold =
id(display_bright_lux_threshold).state;
- if (isnan(blackout_threshold)) {
- blackout_threshold = 2.0f;
- }
-
if (
isnan(dark_threshold) ||
- dark_threshold <= blackout_threshold
+ dark_threshold <= 0.0f
) {
dark_threshold = 20.0f;
}
@@ -3307,7 +3330,7 @@
float brightness_percent;
- if (!isnan(lux) && lux < blackout_threshold) {
+ if (id(display_blackout_active)) {
brightness_percent =
id(display_blackout_brightness).state;
+10
View File
File diff suppressed because one or more lines are too long
+173 -1
View File
@@ -8,6 +8,9 @@
# packages/16a_ev_charging_controls.yaml # packages/16a_ev_charging_controls.yaml
# #
# VERSION: # VERSION:
# V1.4 2026-08-12
# - Consolidated Leaf 16A and 32A session energy tracking in this package.
#
# V1.3 2026-07-24 # V1.3 2026-07-24
# - Added startup catch-up to offpeak_end: when HA starts outside the # - Added startup catch-up to offpeak_end: when HA starts outside the
# offpeak window in Normal mode, the relay is turned off immediately. # offpeak window in Normal mode, the relay is turned off immediately.
@@ -27,7 +30,8 @@
# #
# PURPOSE: # PURPOSE:
# Controls the Leaf 16A charger relay using scheduled offpeak operation and # Controls the Leaf 16A charger relay using scheduled offpeak operation and
# user-selectable immediate or charge-until-full modes. # user-selectable immediate or charge-until-full modes. Tracks completed
# Leaf charging sessions across both the 16A and 32A chargers.
# #
# DEPENDENCIES: # DEPENDENCIES:
# - switch.garage_db_control_16a_wallcharger_operation controls charger power. # - switch.garage_db_control_16a_wallcharger_operation controls charger power.
@@ -450,3 +454,171 @@ automation:
entity_id: input_select.ev_16a_charge_mode entity_id: input_select.ev_16a_charge_mode
data: data:
option: "Normal (Offpeak)" option: "Normal (Offpeak)"
#############################################################################
# SESSION ENERGY TRACKING
#############################################################################
- id: leaf_ev_charger_track_session_energy
alias: "Leaf EV Charger - Track Session Energy"
description: >-
Store the energy used by the most recently completed Leaf charge session
on either charger.
mode: queued
max: 2
triggers:
- trigger: homeassistant
event: start
id: ha_start
- trigger: state
entity_id: binary_sensor.garage_db_control_16a_wallcharger_charging
to: "on"
id: start_16a
- trigger: state
entity_id: binary_sensor.garage_db_control_16a_wallcharger_charging
from: "on"
to: "off"
id: stop_16a
- trigger: state
entity_id: binary_sensor.leaf_charging
to: "on"
id: start_32a
- trigger: state
entity_id: binary_sensor.main_house_3_phase_power_monitor_32a_wallcharger_charging
to: "off"
id: stop_32a
- trigger: state
entity_id: binary_sensor.atto_3_charging
to: "on"
id: cancel_32a
conditions: []
actions:
- choose:
- conditions:
- condition: trigger
id: ha_start
- condition: state
entity_id: binary_sensor.garage_db_control_16a_wallcharger_charging
state: "on"
sequence:
- action: input_number.set_value
target:
entity_id: input_number.leaf_charge_session_start_energy
data:
value: "{{ states('sensor.garage_db_control_16a_wallcharger_total_energy') | float(0) }}"
- conditions:
- condition: trigger
id: ha_start
- condition: state
entity_id: binary_sensor.main_house_3_phase_power_monitor_32a_wallcharger_charging
state: "on"
- condition: state
entity_id: binary_sensor.atto_3_charging
state: "off"
- condition: template
value_template: >-
{{ has_value('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') }}
sequence:
- action: input_number.set_value
target:
entity_id: input_number.leaf_32a_charge_session_start_energy
data:
value: "{{ states('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') | float(0) }}"
- action: input_boolean.turn_on
target:
entity_id: input_boolean.leaf_32a_charge_session_active
- conditions:
- condition: trigger
id: start_16a
sequence:
- action: input_number.set_value
target:
entity_id: input_number.leaf_charge_session_start_energy
data:
value: "{{ states('sensor.garage_db_control_16a_wallcharger_total_energy') | float(0) }}"
- conditions:
- condition: trigger
id: stop_16a
- condition: template
value_template: >
{{ states('sensor.garage_db_control_16a_wallcharger_total_energy') | float(0)
>= states('input_number.leaf_charge_session_start_energy') | float(0) }}
sequence:
- action: input_number.set_value
target:
entity_id: input_number.leaf_last_session_energy
data:
value: >-
{{ (states('sensor.garage_db_control_16a_wallcharger_total_energy') | float(0)
- states('input_number.leaf_charge_session_start_energy') | float(0)) | round(3) }}
- conditions:
- condition: trigger
id: start_32a
- condition: state
entity_id: binary_sensor.main_house_3_phase_power_monitor_32a_wallcharger_charging
state: "on"
- condition: state
entity_id: binary_sensor.atto_3_charging
state: "off"
- condition: template
value_template: >-
{{ has_value('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') }}
sequence:
- action: input_number.set_value
target:
entity_id: input_number.leaf_32a_charge_session_start_energy
data:
value: "{{ states('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') | float(0) }}"
- action: input_boolean.turn_on
target:
entity_id: input_boolean.leaf_32a_charge_session_active
- conditions:
- condition: trigger
id: stop_32a
- condition: state
entity_id: input_boolean.leaf_32a_charge_session_active
state: "on"
sequence:
- choose:
- conditions:
- condition: template
value_template: >-
{{
has_value('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy')
and states('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') | float(0)
>= states('input_number.leaf_32a_charge_session_start_energy') | float(0)
}}
sequence:
- action: input_number.set_value
target:
entity_id: input_number.leaf_last_session_energy
data:
value: >-
{{
(
states('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') | float(0)
- states('input_number.leaf_32a_charge_session_start_energy') | float(0)
) | round(3)
}}
- action: input_boolean.turn_off
target:
entity_id: input_boolean.leaf_32a_charge_session_active
- conditions:
- condition: trigger
id: cancel_32a
- condition: state
entity_id: input_boolean.leaf_32a_charge_session_active
state: "on"
sequence:
- action: input_boolean.turn_off
target:
entity_id: input_boolean.leaf_32a_charge_session_active
@@ -2,8 +2,8 @@
# PACKAGE: Appliance Monitor Processor # PACKAGE: Appliance Monitor Processor
################################################################################ ################################################################################
# #
# Version: 1.5 # Version: 1.7
# Date: 2026-07-26 # Date: 2026-08-17
# #
# Purpose: # Purpose:
# Shared appliance power monitoring processor. # Shared appliance power monitoring processor.
@@ -29,7 +29,7 @@
# operating_power_min: 12 # operating_power_min: 12
# paid_tariff_start: "00:00" # paid_tariff_start: "00:00"
# paid_tariff_end: "21:00" # paid_tariff_end: "21:00"
# paid_tariff_cost: 0.2023 # paid_tariff_cost: sensor.electricity_paid_rate (fallback 0.33235)
# offpeak_tariff_cost: 0.0 # offpeak_tariff_cost: 0.0
# max_sample_gap_seconds: 900 # max_sample_gap_seconds: 900
# min_cycle_notify_seconds: 300 # min_cycle_notify_seconds: 300
@@ -41,6 +41,14 @@
# Finished # Finished
# #
# Notes: # Notes:
# Version 1.7:
# - Use the central GST-inclusive electricity paid-rate sensor.
# - Update the fallback tariff from NZ$0.2023/kWh to NZ$0.33235/kWh.
#
# Version 1.6:
# - Preserve an explicit false power_available value instead of replacing
# it with the default true value.
#
# Version 1.5: # Version 1.5:
# - Standardized Sony TV and Pushover appliance notification titles as # - Standardized Sony TV and Pushover appliance notification titles as
# "Appliance Notification". # "Appliance Notification".
@@ -248,13 +256,21 @@ script:
am_paid_tariff_start: "{{ paid_tariff_start | default('00:00', true) | string }}" am_paid_tariff_start: "{{ paid_tariff_start | default('00:00', true) | string }}"
am_paid_tariff_end: "{{ paid_tariff_end | default('21:00', true) | string }}" am_paid_tariff_end: "{{ paid_tariff_end | default('21:00', true) | string }}"
am_paid_tariff_cost: "{{ paid_tariff_cost | default(0.2023, true) | float(0.2023) }}" am_paid_tariff_cost: >-
{{
paid_tariff_cost
| default(
states('sensor.electricity_paid_rate') | float(0.33235),
true
)
| float(0.33235)
}}
am_offpeak_tariff_cost: "{{ offpeak_tariff_cost | default(0.0, true) | float(0.0) }}" am_offpeak_tariff_cost: "{{ offpeak_tariff_cost | default(0.0, true) | float(0.0) }}"
am_max_sample_gap_seconds: "{{ max_sample_gap_seconds | default(900, true) | int(900) }}" am_max_sample_gap_seconds: "{{ max_sample_gap_seconds | default(900, true) | int(900) }}"
am_min_cycle_notify_seconds: "{{ min_cycle_notify_seconds | default(300, true) | int(300) }}" am_min_cycle_notify_seconds: "{{ min_cycle_notify_seconds | default(300, true) | int(300) }}"
am_power_available: "{{ power_available | default(true, true) | bool(true) }}" am_power_available: "{{ power_available | default(true) | bool(true) }}"
########################################################################## ##########################################################################
# ANNOUNCEMENT DEFAULTS AND PASSED VALUES # ANNOUNCEMENT DEFAULTS AND PASSED VALUES
@@ -537,7 +553,7 @@ script:
am_tariff_rate: >- am_tariff_rate: >-
{% if am_is_paid_interval | bool(false) %} {% if am_is_paid_interval | bool(false) %}
{{ am_paid_tariff_cost | float(0.2023) }} {{ am_paid_tariff_cost | float(0.33235) }}
{% else %} {% else %}
{{ am_offpeak_tariff_cost | float(0.0) }} {{ am_offpeak_tariff_cost | float(0.0) }}
{% endif %} {% endif %}
@@ -679,7 +695,7 @@ script:
{% endif %} {% endif %}
am_cycle_offpeak_savings: >- am_cycle_offpeak_savings: >-
{% set val = ((am_cycle_energy_new_kwh | float(0)) * (am_paid_tariff_cost | float(0.2023))) - (am_cycle_cost_new | float(0)) %} {% set val = ((am_cycle_energy_new_kwh | float(0)) * (am_paid_tariff_cost | float(0.33235))) - (am_cycle_cost_new | float(0)) %}
{{ ([0, val] | max) | round(4) }} {{ ([0, val] | max) | round(4) }}
am_cycle_offpeak_savings_formatted: >- am_cycle_offpeak_savings_formatted: >-
@@ -6,7 +6,7 @@
# appliances_status_monitoring.yaml # appliances_status_monitoring.yaml
# #
# VERSION: # VERSION:
# V1.5 2026-07-11 # V1.8 2026-08-15
# #
# PURPOSE: # PURPOSE:
# Appliance status monitoring package for multiple appliances. # Appliance status monitoring package for multiple appliances.
@@ -51,6 +51,14 @@
# matrix_indicator. # matrix_indicator.
# #
# VERSION HISTORY: # VERSION HISTORY:
# V1.8 2026-08-15
# - Skip appliance processing while its power source is unavailable.
# - Preserve retained cycle state and rolling samples across HA restarts and
# source reconnects instead of treating missing measurements as zero.
#
# V1.7 2026-08-12
# - Enabled MQTT activity-feed publication for every monitored appliance.
#
# V1.6 2026-07-24 # V1.6 2026-07-24
# - Added startup delay guard to prevent false completion announcements. # - Added startup delay guard to prevent false completion announcements.
# - Added power_available field for the processor availability check. # - Added power_available field for the processor availability check.
@@ -217,7 +225,7 @@ automation:
operating_power_min: 10 operating_power_min: 10
announcement_enabled: true announcement_enabled: true
announcement_channels: "pushover_zorruno,pushover_kim,sony_tv_lounge,led_matrix_1" announcement_channels: "pushover_zorruno,pushover_kim,sony_tv_lounge,mqtt_view_rd_feed,led_matrix_1"
short_announcement: "/s/sWashing complete" short_announcement: "/s/sWashing complete"
# long_announcement: # Generated by the processor # long_announcement: # Generated by the processor
announcement_payload: "" announcement_payload: ""
@@ -239,7 +247,7 @@ automation:
operating_power_min: 10 operating_power_min: 10
announcement_enabled: true announcement_enabled: true
announcement_channels: "pushover_zorruno,pushover_kim,sony_tv_lounge,led_matrix_1" announcement_channels: "pushover_zorruno,pushover_kim,sony_tv_lounge,mqtt_view_rd_feed,led_matrix_1"
short_announcement: "/s/sDryer complete" short_announcement: "/s/sDryer complete"
# long_announcement: # Generated by the processor # long_announcement: # Generated by the processor
announcement_payload: "" announcement_payload: ""
@@ -261,7 +269,7 @@ automation:
operating_power_min: 30 operating_power_min: 30
announcement_enabled: true announcement_enabled: true
announcement_channels: "pushover_zorruno,pushover_kim,sony_tv_lounge,led_matrix_1" announcement_channels: "pushover_zorruno,pushover_kim,sony_tv_lounge,mqtt_view_rd_feed,led_matrix_1"
short_announcement: "/sDishwasher complete" short_announcement: "/sDishwasher complete"
# long_announcement: # Generated by the processor # long_announcement: # Generated by the processor
announcement_payload: "" announcement_payload: ""
@@ -283,7 +291,7 @@ automation:
operating_power_min: 30 operating_power_min: 30
announcement_enabled: true announcement_enabled: true
announcement_channels: "pushover_zorruno" announcement_channels: "pushover_zorruno,mqtt_view_rd_feed"
short_announcement: "/sDownstairs Dishwasher complete" short_announcement: "/sDownstairs Dishwasher complete"
# long_announcement: # Generated by the processor # long_announcement: # Generated by the processor
announcement_payload: "" announcement_payload: ""
@@ -306,7 +314,7 @@ automation:
operating_power_min: 100 operating_power_min: 100
announcement_enabled: true announcement_enabled: true
announcement_channels: "pushover_zorruno,pushover_kim" announcement_channels: "pushover_zorruno,pushover_kim,mqtt_view_rd_feed"
short_announcement: "/s/s32A EV Charging complete" short_announcement: "/s/s32A EV Charging complete"
# long_announcement: # Generated by the processor # long_announcement: # Generated by the processor
announcement_payload: "" announcement_payload: ""
@@ -329,7 +337,7 @@ automation:
operating_power_min: 100 operating_power_min: 100
announcement_enabled: true announcement_enabled: true
announcement_channels: "pushover_zorruno" announcement_channels: "pushover_zorruno,mqtt_view_rd_feed"
short_announcement: "/s/s16A EV Charging complete" short_announcement: "/s/s16A EV Charging complete"
# long_announcement: # Generated by the processor # long_announcement: # Generated by the processor
announcement_payload: "" announcement_payload: ""
@@ -351,7 +359,7 @@ automation:
operating_power_min: 20 operating_power_min: 20
announcement_enabled: true announcement_enabled: true
announcement_channels: "pushover_zorruno" announcement_channels: "pushover_zorruno,mqtt_view_rd_feed"
short_announcement: "/s/sRyobi Charging Left" short_announcement: "/s/sRyobi Charging Left"
# long_announcement: # Generated by the processor # long_announcement: # Generated by the processor
announcement_payload: "" announcement_payload: ""
@@ -373,7 +381,7 @@ automation:
operating_power_min: 20 operating_power_min: 20
announcement_enabled: true announcement_enabled: true
announcement_channels: "pushover_zorruno" announcement_channels: "pushover_zorruno,mqtt_view_rd_feed"
short_announcement: "/s/sRyobi Charging Right" short_announcement: "/s/sRyobi Charging Right"
# long_announcement: # Generated by the processor # long_announcement: # Generated by the processor
announcement_payload: "" announcement_payload: ""
@@ -395,7 +403,7 @@ automation:
operating_power_min: 20 operating_power_min: 20
announcement_enabled: true announcement_enabled: true
announcement_channels: "pushover_zorruno" announcement_channels: "pushover_zorruno,mqtt_view_rd_feed"
short_announcement: "Pool Pump Off" short_announcement: "Pool Pump Off"
# long_announcement: # Generated by the processor # long_announcement: # Generated by the processor
announcement_payload: "" announcement_payload: ""
@@ -417,7 +425,7 @@ automation:
operating_power_min: 20 operating_power_min: 20
announcement_enabled: true announcement_enabled: true
announcement_channels: "pushover_zorruno" announcement_channels: "pushover_zorruno,mqtt_view_rd_feed"
short_announcement: "3D printer finished" short_announcement: "3D printer finished"
# long_announcement: # Generated by the processor # long_announcement: # Generated by the processor
announcement_payload: "" announcement_payload: ""
@@ -590,28 +598,32 @@ automation:
am_power_available: >- am_power_available: >-
{{ states(repeat.item.power_entity) not in ['unknown', 'unavailable'] }} {{ states(repeat.item.power_entity) not in ['unknown', 'unavailable'] }}
- action: script.appliance_monitor_process_sample - if:
data: - condition: template
appliance_id: "{{ repeat.item.appliance_id }}" value_template: "{{ am_power_available | bool(false) }}"
appliance_name: "{{ repeat.item.appliance_name }}" then:
appliance_action: "{{ repeat.item.appliance_action }}" - action: script.appliance_monitor_process_sample
data:
appliance_id: "{{ repeat.item.appliance_id }}"
appliance_name: "{{ repeat.item.appliance_name }}"
appliance_action: "{{ repeat.item.appliance_action }}"
power_w: "{{ am_power_w }}" power_w: "{{ am_power_w }}"
power_available: "{{ am_power_available }}" power_available: "{{ am_power_available }}"
monitor_sensor: "{{ am_monitor_sensor }}" monitor_sensor: "{{ am_monitor_sensor }}"
state_topic: "{{ am_state_topic }}" state_topic: "{{ am_state_topic }}"
debug_flow: "{{ am_debug_flow }}" debug_flow: "{{ am_debug_flow }}"
resolution: "{{ am_resolution }}" resolution: "{{ am_resolution }}"
standby_power: "{{ am_standby_power }}" standby_power: "{{ am_standby_power }}"
operating_power_min: "{{ am_operating_power_min }}" operating_power_min: "{{ am_operating_power_min }}"
min_cycle_notify_seconds: "{{ am_min_cycle_notify_seconds }}" min_cycle_notify_seconds: "{{ am_min_cycle_notify_seconds }}"
announcement_enabled: "{{ am_announcement_enabled }}" announcement_enabled: "{{ am_announcement_enabled }}"
announcement_channels: "{{ am_announcement_channels }}" announcement_channels: "{{ am_announcement_channels }}"
title: "{{ am_title }}" title: "{{ am_title }}"
short_announcement: "{{ am_short_announcement }}" short_announcement: "{{ am_short_announcement }}"
long_announcement: "{{ am_long_announcement }}" long_announcement: "{{ am_long_announcement }}"
announcement_payload: "{{ am_announcement_payload }}" announcement_payload: "{{ am_announcement_payload }}"
matrix_indicator: "{{ am_matrix_indicator }}" matrix_indicator: "{{ am_matrix_indicator }}"
mqtt_feed: "{{ am_mqtt_feed }}" mqtt_feed: "{{ am_mqtt_feed }}"
@@ -9,7 +9,7 @@
# jobs_tracker_jobs.yaml # jobs_tracker_jobs.yaml
# #
# VERSION: # VERSION:
# V2.4 2026-07-21 # V3.5 2026-08-15
# #
# PURPOSE: # PURPOSE:
# Defines the job registry, MQTT Discovery entities, completion triggers and # Defines the job registry, MQTT Discovery entities, completion triggers and
@@ -20,9 +20,9 @@
# #
# OPERATION: # OPERATION:
# Add and edit jobs in the JOB REGISTRY section below. Each job has its own # Add and edit jobs in the JOB REGISTRY section below. Each job has its own
# job_id, name, repeat type, due time, MQTT topic, state entity and completion # job_id, name, repeat type, schedule or due entity, MQTT topic, state entity
# entity/entities. Most future job changes should only require editing that # and completion entity/entities. Most future job changes should only require
# registry block. # editing that registry block.
# #
# If a new job uses a completion entity that is already listed in # If a new job uses a completion entity that is already listed in
# jobs_tracker_completion_entities, no trigger changes are needed. If the job # jobs_tracker_completion_entities, no trigger changes are needed. If the job
@@ -41,6 +41,39 @@
# Complete # Complete
# #
# VERSION HISTORY: # VERSION HISTORY:
# V3.5 2026-08-15
# - Empty the Dryer now completes when its door remains open for five seconds.
# - Empty the Washer now completes when its lid remains open for five seconds.
# - Removed Laundry PIR completion from the dryer and washer jobs.
#
# V3.3 2026-07-30
# - Added Empty the Dryer and Empty the Washing Machine Once jobs.
# - Laundry PIR completion sensors for dryer and washing machine.
# - Added initial_due_behavior: ignore to prevent old cycle signals.
# - Discovery device firmware version updated to 3.3.
# - PIR completion entities added to the completion trigger list.
#
# V3.2 2026-07-30
# - Converted Dishwasher Unpacked from daily to Once.
# - Main dishwasher cycle_stop_ts now makes the job Due.
# - The existing kitchen completion button still completes the job.
# - Discovery device firmware version updated to 3.2.
#
# V3.1 2026-07-30
# - Added configurable Once completion hold periods, defaulting to three hours.
# - Added elapsed completion text and minute-level completion age attributes.
# - Added due_attribute support for reliable attribute-driven Once jobs.
# - Discovery device firmware version updated to 3.1.
#
# V3.0 2026-07-29
# - Added event-driven Once jobs with registry-defined due and completion
# entities.
# - Once jobs remain Due until completed, briefly publish Complete, then
# return to Not Due until their next due signal.
# - Added Water TV Pot Plant and Water Table Pot Plant Once jobs.
# - Missing retained job state now bootstraps after the startup wait.
# - Discovery device firmware version updated to 3.0.
#
# V2.4 2026-07-21 # V2.4 2026-07-21
# - Startup now waits for every retained job timestamp before refreshing. # - Startup now waits for every retained job timestamp before refreshing.
# - Minute refreshes skip while retained MQTT state is not ready. # - Minute refreshes skip while retained MQTT state is not ready.
@@ -91,6 +124,18 @@
# - Dog feeding uses completion windows because both feeding jobs share the # - Dog feeding uses completion windows because both feeding jobs share the
# same physical button. # same physical button.
# #
# - Once jobs use due_entity and optional due_state. If due_state is omitted,
# a fresh event entity timestamp or state change starts the job.
# Set due_attribute to trigger from an increasing timestamp or numeric
# attribute such as cycle_stop_ts.
#
# - Once completion entries may be simple entity IDs or mappings with an
# entity_id and optional to_state. Once entities are evaluated by the minute
# refresh, so adding one requires changes only in the registry.
#
# - completion_hold_hours controls how long a completed Once job remains
# Complete. It defaults to 3 hours when omitted.
#
#:########################################################################################:# #:########################################################################################:#
script: script:
@@ -139,7 +184,7 @@ script:
completion_entities: completion_entities:
- event.jobs_dog_fed_button_x14bt_action - event.jobs_dog_fed_button_x14bt_action
completion_window_start: "00:00" completion_window_start: "00:00"
completion_window_end: "11:59" completion_window_end: "14:59"
icon: mdi:dog-side icon: mdi:dog-side
mqtt_topic: "viewroad-status/jobs_tracker/dog_feeding_morning" mqtt_topic: "viewroad-status/jobs_tracker/dog_feeding_morning"
state_entity: "sensor.jobs_tracker_dog_feeding_morning" state_entity: "sensor.jobs_tracker_dog_feeding_morning"
@@ -152,7 +197,7 @@ script:
due_day: 1 due_day: 1
completion_entities: completion_entities:
- event.jobs_dog_fed_button_x14bt_action - event.jobs_dog_fed_button_x14bt_action
completion_window_start: "12:00" completion_window_start: "15:00"
completion_window_end: "23:59" completion_window_end: "23:59"
icon: mdi:dog-side icon: mdi:dog-side
mqtt_topic: "viewroad-status/jobs_tracker/dog_feeding_evening" mqtt_topic: "viewroad-status/jobs_tracker/dog_feeding_evening"
@@ -173,21 +218,20 @@ script:
state_entity: "sensor.jobs_tracker_dog_depooping" state_entity: "sensor.jobs_tracker_dog_depooping"
- job_id: dishwasher_unpacked - job_id: dishwasher_unpacked
job_name: "Dishwasher Unpacked" job_name: "Unpack the dishwasher"
repeat_type: daily repeat_type: once
due_time: "15:30" due_entity: sensor.appliance_monitor_main_dishwasher_monitor
due_weekday: 0 due_state: "Finished"
due_day: 1 initial_due_behavior: ignore
completion_hold_hours: 20
completion_entities: completion_entities:
- event.kitchen_jobs_kitchen_completed_buttons_button_1 - event.kitchen_jobs_kitchen_completed_buttons_button_1
# completion_window_start: "00:00"
# completion_window_end: "23:59"
icon: mdi:dishwasher icon: mdi:dishwasher
mqtt_topic: "viewroad-status/jobs_tracker/dishwasher_unpacked" mqtt_topic: "viewroad-status/jobs_tracker/dishwasher_unpacked"
state_entity: "sensor.jobs_tracker_dishwasher_unpacked" state_entity: "sensor.jobs_tracker_dishwasher_unpacked"
- job_id: rubbish_removed - job_id: rubbish_removed
job_name: "Rubbish Removed" job_name: "Take out rubbish"
repeat_type: daily repeat_type: daily
due_time: "15:30" due_time: "15:30"
due_weekday: 0 due_weekday: 0
@@ -200,6 +244,60 @@ script:
mqtt_topic: "viewroad-status/jobs_tracker/rubbish_removed" mqtt_topic: "viewroad-status/jobs_tracker/rubbish_removed"
state_entity: "sensor.jobs_tracker_rubbish_removed" state_entity: "sensor.jobs_tracker_rubbish_removed"
- job_id: water_tv_pot_plant
job_name: "Water TV Pot Plant"
repeat_type: once
due_entity: binary_sensor.tv_plant_needs_water
due_state: "on"
completion_hold_hours: 3
completion_entities:
- entity_id: binary_sensor.tv_plant_needs_water
to_state: "off"
icon: mdi:watering-can
mqtt_topic: "viewroad-status/jobs_tracker/water_tv_pot_plant"
state_entity: "sensor.jobs_tracker_water_tv_pot_plant"
- job_id: water_table_pot_plant
job_name: "Water Table Pot Plant"
repeat_type: once
due_entity: binary_sensor.table_plant_needs_water
due_state: "on"
completion_hold_hours: 3
completion_entities:
- entity_id: binary_sensor.table_plant_needs_water
to_state: "off"
icon: mdi:watering-can
mqtt_topic: "viewroad-status/jobs_tracker/water_table_pot_plant"
state_entity: "sensor.jobs_tracker_water_table_pot_plant"
- job_id: empty_dryer
job_name: "Empty the Dryer"
repeat_type: once
due_entity: sensor.appliance_monitor_dryer_monitor
due_state: "Finished"
completion_hold_hours: 20
completion_entities:
- entity_id: binary_sensor.dryer_door_status_x34rs_contact
to_state: "on"
icon: mdi:tumble-dryer
mqtt_topic: "viewroad-status/jobs_tracker/empty_dryer"
state_entity: "sensor.jobs_tracker_empty_dryer"
initial_due_behavior: ignore
- job_id: empty_washing_machine
job_name: "Empty the Washer"
repeat_type: once
due_entity: sensor.appliance_monitor_washing_machine_monitor
due_state: "Finished"
completion_hold_hours: 20
completion_entities:
- entity_id: binary_sensor.washing_machine_lid_status_x39rs_contact
to_state: "on"
icon: mdi:washing-machine
mqtt_topic: "viewroad-status/jobs_tracker/empty_washing_machine"
state_entity: "sensor.jobs_tracker_empty_washing_machine"
initial_due_behavior: ignore
#:################################################################################:# #:################################################################################:#
# COMPLETION ENTITY TRIGGER LIST # # COMPLETION ENTITY TRIGGER LIST #
#:################################################################################:# #:################################################################################:#
@@ -225,19 +323,37 @@ script:
- repeat: - repeat:
for_each: "{{ jobs_tracker_jobs }}" for_each: "{{ jobs_tracker_jobs }}"
sequence: sequence:
- action: script.jobs_tracker_update_job - if:
data: - condition: template
job_id: "{{ repeat.item.job_id }}" value_template: "{{ repeat.item.repeat_type == 'once' }}"
job_name: "{{ repeat.item.job_name }}" then:
repeat_type: "{{ repeat.item.repeat_type }}" - action: script.jobs_tracker_update_once_job
due_time: "{{ repeat.item.due_time }}" data:
due_weekday: "{{ repeat.item.due_weekday }}" job_id: "{{ repeat.item.job_id }}"
due_day: "{{ repeat.item.due_day }}" job_name: "{{ repeat.item.job_name }}"
mqtt_topic: "{{ repeat.item.mqtt_topic }}" mqtt_topic: "{{ repeat.item.mqtt_topic }}"
state_entity: "{{ repeat.item.state_entity }}" state_entity: "{{ repeat.item.state_entity }}"
completion_window_start: "{{ repeat.item.completion_window_start | default('') }}" due_entity: "{{ repeat.item.due_entity }}"
completion_window_end: "{{ repeat.item.completion_window_end | default('') }}" due_state: "{{ repeat.item.due_state | default('') }}"
mark_complete: false due_attribute: "{{ repeat.item.due_attribute | default('') }}"
completion_entities: "{{ repeat.item.completion_entities | default([]) }}"
completion_hold_hours: "{{ repeat.item.completion_hold_hours | default(3) }}"
initial_due_behavior: "{{ repeat.item.initial_due_behavior | default('') }}"
mark_complete: false
else:
- action: script.jobs_tracker_update_job
data:
job_id: "{{ repeat.item.job_id }}"
job_name: "{{ repeat.item.job_name }}"
repeat_type: "{{ repeat.item.repeat_type }}"
due_time: "{{ repeat.item.due_time }}"
due_weekday: "{{ repeat.item.due_weekday }}"
due_day: "{{ repeat.item.due_day }}"
mqtt_topic: "{{ repeat.item.mqtt_topic }}"
state_entity: "{{ repeat.item.state_entity }}"
completion_window_start: "{{ repeat.item.completion_window_start | default('') }}"
completion_window_end: "{{ repeat.item.completion_window_end | default('') }}"
mark_complete: false
#:##################################################################################:# #:##################################################################################:#
# Mark one job complete # # Mark one job complete #
@@ -251,20 +367,37 @@ script:
- condition: template - condition: template
value_template: "{{ repeat.item.job_id == job_id }}" value_template: "{{ repeat.item.job_id == job_id }}"
then: then:
- action: script.jobs_tracker_update_job - if:
data: - condition: template
job_id: "{{ repeat.item.job_id }}" value_template: "{{ repeat.item.repeat_type == 'once' }}"
job_name: "{{ repeat.item.job_name }}" then:
repeat_type: "{{ repeat.item.repeat_type }}" - action: script.jobs_tracker_update_once_job
due_time: "{{ repeat.item.due_time }}" data:
due_weekday: "{{ repeat.item.due_weekday }}" job_id: "{{ repeat.item.job_id }}"
due_day: "{{ repeat.item.due_day }}" job_name: "{{ repeat.item.job_name }}"
mqtt_topic: "{{ repeat.item.mqtt_topic }}" mqtt_topic: "{{ repeat.item.mqtt_topic }}"
state_entity: "{{ repeat.item.state_entity }}" state_entity: "{{ repeat.item.state_entity }}"
completion_window_start: "{{ repeat.item.completion_window_start | default('') }}" due_entity: "{{ repeat.item.due_entity }}"
completion_window_end: "{{ repeat.item.completion_window_end | default('') }}" due_state: "{{ repeat.item.due_state | default('') }}"
mark_complete: true due_attribute: "{{ repeat.item.due_attribute | default('') }}"
completion_entities: "{{ repeat.item.completion_entities | default([]) }}"
completion_hold_hours: "{{ repeat.item.completion_hold_hours | default(3) }}"
initial_due_behavior: "{{ repeat.item.initial_due_behavior | default('') }}"
mark_complete: true
else:
- action: script.jobs_tracker_update_job
data:
job_id: "{{ repeat.item.job_id }}"
job_name: "{{ repeat.item.job_name }}"
repeat_type: "{{ repeat.item.repeat_type }}"
due_time: "{{ repeat.item.due_time }}"
due_weekday: "{{ repeat.item.due_weekday }}"
due_day: "{{ repeat.item.due_day }}"
mqtt_topic: "{{ repeat.item.mqtt_topic }}"
state_entity: "{{ repeat.item.state_entity }}"
completion_window_start: "{{ repeat.item.completion_window_start | default('') }}"
completion_window_end: "{{ repeat.item.completion_window_end | default('') }}"
mark_complete: true
# Give MQTT sensors a moment to update from the retained job payloads # Give MQTT sensors a moment to update from the retained job payloads
# before building the summary payload. # before building the summary payload.
- delay: "00:00:01" - delay: "00:00:01"
@@ -282,6 +415,10 @@ script:
description: "Entity ID that triggered completion." description: "Entity ID that triggered completion."
example: "event.jobs_dog_fed_button_x14bt_action" example: "event.jobs_dog_fed_button_x14bt_action"
completion_to_state:
description: "State reached by the completion entity."
example: "off"
manual_job_id: manual_job_id:
description: "Job ID completed by MQTT/manual button." description: "Job ID completed by MQTT/manual button."
example: "dog_feeding_morning" example: "dog_feeding_morning"
@@ -290,6 +427,7 @@ script:
- variables: - variables:
jobs_tracker_jobs: *jobs_tracker_jobs jobs_tracker_jobs: *jobs_tracker_jobs
completion_entity_safe: "{{ completion_entity | default('') }}" completion_entity_safe: "{{ completion_entity | default('') }}"
completion_to_state_safe: "{{ completion_to_state | default('') }}"
manual_job_id_safe: "{{ manual_job_id | default('') }}" manual_job_id_safe: "{{ manual_job_id | default('') }}"
now_minutes: "{{ (now().hour * 60) + now().minute }}" now_minutes: "{{ (now().hour * 60) + now().minute }}"
@@ -320,10 +458,21 @@ script:
{% endif %} {% endif %}
physical_entity_matches: >- physical_entity_matches: >-
{{ {% set ns = namespace(matches=false) %}
completion_entity_safe != '' {% for completion in completion_entities_safe %}
and completion_entity_safe in completion_entities_safe {% if completion is mapping %}
}} {% set entity_id = completion.entity_id | default('') %}
{% set to_state = completion.to_state | default('') %}
{% else %}
{% set entity_id = completion %}
{% set to_state = '' %}
{% endif %}
{% if completion_entity_safe == entity_id
and (to_state == '' or completion_to_state_safe == to_state) %}
{% set ns.matches = true %}
{% endif %}
{% endfor %}
{{ ns.matches }}
manual_job_matches: >- manual_job_matches: >-
{{ {{
@@ -366,7 +515,7 @@ script:
device_name: "Jobs Tracker" device_name: "Jobs Tracker"
device_manufacturer: "View Road Home Assistant" device_manufacturer: "View Road Home Assistant"
device_model: "Shared household jobs tracker" device_model: "Shared household jobs tracker"
device_sw_version: "2.4" device_sw_version: "3.4"
value_template_state: >- value_template_state: >-
{% raw %}{{ value_json.state | default('unknown') }}{% endraw %} {% raw %}{{ value_json.state | default('unknown') }}{% endraw %}
@@ -508,6 +657,7 @@ script:
"name": "Jobs Tracker - " ~ repeat.item.job_name, "name": "Jobs Tracker - " ~ repeat.item.job_name,
"unique_id": "jobs_tracker_" ~ repeat.item.job_id, "unique_id": "jobs_tracker_" ~ repeat.item.job_id,
"object_id": "jobs_tracker_" ~ repeat.item.job_id, "object_id": "jobs_tracker_" ~ repeat.item.job_id,
"default_entity_id": "sensor.jobs_tracker_" ~ repeat.item.job_id,
"state_topic": repeat.item.mqtt_topic, "state_topic": repeat.item.mqtt_topic,
"value_template": value_template_state, "value_template": value_template_state,
"json_attributes_topic": repeat.item.mqtt_topic, "json_attributes_topic": repeat.item.mqtt_topic,
@@ -533,6 +683,7 @@ script:
"name": "Job Complete - " ~ repeat.item.job_name, "name": "Job Complete - " ~ repeat.item.job_name,
"unique_id": "job_complete_" ~ repeat.item.job_id, "unique_id": "job_complete_" ~ repeat.item.job_id,
"object_id": "job_complete_" ~ repeat.item.job_id, "object_id": "job_complete_" ~ repeat.item.job_id,
"default_entity_id": "binary_sensor.job_complete_" ~ repeat.item.job_id,
"state_topic": repeat.item.mqtt_topic, "state_topic": repeat.item.mqtt_topic,
"value_template": value_template_job_complete, "value_template": value_template_job_complete,
"payload_on": "ON", "payload_on": "ON",
@@ -559,6 +710,7 @@ script:
"name": "Complete Job - " ~ repeat.item.job_name, "name": "Complete Job - " ~ repeat.item.job_name,
"unique_id": "jobs_tracker_complete_" ~ repeat.item.job_id, "unique_id": "jobs_tracker_complete_" ~ repeat.item.job_id,
"object_id": "jobs_tracker_complete_" ~ repeat.item.job_id, "object_id": "jobs_tracker_complete_" ~ repeat.item.job_id,
"default_entity_id": "button.jobs_tracker_complete_" ~ repeat.item.job_id,
"command_topic": "viewroad-commands/jobs_tracker/" ~ repeat.item.job_id ~ "/complete", "command_topic": "viewroad-commands/jobs_tracker/" ~ repeat.item.job_id ~ "/complete",
"payload_press": "PRESS", "payload_press": "PRESS",
"icon": repeat.item.icon | default("mdi:clipboard-check-outline"), "icon": repeat.item.icon | default("mdi:clipboard-check-outline"),
@@ -620,9 +772,14 @@ script:
"job_name": job.job_name, "job_name": job.job_name,
"state": s, "state": s,
"repeat_type": job.repeat_type, "repeat_type": job.repeat_type,
"due_time": job.due_time, "due_time": job.due_time | default(""),
"due_weekday": job.due_weekday, "due_weekday": job.due_weekday | default(0),
"due_day": job.due_day, "due_day": job.due_day | default(0),
"due_entity": job.due_entity | default(""),
"due_state": job.due_state | default(""),
"due_attribute": job.due_attribute | default(""),
"initial_due_behavior": job.initial_due_behavior | default(""),
"completion_hold_hours": job.completion_hold_hours | default(3),
"completion_entities": job.completion_entities | default([]), "completion_entities": job.completion_entities | default([]),
"completion_window_start": job.completion_window_start | default(""), "completion_window_start": job.completion_window_start | default(""),
"completion_window_end": job.completion_window_end | default(""), "completion_window_end": job.completion_window_end | default(""),
@@ -636,7 +793,11 @@ script:
"previous_missed_active": state_attr(job.state_entity, 'previous_missed_active'), "previous_missed_active": state_attr(job.state_entity, 'previous_missed_active'),
"minutes_since_required": state_attr(job.state_entity, 'minutes_since_required'), "minutes_since_required": state_attr(job.state_entity, 'minutes_since_required'),
"hours_since_required": state_attr(job.state_entity, 'hours_since_required'), "hours_since_required": state_attr(job.state_entity, 'hours_since_required'),
"hours_since_completed": state_attr(job.state_entity, 'hours_since_completed') "hours_since_completed": state_attr(job.state_entity, 'hours_since_completed'),
"minutes_since_completed": state_attr(job.state_entity, 'minutes_since_completed'),
"completion_elapsed_human": state_attr(job.state_entity, 'completion_elapsed_human'),
"once_pending": state_attr(job.state_entity, 'once_pending'),
"once_due_ts": state_attr(job.state_entity, 'once_due_ts')
} }
] %} ] %}
{% endfor %} {% endfor %}
@@ -717,7 +878,7 @@ automation:
{% endfor %} {% endfor %}
{{ ns.ready }} {{ ns.ready }}
timeout: "00:01:30" timeout: "00:01:30"
continue_on_timeout: false continue_on_timeout: true
# Recalculate each job only after retained state is confirmed ready. # Recalculate each job only after retained state is confirmed ready.
- action: script.jobs_tracker_dispatch - action: script.jobs_tracker_dispatch
@@ -785,6 +946,7 @@ automation:
- action: script.jobs_tracker_route_completion - action: script.jobs_tracker_route_completion
data: data:
completion_entity: "{{ trigger.entity_id }}" completion_entity: "{{ trigger.entity_id }}"
completion_to_state: "{{ trigger.to_state.state }}"
- id: jobs_tracker_mqtt_manual_completion - id: jobs_tracker_mqtt_manual_completion
alias: "Jobs Tracker - MQTT Manual Completion" alias: "Jobs Tracker - MQTT Manual Completion"
@@ -9,7 +9,10 @@
# jobs_tracker_processor.yaml # jobs_tracker_processor.yaml
# #
# VERSION: # VERSION:
# V1.6 2026-07-10 # V2.5 2026-08-02
# - A new Once due signal now refreshes the occurrence timestamp even when
# the job is already Due, so repeated appliance cycles supersede any
# still-uncompleted earlier occurrence.
# #
# PURPOSE: # PURPOSE:
# Generic processing engine for the Jobs Tracker system. # Generic processing engine for the Jobs Tracker system.
@@ -25,6 +28,12 @@
# restart the processor can recalculate the correct state from the last physical # restart the processor can recalculate the correct state from the last physical
# button/action press. # button/action press.
# #
# Event-driven Once jobs use the separate jobs_tracker_update_once_job script.
# Their registry-defined due and completion entities are evaluated during the
# Jobs Tracker minute refresh. Once jobs remain Due until completed, briefly
# publish Complete, then return to Not Due until a new due signal arrives.
# A newer due signal always supersedes an older pending occurrence.
#
# Normal operation is automatic. The job-list package refreshes all jobs on a # Normal operation is automatic. The job-list package refreshes all jobs on a
# regular timer and after Home Assistant starts, and it calls this processor with # regular timer and after Home Assistant starts, and it calls this processor with
# mark_complete: true when a physical button, event, sensor, MQTT button or other # mark_complete: true when a physical button, event, sensor, MQTT button or other
@@ -95,6 +104,29 @@
# - Monthly jobs: 2 weeks after due_time # - Monthly jobs: 2 weeks after due_time
# #
# VERSION HISTORY: # VERSION HISTORY:
# V2.4 2026-07-30
# - Reformatted last_completed_human: shows "15:45" (today),
# "Yesterday @ 15:45" or "X days ago @ 15:45".
#
# V2.3 2026-07-30
# - Added initial_due_behavior: ignore field for Once jobs.
# Records the first due signal but keeps the job Not Due.
#
# V2.2 2026-07-30
# - Ignore historical event/attribute due signals that predate the most
# recent completion while still recording them as processed.
#
# V2.1 2026-07-30
# - Once jobs remain Complete for a configurable hold period.
# - Added minute-level and human-readable completion age attributes.
# - Added increasing numeric/timestamp attribute due signals.
# - New due occurrences override an active completion hold.
#
# V2.0 2026-07-29
# - Added the event-driven Once job processor.
# - Added retained Once occurrence, pending and due-signal state.
# - Added registry-defined due states and filtered completion states.
#
# V1.6 2026-07-10 # V1.6 2026-07-10
# - Renamed displayed state from Missed to Missed. # - Renamed displayed state from Missed to Missed.
# - Added optional completion_window_start and completion_window_end fields. # - Added optional completion_window_start and completion_window_end fields.
@@ -474,11 +506,9 @@ script:
{% set completed_midnight = as_timestamp(strptime(d ~ ' 00:00:00', '%Y-%m-%d %H:%M:%S')) %} {% set completed_midnight = as_timestamp(strptime(d ~ ' 00:00:00', '%Y-%m-%d %H:%M:%S')) %}
{% set days_ago = ((as_timestamp(today_at('00:00')) - completed_midnight) / 86400) | round(0) | int %} {% set days_ago = ((as_timestamp(today_at('00:00')) - completed_midnight) / 86400) | round(0) | int %}
{% if d == today %} {% if d == today %}
Today @ {{ t }} {{ t }}
{% elif d == yesterday %} {% elif d == yesterday %}
Yesterday @ {{ t }} Yesterday @ {{ t }}
{% elif days_ago < 7 %}
Last {{ ts | timestamp_custom('%A', true) }} @ {{ t }}
{% else %} {% else %}
{{ days_ago }} days ago @ {{ t }} {{ days_ago }} days ago @ {{ t }}
{% endif %} {% endif %}
@@ -572,3 +602,332 @@ script:
"last_updated_local": {{ ((now_ts | float(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true)) | tojson }}, "last_updated_local": {{ ((now_ts | float(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true)) | tojson }},
"mqtt_topic": {{ topic_safe | tojson }} "mqtt_topic": {{ topic_safe | tojson }}
} }
jobs_tracker_update_once_job:
alias: "Jobs Tracker - Update Once Job"
description: "Updates one event-driven Once job's retained MQTT JSON state."
mode: queued
max: 20
fields:
job_id:
description: "Machine-safe job ID."
example: "water_tv_pot_plant"
job_name:
description: "Human-readable job name."
example: "Water TV Pot Plant"
mqtt_topic:
description: "MQTT topic used for retained job state."
example: "viewroad-status/jobs_tracker/water_tv_pot_plant"
state_entity:
description: "MQTT sensor entity that reads this job's retained JSON."
example: "sensor.jobs_tracker_water_tv_pot_plant"
due_entity:
description: "Entity that starts a new Once job occurrence."
example: "binary_sensor.tv_plant_needs_water"
due_state:
description: "Optional state that starts the Once job."
example: "on"
due_attribute:
description: "Optional increasing timestamp or numeric attribute that starts the job."
example: "cycle_stop_ts"
completion_entities:
description: "Entities and optional target states that complete the job."
completion_hold_hours:
description: "Hours to remain Complete before returning to Not Due."
example: 3
initial_due_behavior:
description: "ignore - records the first due signal without making the job Due. Prevents old historical cycle timestamps from triggering immediately."
example: "ignore"
mark_complete:
description: "Set true for an accepted manual completion."
example: true
sequence:
- variables:
mark_complete_bool: "{{ mark_complete | default(false) | bool }}"
topic_safe: "{{ mqtt_topic | default('viewroad-status/jobs_tracker/' ~ job_id) }}"
due_entity_safe: "{{ due_entity | default('') }}"
due_state_safe: "{{ due_state | default('') }}"
due_attribute_safe: "{{ due_attribute | default('') }}"
completion_entities_safe: "{{ completion_entities | default([]) }}"
completion_hold_hours_safe: "{{ completion_hold_hours | default(3) | float(3) }}"
completion_hold_seconds: >-
{{ (completion_hold_hours | default(3) | float(3) * 3600) | int(10800) }}
initial_due_behavior_safe: "{{ initial_due_behavior | default('') | lower }}"
now_ts: "{{ as_timestamp(now()) }}"
stored_last_completed_ts: >-
{% set value = state_attr(state_entity, 'last_completed_ts') %}
{{ 0 if value in [none, '', 'unknown', 'unavailable'] else value | float(0) }}
stored_once_due_ts: >-
{% set value = state_attr(state_entity, 'once_due_ts') %}
{{ 0 if value in [none, '', 'unknown', 'unavailable'] else value | float(0) }}
stored_once_pending: >-
{{ state_attr(state_entity, 'once_pending') | default(false, true) | bool }}
stored_due_signal_active: >-
{{ state_attr(state_entity, 'once_due_signal_active') | default(false, true) | bool }}
stored_due_signal_ts: >-
{% set value = state_attr(state_entity, 'once_due_signal_ts') %}
{{ 0 if value in [none, '', 'unknown', 'unavailable'] else value | float(0) }}
- variables:
due_uses_state: >-
{{ due_state_safe not in ['', none] and due_attribute_safe in ['', none] }}
due_uses_attribute: "{{ due_attribute_safe not in ['', none] }}"
due_signal_active: >-
{{
due_uses_state | bool
and due_entity_safe != ''
and is_state(due_entity_safe, due_state_safe)
}}
current_due_signal_ts: >-
{% if due_entity_safe == '' %}
0
{% elif due_uses_attribute | bool %}
{{ state_attr(due_entity_safe, due_attribute_safe) | float(0) }}
{% elif due_entity_safe.startswith('event.') %}
{{ as_timestamp(states(due_entity_safe), 0) }}
{% else %}
{% set source = states[due_entity_safe] %}
{{ as_timestamp(source.last_changed, 0) if source is not none else 0 }}
{% endif %}
due_signal_valid: >-
{{
due_entity_safe != ''
and states(due_entity_safe) not in ['unknown', 'unavailable']
and (current_due_signal_ts | float(0)) > 0
}}
- variables:
new_due_signal: >-
{% if due_uses_state | bool %}
{{
due_signal_valid | bool
and due_signal_active | bool
and not (stored_due_signal_active | bool)
and (initial_due_behavior_safe != 'ignore' or stored_due_signal_ts > 0)
}}
{% else %}
{{
due_signal_valid | bool
and (current_due_signal_ts | float(0)) > (stored_due_signal_ts | float(0))
and (current_due_signal_ts | float(0)) > (stored_last_completed_ts | float(0))
and (initial_due_behavior_safe != 'ignore' or stored_due_signal_ts > 0)
}}
{% endif %}
once_due_ts: >-
{% if new_due_signal | bool %}
{{ current_due_signal_ts if not (due_uses_state | bool) else now_ts }}
{% else %}
{{ stored_once_due_ts }}
{% endif %}
due_signal_active_after: >-
{{ due_signal_active if due_uses_state | bool else false }}
due_signal_ts_after: >-
{{
[stored_due_signal_ts | float(0), current_due_signal_ts | float(0)]
| max
}}
pending_before_completion: >-
{{ stored_once_pending | bool or new_due_signal | bool }}
- variables:
completion_entity_matched: >-
{% set ns = namespace(matches=false) %}
{% if pending_before_completion | bool %}
{% for completion in completion_entities_safe %}
{% if completion is mapping %}
{% set entity_id = completion.entity_id | default('') %}
{% set to_state = completion.to_state | default('') %}
{% else %}
{% set entity_id = completion %}
{% set to_state = '' %}
{% endif %}
{% if entity_id != '' and states(entity_id) not in ['unknown', 'unavailable'] %}
{% if entity_id.startswith('event.') %}
{% set signal_ts = as_timestamp(states(entity_id), 0) %}
{% else %}
{% set source = states[entity_id] %}
{% set signal_ts = as_timestamp(source.last_changed, 0) if source is not none else 0 %}
{% endif %}
{% if signal_ts > (once_due_ts | float(0))
and signal_ts > (stored_last_completed_ts | float(0))
and (to_state == '' or states(entity_id) == to_state) %}
{% set ns.matches = true %}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{{ ns.matches }}
complete_now: >-
{{
pending_before_completion | bool
and (mark_complete_bool | bool or completion_entity_matched | bool)
}}
- variables:
once_pending: >-
{{ pending_before_completion | bool and not (complete_now | bool) }}
last_completed_ts: >-
{{ now_ts if complete_now | bool else stored_last_completed_ts }}
completion_belongs_to_occurrence: >-
{{
(once_due_ts | float(0)) > 0
and (last_completed_ts | float(0)) >= (once_due_ts | float(0))
}}
completion_age_seconds: >-
{% if (last_completed_ts | float(0)) > 0 %}
{{ [(now_ts | float(0)) - (last_completed_ts | float(0)), 0] | max | int(0) }}
{% else %}
0
{% endif %}
completion_hold_active: >-
{{
not (once_pending | bool)
and completion_belongs_to_occurrence | bool
and (completion_age_seconds | int(0)) < (completion_hold_seconds | int(10800))
}}
- variables:
job_status: >-
{% if once_pending | bool %}
Due
{% elif completion_hold_active | bool %}
Complete
{% else %}
Not Due
{% endif %}
minutes_since_required: >-
{% if once_pending | bool and (once_due_ts | float(0)) > 0 %}
{{ (((now_ts | float(0)) - (once_due_ts | float(0))) / 60) | round(0) | int }}
{% else %}
0
{% endif %}
hours_since_required: >-
{% if once_pending | bool and (once_due_ts | float(0)) > 0 %}
{{ (((now_ts | float(0)) - (once_due_ts | float(0))) / 3600) | round(0) | int }}
{% else %}
0
{% endif %}
hours_since_completed: >-
{% if (last_completed_ts | float(0)) > 0 %}
{{ ((completion_age_seconds | int(0)) / 3600) | int(0) }}
{% else %}
unknown
{% endif %}
minutes_since_completed: >-
{% if (last_completed_ts | float(0)) > 0 %}
{{ ((completion_age_seconds | int(0)) / 60) | int(0) }}
{% else %}
unknown
{% endif %}
completion_elapsed_human: >-
{% if (last_completed_ts | float(0)) <= 0 %}
Never
{% elif (completion_age_seconds | int(0)) < 60 %}
Just now
{% elif (completion_age_seconds | int(0)) < 3600 %}
{% set minutes = ((completion_age_seconds | int(0)) / 60) | int(0) %}
{{ minutes }} {{ 'min' if minutes == 1 else 'mins' }}
{% else %}
{% set hours = ((completion_age_seconds | int(0)) / 3600) | int(0) %}
{{ hours }} {{ 'hour' if hours == 1 else 'hours' }}
{% endif %}
last_completed_time: >-
{% if (last_completed_ts | float(0)) > 0 %}
{{ (last_completed_ts | float(0)) | timestamp_custom('%H:%M', true) }}
{% else %}
Never
{% endif %}
last_completed_human: >-
{% set ts = last_completed_ts | float(0) %}
{% if ts <= 0 %}
Never
{% else %}
{% set d = ts | timestamp_custom('%Y-%m-%d', true) %}
{% set t = ts | timestamp_custom('%H:%M', true) %}
{% set today = now().strftime('%Y-%m-%d') %}
{% set yesterday = (as_timestamp(today_at('00:00')) - 86400) | timestamp_custom('%Y-%m-%d', true) %}
{% set completed_midnight = as_timestamp(strptime(d ~ ' 00:00:00', '%Y-%m-%d %H:%M:%S')) %}
{% set days_ago = ((as_timestamp(today_at('00:00')) - completed_midnight) / 86400) | round(0) | int %}
{% if d == today %}
{{ t }}
{% elif d == yesterday %}
Yesterday @ {{ t }}
{% else %}
{{ days_ago }} days ago @ {{ t }}
{% endif %}
{% endif %}
next_due_human: >-
{{ 'Due now' if once_pending | bool else 'Waiting for trigger' }}
completed_for_due_ts: >-
{% if (last_completed_ts | float(0)) >= (once_due_ts | float(0))
and (once_due_ts | float(0)) > 0 %}
{{ once_due_ts | float(0) }}
{% else %}
0
{% endif %}
- action: mqtt.publish
data:
topic: "{{ topic_safe }}"
retain: true
qos: 1
payload: >-
{{
{
"job_id": job_id,
"job_name": job_name,
"repeat": "once",
"repeat_type": "once",
"due_time": "",
"due_weekday": 0,
"due_day": 0,
"due_entity": due_entity_safe,
"due_state": due_state_safe,
"due_attribute": due_attribute_safe,
"completion_hold_hours": completion_hold_hours_safe | float(3),
"initial_due_behavior": initial_due_behavior_safe,
"state": job_status | trim,
"complete": completion_hold_active | bool,
"last_completed_ts": last_completed_ts | float(0),
"last_completed_time": last_completed_time,
"last_completed_human": last_completed_human,
"previous_period_start_ts": 0,
"previous_due_ts": 0,
"previous_completed": false,
"previous_missed": false,
"previous_missed_active": false,
"missed": false,
"completion_window_start": "",
"completion_window_end": "",
"has_completion_window": false,
"completion_window_missed": false,
"current_completion_window_start_ts": 0,
"current_completion_window_end_ts": 0,
"completed_for_period_ts": completed_for_due_ts | float(0),
"completed_for_due_ts": completed_for_due_ts | float(0),
"current_period_start_ts": once_due_ts | float(0),
"next_period_start_ts": 0,
"current_due_ts": once_due_ts | float(0),
"next_due_ts": 0,
"display_due_ts": (once_due_ts | float(0)) if once_pending | bool else 0,
"next_due_human": next_due_human,
"due_started": once_pending | bool,
"overdue_started": false,
"minutes_since_required": minutes_since_required | int(0),
"hours_since_required": hours_since_required | int(0),
"overdue_after_minutes": 0,
"overdue_minutes": 0,
"overdue_hours": 0,
"hours_since_completed": hours_since_completed,
"minutes_since_completed": minutes_since_completed,
"completion_elapsed_human": completion_elapsed_human,
"once_pending": once_pending | bool,
"once_due_ts": once_due_ts | float(0),
"once_due_signal_active": due_signal_active_after | bool,
"once_due_signal_ts": due_signal_ts_after | float(0),
"last_updated_local": (now_ts | float(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true),
"mqtt_topic": topic_safe
} | tojson
}}
@@ -0,0 +1,58 @@
#:########################################################################################:#
# PACKAGE: Office Lights Home Assistant Fallback Synchronization
# FILE: Lighting_2Way_Sync/office_lights_ha_fallback_sync.yaml
# VERSION: v1.2.0
# DATE: 2026-09-12
#:########################################################################################:#
#
# PURPOSE:
# Provides Home Assistant-primary synchronization for the Office two-way
# light controls. Direct MQTT peer control is the HA-down transport.
#
# OPERATION:
# - Synchronizes the Office bunker and overhead control pairs in both directions.
# - ESPHome permits direct MQTT peer synchronization only while HA's native
# API state subscription is disconnected, preventing competing controllers.
# - Does not reconcile state during Home Assistant startup, preventing a stale
# restored entity state from changing a physical light.
#
# DEPENDENCIES:
# - Blueprint: zorruno/two_three_way_sync.yaml
# - esp-officelights v3.2 and esp-officeduallights v3.0 or later.
#
# VERSION HISTORY:
# - v1.2.0 (2026-09-12): Enable after guarded HA-primary firmware deployment.
# - v1.1.0 (2026-09-12): Disabled until guarded HA-primary firmware deployment.
# - v1.0.0 (2026-08-16): Initial MQTT-down HA fallback for Office lights.
#:########################################################################################:#
automation:
#:##########################################################################:#
# BUNKER LIGHTS
#:##########################################################################:#
- id: office_bunker_lights_ha_fallback_sync
alias: "Office Bunker Lights HA Fallback Sync"
initial_state: true
use_blueprint:
path: zorruno/two_three_way_sync.yaml
input:
members:
- switch.office_office_main_lightswitch_3_relay_1_bunker_lights
- light.esp_officeduallights_light_1_right_hand_bunker_light
sync_on_start: false
startup_delay: 5
#:##########################################################################:#
# OVERHEAD LIGHTS
#:##########################################################################:#
- id: office_overhead_lights_ha_fallback_sync
alias: "Office Overhead Lights HA Fallback Sync"
initial_state: true
use_blueprint:
path: zorruno/two_three_way_sync.yaml
input:
members:
- switch.office_office_main_lightswitch_3_relay_2_overhead_lights
- light.esp_officeduallights_light_2_cool_white_overhead_lights
sync_on_start: false
startup_delay: 5
@@ -0,0 +1,57 @@
#:########################################################################################:#
# PACKAGE: Office Lights Home Assistant Fallback Synchronization
# FILE: Lighting_2Way_Sync/office_lights_ha_fallback_sync.yaml
# VERSION: v1.1.0
# DATE: 2026-09-12
#:########################################################################################:#
#
# PURPOSE:
# Provides the Home Assistant API transport for Office two-way light control
# when MQTT is unavailable. Direct MQTT remains the HA-down transport.
#
# OPERATION:
# - Synchronizes the Office bunker and overhead control pairs in both directions.
# - The automations remain disabled until both ESPHome devices enforce the
# HA-primary/MQTT-fallback authority boundary.
# - Does not reconcile state during Home Assistant startup, preventing a stale
# restored entity state from changing a physical light.
#
# DEPENDENCIES:
# - Blueprint: zorruno/two_three_way_sync.yaml
# - esp-officelights and esp-officeduallights native ESPHome API connections.
#
# VERSION HISTORY:
# - v1.1.0 (2026-09-12): Disable until guarded HA-primary firmware is deployed.
# - v1.0.0 (2026-08-16): Initial MQTT-down HA fallback for Office lights.
#:########################################################################################:#
automation:
#:##########################################################################:#
# BUNKER LIGHTS
#:##########################################################################:#
- id: office_bunker_lights_ha_fallback_sync
alias: "Office Bunker Lights HA Fallback Sync"
initial_state: false
use_blueprint:
path: zorruno/two_three_way_sync.yaml
input:
members:
- switch.office_office_main_lightswitch_3_relay_1_bunker_lights
- light.esp_officeduallights_light_1_right_hand_bunker_light
sync_on_start: false
startup_delay: 5
#:##########################################################################:#
# OVERHEAD LIGHTS
#:##########################################################################:#
- id: office_overhead_lights_ha_fallback_sync
alias: "Office Overhead Lights HA Fallback Sync"
initial_state: false
use_blueprint:
path: zorruno/two_three_way_sync.yaml
input:
members:
- switch.office_office_main_lightswitch_3_relay_2_overhead_lights
- light.esp_officeduallights_light_2_cool_white_overhead_lights
sync_on_start: false
startup_delay: 5
@@ -9,9 +9,13 @@
# packages/Pool_Controls/pool_light_operation.yaml # packages/Pool_Controls/pool_light_operation.yaml
# #
# VERSION: # VERSION:
# V1.2 2026-07-20 # V1.3 2026-09-06
# #
# VERSION HISTORY: # VERSION HISTORY:
# V1.3 2026-09-06
# - Treat the colour selector's "0= OFF" as a true Operation Mode OFF and
# arm the midnight TIMER reset.
#
# V1.2 2026-07-20 # V1.2 2026-07-20
# - Corrected the documented package filename. # - Corrected the documented package filename.
# - Documented the relationship with the native ESPHome controls. # - Documented the relationship with the native ESPHome controls.
@@ -151,6 +155,40 @@ automation:
target: target:
entity_id: input_boolean.timer_light_midnight_pending entity_id: input_boolean.timer_light_midnight_pending
- id: pool_light_schedule_timer_when_operation_mode_off
alias: "Pool Light - Schedule TIMER When Operation Mode Is OFF"
description: "Arms the midnight TIMER reset for direct Operation Mode OFF commands."
mode: single
trigger:
- platform: state
entity_id: select.esp_poollightpower2_operation_mode
to: "OFF"
action:
- action: input_boolean.turn_on
target:
entity_id: input_boolean.timer_light_midnight_pending
- id: pool_light_send_off_when_colour_off_selected
alias: "Pool Light - Send OFF When Colour Off Is Selected"
description: "Converts colour selector 0= OFF into Operation Mode OFF with midnight reset."
mode: restart
trigger:
- platform: state
entity_id: select.esp_poollightpower2_pool_light_mode
to: "0= OFF"
action:
- action: mqtt.publish
data:
topic: "viewroad-commands/poollight-timer/operation"
payload: "OFF"
- action: input_boolean.turn_on
target:
entity_id: input_boolean.timer_light_midnight_pending
- id: pool_light_sync_ui_switch_with_real_output - id: pool_light_sync_ui_switch_with_real_output
alias: "Pool Light - Sync UI Switch With Real Output" alias: "Pool Light - Sync UI Switch With Real Output"
description: "Keeps the UI switch aligned with the real ESPHome pool light output without sending MQTT commands." description: "Keeps the UI switch aligned with the real ESPHome pool light output without sending MQTT commands."
-91
View File
@@ -1,91 +0,0 @@
#:########################################################################################:#
# Alexa TP-Link Guest Wi-Fi Package #
#:########################################################################################:#
#
# TITLE:
# Alexa TP-Link Guest Wi-Fi
#
# FILE:
# packages/alexa_tplink_actions.yaml
#
# VERSION:
# V1.1 2026-07-20
#
# VERSION HISTORY:
# V1.1 2026-07-20
# - Added structured documentation and automation descriptions.
#
# V1.0
# - Initial Alexa-mediated TP-Link guest-network controls.
#
# PURPOSE:
# Enables or disables the TP-Link guest Wi-Fi network by sending custom Alexa
# requests through the kitchen Echo Show.
#
# DEPENDENCIES:
# - input_boolean.wifi_guest_network is a UI-managed helper that represents
# the requested guest-network state.
# - media_player.view_rd_kitchen_echo_show must be available through the Alexa
# Media Player integration.
# - The Alexa account must have a working, linked TP-Link skill.
#
# ALEXA REQUESTS:
# - On: ask t.p.link to enable guest network
# - Off: ask t.p.link to disable guest network
#
# OPERATION NOTES:
# - The helper records requested state; it does not confirm router state.
# - If Alexa or the TP-Link skill fails, the helper can disagree with the
# actual guest-network state.
# - After the helper remains on for two hours, it is turned off. That state
# change then sends the Alexa disable request.
# - The two-hour state-trigger period does not persist through a Home Assistant
# restart or automation reload.
#
#:########################################################################################:#
automation:
- alias: "Turn on the Wifi Guest Network"
description: "Asks the linked TP-Link Alexa skill to enable the guest Wi-Fi network."
mode: single
triggers:
- trigger: state
entity_id: input_boolean.wifi_guest_network
to: "on"
actions:
- action: media_player.play_media
target:
entity_id: media_player.view_rd_kitchen_echo_show
data:
media_content_type: custom
media_content_id: ask t.p.link to enable guest network
- alias: "Turn off the Wifi Guest Network"
description: "Asks the linked TP-Link Alexa skill to disable the guest Wi-Fi network."
mode: single
triggers:
- trigger: state
entity_id: input_boolean.wifi_guest_network
to: "off"
actions:
- action: media_player.play_media
target:
entity_id: media_player.view_rd_kitchen_echo_show
data:
media_content_type: custom
media_content_id: ask t.p.link to disable guest network
- alias: "Switch guest network off after time"
description: "Turns off the guest-network helper after it remains on for two hours."
mode: single
triggers:
- trigger: state
entity_id: input_boolean.wifi_guest_network
from: "off"
to: "on"
for: "02:00:00"
conditions: []
actions:
- action: input_boolean.turn_off
target:
entity_id: input_boolean.wifi_guest_network
@@ -0,0 +1,442 @@
################################################################################
# Appliance Remaining Time Predictor Package
################################################################################
#
# File: appliance_remaining_time_predictor.yaml
# Version: 1.2.0
# Date: 2026-08-03
#
# Purpose:
# - Record recent valid cycle durations for the washing machine, dryer, and
# main dishwasher.
# - Estimate minutes remaining while each appliance monitor is Operating.
# - Retain the latest ten valid durations across Home Assistant restarts.
# - Preserve the previous completed cycle's timestamps, energy, and cost while
# a new cycle is running.
#
# Prediction method:
# - Reject cycles outside appliance-specific duration limits to avoid false
# starts and stale cycles.
# - Keep the ten most recent valid durations.
# - With five or more samples, discard the shortest and longest duration before
# calculating the average. This limits the effect of unusual loads while
# retaining recent behavior.
# - Subtract the current cycle elapsed time from the predicted total duration.
#
# Version history:
# - 1.2.0 (2026-08-06): Learn durations to the monitor's Finished transition,
# matching the completion announcement instead of the inferred power stop.
# - 1.1.0 (2026-08-03): Preserved last-completed cycle display values.
# - 1.0.0 (2026-08-03): Initial three-appliance implementation.
################################################################################
input_text:
appliance_predictor_washing_machine_durations:
name: "Appliance Predictor Washing Machine Durations"
max: 255
appliance_predictor_dryer_durations:
name: "Appliance Predictor Dryer Durations"
max: 255
appliance_predictor_main_dishwasher_durations:
name: "Appliance Predictor Main Dishwasher Durations"
max: 255
appliance_predictor_washing_machine_last_completed:
name: "Appliance Predictor Washing Machine Last Completed"
max: 255
appliance_predictor_dryer_last_completed:
name: "Appliance Predictor Dryer Last Completed"
max: 255
appliance_predictor_main_dishwasher_last_completed:
name: "Appliance Predictor Main Dishwasher Last Completed"
max: 255
script:
appliance_predictor_record_duration:
alias: "Appliance Predictor - Record Duration"
description: >
Validates and appends one completed cycle duration to the selected
appliance's retained ten-cycle history.
mode: queued
max: 20
fields:
appliance_id:
name: "Appliance ID"
description: "washing_machine, dryer, or main_dishwasher"
example: washing_machine
duration_seconds:
name: "Duration seconds"
description: "Completed cycle duration in seconds."
example: 3600
sequence:
- variables:
duration: "{{ duration_seconds | int(0) }}"
history_entity: >-
{% if appliance_id == 'washing_machine' %}
input_text.appliance_predictor_washing_machine_durations
{% elif appliance_id == 'dryer' %}
input_text.appliance_predictor_dryer_durations
{% elif appliance_id == 'main_dishwasher' %}
input_text.appliance_predictor_main_dishwasher_durations
{% endif %}
minimum_duration: >-
{{ 900 if appliance_id == 'washing_machine' else 1800 }}
maximum_duration: >-
{{ 14400 if appliance_id == 'washing_machine' else 21600 }}
- condition: template
value_template: >-
{{
history_entity | trim | length > 0
and duration >= (minimum_duration | int(0))
and duration <= (maximum_duration | int(0))
}}
- variables:
updated_history: >-
{% set ns = namespace(values=[]) %}
{% for item in states(history_entity).split(',') %}
{% set value = item | int(0) %}
{% if value > 0 %}
{% set ns.values = ns.values + [value] %}
{% endif %}
{% endfor %}
{% set ns.values = ns.values + [duration] %}
{{ ns.values[-10:] | join(',') }}
- action: input_text.set_value
target:
entity_id: "{{ history_entity | trim }}"
data:
value: "{{ updated_history | trim }}"
automation:
- id: appliance_predictor_record_completed_cycles
alias: "Appliance Predictor - Record Completed Cycles"
description: >
Records valid cycle durations when one of the three appliance monitors
changes to Finished.
mode: queued
max: 10
triggers:
- trigger: state
entity_id:
- sensor.appliance_monitor_washing_machine_monitor
- sensor.appliance_monitor_dryer_monitor
- sensor.appliance_monitor_main_dishwasher_monitor
to: "Finished"
actions:
- variables:
appliance_id: >-
{% if trigger.entity_id == 'sensor.appliance_monitor_washing_machine_monitor' %}
washing_machine
{% elif trigger.entity_id == 'sensor.appliance_monitor_dryer_monitor' %}
dryer
{% elif trigger.entity_id == 'sensor.appliance_monitor_main_dishwasher_monitor' %}
main_dishwasher
{% endif %}
duration: >-
{{
as_timestamp(trigger.to_state.last_changed)
- (trigger.to_state.attributes.cycle_start_ts | float(0))
}}
completed_record_entity: >-
{% if trigger.entity_id == 'sensor.appliance_monitor_washing_machine_monitor' %}
input_text.appliance_predictor_washing_machine_last_completed
{% elif trigger.entity_id == 'sensor.appliance_monitor_dryer_monitor' %}
input_text.appliance_predictor_dryer_last_completed
{% elif trigger.entity_id == 'sensor.appliance_monitor_main_dishwasher_monitor' %}
input_text.appliance_predictor_main_dishwasher_last_completed
{% endif %}
completed_record: >-
{{
[
trigger.to_state.attributes.cycle_start_ts | float(0),
trigger.to_state.attributes.cycle_stop_ts | float(0),
trigger.to_state.attributes.cycle_energy_kwh | float(0),
trigger.to_state.attributes.cycle_cost | float(0)
] | join(',')
}}
- action: input_text.set_value
target:
entity_id: "{{ completed_record_entity | trim }}"
data:
value: "{{ completed_record | trim }}"
- action: script.appliance_predictor_record_duration
data:
appliance_id: "{{ appliance_id | trim }}"
duration_seconds: "{{ duration }}"
- id: appliance_predictor_seed_empty_histories
alias: "Appliance Predictor - Seed Empty Histories"
description: >
Seeds an empty history from the retained last completed monitor cycle
after Home Assistant starts. Existing learned histories are not changed.
mode: single
triggers:
- trigger: homeassistant
event: start
actions:
- delay: "00:00:30"
- if:
- condition: template
value_template: >-
{{ states('input_text.appliance_predictor_washing_machine_durations') | trim | length == 0 }}
then:
- action: script.appliance_predictor_record_duration
data:
appliance_id: washing_machine
duration_seconds: >-
{{ state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_duration_seconds') | int(0) }}
- if:
- condition: template
value_template: >-
{{ states('input_text.appliance_predictor_dryer_durations') | trim | length == 0 }}
then:
- action: script.appliance_predictor_record_duration
data:
appliance_id: dryer
duration_seconds: >-
{{ state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_duration_seconds') | int(0) }}
- if:
- condition: template
value_template: >-
{{ states('input_text.appliance_predictor_main_dishwasher_durations') | trim | length == 0 }}
then:
- action: script.appliance_predictor_record_duration
data:
appliance_id: main_dishwasher
duration_seconds: >-
{{ state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_duration_seconds') | int(0) }}
- if:
- condition: template
value_template: >-
{{
states('input_text.appliance_predictor_washing_machine_last_completed') | trim | length == 0
and not is_state('sensor.appliance_monitor_washing_machine_monitor', 'Operating')
}}
then:
- action: input_text.set_value
target:
entity_id: input_text.appliance_predictor_washing_machine_last_completed
data:
value: >-
{{
[
state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_start_ts') | float(0),
state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_stop_ts') | float(0),
state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_energy_kwh') | float(0),
state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_cost') | float(0)
] | join(',')
}}
- if:
- condition: template
value_template: >-
{{
states('input_text.appliance_predictor_dryer_last_completed') | trim | length == 0
and not is_state('sensor.appliance_monitor_dryer_monitor', 'Operating')
}}
then:
- action: input_text.set_value
target:
entity_id: input_text.appliance_predictor_dryer_last_completed
data:
value: >-
{{
[
state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_start_ts') | float(0),
state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_stop_ts') | float(0),
state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_energy_kwh') | float(0),
state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_cost') | float(0)
] | join(',')
}}
- if:
- condition: template
value_template: >-
{{
states('input_text.appliance_predictor_main_dishwasher_last_completed') | trim | length == 0
and not is_state('sensor.appliance_monitor_main_dishwasher_monitor', 'Operating')
}}
then:
- action: input_text.set_value
target:
entity_id: input_text.appliance_predictor_main_dishwasher_last_completed
data:
value: >-
{{
[
state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_start_ts') | float(0),
state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_stop_ts') | float(0),
state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_energy_kwh') | float(0),
state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_cost') | float(0)
] | join(',')
}}
template:
- triggers:
- trigger: time_pattern
minutes: "/1"
- trigger: state
entity_id:
- sensor.appliance_monitor_washing_machine_monitor
- sensor.appliance_monitor_dryer_monitor
- sensor.appliance_monitor_main_dishwasher_monitor
- input_text.appliance_predictor_washing_machine_durations
- input_text.appliance_predictor_dryer_durations
- input_text.appliance_predictor_main_dishwasher_durations
- input_text.appliance_predictor_washing_machine_last_completed
- input_text.appliance_predictor_dryer_last_completed
- input_text.appliance_predictor_main_dishwasher_last_completed
sensor:
- name: "Washing Machine Estimated Time Remaining"
unique_id: washing_machine_estimated_time_remaining
icon: mdi:timer-sand
unit_of_measurement: "min"
state: >-
{% set ns = namespace(values=[]) %}
{% for item in states('input_text.appliance_predictor_washing_machine_durations').split(',') %}
{% set value = item | int(0) %}
{% if value > 0 %}
{% set ns.values = ns.values + [value] %}
{% endif %}
{% endfor %}
{% set ordered = ns.values | sort %}
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
{% set average = (samples | sum / samples | count) if samples | count > 0 else 0 %}
{% set start = state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_start_ts') | float(0) %}
{% set elapsed = [0, as_timestamp(now()) - start] | max if start > 0 else 0 %}
{% set remaining = [0, (average - elapsed) / 60] | max %}
{{ remaining | round(0, 'ceil') | int(0) if is_state('sensor.appliance_monitor_washing_machine_monitor', 'Operating') else 0 }}
attributes:
sample_count: >-
{% set ns = namespace(count=0) %}
{% for item in states('input_text.appliance_predictor_washing_machine_durations').split(',') %}
{% if item | int(0) > 0 %}{% set ns.count = ns.count + 1 %}{% endif %}
{% endfor %}
{{ ns.count }}
predicted_cycle_minutes: >-
{% set ns = namespace(values=[]) %}
{% for item in states('input_text.appliance_predictor_washing_machine_durations').split(',') %}
{% set value = item | int(0) %}
{% if value > 0 %}{% set ns.values = ns.values + [value] %}{% endif %}
{% endfor %}
{% set ordered = ns.values | sort %}
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
{{ ((samples | sum / samples | count) / 60) | round(0) | int(0) if samples | count > 0 else 0 }}
confidence: >-
{% set count = states('input_text.appliance_predictor_washing_machine_durations').split(',') | reject('equalto', '') | list | count %}
{{ 'High' if count >= 7 else 'Medium' if count >= 3 else 'Low' }}
last_completed_start_ts: >-
{{ states('input_text.appliance_predictor_washing_machine_last_completed').split(',')[0] | float(0) }}
last_completed_stop_ts: >-
{{ states('input_text.appliance_predictor_washing_machine_last_completed').split(',')[1] | default(0) | float(0) }}
last_completed_energy_kwh: >-
{{ states('input_text.appliance_predictor_washing_machine_last_completed').split(',')[2] | default(0) | float(0) }}
last_completed_cost: >-
{{ states('input_text.appliance_predictor_washing_machine_last_completed').split(',')[3] | default(0) | float(0) }}
- name: "Dryer Estimated Time Remaining"
unique_id: dryer_estimated_time_remaining
icon: mdi:timer-sand
unit_of_measurement: "min"
state: >-
{% set ns = namespace(values=[]) %}
{% for item in states('input_text.appliance_predictor_dryer_durations').split(',') %}
{% set value = item | int(0) %}
{% if value > 0 %}
{% set ns.values = ns.values + [value] %}
{% endif %}
{% endfor %}
{% set ordered = ns.values | sort %}
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
{% set average = (samples | sum / samples | count) if samples | count > 0 else 0 %}
{% set start = state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_start_ts') | float(0) %}
{% set elapsed = [0, as_timestamp(now()) - start] | max if start > 0 else 0 %}
{% set remaining = [0, (average - elapsed) / 60] | max %}
{{ remaining | round(0, 'ceil') | int(0) if is_state('sensor.appliance_monitor_dryer_monitor', 'Operating') else 0 }}
attributes:
sample_count: >-
{% set ns = namespace(count=0) %}
{% for item in states('input_text.appliance_predictor_dryer_durations').split(',') %}
{% if item | int(0) > 0 %}{% set ns.count = ns.count + 1 %}{% endif %}
{% endfor %}
{{ ns.count }}
predicted_cycle_minutes: >-
{% set ns = namespace(values=[]) %}
{% for item in states('input_text.appliance_predictor_dryer_durations').split(',') %}
{% set value = item | int(0) %}
{% if value > 0 %}{% set ns.values = ns.values + [value] %}{% endif %}
{% endfor %}
{% set ordered = ns.values | sort %}
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
{{ ((samples | sum / samples | count) / 60) | round(0) | int(0) if samples | count > 0 else 0 }}
confidence: >-
{% set count = states('input_text.appliance_predictor_dryer_durations').split(',') | reject('equalto', '') | list | count %}
{{ 'High' if count >= 7 else 'Medium' if count >= 3 else 'Low' }}
last_completed_start_ts: >-
{{ states('input_text.appliance_predictor_dryer_last_completed').split(',')[0] | float(0) }}
last_completed_stop_ts: >-
{{ states('input_text.appliance_predictor_dryer_last_completed').split(',')[1] | default(0) | float(0) }}
last_completed_energy_kwh: >-
{{ states('input_text.appliance_predictor_dryer_last_completed').split(',')[2] | default(0) | float(0) }}
last_completed_cost: >-
{{ states('input_text.appliance_predictor_dryer_last_completed').split(',')[3] | default(0) | float(0) }}
- name: "Main Dishwasher Estimated Time Remaining"
unique_id: main_dishwasher_estimated_time_remaining
icon: mdi:timer-sand
unit_of_measurement: "min"
state: >-
{% set ns = namespace(values=[]) %}
{% for item in states('input_text.appliance_predictor_main_dishwasher_durations').split(',') %}
{% set value = item | int(0) %}
{% if value > 0 %}
{% set ns.values = ns.values + [value] %}
{% endif %}
{% endfor %}
{% set ordered = ns.values | sort %}
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
{% set average = (samples | sum / samples | count) if samples | count > 0 else 0 %}
{% set start = state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_start_ts') | float(0) %}
{% set elapsed = [0, as_timestamp(now()) - start] | max if start > 0 else 0 %}
{% set remaining = [0, (average - elapsed) / 60] | max %}
{{ remaining | round(0, 'ceil') | int(0) if is_state('sensor.appliance_monitor_main_dishwasher_monitor', 'Operating') else 0 }}
attributes:
sample_count: >-
{% set ns = namespace(count=0) %}
{% for item in states('input_text.appliance_predictor_main_dishwasher_durations').split(',') %}
{% if item | int(0) > 0 %}{% set ns.count = ns.count + 1 %}{% endif %}
{% endfor %}
{{ ns.count }}
predicted_cycle_minutes: >-
{% set ns = namespace(values=[]) %}
{% for item in states('input_text.appliance_predictor_main_dishwasher_durations').split(',') %}
{% set value = item | int(0) %}
{% if value > 0 %}{% set ns.values = ns.values + [value] %}{% endif %}
{% endfor %}
{% set ordered = ns.values | sort %}
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
{{ ((samples | sum / samples | count) / 60) | round(0) | int(0) if samples | count > 0 else 0 }}
confidence: >-
{% set count = states('input_text.appliance_predictor_main_dishwasher_durations').split(',') | reject('equalto', '') | list | count %}
{{ 'High' if count >= 7 else 'Medium' if count >= 3 else 'Low' }}
last_completed_start_ts: >-
{{ states('input_text.appliance_predictor_main_dishwasher_last_completed').split(',')[0] | float(0) }}
last_completed_stop_ts: >-
{{ states('input_text.appliance_predictor_main_dishwasher_last_completed').split(',')[1] | default(0) | float(0) }}
last_completed_energy_kwh: >-
{{ states('input_text.appliance_predictor_main_dishwasher_last_completed').split(',')[2] | default(0) | float(0) }}
last_completed_cost: >-
{{ states('input_text.appliance_predictor_main_dishwasher_last_completed').split(',')[3] | default(0) | float(0) }}
+4 -7
View File
@@ -1,8 +1,8 @@
#:########################################################################################:# #:########################################################################################:#
# Package: Appliance Activity Feed Sensors # Package: Appliance Activity Feed Sensors
# File: appliance_status_mqttfeed.yaml # File: appliance_status_mqttfeed.yaml
# Version: v1.2.0 # Version: v1.3.0
# Date: 2026-07-20 # Date: 2026-08-12
# #
# Purpose: # Purpose:
# Provides MQTT sensors for appliance, vehicle-charger, and tool-charger # Provides MQTT sensors for appliance, vehicle-charger, and tool-charger
@@ -13,12 +13,9 @@
# - External publishers or the announcement router must publish completion # - External publishers or the announcement router must publish completion
# payloads to the configured activity-feed topics. # payloads to the configured activity-feed topics.
# #
# Known Limitation:
# - sensor.washer_finished has device_class timestamp and therefore requires
# an ISO 8601 timestamp. P013-F018 records that the current router may publish
# a time-only payload, which is not valid for this device class.
#
# Version History: # Version History:
# - v1.3.0 (2026-08-12): Activity feed payloads are ISO 8601 timestamps,
# compatible with the washer timestamp sensor.
# - v1.2.0 (2026-07-20): Added the downstairs dishwasher completion sensor. # - v1.2.0 (2026-07-20): Added the downstairs dishwasher completion sensor.
# - v1.1.0 (2025-05-18): Set the washer completion sensor as a timestamp. # - v1.1.0 (2025-05-18): Set the washer completion sensor as a timestamp.
# - v1.0.0 (2023-10-20): Added the initial MQTT activity-feed sensors. # - v1.0.0 (2023-10-20): Added the initial MQTT activity-feed sensors.
+636
View File
@@ -0,0 +1,636 @@
#:########################################################################################:#
# Battery Notes Alerts Package #
#:########################################################################################:#
#
# TITLE:
# Battery Notes Alerts
#
# FILE:
# packages/battery_notes_alerts.yaml
#
# VERSION:
# V1.1.0 2026-08-26
#
# PURPOSE:
# Sends immediate Pushover alerts when a non-temporary Battery Notes device newly
# reports a low battery or becomes unavailable, plus a Thursday weekly digest.
#
# Immediate alerts are limited to one alert of each type per device per calendar day.
#
# SAFETY / PERFORMANCE DESIGN:
# - No automation-level templated variables.
# - Low battery detection uses the native Battery Notes threshold event.
# - Weekly Battery Notes checks use raise_events: false.
# - Immediate automations never scan all Home Assistant states.
# - Immediate automations never call device_entities().
# - The offline watcher does only cheap filtering on state_changed events.
# - Offline alerts are inhibited for 10 minutes after Home Assistant starts.
# - Weekly list processing is bounded to 100 returned items per category.
# - Devices with "Temporary" anywhere in the Battery Notes note are always ignored.
#
# CONTROLS:
# - input_boolean.battery_notes_alerts_master_enable
# Master enable/disable for all notifications from this package.
#
# - Battery Notes - Immediate Low Alert
# May also be independently enabled/disabled in Automations.
#
# - Battery Notes - Immediate Offline Alert
# May also be independently enabled/disabled in Automations.
#
# - Battery Notes - Weekly Digest
# May also be independently enabled/disabled in Automations.
#
# OPERATION:
# - LOW:
# Native Battery Notes threshold event only.
# Reminder-generated events are ignored.
# Maximum one low alert per device per calendar day.
#
# - OFFLINE:
# Battery Notes Battery+ entities changing from a valid state to
# unknown/unavailable.
# Suppressed for 10 minutes after Home Assistant starts.
# Maximum one offline alert per device per calendar day.
#
# - WEEKLY:
# Thursday at 19:00.
# Reports current low batteries and batteries not reporting for 2+ days.
# Battery Notes devices marked Temporary are excluded.
#
# DEPENDENCIES:
# - Battery Notes integration
# - packages/view_road_announcement_router.yaml
#
# VERSION HISTORY:
# - V1.1.0 2026-08-26: Complete redesign using native Battery Notes events/actions.
# Removes high-frequency state scanning and top-level templates.
# Adds Temporary filtering, daily per-device alert suppression,
# startup offline suppression and master enable control.
# - V1.0.2 2026-08-25: RETIRED - unsafe high-frequency architecture.
# - V1.0.1 2026-08-25: RETIRED - unsafe high-frequency architecture.
# - V1.0.0 2026-08-25: RETIRED - unsafe high-frequency architecture.
#
#:########################################################################################:#
#:########################################################################################:#
# HELPERS #
#:########################################################################################:#
input_boolean:
# Master safety switch.
#
# No "initial" value is specified intentionally:
# - First creation defaults to OFF.
# - Thereafter Home Assistant restores the previous state.
battery_notes_alerts_master_enable:
name: "Battery Notes Alerts - Master Enable"
icon: mdi:battery-alert
# Internal startup guard.
#
# This MUST start false on every Home Assistant restart.
battery_notes_offline_alerts_armed:
name: "Battery Notes Alerts - Offline Armed"
icon: mdi:shield-clock
initial: false
input_text:
# Daily de-duplication caches.
#
# Format:
# YYYY-MM-DD|xxxxxxxxxx|xxxxxxxxxx|...
#
# Only the final 10 characters of the device identifier are stored.
# A maximum of the 20 most recent device tokens is retained.
#
# No initial value is set so the cache survives Home Assistant restarts.
battery_notes_low_alert_cache:
name: "Battery Notes - Low Alert Cache"
max: 255
icon: mdi:battery-alert-variant
battery_notes_offline_alert_cache:
name: "Battery Notes - Offline Alert Cache"
max: 255
icon: mdi:battery-off-outline
#:########################################################################################:#
# AUTOMATIONS #
#:########################################################################################:#
automation:
#:######################################################################################:#
# Offline Alert Startup Guard #
#:######################################################################################:#
- id: battery_notes_arm_offline_alerts
alias: "Battery Notes - Arm Offline Alerts"
description: >
Arms Battery Notes offline notifications 10 minutes after Home Assistant starts.
This prevents normal startup/integration initialisation from being interpreted as
devices suddenly going offline.
mode: restart
triggers:
- trigger: homeassistant
event: start
actions:
- action: input_boolean.turn_off
target:
entity_id: input_boolean.battery_notes_offline_alerts_armed
- delay: "00:10:00"
- action: input_boolean.turn_on
target:
entity_id: input_boolean.battery_notes_offline_alerts_armed
#:######################################################################################:#
# Immediate Low Battery Alert #
#:######################################################################################:#
- id: battery_notes_immediate_low_alert
alias: "Battery Notes - Immediate Low Alert"
description: >
Sends one immediate Pushover alert when a non-temporary Battery Notes device
genuinely crosses into its low-battery state. Reminder-generated Battery Notes
events are ignored. A device can generate at most one low alert per calendar day.
mode: queued
max: 10
triggers:
- trigger: event
event_type: battery_notes_battery_threshold
event_data:
battery_low: true
reminder: false
conditions:
# Master control.
- condition: state
entity_id: input_boolean.battery_notes_alerts_master_enable
state: "on"
# Temporary Battery Notes devices must never generate notifications.
- condition: template
value_template: >-
{% set note = trigger.event.data.note | default('', true) | lower %}
{{ 'temporary' not in note }}
# Require some stable identifier for daily de-duplication.
- condition: template
value_template: >-
{{ (trigger.event.data.device_id | default('', true)) != ''
or (trigger.event.data.source_entity_id | default('', true)) != '' }}
actions:
# IMPORTANT:
# This de-duplication check is deliberately an ACTION condition rather than an
# automation-level condition. Because this automation is mode: queued, two events
# arriving together are serialised before checking/updating the cache.
- condition: template
value_template: >-
{% set identifier =
trigger.event.data.device_id | default('', true)
or trigger.event.data.source_entity_id | default('', true) %}
{% set token = identifier[-10:] %}
{% set today = now().date() | string %}
{% set cache = states('input_text.battery_notes_low_alert_cache') %}
{% set previous =
cache.split('|')[1:]
if cache.startswith(today ~ '|')
else [] %}
{{ token not in previous }}
- action: script.view_road_announcement
data:
channels: "pushover_zorruno"
title: "Battery Alert"
short_announcement: >-
{% set name =
trigger.event.data.device_name
| default('Unknown Battery Device', true) %}
{% set level =
trigger.event.data.battery_level
| default(-1, true)
| float(-1) %}
{% set battery_type =
trigger.event.data.battery_type_and_quantity
| default('Not recorded', true) %}
New Alert: {{ name }}, Battery Low{% if level >= 0 %}
{{ level | round(0) | int }}%{% endif %} {{ battery_type }}
long_announcement: >-
{% set name =
trigger.event.data.device_name
| default('Unknown Battery Device', true) %}
{% set level =
trigger.event.data.battery_level
| default(-1, true)
| float(-1) %}
{% set battery_type =
trigger.event.data.battery_type_and_quantity
| default('Not recorded', true) %}
New Alert: {{ name }}, Battery Low{% if level >= 0 %}
{{ level | round(0) | int }}%{% endif %} {{ battery_type }}
# Record this device only after the notification action has completed.
- action: input_text.set_value
target:
entity_id: input_text.battery_notes_low_alert_cache
data:
value: >-
{% set identifier =
trigger.event.data.device_id | default('', true)
or trigger.event.data.source_entity_id | default('', true) %}
{% set token = identifier[-10:] %}
{% set today = now().date() | string %}
{% set cache = states('input_text.battery_notes_low_alert_cache') %}
{% set previous =
cache.split('|')[1:]
if cache.startswith(today ~ '|')
else [] %}
{% set updated = (previous + [token])[-20:] %}
{{ today ~ '|' ~ updated | join('|') }}
#:######################################################################################:#
# Immediate Offline Alert #
#:######################################################################################:#
- id: battery_notes_immediate_offline_alert
alias: "Battery Notes - Immediate Offline Alert"
description: >
Sends one immediate Pushover alert when a non-temporary Battery Notes Battery+
entity genuinely changes from an available state to unknown/unavailable.
Alerts are inhibited for the first 10 minutes after Home Assistant starts.
A device can generate at most one offline alert per calendar day.
mode: queued
max: 20
triggers:
- trigger: event
event_type: state_changed
conditions:
# Keep the first checks extremely cheap because state_changed is a global event.
- condition: state
entity_id: input_boolean.battery_notes_alerts_master_enable
state: "on"
- condition: state
entity_id: input_boolean.battery_notes_offline_alerts_armed
state: "on"
# Only Battery Notes generated Battery+ entities are interesting.
# No regex and no global state lookup is required.
- condition: template
value_template: >-
{% set entity_id = trigger.event.data.entity_id | default('', true) %}
{{ (entity_id.startswith('sensor.')
and entity_id.endswith('_battery_plus'))
or
(entity_id.startswith('binary_sensor.')
and entity_id.endswith('_battery_plus_low')) }}
# Require a genuine transition from available -> unavailable.
- condition: template
value_template: >-
{% set old = trigger.event.data.old_state %}
{% set new = trigger.event.data.new_state %}
{{ old is not none
and new is not none
and old.state not in ['unknown', 'unavailable']
and new.state in ['unknown', 'unavailable'] }}
# Temporary devices are ignored.
#
# Prefer the new state's note but fall back to the old state's attributes because
# integrations do not always retain every attribute on an unavailable state.
- condition: template
value_template: >-
{% set old = trigger.event.data.old_state %}
{% set new = trigger.event.data.new_state %}
{% set note =
new.attributes.get(
'note',
old.attributes.get('note', '')
)
| default('', true)
| lower %}
{{ 'temporary' not in note }}
actions:
# As with low alerts, do this after entering the queued action sequence so two
# related Battery+ entities becoming unavailable together cannot both notify.
- condition: template
value_template: >-
{% set old = trigger.event.data.old_state %}
{% set new = trigger.event.data.new_state %}
{% set identifier =
new.attributes.get('device_id', '')
or old.attributes.get('device_id', '')
or new.entity_id %}
{% set token = identifier[-10:] %}
{% set today = now().date() | string %}
{% set cache = states('input_text.battery_notes_offline_alert_cache') %}
{% set previous =
cache.split('|')[1:]
if cache.startswith(today ~ '|')
else [] %}
{{ token not in previous }}
- action: script.view_road_announcement
data:
channels: "pushover_zorruno"
title: "Battery Alert"
short_announcement: >-
{% set old = trigger.event.data.old_state %}
{% set new = trigger.event.data.new_state %}
{% set name =
new.attributes.get('device_name')
or old.attributes.get('device_name')
or new.attributes.get('friendly_name')
or old.attributes.get('friendly_name')
or new.name %}
{% set battery_type =
new.attributes.get('battery_type_and_quantity')
or old.attributes.get('battery_type_and_quantity')
or 'Not recorded' %}
New Alert: {{ name }}, Battery Unavailable {{ battery_type }}
long_announcement: >-
{% set old = trigger.event.data.old_state %}
{% set new = trigger.event.data.new_state %}
{% set name =
new.attributes.get('device_name')
or old.attributes.get('device_name')
or new.attributes.get('friendly_name')
or old.attributes.get('friendly_name')
or new.name %}
{% set battery_type =
new.attributes.get('battery_type_and_quantity')
or old.attributes.get('battery_type_and_quantity')
or 'Not recorded' %}
New Alert: {{ name }}, Battery Unavailable {{ battery_type }}
- action: input_text.set_value
target:
entity_id: input_text.battery_notes_offline_alert_cache
data:
value: >-
{% set old = trigger.event.data.old_state %}
{% set new = trigger.event.data.new_state %}
{% set identifier =
new.attributes.get('device_id', '')
or old.attributes.get('device_id', '')
or new.entity_id %}
{% set token = identifier[-10:] %}
{% set today = now().date() | string %}
{% set cache = states('input_text.battery_notes_offline_alert_cache') %}
{% set previous =
cache.split('|')[1:]
if cache.startswith(today ~ '|')
else [] %}
{% set updated = (previous + [token])[-20:] %}
{{ today ~ '|' ~ updated | join('|') }}
#:######################################################################################:#
# Weekly Digest #
#:######################################################################################:#
- id: battery_notes_weekly_digest
alias: "Battery Notes - Weekly Digest"
description: >
Sends one Battery Notes summary every Thursday at 19:00. Reports current low
batteries and batteries which have not reported for at least two days.
Temporary Battery Notes devices are excluded.
mode: single
triggers:
- trigger: time
at: "19:00:00"
conditions:
- condition: time
weekday:
- thu
- condition: state
entity_id: input_boolean.battery_notes_alerts_master_enable
state: "on"
actions:
# IMPORTANT:
# raise_events MUST remain false.
#
# This retrieves the current low list without creating Battery Notes threshold
# reminder events.
- action: battery_notes.check_battery_low
data:
raise_events: false
response_variable: battery_low_response
# Ask Battery Notes for devices which have not reported for at least two days.
# Again, retrieve a response only; do not raise one event per device.
- action: battery_notes.check_battery_last_reported
data:
days_last_reported: 2
raise_events: false
response_variable: battery_report_response
- action: script.view_road_announcement
data:
channels: "pushover_zorruno"
title: "Weekly Battery Digest"
short_announcement: "Weekly Battery Digest"
long_announcement: >-
{# ------------------------------------------------------------------------- #}
{# Obtain response arrays. #}
{# #}
{# check_battery_battery_low is confirmed from this HA installation. #}
{# Two possible last-reported response keys are accepted defensively. #}
{# ------------------------------------------------------------------------- #}
{% set low_items =
battery_low_response.get(
'check_battery_battery_low',
battery_low_response.get('check_battery_low', [])
) %}
{% set report_items =
battery_report_response.get(
'check_battery_last_reported',
battery_report_response.get(
'check_battery_battery_last_reported',
[]
)
) %}
{# ------------------------------------------------------------------------- #}
{# Resolve the Battery Notes note. #}
{# #}
{# The low action response on this installation does not include "note", #}
{# so fall back to the attributes of Battery Notes entities. #}
{# This lookup occurs ONCE PER WEEK ONLY and is bounded below. #}
{# ------------------------------------------------------------------------- #}
{% macro battery_note(item) -%}
{% set ns =
namespace(
note=item.get('note', '') | default('', true)
) %}
{% if not ns.note %}
{% set item_device_id =
item.get('device_id', '') | default('', true) %}
{% set item_source_id =
item.get('source_entity_id', '') | default('', true) %}
{% for entity_id in integration_entities('battery_notes') %}
{% if not ns.note %}
{% set entity_device_id =
state_attr(entity_id, 'device_id')
| default('', true) %}
{% set entity_source_id =
state_attr(entity_id, 'source_entity_id')
| default('', true) %}
{% if
(item_device_id
and entity_device_id == item_device_id)
or
(item_source_id
and entity_source_id == item_source_id) %}
{% set found_note =
state_attr(entity_id, 'note')
| default('', true) %}
{% if found_note %}
{% set ns.note = found_note %}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{{ ns.note }}
{%- endmacro %}
{# ------------------------------------------------------------------------- #}
{# Build bounded problem lists, excluding Temporary devices. #}
{# ------------------------------------------------------------------------- #}
{% set summary = namespace(low=[], not_reporting=[]) %}
{% for item in low_items[:100] %}
{% set note = battery_note(item) | trim | lower %}
{% if 'temporary' not in note %}
{% set name =
item.get('device_name', 'Unknown Battery Device')
| default('Unknown Battery Device', true) %}
{% set level =
item.get('battery_level', -1)
| default(-1, true)
| float(-1) %}
{% set battery_type =
item.get('battery_type_and_quantity', 'Not recorded')
| default('Not recorded', true) %}
{% if level >= 0 %}
{% set line =
name
~ ' - '
~ (level | round(0) | int)
~ '% - '
~ battery_type %}
{% else %}
{% set line =
name
~ ' - '
~ battery_type %}
{% endif %}
{% set summary.low = summary.low + [line] %}
{% endif %}
{% endfor %}
{% for item in report_items[:100] %}
{% set note = battery_note(item) | trim | lower %}
{% if 'temporary' not in note %}
{% set name =
item.get('device_name', 'Unknown Battery Device')
| default('Unknown Battery Device', true) %}
{% set days =
item.get('battery_last_reported_days', 0)
| default(0, true)
| int(0) %}
{% set last_level =
item.get('battery_last_reported_level', -1)
| default(-1, true)
| float(-1) %}
{% set battery_type =
item.get('battery_type_and_quantity', 'Not recorded')
| default('Not recorded', true) %}
{% if last_level >= 0 %}
{% set line =
name
~ ' - '
~ days
~ ' days - last '
~ (last_level | round(0) | int)
~ '% - '
~ battery_type %}
{% else %}
{% set line =
name
~ ' - '
~ days
~ ' days - '
~ battery_type %}
{% endif %}
{% set summary.not_reporting =
summary.not_reporting + [line] %}
{% endif %}
{% endfor %}
Weekly Battery Digest
{% if summary.low %}
Low Battery: {{ summary.low | count }}
{% for line in summary.low %}
• {{ line }}
{% endfor %}
{% else %}
Low Battery: 0
{% endif %}
{% if summary.not_reporting %}
Not Reporting (2+ days): {{ summary.not_reporting | count }}
{% for line in summary.not_reporting %}
• {{ line }}
{% endfor %}
{% else %}
Not Reporting (2+ days): 0
{% endif %}
{% if not summary.low and not summary.not_reporting %}
All monitored Battery Notes devices are OK.
{% endif %}
+195
View File
@@ -0,0 +1,195 @@
#:########################################################################################:#
# Battery Notes Alerts Package #
#:########################################################################################:#
#
# TITLE:
# Battery Notes Alerts
#
# FILE:
# packages/battery_notes_alerts.yaml
#
# VERSION:
# V1.0.0 2026-08-25
#
# PURPOSE:
# Sends immediate and weekly Pushover alerts for non-temporary Battery Notes devices.
# Device lists are generated from Battery Notes at runtime, so adding a note starts
# monitoring the device without maintaining a second entity list.
#
# CONTROLS:
# - Battery Notes - Immediate Alert and Battery Notes - Weekly Digest are independent
# automation entities. Each can be enabled or disabled in Home Assistant Automations.
#
# DEPENDENCIES:
# - Battery Notes integration
# - packages/view_road_announcement_router.yaml
#
# VERSION HISTORY:
# - V1.0.0 2026-08-25: Initial immediate and Thursday weekly battery alerts.
#
#:########################################################################################:#
automation:
- id: battery_notes_immediate_alert
alias: "Battery Notes - Immediate Alert"
description: >
Sends a Pushover alert when a monitored Battery Notes device newly reports
low battery or becomes unavailable. Temporary Battery Notes devices are ignored.
mode: queued
max: 10
triggers:
- trigger: event
event_type: state_changed
conditions:
- condition: template
value_template: >-
{{ trigger.event.data.entity_id is match('^(sensor|binary_sensor)\\..*_battery_plus(?:_low)?$') }}
- condition: template
value_template: >-
{% set old = trigger.event.data.old_state %}
{% set new = trigger.event.data.new_state %}
{{ old is not none
and new is not none
and old.state not in ['unknown', 'unavailable']
and old.state != new.state }}
- condition: template
value_template: >-
{% set note = trigger.event.data.new_state.attributes.note | default('', true) | lower %}
{{ trigger.event.data.new_state.attributes.device_id is defined
and 'temporary' not in note }}
- condition: template
value_template: >-
{% set old = trigger.event.data.old_state %}
{% set new = trigger.event.data.new_state %}
{% set device_id = new.attributes.device_id %}
{% set is_percentage = new.entity_id.startswith('sensor.') and new.entity_id.endswith('_battery_plus') %}
{% set companion = namespace(percentage=none) %}
{% for item in states %}
{% if item.attributes.device_id | default('') == device_id
and item.entity_id.startswith('sensor.')
and item.entity_id.endswith('_battery_plus') %}
{% set companion.percentage = item %}
{% endif %}
{% endfor %}
{% set source = companion.percentage if companion.percentage else new %}
{% set qualitative_id = source.entity_id
| replace('_battery_plus_low', '_battery_state')
| replace('_battery_plus', '_battery_state') %}
{% set qualitative = states[qualitative_id] %}
{% set qualitative_available = qualitative is not none
and qualitative.state in ['high', 'medium', 'low'] %}
{% set newly_low = (new.attributes.battery_low | default(false))
and not (old.attributes.battery_low | default(false)) %}
{% set newly_binary_low = new.entity_id.startswith('binary_sensor.')
and new.state == 'on' %}
{% set newly_unavailable = new.state in ['unknown', 'unavailable']
and source.entity_id == new.entity_id
and not qualitative_available %}
{{ newly_low or newly_binary_low or newly_unavailable }}
variables:
alert_name: >-
{% set new = trigger.event.data.new_state %}
{{ new.attributes.device_name | default(new.attributes.friendly_name, true) | default(new.name, true) }}
alert_type: >-
{% set new = trigger.event.data.new_state %}
{{ new.attributes.battery_type_and_quantity | default('Not recorded', true) }}
alert_level: >-
{% set new = trigger.event.data.new_state %}
{% set device_id = new.attributes.device_id %}
{% set level = namespace(value=none) %}
{% for item in states %}
{% if item.attributes.device_id | default('') == device_id
and item.entity_id.startswith('sensor.')
and item.entity_id.endswith('_battery_plus')
and item.state not in ['unknown', 'unavailable'] %}
{% set level.value = item.state %}
{% endif %}
{% endfor %}
{{ level.value ~ '%' if level.value is not none else '' }}
alert_line: >-
{% set new = trigger.event.data.new_state %}
{% if new.state in ['unknown', 'unavailable'] %}
New Alert: {{ alert_name }}, Battery Unavailable {{ alert_type }}
{% else %}
New Alert: {{ alert_name }}, Battery Low {{ alert_level }} {{ alert_type }}
{% endif %}
alert_summary: &battery_notes_summary >-
{% set monitored = namespace(device_ids=[], low=[], unavailable=[]) %}
{% for item in states %}
{% set note = item.attributes.note | default('', true) | lower %}
{% set device_id = item.attributes.device_id | default('') %}
{% if item.entity_id is match('^(sensor|binary_sensor)\\..*_battery_plus(?:_low)?$')
and device_id
and 'temporary' not in note
and device_id not in monitored.device_ids %}
{% set monitored.device_ids = monitored.device_ids + [device_id] %}
{% set device = namespace(percentage=none, low_sensor=none) %}
{% for candidate in states %}
{% if candidate.attributes.device_id | default('') == device_id %}
{% if candidate.entity_id.startswith('sensor.') and candidate.entity_id.endswith('_battery_plus') %}
{% set device.percentage = candidate %}
{% elif candidate.entity_id.startswith('binary_sensor.') and candidate.entity_id.endswith('_battery_plus_low') %}
{% set device.low_sensor = candidate %}
{% endif %}
{% endif %}
{% endfor %}
{% set source = device.percentage if device.percentage else device.low_sensor %}
{% set qualitative_id = source.entity_id
| replace('_battery_plus_low', '_battery_state')
| replace('_battery_plus', '_battery_state') %}
{% set qualitative = states[qualitative_id] %}
{% set qualitative_state = qualitative.state if qualitative is not none else '' %}
{% set name = source.attributes.device_name
| default(source.attributes.friendly_name, true)
| default(source.name, true) %}
{% set battery_type = source.attributes.battery_type_and_quantity | default('Not recorded', true) %}
{% set low = source.attributes.battery_low | default(false)
or (device.low_sensor is not none and device.low_sensor.state == 'on')
or qualitative_state == 'low' %}
{% set unavailable = source.state in ['unknown', 'unavailable']
and qualitative_state not in ['high', 'medium', 'low'] %}
{% if low %}
{% set level = source.state ~ '%' if device.percentage is not none and source.state not in ['unknown', 'unavailable'] else '' %}
{% set monitored.low = monitored.low + [name ~ ' (' ~ level ~ (', ' if level else '') ~ battery_type ~ ')'] %}
{% endif %}
{% if unavailable %}
{% set monitored.unavailable = monitored.unavailable + [name ~ ' (' ~ battery_type ~ ')'] %}
{% endif %}
{% endif %}
{% endfor %}
Summary: {{ monitored.device_ids | count }} Battery Notes devices, {{ monitored.unavailable | count }} unavailable, {{ monitored.low | count }} battery low.
Low Battery Devices: {{ monitored.low | join(', ') if monitored.low else 'None' }}
Unavailable Devices: {{ monitored.unavailable | join(', ') if monitored.unavailable else 'None' }}
actions:
- action: script.view_road_announcement
data:
channels: "pushover_zorruno"
title: "Battery Alert"
short_announcement: "{{ alert_line }}"
long_announcement: >-
{{ alert_line }}
{{ alert_summary }}
- id: battery_notes_weekly_digest
alias: "Battery Notes - Weekly Digest"
description: >
Sends the Battery Notes health summary to Pushover every Thursday at 19:00.
Temporary Battery Notes devices are excluded.
mode: single
triggers:
- trigger: time
at: "19:00:00"
conditions:
- condition: time
weekday:
- thu
variables:
alert_summary: *battery_notes_summary
actions:
- action: script.view_road_announcement
data:
channels: "pushover_zorruno"
title: "Weekly Battery Digest"
short_announcement: "Weekly Battery Digest"
long_announcement: "{{ alert_summary }}"
@@ -0,0 +1,197 @@
#:########################################################################################:#
# Battery Notes Alerts Package #
#:########################################################################################:#
#
# TITLE:
# Battery Notes Alerts
#
# FILE:
# packages/battery_notes_alerts.yaml
#
# VERSION:
# V1.0.1 2026-08-25
#
# PURPOSE:
# Sends immediate and weekly Pushover alerts for non-temporary Battery Notes devices.
# Device lists are generated from Battery Notes at runtime, so adding a note starts
# monitoring the device without maintaining a second entity list.
#
# CONTROLS:
# - Battery Notes - Immediate Alert and Battery Notes - Weekly Digest are independent
# automation entities. Each can be enabled or disabled in Home Assistant Automations.
#
# DEPENDENCIES:
# - Battery Notes integration
# - packages/view_road_announcement_router.yaml
#
# VERSION HISTORY:
# - V1.0.1 2026-08-25: Bound companion lookups and prevent oversized template output.
# - V1.0.0 2026-08-25: Initial immediate and Thursday weekly battery alerts.
#
#:########################################################################################:#
automation:
- id: battery_notes_immediate_alert
alias: "Battery Notes - Immediate Alert"
description: >
Sends a Pushover alert when a monitored Battery Notes device newly reports
low battery or becomes unavailable. Temporary Battery Notes devices are ignored.
mode: single
max_exceeded: silent
triggers:
- trigger: event
event_type: state_changed
conditions:
- condition: template
value_template: >-
{{ trigger.event.data.entity_id is match('^(sensor|binary_sensor)\\..*_battery_plus(?:_low)?$') }}
- condition: template
value_template: >-
{% set old = trigger.event.data.old_state %}
{% set new = trigger.event.data.new_state %}
{{ old is not none
and new is not none
and old.state not in ['unknown', 'unavailable']
and old.state != new.state }}
- condition: template
value_template: >-
{% set new = trigger.event.data.new_state %}
{% set note = new.attributes.note | default('', true) | lower %}
{{ new.attributes.device_id | default('') | bool
and 'temporary' not in note }}
- condition: template
value_template: >-
{%- set old = trigger.event.data.old_state -%}
{%- set new = trigger.event.data.new_state -%}
{%- set device_id = new.attributes.device_id -%}
{%- set companions = namespace(percentage_id='', qualitative_id='') -%}
{%- for entity_id in device_entities(device_id) -%}
{%- if entity_id.startswith('sensor.') and entity_id.endswith('_battery_plus') -%}
{%- set companions.percentage_id = entity_id -%}
{%- elif entity_id.startswith('sensor.') and entity_id.endswith('_battery_state') -%}
{%- set companions.qualitative_id = entity_id -%}
{%- endif -%}
{%- endfor -%}
{%- set source_id = companions.percentage_id or new.entity_id -%}
{%- set qualitative_available = companions.qualitative_id
and states(companions.qualitative_id) in ['high', 'medium', 'low'] -%}
{%- set newly_low = new.attributes.battery_low | default(false)
and not old.attributes.battery_low | default(false) -%}
{%- set newly_binary_low = new.entity_id.startswith('binary_sensor.')
and new.state == 'on' -%}
{%- set newly_unavailable = new.state in ['unknown', 'unavailable']
and source_id == new.entity_id
and not qualitative_available -%}
{{ newly_low or newly_binary_low or newly_unavailable }}
variables:
alert_name: >-
{% set new = trigger.event.data.new_state %}
{{ new.attributes.device_name
| default(new.attributes.friendly_name, true)
| default(new.name, true) }}
alert_type: >-
{{ trigger.event.data.new_state.attributes.battery_type_and_quantity
| default('Not recorded', true) }}
alert_level: >-
{%- set device_id = trigger.event.data.new_state.attributes.device_id -%}
{%- set level = namespace(value='') -%}
{%- for entity_id in device_entities(device_id) -%}
{%- if entity_id.startswith('sensor.')
and entity_id.endswith('_battery_plus')
and states(entity_id) not in ['unknown', 'unavailable'] -%}
{%- set level.value = states(entity_id) -%}
{%- endif -%}
{%- endfor -%}
{{ level.value ~ '%' if level.value else '' }}
alert_line: >-
{% if trigger.event.data.new_state.state in ['unknown', 'unavailable'] %}
New Alert: {{ alert_name }}, Battery Unavailable {{ alert_type }}
{% else %}
New Alert: {{ alert_name }}, Battery Low {{ alert_level }} {{ alert_type }}
{% endif %}
alert_summary: &battery_notes_summary >-
{%- set monitored = namespace(device_ids=[], low=[], unavailable=[]) -%}
{%- for item in states -%}
{%- set note = item.attributes.note | default('', true) | lower -%}
{%- set device_id = item.attributes.device_id | default('') -%}
{%- if item.entity_id is match('^(sensor|binary_sensor)\\..*_battery_plus(?:_low)?$')
and device_id
and 'temporary' not in note
and device_id not in monitored.device_ids -%}
{%- set monitored.device_ids = monitored.device_ids + [device_id] -%}
{%- endif -%}
{%- endfor -%}
{%- for device_id in monitored.device_ids -%}
{%- set device = namespace(percentage_id='', low_id='', qualitative_id='') -%}
{%- for entity_id in device_entities(device_id) -%}
{%- if entity_id.startswith('sensor.') and entity_id.endswith('_battery_plus') -%}
{%- set device.percentage_id = entity_id -%}
{%- elif entity_id.startswith('binary_sensor.') and entity_id.endswith('_battery_plus_low') -%}
{%- set device.low_id = entity_id -%}
{%- elif entity_id.startswith('sensor.') and entity_id.endswith('_battery_state') -%}
{%- set device.qualitative_id = entity_id -%}
{%- endif -%}
{%- endfor -%}
{%- set source_id = device.percentage_id or device.low_id -%}
{%- set source_state = states(source_id) -%}
{%- set qualitative_state = states(device.qualitative_id) if device.qualitative_id else '' -%}
{%- set name = state_attr(source_id, 'device_name')
or state_attr(source_id, 'friendly_name')
or source_id -%}
{%- set battery_type = state_attr(source_id, 'battery_type_and_quantity')
or 'Not recorded' -%}
{%- set low = state_attr(source_id, 'battery_low') | default(false, true)
or (device.low_id and is_state(device.low_id, 'on'))
or qualitative_state == 'low' -%}
{%- set unavailable = source_state in ['unknown', 'unavailable']
and qualitative_state not in ['high', 'medium', 'low'] -%}
{%- if low -%}
{%- set level = source_state ~ '%'
if device.percentage_id and source_state not in ['unknown', 'unavailable']
else '' -%}
{%- set monitored.low = monitored.low
+ [name ~ ' (' ~ level ~ (', ' if level else '') ~ battery_type ~ ')'] -%}
{%- endif -%}
{%- if unavailable -%}
{%- set monitored.unavailable = monitored.unavailable
+ [name ~ ' (' ~ battery_type ~ ')'] -%}
{%- endif -%}
{%- endfor -%}
Summary: {{ monitored.device_ids | count }} Battery Notes devices,
{{ monitored.unavailable | count }} unavailable,
{{ monitored.low | count }} battery low.
Low Battery Devices: {{ monitored.low | join(', ') if monitored.low else 'None' }}
Unavailable Devices: {{ monitored.unavailable | join(', ') if monitored.unavailable else 'None' }}
actions:
- action: script.view_road_announcement
data:
channels: "pushover_zorruno"
title: "Battery Alert"
short_announcement: "{{ alert_line }}"
long_announcement: >-
{{ alert_line }}
{{ alert_summary }}
- id: battery_notes_weekly_digest
alias: "Battery Notes - Weekly Digest"
description: >
Sends the Battery Notes health summary to Pushover every Thursday at 19:00.
Temporary Battery Notes devices are excluded.
mode: single
triggers:
- trigger: time
at: "19:00:00"
conditions:
- condition: time
weekday:
- thu
variables:
alert_summary: *battery_notes_summary
actions:
- action: script.view_road_announcement
data:
channels: "pushover_zorruno"
title: "Weekly Battery Digest"
short_announcement: "Weekly Battery Digest"
long_announcement: "{{ alert_summary }}"
+85
View File
@@ -0,0 +1,85 @@
# PACKAGE: Bedroom 3 Climate Controls
#
# Purpose: Configurable automatic heat-pump schedule for Mollie's bedroom.
# Notes: The schedule runs only on enabled weekday and/or weekend day types.
# Version: 2.0.0
#
#:########################################################################################:#
input_boolean:
bedroom_3_morning_climate_enabled:
name: "Bedroom 3 Heat Pump Auto Control"
icon: mdi:hvac
bedroom_3_morning_climate_weekdays:
name: "Bedroom 3 Heat Pump Weekdays"
icon: mdi:calendar-week
bedroom_3_morning_climate_weekends:
name: "Bedroom 3 Heat Pump Weekends"
icon: mdi:calendar-weekend
input_datetime:
bedroom_3_morning_climate_start_time:
name: "Bedroom 3 Heat Pump On Time"
icon: mdi:clock-start
has_date: false
has_time: true
bedroom_3_morning_climate_stop_time:
name: "Bedroom 3 Heat Pump Off Time"
icon: mdi:clock-end
has_date: false
has_time: true
#:########################################################################################:#
# Automations
#:########################################################################################:#
automation:
- id: bedroom_3_morning_climate_start
alias: "Bedroom 3 Heat Pump Auto Start"
description: "Starts the heat pump at the selected time on enabled day types."
triggers:
- trigger: time_pattern
minutes: "/1"
conditions:
- condition: state
entity_id: input_boolean.bedroom_3_morning_climate_enabled
state: "on"
- condition: template
value_template: >-
{{ now().strftime('%H:%M') ==
states('input_datetime.bedroom_3_morning_climate_start_time')[:5] }}
- condition: template
value_template: >-
{{ (now().weekday() < 5 and
is_state('input_boolean.bedroom_3_morning_climate_weekdays', 'on')) or
(now().weekday() >= 5 and
is_state('input_boolean.bedroom_3_morning_climate_weekends', 'on')) }}
actions:
- action: climate.turn_on
target:
entity_id: climate.mollie_s_room
- id: bedroom_3_morning_climate_stop
alias: "Bedroom 3 Heat Pump Auto Stop"
description: "Stops the heat pump at the selected time on enabled day types."
triggers:
- trigger: time_pattern
minutes: "/1"
conditions:
- condition: template
value_template: >-
{{ now().strftime('%H:%M') ==
states('input_datetime.bedroom_3_morning_climate_stop_time')[:5] }}
- condition: template
value_template: >-
{{ (now().weekday() < 5 and
is_state('input_boolean.bedroom_3_morning_climate_weekdays', 'on')) or
(now().weekday() >= 5 and
is_state('input_boolean.bedroom_3_morning_climate_weekends', 'on')) }}
actions:
- action: climate.turn_off
target:
entity_id: climate.mollie_s_room
+24
View File
@@ -0,0 +1,24 @@
# Package: BYD Lock Button
# Purpose:
# Provides a voice-assistant-safe BYD lock control that can only issue a lock command.
# Notes:
# The icon reflects the live state of the underlying BYD lock. This button has no
# unlock action.
# Version history:
# - 1.0.0: Initial lock-only BYD button.
#
#:########################################################################################:#
# Template Button
#:########################################################################################:#
template:
- button:
- name: "BYD Lock"
unique_id: byd_lock_button
default_entity_id: button.byd_lock
availability: "{{ has_value('lock.atto_3_lock') }}"
icon: >-
{{ 'mdi:lock' if is_state('lock.atto_3_lock', 'locked') else 'mdi:lock-open' }}
press:
- action: lock.lock
target:
entity_id: lock.atto_3_lock
+37 -4
View File
@@ -1,8 +1,8 @@
#:########################################################################################:# #:########################################################################################:#
# Package: CCTV Motion Feed # Package: CCTV Motion Feed
# File: cctv_controls.yaml # File: cctv_controls.yaml
# Version: v1.1.0 # Version: v1.2.0
# Date: 2026-07-24 # Date: 2026-08-06
# #
# Purpose: # Purpose:
# Provides a Home Assistant binary sensor for front-door motion messages # Provides a Home Assistant binary sensor for front-door motion messages
@@ -14,6 +14,7 @@
# #
# #
# Version History: # Version History:
# - v1.2.0 (2026-08-06): Match MotionEye HA plugin ON/OFF MQTT payloads.
# - v1.1.0 (2026-07-24): Added explicit numeric ON/OFF payloads (P013-F039). # - v1.1.0 (2026-07-24): Added explicit numeric ON/OFF payloads (P013-F039).
# - v1.0.0 (2022-10-11): Initial CCTV front-door motion sensor package. # - v1.0.0 (2022-10-11): Initial CCTV front-door motion sensor package.
#:########################################################################################:# #:########################################################################################:#
@@ -23,6 +24,38 @@ mqtt:
- unique_id: front_door_movement_camera_corner - unique_id: front_door_movement_camera_corner
name: "Front Door Movement" name: "Front Door Movement"
state_topic: "viewroad-status/cctv/front_door_motion" state_topic: "viewroad-status/cctv/front_door_motion"
payload_on: "1" payload_on: "ON"
payload_off: "0" payload_off: "OFF"
icon: mdi:exit-run icon: mdi:exit-run
input_datetime:
entranceway_last_detection:
name: Entranceway Last Detection
has_date: true
has_time: true
template:
- binary_sensor:
- name: Entranceway Detection
unique_id: entranceway_detection
state: >-
{{ is_state('binary_sensor.view_road_front_door_motion', 'on')
or is_state('binary_sensor.front_door_movement', 'on') }}
automation:
- id: entranceway_detection_last_seen
alias: Entranceway Detection - Record Last Activity
mode: restart
trigger:
- platform: state
entity_id: binary_sensor.view_road_front_door_motion
to: "on"
- platform: mqtt
topic: viewroad-status/cctv/front_door_motion
payload: "ON"
action:
- service: input_datetime.set_datetime
target:
entity_id: input_datetime.entranceway_last_detection
data:
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
+941
View File
@@ -0,0 +1,941 @@
################################################################################
# PACKAGE: Energy Cost Monitoring
################################################################################
#
# Version: 1.3.1
# Date: 2026-09-01
#
# Purpose:
# Track GST-inclusive electricity costs for the three-phase grid supply and
# every appliance configured on the Energy dashboard.
#
# Notes:
# - Charged period: 00:00 to 21:00.
# - Free period: 21:00 to 00:00.
# - Bill rates are GST-exclusive; calculated costs include 15% GST.
# - Fixed daily charges are kept separate from appliance costs.
# - Integral sensors use the left method so tariff changes are applied at the
# boundary without averaging the old and new rates.
#
# Version history:
# 1.3.1 (2026-09-01): Move Downstairs Fridge and Freezer shared power-cost
# telemetry from X41PP to X68PM.
# 1.3.0 (2026-08-24): Add Drinks Fridge and Vertical Freezer energy and
# cost monitoring.
# 1.2.0 (2026-08-23): Add Downstairs Lounge North GPO (X48PM) energy and
# cost monitoring.
# 1.1.1 (2026-08-22): Use the Zigbee X31PP plug for Main Fridge power costs.
# 1.1.0 (2026-08-17): Add sorted daily, weekly, and monthly appliance ranking
# meters and dynamic chart scales.
# 1.0.0 (2026-08-17): Initial tariff, grid, appliance, and fixed-cost tracking.
#
#:########################################################################################:#
# TARIFF HELPERS
#:########################################################################################:#
input_number:
electricity_paid_rate_ex_gst:
name: Electricity Paid Rate Ex GST
icon: mdi:cash
initial: 0.289
min: 0
max: 2
step: 0.001
mode: box
unit_of_measurement: "NZD/kWh"
electricity_daily_charge_ex_gst:
name: Electricity Daily Charge Ex GST
icon: mdi:calendar-cash
initial: 3.036
min: 0
max: 20
step: 0.001
mode: box
unit_of_measurement: "NZD/day"
#:########################################################################################:#
# TARIFF AND COST-RATE SENSORS
#:########################################################################################:#
template:
- sensor:
- name: Electricity Paid Rate
default_entity_id: sensor.electricity_paid_rate
unique_id: electricity_paid_rate
icon: mdi:cash
unit_of_measurement: "NZD/kWh"
state_class: measurement
state: >-
{{
(
states('input_number.electricity_paid_rate_ex_gst')
| float(0.289)
* 1.15
) | round(5)
}}
attributes:
gst_rate: 0.15
rate_ex_gst: >-
{{
states('input_number.electricity_paid_rate_ex_gst')
| float(0.289)
}}
- name: Electricity Current Price
default_entity_id: sensor.electricity_current_price
unique_id: electricity_current_price
icon: mdi:currency-usd
unit_of_measurement: "NZD/kWh"
state_class: measurement
state: >-
{% set minute = now().hour * 60 + now().minute %}
{{
0
if minute >= 21 * 60
else states('sensor.electricity_paid_rate') | float(0.33235)
}}
attributes:
tariff: >-
{{ "free" if now().hour >= 21 else "charged" }}
charged_period: "00:00-21:00"
free_period: "21:00-00:00"
gst_included: true
- name: Electricity Daily Charge
default_entity_id: sensor.electricity_daily_charge
unique_id: electricity_daily_charge
icon: mdi:calendar-cash
unit_of_measurement: "NZD/day"
state_class: measurement
state: >-
{{
(
states('input_number.electricity_daily_charge_ex_gst')
| float(3.036)
* 1.15
) | round(5)
}}
attributes:
gst_rate: 0.15
charge_ex_gst: >-
{{
states('input_number.electricity_daily_charge_ex_gst')
| float(3.036)
}}
- name: Electricity Grid Cost Rate
default_entity_id: sensor.electricity_grid_cost_rate
unique_id: electricity_grid_cost_rate
icon: mdi:transmission-tower
unit_of_measurement: "NZD/h"
state_class: measurement
availability: >-
{{
has_value('sensor.main_house_3_phase_power_monitor_main_power_red_active_power')
and has_value('sensor.main_house_3_phase_power_monitor_main_power_yellow_active_power')
and has_value('sensor.main_house_3_phase_power_monitor_main_power_blue_active_power')
}}
state: >-
{% set red = states('sensor.main_house_3_phase_power_monitor_main_power_red_active_power') | float(0) %}
{% set yellow = states('sensor.main_house_3_phase_power_monitor_main_power_yellow_active_power') | float(0) %}
{% set blue = states('sensor.main_house_3_phase_power_monitor_main_power_blue_active_power') | float(0) %}
{% set price = states('sensor.electricity_current_price') | float(0) %}
{{ ([red + yellow + blue, 0] | max / 1000 * price) | round(6) }}
- name: Energy Cost 16A EV Wallcharger Rate
default_entity_id: sensor.energy_cost_16a_ev_wallcharger_rate
unique_id: energy_cost_16a_ev_wallcharger_rate
unit_of_measurement: "NZD/h"
state_class: measurement
availability: "{{ has_value('sensor.garage_db_control_16a_wallcharger_power') }}"
state: >-
{{
(
states('sensor.garage_db_control_16a_wallcharger_power') | float(0)
/ 1000
* states('sensor.electricity_current_price') | float(0)
) | round(6)
}}
- name: Energy Cost 32A EV Charger Rate
default_entity_id: sensor.energy_cost_32a_ev_charger_rate
unique_id: energy_cost_32a_ev_charger_rate
unit_of_measurement: "NZD/h"
state_class: measurement
availability: "{{ has_value('sensor.main_house_3_phase_power_monitor_32a_wallcharger_power') }}"
state: >-
{{
(
states('sensor.main_house_3_phase_power_monitor_32a_wallcharger_power') | float(0)
/ 1000
* states('sensor.electricity_current_price') | float(0)
) | round(6)
}}
- name: Energy Cost Underhouse Dehumidifier Rate
default_entity_id: sensor.energy_cost_underhouse_dehumidifier_rate
unique_id: energy_cost_underhouse_dehumidifier_rate
unit_of_measurement: "NZD/h"
state_class: measurement
availability: "{{ has_value('sensor.underhouse_dehumidifier_power_plug_x10pp_power') }}"
state: >-
{{
(
states('sensor.underhouse_dehumidifier_power_plug_x10pp_power') | float(0)
/ 1000
* states('sensor.electricity_current_price') | float(0)
) | round(6)
}}
- name: Energy Cost Pool Pump Rate
default_entity_id: sensor.energy_cost_pool_pump_rate
unique_id: energy_cost_pool_pump_rate
unit_of_measurement: "NZD/h"
state_class: measurement
availability: "{{ has_value('sensor.esp_poolpumppower_power') }}"
state: >-
{{
(
states('sensor.esp_poolpumppower_power') | float(0)
/ 1000
* states('sensor.electricity_current_price') | float(0)
) | round(6)
}}
- name: Energy Cost Data Cupboard Left Rate
default_entity_id: sensor.energy_cost_data_cupboard_left_rate
unique_id: energy_cost_data_cupboard_left_rate
unit_of_measurement: "NZD/h"
state_class: measurement
availability: "{{ has_value('sensor.esp_datacupboardpowermon_left_power') }}"
state: >-
{{
(
states('sensor.esp_datacupboardpowermon_left_power') | float(0)
/ 1000
* states('sensor.electricity_current_price') | float(0)
) | round(6)
}}
- name: Energy Cost Data Cupboard Right Rate
default_entity_id: sensor.energy_cost_data_cupboard_right_rate
unique_id: energy_cost_data_cupboard_right_rate
unit_of_measurement: "NZD/h"
state_class: measurement
availability: "{{ has_value('sensor.esp_datacupboardpowermon_right_power') }}"
state: >-
{{
(
states('sensor.esp_datacupboardpowermon_right_power') | float(0)
/ 1000
* states('sensor.electricity_current_price') | float(0)
) | round(6)
}}
- name: Energy Cost Laundry Dryer Rate
default_entity_id: sensor.energy_cost_laundry_dryer_rate
unique_id: energy_cost_laundry_dryer_rate
unit_of_measurement: "NZD/h"
state_class: measurement
availability: "{{ has_value('sensor.esp_laundrydrypow_load_power') }}"
state: >-
{{
(
states('sensor.esp_laundrydrypow_load_power') | float(0)
/ 1000
* states('sensor.electricity_current_price') | float(0)
) | round(6)
}}
- name: Energy Cost Laundry Washing Machine Rate
default_entity_id: sensor.energy_cost_laundry_washing_machine_rate
unique_id: energy_cost_laundry_washing_machine_rate
unit_of_measurement: "NZD/h"
state_class: measurement
availability: "{{ has_value('sensor.esp_laundrywashpow_load_power') }}"
state: >-
{{
(
states('sensor.esp_laundrywashpow_load_power') | float(0)
/ 1000
* states('sensor.electricity_current_price') | float(0)
) | round(6)
}}
- name: Energy Cost Main Dishwasher Rate
default_entity_id: sensor.energy_cost_main_dishwasher_rate
unique_id: energy_cost_main_dishwasher_rate
unit_of_measurement: "NZD/h"
state_class: measurement
availability: "{{ has_value('sensor.esp_maindishwasherpower_power') }}"
state: >-
{{
(
states('sensor.esp_maindishwasherpower_power') | float(0)
/ 1000
* states('sensor.electricity_current_price') | float(0)
) | round(6)
}}
- name: Energy Cost 3D Printer Rate
default_entity_id: sensor.energy_cost_3d_printer_rate
unique_id: energy_cost_3d_printer_rate
unit_of_measurement: "NZD/h"
state_class: measurement
availability: "{{ has_value('sensor.esp_3dprinterpow_load_power') }}"
state: >-
{{
(
states('sensor.esp_3dprinterpow_load_power') | float(0)
/ 1000
* states('sensor.electricity_current_price') | float(0)
) | round(6)
}}
- name: Energy Cost Downstairs Dishwasher Rate
default_entity_id: sensor.energy_cost_downstairs_dishwasher_rate
unique_id: energy_cost_downstairs_dishwasher_rate
unit_of_measurement: "NZD/h"
state_class: measurement
availability: "{{ has_value('sensor.esp_downstdishwasher_power') }}"
state: >-
{{
(
states('sensor.esp_downstdishwasher_power') | float(0)
/ 1000
* states('sensor.electricity_current_price') | float(0)
) | round(6)
}}
- name: Energy Cost Main Fridge Rate
default_entity_id: sensor.energy_cost_main_fridge_rate
unique_id: energy_cost_main_fridge_rate
unit_of_measurement: "NZD/h"
state_class: measurement
availability: "{{ has_value('sensor.upstairs_fridge_power_x31pp_power') }}"
state: >-
{{
(
states('sensor.upstairs_fridge_power_x31pp_power') | float(0)
/ 1000
* states('sensor.electricity_current_price') | float(0)
) | round(6)
}}
- name: Energy Cost Downstairs Fridge Rate
default_entity_id: sensor.energy_cost_downstairs_fridge_rate
unique_id: energy_cost_downstairs_fridge_rate
unit_of_measurement: "NZD/h"
state_class: measurement
availability: "{{ has_value('sensor.downstairs_fridge_gpo_power_x68pm_power') }}"
state: >-
{{
(
states('sensor.downstairs_fridge_gpo_power_x68pm_power') | float(0)
/ 1000
* states('sensor.electricity_current_price') | float(0)
) | round(6)
}}
- name: Energy Cost Drinks Fridge Rate
default_entity_id: sensor.energy_cost_drinks_fridge_rate
unique_id: energy_cost_drinks_fridge_rate
unit_of_measurement: "NZD/h"
state_class: measurement
availability: "{{ has_value('sensor.drinks_fridge_power_x59pm_power') }}"
state: >-
{{
(
states('sensor.drinks_fridge_power_x59pm_power') | float(0)
/ 1000
* states('sensor.electricity_current_price') | float(0)
) | round(6)
}}
- name: Energy Cost Vertical Freezer Rate
default_entity_id: sensor.energy_cost_vertical_freezer_rate
unique_id: energy_cost_vertical_freezer_rate
unit_of_measurement: "NZD/h"
state_class: measurement
availability: "{{ has_value('sensor.vertical_freezer_power_x60pm_power') }}"
state: >-
{{
(
states('sensor.vertical_freezer_power_x60pm_power') | float(0)
/ 1000
* states('sensor.electricity_current_price') | float(0)
) | round(6)
}}
- name: Energy Cost Arcade Cabinet 1 Rate
default_entity_id: sensor.energy_cost_arcade_cabinet_1_rate
unique_id: energy_cost_arcade_cabinet_1_rate
unit_of_measurement: "NZD/h"
state_class: measurement
availability: "{{ has_value('sensor.downstairs_flat_arcade_cabinet_1_power') }}"
state: >-
{{
(
states('sensor.downstairs_flat_arcade_cabinet_1_power') | float(0)
/ 1000
* states('sensor.electricity_current_price') | float(0)
) | round(6)
}}
- name: Energy Cost Downstairs Lounge North GPO Rate
default_entity_id: sensor.energy_cost_downstairs_lounge_north_gpo_rate
unique_id: energy_cost_downstairs_lounge_north_gpo_rate
unit_of_measurement: "NZD/h"
state_class: measurement
availability: "{{ has_value('sensor.downstairs_lounge_north_gpo_x48pm_power') }}"
state: >-
{{
(
states('sensor.downstairs_lounge_north_gpo_x48pm_power') | float(0)
/ 1000
* states('sensor.electricity_current_price') | float(0)
) | round(6)
}}
- name: Electricity Total Cost
default_entity_id: sensor.electricity_total_cost
unique_id: electricity_total_cost
icon: mdi:cash-multiple
device_class: monetary
unit_of_measurement: "NZD"
state_class: total_increasing
availability: >-
{{
has_value('sensor.electricity_grid_usage_cost')
and has_value('sensor.electricity_fixed_charges_total')
}}
state: >-
{{
(
states('sensor.electricity_grid_usage_cost') | float(0)
+ states('sensor.electricity_fixed_charges_total') | float(0)
) | round(5)
}}
attributes:
usage_cost: "{{ states('sensor.electricity_grid_usage_cost') | float(0) }}"
fixed_cost: "{{ states('sensor.electricity_fixed_charges_total') | float(0) }}"
gst_included: true
- name: Energy Cost Daily Ranking Max
default_entity_id: sensor.energy_cost_daily_ranking_max
unique_id: energy_cost_daily_ranking_max
unit_of_measurement: "NZD"
state_class: measurement
state: >-
{% set values = [
states('sensor.16a_ev_wallcharger') | float(0),
states('sensor.32a_ev_charger') | float(0),
states('sensor.underhouse_dehumidifier') | float(0),
states('sensor.pool_pump') | float(0),
states('sensor.data_cupboard_left') | float(0),
states('sensor.data_cupboard_right') | float(0),
states('sensor.laundry_dryer') | float(0),
states('sensor.washing_machine') | float(0),
states('sensor.main_dishwasher') | float(0),
states('sensor.3d_printer') | float(0),
states('sensor.downstairs_dishwasher') | float(0),
states('sensor.main_fridge') | float(0),
states('sensor.downstairs_fridge') | float(0),
states('sensor.drinks_fridge') | float(0),
states('sensor.vertical_freezer') | float(0),
states('sensor.arcade_cabinet_1') | float(0),
states('sensor.downstairs_lounge_north_gpo_x48pm') | float(0)
] %}
{{ ([values | max * 1.1, 0.01] | max) | round(5) }}
- name: Energy Cost Weekly Ranking Max
default_entity_id: sensor.energy_cost_weekly_ranking_max
unique_id: energy_cost_weekly_ranking_max
unit_of_measurement: "NZD"
state_class: measurement
state: >-
{% set values = [
states('sensor.16a_ev_wallcharger_2') | float(0),
states('sensor.32a_ev_charger_2') | float(0),
states('sensor.underhouse_dehumidifier_2') | float(0),
states('sensor.pool_pump_2') | float(0),
states('sensor.data_cupboard_left_2') | float(0),
states('sensor.data_cupboard_right_2') | float(0),
states('sensor.laundry_dryer_2') | float(0),
states('sensor.washing_machine_2') | float(0),
states('sensor.main_dishwasher_2') | float(0),
states('sensor.3d_printer_2') | float(0),
states('sensor.downstairs_dishwasher_2') | float(0),
states('sensor.main_fridge_2') | float(0),
states('sensor.downstairs_fridge_2') | float(0),
states('sensor.drinks_fridge_2') | float(0),
states('sensor.vertical_freezer_2') | float(0),
states('sensor.arcade_cabinet_1_2') | float(0),
states('sensor.downstairs_lounge_north_gpo_x48pm_2') | float(0)
] %}
{{ ([values | max * 1.1, 0.01] | max) | round(5) }}
- name: Energy Cost Monthly Ranking Max
default_entity_id: sensor.energy_cost_monthly_ranking_max
unique_id: energy_cost_monthly_ranking_max
unit_of_measurement: "NZD"
state_class: measurement
state: >-
{% set values = [
states('sensor.16a_ev_wallcharger_3') | float(0),
states('sensor.32a_ev_charger_3') | float(0),
states('sensor.underhouse_dehumidifier_3') | float(0),
states('sensor.pool_pump_3') | float(0),
states('sensor.data_cupboard_left_3') | float(0),
states('sensor.data_cupboard_right_3') | float(0),
states('sensor.laundry_dryer_3') | float(0),
states('sensor.washing_machine_3') | float(0),
states('sensor.main_dishwasher_3') | float(0),
states('sensor.3d_printer_3') | float(0),
states('sensor.downstairs_dishwasher_3') | float(0),
states('sensor.main_fridge_3') | float(0),
states('sensor.downstairs_fridge_3') | float(0),
states('sensor.drinks_fridge_3') | float(0),
states('sensor.vertical_freezer_3') | float(0),
states('sensor.arcade_cabinet_1_3') | float(0),
states('sensor.downstairs_lounge_north_gpo_x48pm_3') | float(0)
] %}
{{ ([values | max * 1.1, 0.01] | max) | round(5) }}
- triggers:
- trigger: homeassistant
event: start
- trigger: time
at: "00:00:00"
- trigger: event
event_type: event_template_reloaded
sensor:
- name: Electricity Fixed Charges Total
default_entity_id: sensor.electricity_fixed_charges_total
unique_id: electricity_fixed_charges_total
icon: mdi:calendar-cash
device_class: monetary
unit_of_measurement: "NZD"
state_class: total_increasing
state: >-
{% set rate = states('sensor.electricity_daily_charge') | float(3.4914) %}
{% set last = this.attributes.get('last_charge_date') %}
{% if last %}
{% set days = (now().date() - strptime(last, '%Y-%m-%d').date()).days %}
{{
(
this.state | float(0)
+ ([days, 0] | max) * rate
) | round(5)
}}
{% else %}
{{ rate | round(5) }}
{% endif %}
attributes:
last_charge_date: "{{ now().date() }}"
daily_charge: "{{ states('sensor.electricity_daily_charge') | float(3.4914) }}"
gst_included: true
#:########################################################################################:#
# CUMULATIVE USAGE COSTS
#:########################################################################################:#
sensor:
- platform: integration
source: sensor.electricity_grid_cost_rate
name: Electricity Grid Usage Cost
unique_id: electricity_grid_usage_cost
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
- platform: integration
source: sensor.energy_cost_16a_ev_wallcharger_rate
name: Energy Cost 16A EV Wallcharger
unique_id: energy_cost_16a_ev_wallcharger
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
- platform: integration
source: sensor.energy_cost_32a_ev_charger_rate
name: Energy Cost 32A EV Charger
unique_id: energy_cost_32a_ev_charger
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
- platform: integration
source: sensor.energy_cost_underhouse_dehumidifier_rate
name: Energy Cost Underhouse Dehumidifier
unique_id: energy_cost_underhouse_dehumidifier
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
- platform: integration
source: sensor.energy_cost_pool_pump_rate
name: Energy Cost Pool Pump
unique_id: energy_cost_pool_pump
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
- platform: integration
source: sensor.energy_cost_data_cupboard_left_rate
name: Energy Cost Data Cupboard Left
unique_id: energy_cost_data_cupboard_left
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
- platform: integration
source: sensor.energy_cost_data_cupboard_right_rate
name: Energy Cost Data Cupboard Right
unique_id: energy_cost_data_cupboard_right
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
- platform: integration
source: sensor.energy_cost_laundry_dryer_rate
name: Energy Cost Laundry Dryer
unique_id: energy_cost_laundry_dryer
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
- platform: integration
source: sensor.energy_cost_laundry_washing_machine_rate
name: Energy Cost Laundry Washing Machine
unique_id: energy_cost_laundry_washing_machine
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
- platform: integration
source: sensor.energy_cost_main_dishwasher_rate
name: Energy Cost Main Dishwasher
unique_id: energy_cost_main_dishwasher
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
- platform: integration
source: sensor.energy_cost_3d_printer_rate
name: Energy Cost 3D Printer
unique_id: energy_cost_3d_printer
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
- platform: integration
source: sensor.energy_cost_downstairs_dishwasher_rate
name: Energy Cost Downstairs Dishwasher
unique_id: energy_cost_downstairs_dishwasher
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
- platform: integration
source: sensor.energy_cost_main_fridge_rate
name: Energy Cost Main Fridge
unique_id: energy_cost_main_fridge
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
- platform: integration
source: sensor.energy_cost_downstairs_fridge_rate
name: Energy Cost Downstairs Fridge
unique_id: energy_cost_downstairs_fridge
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
- platform: integration
source: sensor.energy_cost_drinks_fridge_rate
name: Energy Cost Drinks Fridge
unique_id: energy_cost_drinks_fridge
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
- platform: integration
source: sensor.energy_cost_vertical_freezer_rate
name: Energy Cost Vertical Freezer
unique_id: energy_cost_vertical_freezer
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
- platform: integration
source: sensor.energy_cost_arcade_cabinet_1_rate
name: Energy Cost Arcade Cabinet 1
unique_id: energy_cost_arcade_cabinet_1
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
- platform: integration
source: sensor.energy_cost_downstairs_lounge_north_gpo_rate
name: Energy Cost Downstairs Lounge North GPO
unique_id: energy_cost_downstairs_lounge_north_gpo
unit_time: h
method: left
round: 5
max_sub_interval:
minutes: 5
#:########################################################################################:#
# APPLIANCE COST RANKING METERS
#:########################################################################################:#
utility_meter:
energy_cost_daily_16a_ev_wallcharger: &energy_cost_daily_meter
source: sensor.energy_cost_16a_ev_wallcharger
name: 16A EV Wallcharger
cycle: daily
periodically_resetting: false
always_available: true
energy_cost_daily_32a_ev_charger:
<<: *energy_cost_daily_meter
source: sensor.energy_cost_32a_ev_charger
name: 32A EV Charger
energy_cost_daily_underhouse_dehumidifier:
<<: *energy_cost_daily_meter
source: sensor.energy_cost_underhouse_dehumidifier
name: Underhouse Dehumidifier
energy_cost_daily_pool_pump:
<<: *energy_cost_daily_meter
source: sensor.energy_cost_pool_pump
name: Pool Pump
energy_cost_daily_data_cupboard_left:
<<: *energy_cost_daily_meter
source: sensor.energy_cost_data_cupboard_left
name: Data Cupboard Left
energy_cost_daily_data_cupboard_right:
<<: *energy_cost_daily_meter
source: sensor.energy_cost_data_cupboard_right
name: Data Cupboard Right
energy_cost_daily_laundry_dryer:
<<: *energy_cost_daily_meter
source: sensor.energy_cost_laundry_dryer
name: Laundry Dryer
energy_cost_daily_laundry_washing_machine:
<<: *energy_cost_daily_meter
source: sensor.energy_cost_laundry_washing_machine
name: Washing Machine
energy_cost_daily_main_dishwasher:
<<: *energy_cost_daily_meter
source: sensor.energy_cost_main_dishwasher
name: Main Dishwasher
energy_cost_daily_3d_printer:
<<: *energy_cost_daily_meter
source: sensor.energy_cost_3d_printer
name: 3D Printer
energy_cost_daily_downstairs_dishwasher:
<<: *energy_cost_daily_meter
source: sensor.energy_cost_downstairs_dishwasher
name: Downstairs Dishwasher
energy_cost_daily_main_fridge:
<<: *energy_cost_daily_meter
source: sensor.energy_cost_main_fridge
name: Main Fridge
energy_cost_daily_downstairs_fridge:
<<: *energy_cost_daily_meter
source: sensor.energy_cost_downstairs_fridge
name: Downstairs Fridge
energy_cost_daily_drinks_fridge:
<<: *energy_cost_daily_meter
source: sensor.energy_cost_drinks_fridge
name: Drinks Fridge
energy_cost_daily_vertical_freezer:
<<: *energy_cost_daily_meter
source: sensor.energy_cost_vertical_freezer
name: Vertical Freezer
energy_cost_daily_arcade_cabinet_1:
<<: *energy_cost_daily_meter
source: sensor.energy_cost_arcade_cabinet_1
name: Arcade Cabinet 1
energy_cost_daily_downstairs_lounge_north_gpo:
<<: *energy_cost_daily_meter
source: sensor.energy_cost_downstairs_lounge_north_gpo
name: Downstairs Lounge North GPO (X48PM)
energy_cost_weekly_16a_ev_wallcharger: &energy_cost_weekly_meter
source: sensor.energy_cost_16a_ev_wallcharger
name: 16A EV Wallcharger
cycle: weekly
periodically_resetting: false
always_available: true
energy_cost_weekly_32a_ev_charger:
<<: *energy_cost_weekly_meter
source: sensor.energy_cost_32a_ev_charger
name: 32A EV Charger
energy_cost_weekly_underhouse_dehumidifier:
<<: *energy_cost_weekly_meter
source: sensor.energy_cost_underhouse_dehumidifier
name: Underhouse Dehumidifier
energy_cost_weekly_pool_pump:
<<: *energy_cost_weekly_meter
source: sensor.energy_cost_pool_pump
name: Pool Pump
energy_cost_weekly_data_cupboard_left:
<<: *energy_cost_weekly_meter
source: sensor.energy_cost_data_cupboard_left
name: Data Cupboard Left
energy_cost_weekly_data_cupboard_right:
<<: *energy_cost_weekly_meter
source: sensor.energy_cost_data_cupboard_right
name: Data Cupboard Right
energy_cost_weekly_laundry_dryer:
<<: *energy_cost_weekly_meter
source: sensor.energy_cost_laundry_dryer
name: Laundry Dryer
energy_cost_weekly_laundry_washing_machine:
<<: *energy_cost_weekly_meter
source: sensor.energy_cost_laundry_washing_machine
name: Washing Machine
energy_cost_weekly_main_dishwasher:
<<: *energy_cost_weekly_meter
source: sensor.energy_cost_main_dishwasher
name: Main Dishwasher
energy_cost_weekly_3d_printer:
<<: *energy_cost_weekly_meter
source: sensor.energy_cost_3d_printer
name: 3D Printer
energy_cost_weekly_downstairs_dishwasher:
<<: *energy_cost_weekly_meter
source: sensor.energy_cost_downstairs_dishwasher
name: Downstairs Dishwasher
energy_cost_weekly_main_fridge:
<<: *energy_cost_weekly_meter
source: sensor.energy_cost_main_fridge
name: Main Fridge
energy_cost_weekly_downstairs_fridge:
<<: *energy_cost_weekly_meter
source: sensor.energy_cost_downstairs_fridge
name: Downstairs Fridge
energy_cost_weekly_drinks_fridge:
<<: *energy_cost_weekly_meter
source: sensor.energy_cost_drinks_fridge
name: Drinks Fridge
energy_cost_weekly_vertical_freezer:
<<: *energy_cost_weekly_meter
source: sensor.energy_cost_vertical_freezer
name: Vertical Freezer
energy_cost_weekly_arcade_cabinet_1:
<<: *energy_cost_weekly_meter
source: sensor.energy_cost_arcade_cabinet_1
name: Arcade Cabinet 1
energy_cost_weekly_downstairs_lounge_north_gpo:
<<: *energy_cost_weekly_meter
source: sensor.energy_cost_downstairs_lounge_north_gpo
name: Downstairs Lounge North GPO (X48PM)
energy_cost_monthly_16a_ev_wallcharger: &energy_cost_monthly_meter
source: sensor.energy_cost_16a_ev_wallcharger
name: 16A EV Wallcharger
cycle: monthly
periodically_resetting: false
always_available: true
energy_cost_monthly_32a_ev_charger:
<<: *energy_cost_monthly_meter
source: sensor.energy_cost_32a_ev_charger
name: 32A EV Charger
energy_cost_monthly_underhouse_dehumidifier:
<<: *energy_cost_monthly_meter
source: sensor.energy_cost_underhouse_dehumidifier
name: Underhouse Dehumidifier
energy_cost_monthly_pool_pump:
<<: *energy_cost_monthly_meter
source: sensor.energy_cost_pool_pump
name: Pool Pump
energy_cost_monthly_data_cupboard_left:
<<: *energy_cost_monthly_meter
source: sensor.energy_cost_data_cupboard_left
name: Data Cupboard Left
energy_cost_monthly_data_cupboard_right:
<<: *energy_cost_monthly_meter
source: sensor.energy_cost_data_cupboard_right
name: Data Cupboard Right
energy_cost_monthly_laundry_dryer:
<<: *energy_cost_monthly_meter
source: sensor.energy_cost_laundry_dryer
name: Laundry Dryer
energy_cost_monthly_laundry_washing_machine:
<<: *energy_cost_monthly_meter
source: sensor.energy_cost_laundry_washing_machine
name: Washing Machine
energy_cost_monthly_main_dishwasher:
<<: *energy_cost_monthly_meter
source: sensor.energy_cost_main_dishwasher
name: Main Dishwasher
energy_cost_monthly_3d_printer:
<<: *energy_cost_monthly_meter
source: sensor.energy_cost_3d_printer
name: 3D Printer
energy_cost_monthly_downstairs_dishwasher:
<<: *energy_cost_monthly_meter
source: sensor.energy_cost_downstairs_dishwasher
name: Downstairs Dishwasher
energy_cost_monthly_main_fridge:
<<: *energy_cost_monthly_meter
source: sensor.energy_cost_main_fridge
name: Main Fridge
energy_cost_monthly_downstairs_fridge:
<<: *energy_cost_monthly_meter
source: sensor.energy_cost_downstairs_fridge
name: Downstairs Fridge
energy_cost_monthly_drinks_fridge:
<<: *energy_cost_monthly_meter
source: sensor.energy_cost_drinks_fridge
name: Drinks Fridge
energy_cost_monthly_vertical_freezer:
<<: *energy_cost_monthly_meter
source: sensor.energy_cost_vertical_freezer
name: Vertical Freezer
energy_cost_monthly_arcade_cabinet_1:
<<: *energy_cost_monthly_meter
source: sensor.energy_cost_arcade_cabinet_1
name: Arcade Cabinet 1
energy_cost_monthly_downstairs_lounge_north_gpo:
<<: *energy_cost_monthly_meter
source: sensor.energy_cost_downstairs_lounge_north_gpo
name: Downstairs Lounge North GPO (X48PM)
+44
View File
@@ -0,0 +1,44 @@
shell_command:
flounder_refresh_motioneye_snapshots: >-
python3 /config/scripts/flounder_motioneye_snapshots.py
"{{ state_attr('camera.camera2', 'entity_picture') }}"
"{{ state_attr('camera.camera5', 'entity_picture') }}"
"{{ state_attr('camera.camera6', 'entity_picture') }}"
"{{ state_attr('camera.trailer', 'entity_picture') }}"
"{{ state_attr('camera.camera4', 'entity_picture') }}"
flounder_refresh_primary_motioneye_snapshots: >-
python3 /config/scripts/flounder_motioneye_snapshots.py --primary
"{{ state_attr('camera.camera2', 'entity_picture') }}"
"{{ state_attr('camera.camera5', 'entity_picture') }}"
"{{ state_attr('camera.camera6', 'entity_picture') }}"
automation:
- id: flounder_refresh_motioneye_snapshots
alias: "Flounder: Refresh MotionEye snapshots"
description: >-
Sequentially refreshes durable local CCTV previews without loading live
streams in the kiosk browser.
triggers:
- trigger: time_pattern
minutes: "*"
id: tick
- trigger: state
entity_id: binary_sensor.flounder_kiosk_screen_on
from: "off"
to: "on"
id: screen_on
conditions:
- condition: state
entity_id: binary_sensor.flounder_kiosk_screen_on
state: "on"
actions:
- choose:
- conditions:
- condition: template
value_template: >-
{{ trigger.id == 'tick' and now().minute % 5 == 0 }}
sequence:
- action: shell_command.flounder_refresh_motioneye_snapshots
default:
- action: shell_command.flounder_refresh_primary_motioneye_snapshots
mode: single
+374
View File
@@ -0,0 +1,374 @@
################################################################################
# Optimistic state helpers for the Flounder dashboard heat-pump controls.
################################################################################
input_select:
lounge_hvac_requested_mode:
name: Lounge HVAC Requested Mode
options:
- none
- "off"
- heat
- cool
initial: none
flounder_lounge_power_request:
name: Flounder Lounge Power Request
options: [idle, "on", "off"]
initial: idle
flounder_master_bedroom_power_request:
name: Flounder Master Bedroom Power Request
options: [idle, "on", "off"]
initial: idle
flounder_lounge_nanoe_request:
name: Flounder Lounge Nanoe Request
options: [idle, "on", "off"]
initial: idle
flounder_master_bedroom_nanoe_request:
name: Flounder Master Bedroom Nanoe Request
options: [idle, "on", "off"]
initial: idle
flounder_lounge_preset_request:
name: Flounder Lounge Preset Request
options: [idle, none, powerful, quiet]
initial: idle
flounder_master_bedroom_preset_request:
name: Flounder Master Bedroom Preset Request
options: [idle, none, powerful, quiet]
initial: idle
input_number:
lounge_setpoint_ui:
name: Lounge Setpoint UI
min: 16
max: 30
step: 0.5
mode: box
unit_of_measurement: "°C"
icon: mdi:thermometer
input_boolean:
flounder_lounge_setpoint_pending:
name: Flounder Lounge Setpoint Pending
flounder_master_bedroom_setpoint_pending:
name: Flounder Master Bedroom Setpoint Pending
script:
flounder_lounge_power_toggle:
alias: Flounder Lounge Heat Pump Power Toggle
mode: restart
sequence:
- variables:
pending: "{{ states('input_select.flounder_lounge_power_request') }}"
requested_mode: "{{ states('input_select.lounge_hvac_requested_mode') }}"
desired: >-
{% set is_on = requested_mode in ['heat', 'cool'] or pending == 'on' or (pending == 'idle' and not is_state('climate.lounge', 'off')) %}
{{ 'off' if is_on else 'on' }}
- service: input_select.select_option
target:
entity_id: input_select.flounder_lounge_power_request
data:
option: "{{ desired }}"
- service: input_select.select_option
target:
entity_id: input_select.lounge_hvac_requested_mode
data:
option: "{{ 'off' if desired == 'off' else 'none' }}"
- service: "climate.turn_{{ desired }}"
target:
entity_id: climate.lounge
- delay: "00:00:30"
- service: input_select.select_option
target:
entity_id: input_select.flounder_lounge_power_request
data:
option: idle
flounder_master_bedroom_power_toggle:
alias: Flounder Master Bedroom Heat Pump Power Toggle
mode: restart
sequence:
- variables:
pending: "{{ states('input_select.flounder_master_bedroom_power_request') }}"
requested_mode: "{{ states('input_select.master_bedroom_hvac_requested_mode') }}"
desired: >-
{% set is_on = requested_mode in ['heat', 'cool', 'dry'] or pending == 'on' or (pending == 'idle' and not is_state('climate.master_bedroom', 'off')) %}
{{ 'off' if is_on else 'on' }}
- service: input_select.select_option
target:
entity_id: input_select.flounder_master_bedroom_power_request
data:
option: "{{ desired }}"
- service: input_select.select_option
target:
entity_id: input_select.master_bedroom_hvac_requested_mode
data:
option: "{{ 'off' if desired == 'off' else 'none' }}"
- service: "climate.turn_{{ desired }}"
target:
entity_id: climate.master_bedroom
- delay: "00:00:30"
- service: input_select.select_option
target:
entity_id: input_select.flounder_master_bedroom_power_request
data:
option: idle
flounder_lounge_hvac_mode:
alias: Flounder Lounge HVAC Mode
mode: restart
fields:
mode:
description: HVAC mode to request
sequence:
- service: input_select.select_option
target:
entity_id: input_select.lounge_hvac_requested_mode
data:
option: "{{ mode }}"
- choose:
- conditions:
- condition: template
value_template: "{{ mode == 'off' }}"
sequence:
- service: climate.turn_off
target:
entity_id: climate.lounge
default:
- service: climate.set_hvac_mode
target:
entity_id: climate.lounge
data:
hvac_mode: "{{ mode }}"
flounder_lounge_setpoint_adjust:
alias: Flounder Lounge Setpoint Adjust
mode: restart
fields:
step:
description: Setpoint change in degrees C
sequence:
- variables:
current_ui: "{{ states('input_number.lounge_setpoint_ui') | float(0) }}"
current_climate: "{{ state_attr('climate.lounge', 'temperature') | float(22) }}"
current: "{{ current_ui if current_ui >= 16 else current_climate }}"
requested: "{{ [30, [16, current + (step | float(0))] | max] | min | round(1) }}"
- service: input_boolean.turn_on
target:
entity_id: input_boolean.flounder_lounge_setpoint_pending
- service: input_number.set_value
target:
entity_id: input_number.lounge_setpoint_ui
data:
value: "{{ requested }}"
- service: climate.set_temperature
target:
entity_id: climate.lounge
data:
temperature: "{{ requested }}"
- wait_template: >-
{{ (((state_attr('climate.lounge', 'temperature') | float(0)) - (requested | float(0))) | abs) < 0.01 }}
timeout: "00:00:30"
continue_on_timeout: true
- service: input_boolean.turn_off
target:
entity_id: input_boolean.flounder_lounge_setpoint_pending
flounder_master_bedroom_setpoint_adjust:
alias: Flounder Master Bedroom Setpoint Adjust
mode: restart
fields:
step:
description: Setpoint change in degrees C
sequence:
- variables:
current_ui: "{{ states('input_number.master_bedroom_setpoint_ui') | float(0) }}"
current_climate: "{{ state_attr('climate.master_bedroom', 'temperature') | float(22) }}"
current: "{{ current_ui if current_ui >= 16 else current_climate }}"
requested: "{{ [30, [16, current + (step | float(0))] | max] | min | round(1) }}"
- service: input_boolean.turn_on
target:
entity_id: input_boolean.flounder_master_bedroom_setpoint_pending
- service: input_number.set_value
target:
entity_id: input_number.master_bedroom_setpoint_ui
data:
value: "{{ requested }}"
- service: climate.set_temperature
target:
entity_id: climate.master_bedroom
data:
temperature: "{{ requested }}"
- wait_template: >-
{{ (((state_attr('climate.master_bedroom', 'temperature') | float(0)) - (requested | float(0))) | abs) < 0.01 }}
timeout: "00:00:30"
continue_on_timeout: true
- service: input_boolean.turn_off
target:
entity_id: input_boolean.flounder_master_bedroom_setpoint_pending
flounder_lounge_nanoe_toggle:
alias: Flounder Lounge Nanoe Toggle
mode: restart
sequence:
- variables:
pending: "{{ states('input_select.flounder_lounge_nanoe_request') }}"
desired: >-
{% set is_on = pending == 'on' or (pending == 'idle' and is_state('switch.lounge_nanoe', 'on')) %}
{{ 'off' if is_on else 'on' }}
- service: input_select.select_option
target:
entity_id: input_select.flounder_lounge_nanoe_request
data:
option: "{{ desired }}"
- service: "switch.turn_{{ desired }}"
target:
entity_id: switch.lounge_nanoe
- delay: "00:00:30"
- service: input_select.select_option
target:
entity_id: input_select.flounder_lounge_nanoe_request
data:
option: idle
flounder_master_bedroom_nanoe_toggle:
alias: Flounder Master Bedroom Nanoe Toggle
mode: restart
sequence:
- variables:
pending: "{{ states('input_select.flounder_master_bedroom_nanoe_request') }}"
desired: >-
{% set is_on = pending == 'on' or (pending == 'idle' and is_state('switch.master_bedroom_nanoe', 'on')) %}
{{ 'off' if is_on else 'on' }}
- service: input_select.select_option
target:
entity_id: input_select.flounder_master_bedroom_nanoe_request
data:
option: "{{ desired }}"
- service: "switch.turn_{{ desired }}"
target:
entity_id: switch.master_bedroom_nanoe
- delay: "00:00:30"
- service: input_select.select_option
target:
entity_id: input_select.flounder_master_bedroom_nanoe_request
data:
option: idle
flounder_lounge_preset_toggle:
alias: Flounder Lounge Preset Toggle
mode: restart
fields:
preset:
description: Preset to toggle
sequence:
- variables:
pending: "{{ states('input_select.flounder_lounge_preset_request') }}"
current: "{{ pending if pending != 'idle' else (state_attr('climate.lounge', 'preset_mode') or 'none') }}"
desired: "{{ 'none' if current == preset else preset }}"
- service: input_select.select_option
target:
entity_id: input_select.flounder_lounge_preset_request
data:
option: "{{ desired }}"
- service: climate.set_preset_mode
target:
entity_id: climate.lounge
data:
preset_mode: "{{ desired }}"
- delay: "00:00:30"
- service: input_select.select_option
target:
entity_id: input_select.flounder_lounge_preset_request
data:
option: idle
flounder_master_bedroom_preset_toggle:
alias: Flounder Master Bedroom Preset Toggle
mode: restart
fields:
preset:
description: Preset to toggle
sequence:
- variables:
pending: "{{ states('input_select.flounder_master_bedroom_preset_request') }}"
current: "{{ pending if pending != 'idle' else (state_attr('climate.master_bedroom', 'preset_mode') or 'none') }}"
desired: "{{ 'none' if current == preset else preset }}"
- service: input_select.select_option
target:
entity_id: input_select.flounder_master_bedroom_preset_request
data:
option: "{{ desired }}"
- service: climate.set_preset_mode
target:
entity_id: climate.master_bedroom
data:
preset_mode: "{{ desired }}"
- delay: "00:00:30"
- service: input_select.select_option
target:
entity_id: input_select.flounder_master_bedroom_preset_request
data:
option: idle
automation:
- id: flounder_lounge_setpoint_sync
alias: Flounder Lounge Setpoint Sync
mode: restart
trigger:
- platform: state
entity_id: climate.lounge
- platform: homeassistant
event: start
condition:
- condition: state
entity_id: input_boolean.flounder_lounge_setpoint_pending
state: "off"
action:
- variables:
setpoint: "{{ state_attr('climate.lounge', 'temperature') | float(0) }}"
- condition: template
value_template: "{{ setpoint >= 16 }}"
- service: input_number.set_value
target:
entity_id: input_number.lounge_setpoint_ui
data:
value: "{{ setpoint }}"
- id: lounge_hvac_clear_requested_mode
alias: Lounge HVAC - Clear Requested Mode
mode: restart
trigger:
- platform: state
entity_id: climate.lounge
- platform: state
entity_id: input_select.lounge_hvac_requested_mode
to: ["off", heat, cool]
condition:
- condition: template
value_template: "{{ states('input_select.lounge_hvac_requested_mode') != 'none' }}"
action:
- delay: "00:00:03"
- choose:
- conditions: >-
{% set requested = states('input_select.lounge_hvac_requested_mode') %}
{{ requested == states('climate.lounge') }}
sequence:
- service: input_select.select_option
target:
entity_id: input_select.lounge_hvac_requested_mode
data:
option: none
default:
- delay: "00:00:27"
- service: input_select.select_option
target:
entity_id: input_select.lounge_hvac_requested_mode
data:
option: none
+32 -1
View File
@@ -9,7 +9,7 @@
# flounder_kiosk.yaml # flounder_kiosk.yaml
# #
# VERSION: # VERSION:
# V2.0 - 2026-07-28 # V2.3 - 2026-08-02
# #
# PURPOSE: # PURPOSE:
# Provides Home Assistant controls and status entities for the Flounder portrait # Provides Home Assistant controls and status entities for the Flounder portrait
@@ -131,6 +131,21 @@
# - Actual Brightness reports the physical backlight percentage. # - Actual Brightness reports the physical backlight percentage.
# #
# VERSION HISTORY: # VERSION HISTORY:
# V2.3 - 2026-08-02
# - Removed the Home Assistant Display Mode Restoration automation.
# - Prevented startup and availability timing races from republishing an
# incorrect display mode, including unexpected Screen On commands.
# - Display-mode persistence and enforcement remain handled locally by the
# Flounder screen-control service.
#
# V2.2 - 2026-07-30
# - Enforce Screen Lock every ten minutes because the kiosk can incorrectly report
# DPMS off after a restart while the physical panel remains dimly visible.
#
# V2.1 - 2026-07-30
# - Reapply the selected display mode when the kiosk or Home Assistant restarts,
# ensuring its physical panel, touchscreen and screensaver state are restored.
#
# V2.0 - 2026-07-28 # V2.0 - 2026-07-28
# - Replaced the separate Screen and Screensaver switches with one persistent # - Replaced the separate Screen and Screensaver switches with one persistent
# four-option Display Mode select. # four-option Display Mode select.
@@ -169,6 +184,22 @@
#:########################################################################################:# #:########################################################################################:#
mqtt: mqtt:
- button:
name: "Flounder Restart Chrome"
unique_id: flounder_restart_chrome
icon: mdi:google-chrome
device:
identifiers:
- flounder_kiosk
name: "Flounder Kiosk"
manufacturer: "Dell"
model: "Portrait Touchscreen Kiosk"
suggested_area: "Lounge"
command_topic: "viewroad-commands/flounder/chrome/restart"
payload_press: "PRESS"
#:########################################################################################:# #:########################################################################################:#
# Flounder Kiosk Display Mode # # Flounder Kiosk Display Mode #
#:########################################################################################:# #:########################################################################################:#
+12 -3
View File
@@ -2,6 +2,11 @@
# PACKAGE: Free Power Time Automation # # PACKAGE: Free Power Time Automation #
#:########################################################################################:# #:########################################################################################:#
# VERSION: # VERSION:
# 1.2.1 - 2026-08-17
# - Allow the configured end-time trigger to run without waiting for the
# Free Power Time Active template sensor to update at the same instant.
# The inactive-window check now applies only to startup catch-up.
#
# 1.2.0 - 2026-07-24 # 1.2.0 - 2026-07-24
# - Added startup catch-up to end_actions: when HA starts outside the # - Added startup catch-up to end_actions: when HA starts outside the
# free power window, the end actions (chargers off, dehumidifier off) # free power window, the end actions (chargers off, dehumidifier off)
@@ -321,11 +326,15 @@ automation:
triggers: triggers:
- trigger: time - trigger: time
at: input_datetime.free_power_end_time at: input_datetime.free_power_end_time
id: scheduled_end
- trigger: homeassistant - trigger: homeassistant
event: start event: start
id: startup
conditions: conditions:
- condition: state # The scheduled end must not race the template sensor's minute update.
entity_id: binary_sensor.free_power_time_active # Startup catch-up still runs only outside the free power window.
state: "off" - condition: template
value_template: >-
{{ trigger.id == 'scheduled_end' or is_state('binary_sensor.free_power_time_active', 'off') }}
actions: actions:
- action: script.free_power_end_actions - action: script.free_power_end_actions
+980
View File
@@ -0,0 +1,980 @@
#:########################################################################################:#
# PACKAGE: Fridge and Freezer Monitoring
# FILE: packages/fridge_freezer_monitoring.yaml
# VERSION: v1.13.0 2026-09-03
# - Moves Downstairs Freezer temperature, humidity, and battery monitoring
# from X62TS to X70TS.
# - Moves shared Downstairs Fridge and Freezer power monitoring from X41PP to
# the metering-only X68PM GPO.
# - Reports an unavailable Main Kitchen Fridge battery monitor explicitly,
# rather than showing an unqualified issue.
# - Moves Main Kitchen Fridge temperature, humidity, and battery monitoring to
# X65TS and adds X64LS illuminance-based door monitoring.
# - v1.10.0 2026-08-29: Adds Downstairs Freezer temperature, battery, door, humidity, and shared
# Downstairs Fridge/Freezer plug monitoring.
# - Uses a four-hour sustained low-draw power-loss rule for all monitored
# fridges and freezers, including the shared Drinks Fridge/Freezer plug.
# - v1.8.4 2026-08-24: Flags a Drinks Fridge power loss only after six
# continuous hours below 1 W.
# - v1.8.3 2026-08-24: Excludes normal Drinks Fridge compressor-off cycles
# from power alerts.
# - v1.8.2 2026-08-24: Widens Drinks Fridge and Vertical Freezer adaptive
# temperature ceilings to accommodate confirmed normal compressor cycles.
# - v1.8.1 2026-08-24: Excludes normal Vertical Freezer compressor-off cycles
# from power alerts and widens its learned temperature allowance.
# - v1.8.0 2026-08-24: Adds Drinks Fridge and Vertical Freezer plug monitoring.
# - Adds illuminance-based door monitoring for the Downstairs Fridge.
# - Adds Drinks Fridge door detection from its illuminance sensor.
# - Makes an open door an issue after seven minutes and delays temperature
# alerts while the door is open or cooling recovers after closure.
# - v1.5.0 2026-08-22
# - Requires five minutes of sub-1 W draw before reporting Main Kitchen Fridge power loss.
# - Moved Main Kitchen Fridge power monitoring to the Zigbee X31PP plug.
# - v1.3.0 2026-08-17: Uses the calibrated Main Kitchen Fridge temperature entity throughout.
# - Uses the calibrated Main Kitchen Fridge temperature entity throughout.
# - Added adaptive temperature, battery, humidity, and power monitoring for
# the Downstairs Fridge.
# - v1.1.0 2026-08-16: Reduced the adaptive temperature margin and issue persistence time.
#:########################################################################################:#
# PURPOSE:
# Detect missing monitoring, low batteries, loss of Main Fridge power, and
# abnormal warming, including door-aware handling where a sensor is fitted.
#:########################################################################################:#
# BEHAVIOUR:
# - Learns a continuously rolling seven-day average and 95th percentile.
# - Limits each learned threshold with an appliance-specific safety maximum.
# - Measures the temperature change rate over a smoothed 30-minute window.
# - Requires an issue condition to persist for one minute, except Main Kitchen
# Fridge low-power draw, which persists for five minutes.
# - Treats the Main Kitchen, Drinks, and Downstairs Fridges as open at 10 lx or higher. An
# open door is an issue after seven minutes, while temperature alerting
# stays deferred through a 15-minute post-close recovery window.
# - Exposes one issue binary sensor per appliance and one summary sensor.
#:########################################################################################:#
# NOTES:
# - Learned thresholds use the lower of 95th percentile + 0.5 C or the fixed
# safety limit, so a cooling failure cannot gradually become normal.
# - A rise rate of at least 2 C/hour labels an over-temperature condition as
# possible open-door warming; slower warming is labelled loss of cooling.
# - Main Fridge power below 1 W is treated as loss only after five minutes to
# avoid normal compressor-off periods; unplugged or unavailable monitoring
# remains an issue after one minute.
# - Main Fridge X65TS temperature is adjusted by -0.84 C to match the
# reference thermometer.
# - Wine-fridge ESPHome telemetry is intentionally excluded.
#:########################################################################################:#
# DEPENDENCIES:
# - Seven configured fridge/freezer temperature and battery monitor pairs.
# - switch.upstairs_fridge_power_x31pp
# - sensor.upstairs_fridge_power_x31pp_power
# - sensor.downstairs_fridge_gpo_power_x68pm_power
# - switch.drinks_fridge_power_x59pm
# - sensor.drinks_fridge_power_x59pm_power
# - switch.vertical_freezer_power_x60pm
# - sensor.vertical_freezer_power_x60pm_power
# - sensor.drinks_fridge_light_sensor_x35ls_illuminance
# - sensor.downstairs_fridge_light_sensor_x58ls_illuminance
# - sensor.downstairs_freezer_light_sensor_x61ls_illuminance
# - sensor.main_kitchen_fridge_light_sensor_x64ls_illuminance
#:########################################################################################:#
sensor:
#:######################################################################################:#
# MAIN KITCHEN FRIDGE
#:######################################################################################:#
- platform: statistics
name: "Main Kitchen Fridge Temperature Average 7 Days"
unique_id: main_kitchen_fridge_temperature_average_7_days
entity_id: sensor.main_kitchen_fridge_temperature_calibrated
state_characteristic: average_step
max_age:
days: 7
precision: 1
- platform: statistics
name: "Main Kitchen Fridge Temperature 95th Percentile 7 Days"
unique_id: main_kitchen_fridge_temperature_95th_percentile_7_days
entity_id: sensor.main_kitchen_fridge_temperature_calibrated
state_characteristic: percentile
percentile: 95
max_age:
days: 7
precision: 1
- platform: derivative
name: "Main Kitchen Fridge Temperature Change Rate"
unique_id: main_kitchen_fridge_temperature_change_rate
source: sensor.main_kitchen_fridge_temperature_calibrated
unit_time: h
time_window: "00:30:00"
max_sub_interval: "00:05:00"
round: 1
#:######################################################################################:#
# DRINKS FRIDGE
#:######################################################################################:#
- platform: statistics
name: "Drinks Fridge Temperature Average 7 Days"
unique_id: drinks_fridge_temperature_average_7_days
entity_id: sensor.drinks_fridge_environment_lcd_btt01_temperature
state_characteristic: average_step
max_age:
days: 7
precision: 1
- platform: statistics
name: "Drinks Fridge Temperature 95th Percentile 7 Days"
unique_id: drinks_fridge_temperature_95th_percentile_7_days
entity_id: sensor.drinks_fridge_environment_lcd_btt01_temperature
state_characteristic: percentile
percentile: 95
max_age:
days: 7
precision: 1
- platform: derivative
name: "Drinks Fridge Temperature Change Rate"
unique_id: drinks_fridge_temperature_change_rate
source: sensor.drinks_fridge_environment_lcd_btt01_temperature
unit_time: h
time_window: "00:30:00"
max_sub_interval: "00:05:00"
round: 1
#:######################################################################################:#
# DOWNSTAIRS FRIDGE
#:######################################################################################:#
- platform: statistics
name: "Downstairs Fridge Temperature Average 7 Days"
unique_id: downstairs_fridge_temperature_average_7_days
entity_id: sensor.downstairs_fridge_temperature_x43ts_temperature
state_characteristic: average_step
max_age:
days: 7
precision: 1
- platform: statistics
name: "Downstairs Fridge Temperature 95th Percentile 7 Days"
unique_id: downstairs_fridge_temperature_95th_percentile_7_days
entity_id: sensor.downstairs_fridge_temperature_x43ts_temperature
state_characteristic: percentile
percentile: 95
max_age:
days: 7
precision: 1
- platform: derivative
name: "Downstairs Fridge Temperature Change Rate"
unique_id: downstairs_fridge_temperature_change_rate
source: sensor.downstairs_fridge_temperature_x43ts_temperature
unit_time: h
time_window: "00:30:00"
max_sub_interval: "00:05:00"
round: 1
#:######################################################################################:#
# DOWNSTAIRS FREEZER
#:######################################################################################:#
- platform: statistics
name: "Downstairs Freezer Temperature Average 7 Days"
unique_id: downstairs_freezer_temperature_average_7_days
entity_id: sensor.downstairs_freezer_temperature_x70ts_temperature
state_characteristic: average_step
max_age:
days: 7
precision: 1
- platform: statistics
name: "Downstairs Freezer Temperature 95th Percentile 7 Days"
unique_id: downstairs_freezer_temperature_95th_percentile_7_days
entity_id: sensor.downstairs_freezer_temperature_x70ts_temperature
state_characteristic: percentile
percentile: 95
max_age:
days: 7
precision: 1
- platform: derivative
name: "Downstairs Freezer Temperature Change Rate"
unique_id: downstairs_freezer_temperature_change_rate
source: sensor.downstairs_freezer_temperature_x70ts_temperature
unit_time: h
time_window: "00:30:00"
max_sub_interval: "00:05:00"
round: 1
#:######################################################################################:#
# WINE FRIDGE
#:######################################################################################:#
- platform: statistics
name: "Wine Fridge Temperature Average 7 Days"
unique_id: wine_fridge_temperature_average_7_days
entity_id: sensor.lcd_temp_humidity_btt04_temperature
state_characteristic: average_step
max_age:
days: 7
precision: 1
- platform: statistics
name: "Wine Fridge Temperature 95th Percentile 7 Days"
unique_id: wine_fridge_temperature_95th_percentile_7_days
entity_id: sensor.lcd_temp_humidity_btt04_temperature
state_characteristic: percentile
percentile: 95
max_age:
days: 7
precision: 1
- platform: derivative
name: "Wine Fridge Temperature Change Rate"
unique_id: wine_fridge_temperature_change_rate
source: sensor.lcd_temp_humidity_btt04_temperature
unit_time: h
time_window: "00:30:00"
max_sub_interval: "00:05:00"
round: 1
#:######################################################################################:#
# DRINKS FREEZER
#:######################################################################################:#
- platform: statistics
name: "Drinks Freezer Temperature Average 7 Days"
unique_id: drinks_freezer_temperature_average_7_days
entity_id: sensor.drinks_freezer_environment_lcd_btt02_temperature
state_characteristic: average_step
max_age:
days: 7
precision: 1
- platform: statistics
name: "Drinks Freezer Temperature 95th Percentile 7 Days"
unique_id: drinks_freezer_temperature_95th_percentile_7_days
entity_id: sensor.drinks_freezer_environment_lcd_btt02_temperature
state_characteristic: percentile
percentile: 95
max_age:
days: 7
precision: 1
- platform: derivative
name: "Drinks Freezer Temperature Change Rate"
unique_id: drinks_freezer_temperature_change_rate
source: sensor.drinks_freezer_environment_lcd_btt02_temperature
unit_time: h
time_window: "00:30:00"
max_sub_interval: "00:05:00"
round: 1
#:######################################################################################:#
# VERTICAL FREEZER
#:######################################################################################:#
- platform: statistics
name: "Vertical Freezer Temperature Average 7 Days"
unique_id: vertical_freezer_temperature_average_7_days
entity_id: sensor.lcd_temp_humidity_btt03_temperature
state_characteristic: average_step
max_age:
days: 7
precision: 1
- platform: statistics
name: "Vertical Freezer Temperature 95th Percentile 7 Days"
unique_id: vertical_freezer_temperature_95th_percentile_7_days
entity_id: sensor.lcd_temp_humidity_btt03_temperature
state_characteristic: percentile
percentile: 95
max_age:
days: 7
precision: 1
- platform: derivative
name: "Vertical Freezer Temperature Change Rate"
unique_id: vertical_freezer_temperature_change_rate
source: sensor.lcd_temp_humidity_btt03_temperature
unit_time: h
time_window: "00:30:00"
max_sub_interval: "00:05:00"
round: 1
template:
#:######################################################################################:#
# DOOR DETECTION
#:######################################################################################:#
- binary_sensor:
- name: "Main Kitchen Fridge Door Open"
unique_id: main_kitchen_fridge_door_open
device_class: door
state: >-
{{
has_value('sensor.main_kitchen_fridge_light_sensor_x64ls_illuminance')
and (states('sensor.main_kitchen_fridge_light_sensor_x64ls_illuminance') | float(0)) >= 10
}}
# Retain the grace period after closure so the fridge can start cooling.
- name: "Main Kitchen Fridge Temperature Recovery Grace"
unique_id: main_kitchen_fridge_temperature_recovery_grace
delay_off: "00:15:00"
state: "{{ is_state('binary_sensor.main_kitchen_fridge_door_open', 'on') }}"
- name: "Main Kitchen Fridge Door Open Too Long"
unique_id: main_kitchen_fridge_door_open_too_long
device_class: problem
delay_on: "00:07:00"
state: "{{ is_state('binary_sensor.main_kitchen_fridge_door_open', 'on') }}"
- name: "Drinks Fridge Door Open"
unique_id: drinks_fridge_door_open
device_class: door
state: >-
{{
has_value('sensor.drinks_fridge_light_sensor_x35ls_illuminance')
and (states('sensor.drinks_fridge_light_sensor_x35ls_illuminance') | float(0)) >= 10
}}
# Retain the grace period after closure so the fridge can start cooling.
- name: "Drinks Fridge Temperature Recovery Grace"
unique_id: drinks_fridge_temperature_recovery_grace
delay_off: "00:15:00"
state: "{{ is_state('binary_sensor.drinks_fridge_door_open', 'on') }}"
- name: "Drinks Fridge Door Open Too Long"
unique_id: drinks_fridge_door_open_too_long
device_class: problem
delay_on: "00:07:00"
state: "{{ is_state('binary_sensor.drinks_fridge_door_open', 'on') }}"
- name: "Downstairs Fridge Door Open"
unique_id: downstairs_fridge_door_open
device_class: door
state: >-
{{
has_value('sensor.downstairs_fridge_light_sensor_x58ls_illuminance')
and (states('sensor.downstairs_fridge_light_sensor_x58ls_illuminance') | float(0)) >= 10
}}
# Retain the grace period after closure so the fridge can start cooling.
- name: "Downstairs Fridge Temperature Recovery Grace"
unique_id: downstairs_fridge_temperature_recovery_grace
delay_off: "00:15:00"
state: "{{ is_state('binary_sensor.downstairs_fridge_door_open', 'on') }}"
- name: "Downstairs Fridge Door Open Too Long"
unique_id: downstairs_fridge_door_open_too_long
device_class: problem
delay_on: "00:07:00"
state: "{{ is_state('binary_sensor.downstairs_fridge_door_open', 'on') }}"
- name: "Downstairs Freezer Door Open"
unique_id: downstairs_freezer_door_open
device_class: door
state: >-
{{
has_value('sensor.downstairs_freezer_light_sensor_x61ls_illuminance')
and (states('sensor.downstairs_freezer_light_sensor_x61ls_illuminance') | float(0)) >= 10
}}
# Retain the grace period after closure so the freezer can start cooling.
- name: "Downstairs Freezer Temperature Recovery Grace"
unique_id: downstairs_freezer_temperature_recovery_grace
delay_off: "00:15:00"
state: "{{ is_state('binary_sensor.downstairs_freezer_door_open', 'on') }}"
- name: "Downstairs Freezer Door Open Too Long"
unique_id: downstairs_freezer_door_open_too_long
device_class: problem
delay_on: "00:07:00"
state: "{{ is_state('binary_sensor.downstairs_freezer_door_open', 'on') }}"
#:######################################################################################:#
# APPLIANCE ISSUE SENSORS
#:######################################################################################:#
- binary_sensor:
- name: "Main Kitchen Fridge Power Draw Lost"
unique_id: main_kitchen_fridge_power_draw_lost
device_class: problem
delay_on: "04:00:00"
state: >-
{{
is_state('switch.upstairs_fridge_power_x31pp', 'on')
and has_value('sensor.upstairs_fridge_power_x31pp_power')
and (states('sensor.upstairs_fridge_power_x31pp_power') | float(0)) < 1
}}
# A compressor-off interval is normal; measure the uninterrupted idle time
# explicitly so a template reload cannot restore this as an active fault.
- name: "Drinks Fridge Power Draw Lost"
unique_id: drinks_fridge_power_draw_lost
device_class: problem
state: >-
{% set power = states('sensor.drinks_fridge_power_x59pm_power') | float(none) %}
{% set changed = states.sensor.drinks_fridge_power_x59pm_power.last_changed %}
{{
is_state('switch.drinks_fridge_power_x59pm', 'on')
and power is not none
and power < 1
and (now() - changed).total_seconds() >= 4 * 60 * 60
}}
- name: "Downstairs Fridge Power Draw Lost"
unique_id: downstairs_fridge_power_draw_lost
device_class: problem
state: >-
{% set power = states('sensor.downstairs_fridge_gpo_power_x68pm_power') | float(none) %}
{% set changed = states.sensor.downstairs_fridge_gpo_power_x68pm_power.last_changed %}
{{
power is not none
and power < 1
and (now() - changed).total_seconds() >= 4 * 60 * 60
}}
- name: "Vertical Freezer Power Draw Lost"
unique_id: vertical_freezer_power_draw_lost
device_class: problem
state: >-
{% set power = states('sensor.vertical_freezer_power_x60pm_power') | float(none) %}
{% set changed = states.sensor.vertical_freezer_power_x60pm_power.last_changed %}
{{
is_state('switch.vertical_freezer_power_x60pm', 'on')
and power is not none
and power < 1
and (now() - changed).total_seconds() >= 4 * 60 * 60
}}
- name: "Main Kitchen Fridge Issue"
unique_id: main_kitchen_fridge_issue
device_class: problem
delay_on: "00:01:00"
state: >-
{% set temperature_missing = not has_value('sensor.main_kitchen_fridge_temperature_calibrated') %}
{% set battery_missing = not has_value('sensor.main_kitchen_fridge_temperature_x65ts_battery') %}
{% set illuminance_missing = not has_value('sensor.main_kitchen_fridge_light_sensor_x64ls_illuminance') %}
{% set power_missing = not has_value('sensor.upstairs_fridge_power_x31pp_power') %}
{% set percentile = states('sensor.main_kitchen_fridge_temperature_95th_percentile_7_days') | float(none) %}
{% set threshold = [10, percentile + 0.5] | min if percentile is not none else 10 %}
{{
temperature_missing
or illuminance_missing
or battery_missing
or (
not battery_missing
and (states('sensor.main_kitchen_fridge_temperature_x65ts_battery') | float(0)) <= 20
)
or is_state('binary_sensor.main_kitchen_fridge_door_open_too_long', 'on')
or (
not temperature_missing
and not is_state('binary_sensor.main_kitchen_fridge_temperature_recovery_grace', 'on')
and (states('sensor.main_kitchen_fridge_temperature_calibrated') | float(0)) > threshold
)
or states('switch.upstairs_fridge_power_x31pp') != 'on'
or power_missing
or is_state('binary_sensor.main_kitchen_fridge_power_draw_lost', 'on')
}}
attributes:
appliance: "Main Kitchen Fridge"
issues: >-
{% set ns = namespace(items=[]) %}
{% set temperature_missing = not has_value('sensor.main_kitchen_fridge_temperature_calibrated') %}
{% set battery_missing = not has_value('sensor.main_kitchen_fridge_temperature_x65ts_battery') %}
{% set illuminance_missing = not has_value('sensor.main_kitchen_fridge_light_sensor_x64ls_illuminance') %}
{% set power_missing = not has_value('sensor.upstairs_fridge_power_x31pp_power') %}
{% set percentile = states('sensor.main_kitchen_fridge_temperature_95th_percentile_7_days') | float(none) %}
{% set threshold = [10, percentile + 0.5] | min if percentile is not none else 10 %}
{% if temperature_missing %}
{% set ns.items = ns.items + ['Temperature monitor unavailable'] %}
{% endif %}
{% if illuminance_missing %}
{% set ns.items = ns.items + ['Door monitor unavailable'] %}
{% endif %}
{% if battery_missing %}
{% set ns.items = ns.items + ['Battery monitor unavailable'] %}
{% elif (states('sensor.main_kitchen_fridge_temperature_x65ts_battery') | float(0)) <= 20 %}
{% set ns.items = ns.items + ['Low monitor battery'] %}
{% endif %}
{% if states('switch.upstairs_fridge_power_x31pp') in ['unknown', 'unavailable'] or power_missing %}
{% set ns.items = ns.items + ['Power monitoring unavailable'] %}
{% elif not is_state('switch.upstairs_fridge_power_x31pp', 'on') %}
{% set ns.items = ns.items + ['Power switched off'] %}
{% elif is_state('binary_sensor.main_kitchen_fridge_power_draw_lost', 'on') %}
{% set ns.items = ns.items + ['Power lost - no power draw'] %}
{% endif %}
{% if is_state('binary_sensor.main_kitchen_fridge_door_open_too_long', 'on') %}
{% set ns.items = ns.items + ['Door open too long'] %}
{% elif not temperature_missing and is_state('binary_sensor.main_kitchen_fridge_temperature_recovery_grace', 'on') %}
{% set ns.items = ns.items + ['Temperature alert delayed - door recently open'] %}
{% elif not temperature_missing and (states('sensor.main_kitchen_fridge_temperature_calibrated') | float(0)) > threshold %}
{% if (states('sensor.main_kitchen_fridge_temperature_change_rate') | float(0)) >= 2 %}
{% set ns.items = ns.items + ['Warming rapidly - possible open door'] %}
{% else %}
{% set ns.items = ns.items + ['Temperature too high - possible loss of cooling'] %}
{% endif %}
{% endif %}
{{ ns.items }}
temperature: "{{ states('sensor.main_kitchen_fridge_temperature_calibrated') }}"
average_7_days: "{{ states('sensor.main_kitchen_fridge_temperature_average_7_days') }}"
alert_threshold: >-
{% set percentile = states('sensor.main_kitchen_fridge_temperature_95th_percentile_7_days') | float(none) %}
{{ [10, percentile + 0.5] | min if percentile is not none else 10 }}
change_rate: "{{ states('sensor.main_kitchen_fridge_temperature_change_rate') }}"
humidity: "{{ states('sensor.main_kitchen_fridge_temperature_x65ts_humidity') }}"
battery: "{{ states('sensor.main_kitchen_fridge_temperature_x65ts_battery') }}"
power: "{{ states('sensor.upstairs_fridge_power_x31pp_power') }}"
door_open: "{{ is_state('binary_sensor.main_kitchen_fridge_door_open', 'on') }}"
illuminance: "{{ states('sensor.main_kitchen_fridge_light_sensor_x64ls_illuminance') }}"
- name: "Drinks Fridge Issue"
unique_id: drinks_fridge_issue
device_class: problem
delay_on: "00:01:00"
state: >-
{% set temperature_missing = not has_value('sensor.drinks_fridge_environment_lcd_btt01_temperature') %}
{% set battery_missing = not has_value('sensor.drinks_fridge_environment_lcd_btt01_battery') %}
{% set illuminance_missing = not has_value('sensor.drinks_fridge_light_sensor_x35ls_illuminance') %}
{% set power_missing = not has_value('sensor.drinks_fridge_power_x59pm_power') %}
{% set percentile = states('sensor.drinks_fridge_temperature_95th_percentile_7_days') | float(none) %}
{% set threshold = [10, percentile + 3] | min if percentile is not none else 10 %}
{{
temperature_missing
or illuminance_missing
or (states('sensor.drinks_fridge_environment_lcd_btt01_battery') | float(-1)) <= 20
or states('switch.drinks_fridge_power_x59pm') != 'on'
or power_missing
or is_state('binary_sensor.drinks_fridge_power_draw_lost', 'on')
or is_state('binary_sensor.drinks_fridge_door_open_too_long', 'on')
or (
not temperature_missing
and not is_state('binary_sensor.drinks_fridge_temperature_recovery_grace', 'on')
and (states('sensor.drinks_fridge_environment_lcd_btt01_temperature') | float(0)) > threshold
)
}}
attributes:
appliance: "Drinks Fridge"
issues: >-
{% set ns = namespace(items=[]) %}
{% set temperature_missing = not has_value('sensor.drinks_fridge_environment_lcd_btt01_temperature') %}
{% set battery_missing = not has_value('sensor.drinks_fridge_environment_lcd_btt01_battery') %}
{% set illuminance_missing = not has_value('sensor.drinks_fridge_light_sensor_x35ls_illuminance') %}
{% set power_missing = not has_value('sensor.drinks_fridge_power_x59pm_power') %}
{% set percentile = states('sensor.drinks_fridge_temperature_95th_percentile_7_days') | float(none) %}
{% set threshold = [10, percentile + 3] | min if percentile is not none else 10 %}
{% if temperature_missing %}
{% set ns.items = ns.items + ['Temperature monitor unavailable'] %}
{% endif %}
{% if illuminance_missing %}
{% set ns.items = ns.items + ['Door monitor unavailable'] %}
{% endif %}
{% if not battery_missing and (states('sensor.drinks_fridge_environment_lcd_btt01_battery') | float(-1)) <= 20 %}
{% set ns.items = ns.items + ['Low monitor battery'] %}
{% endif %}
{% if states('switch.drinks_fridge_power_x59pm') in ['unknown', 'unavailable'] or power_missing %}
{% set ns.items = ns.items + ['Power monitoring unavailable'] %}
{% elif not is_state('switch.drinks_fridge_power_x59pm', 'on') %}
{% set ns.items = ns.items + ['Power switched off'] %}
{% elif is_state('binary_sensor.drinks_fridge_power_draw_lost', 'on') %}
{% set ns.items = ns.items + ['Power lost - no power draw for 4 hours'] %}
{% endif %}
{% if is_state('binary_sensor.drinks_fridge_door_open_too_long', 'on') %}
{% set ns.items = ns.items + ['Door open too long'] %}
{% elif not temperature_missing and is_state('binary_sensor.drinks_fridge_temperature_recovery_grace', 'on') %}
{% set ns.items = ns.items + ['Temperature alert delayed - door recently open'] %}
{% elif not temperature_missing and (states('sensor.drinks_fridge_environment_lcd_btt01_temperature') | float(0)) > threshold %}
{% if (states('sensor.drinks_fridge_temperature_change_rate') | float(0)) >= 2 %}
{% set ns.items = ns.items + ['Warming rapidly - possible open door'] %}
{% else %}
{% set ns.items = ns.items + ['Temperature too high - possible loss of cooling'] %}
{% endif %}
{% endif %}
{{ ns.items }}
temperature: "{{ states('sensor.drinks_fridge_environment_lcd_btt01_temperature') }}"
average_7_days: "{{ states('sensor.drinks_fridge_temperature_average_7_days') }}"
alert_threshold: >-
{% set percentile = states('sensor.drinks_fridge_temperature_95th_percentile_7_days') | float(none) %}
{{ [10, percentile + 3] | min if percentile is not none else 10 }}
change_rate: "{{ states('sensor.drinks_fridge_temperature_change_rate') }}"
battery: "{{ states('sensor.drinks_fridge_environment_lcd_btt01_battery') }}"
power: "{{ states('sensor.drinks_fridge_power_x59pm_power') }}"
door_open: "{{ is_state('binary_sensor.drinks_fridge_door_open', 'on') }}"
illuminance: "{{ states('sensor.drinks_fridge_light_sensor_x35ls_illuminance') }}"
- name: "Wine Fridge Issue"
unique_id: wine_fridge_issue
device_class: problem
delay_on: "00:01:00"
state: >-
{% set temperature_missing = not has_value('sensor.lcd_temp_humidity_btt04_temperature') %}
{% set battery_missing = not has_value('sensor.lcd_temp_humidity_btt04_battery') %}
{% set percentile = states('sensor.wine_fridge_temperature_95th_percentile_7_days') | float(none) %}
{% set threshold = [9, percentile + 0.5] | min if percentile is not none else 9 %}
{{
temperature_missing
or (states('sensor.lcd_temp_humidity_btt04_battery') | float(-1)) <= 20
or (not temperature_missing and (states('sensor.lcd_temp_humidity_btt04_temperature') | float(0)) > threshold)
}}
attributes:
appliance: "Wine Fridge"
issues: >-
{% set ns = namespace(items=[]) %}
{% set temperature_missing = not has_value('sensor.lcd_temp_humidity_btt04_temperature') %}
{% set battery_missing = not has_value('sensor.lcd_temp_humidity_btt04_battery') %}
{% set percentile = states('sensor.wine_fridge_temperature_95th_percentile_7_days') | float(none) %}
{% set threshold = [9, percentile + 0.5] | min if percentile is not none else 9 %}
{% if temperature_missing %}
{% set ns.items = ns.items + ['Temperature monitor unavailable'] %}
{% endif %}
{% if not battery_missing and (states('sensor.lcd_temp_humidity_btt04_battery') | float(-1)) <= 20 %}
{% set ns.items = ns.items + ['Low monitor battery'] %}
{% endif %}
{% if not temperature_missing and (states('sensor.lcd_temp_humidity_btt04_temperature') | float(0)) > threshold %}
{% if (states('sensor.wine_fridge_temperature_change_rate') | float(0)) >= 2 %}
{% set ns.items = ns.items + ['Warming rapidly - possible open door'] %}
{% else %}
{% set ns.items = ns.items + ['Temperature too high - possible loss of cooling'] %}
{% endif %}
{% endif %}
{{ ns.items }}
temperature: "{{ states('sensor.lcd_temp_humidity_btt04_temperature') }}"
average_7_days: "{{ states('sensor.wine_fridge_temperature_average_7_days') }}"
alert_threshold: >-
{% set percentile = states('sensor.wine_fridge_temperature_95th_percentile_7_days') | float(none) %}
{{ [9, percentile + 0.5] | min if percentile is not none else 9 }}
change_rate: "{{ states('sensor.wine_fridge_temperature_change_rate') }}"
battery: "{{ states('sensor.lcd_temp_humidity_btt04_battery') }}"
- name: "Downstairs Fridge Issue"
unique_id: downstairs_fridge_issue
device_class: problem
delay_on: "00:01:00"
state: >-
{% set temperature_missing = not has_value('sensor.downstairs_fridge_temperature_x43ts_temperature') %}
{% set battery_missing = not has_value('sensor.downstairs_fridge_temperature_x43ts_battery') %}
{% set illuminance_missing = not has_value('sensor.downstairs_fridge_light_sensor_x58ls_illuminance') %}
{% set power_missing = not has_value('sensor.downstairs_fridge_gpo_power_x68pm_power') %}
{% set percentile = states('sensor.downstairs_fridge_temperature_95th_percentile_7_days') | float(none) %}
{% set threshold = [10, percentile + 0.5] | min if percentile is not none else 10 %}
{{
temperature_missing
or illuminance_missing
or (states('sensor.downstairs_fridge_temperature_x43ts_battery') | float(-1)) <= 20
or is_state('binary_sensor.downstairs_fridge_door_open_too_long', 'on')
or (
not temperature_missing
and not is_state('binary_sensor.downstairs_fridge_temperature_recovery_grace', 'on')
and (states('sensor.downstairs_fridge_temperature_x43ts_temperature') | float(0)) > threshold
)
or power_missing
or is_state('binary_sensor.downstairs_fridge_power_draw_lost', 'on')
}}
attributes:
appliance: "Downstairs Fridge"
issues: >-
{% set ns = namespace(items=[]) %}
{% set temperature_missing = not has_value('sensor.downstairs_fridge_temperature_x43ts_temperature') %}
{% set battery_missing = not has_value('sensor.downstairs_fridge_temperature_x43ts_battery') %}
{% set illuminance_missing = not has_value('sensor.downstairs_fridge_light_sensor_x58ls_illuminance') %}
{% set power_missing = not has_value('sensor.downstairs_fridge_gpo_power_x68pm_power') %}
{% set percentile = states('sensor.downstairs_fridge_temperature_95th_percentile_7_days') | float(none) %}
{% set threshold = [10, percentile + 0.5] | min if percentile is not none else 10 %}
{% if temperature_missing %}
{% set ns.items = ns.items + ['Temperature monitor unavailable'] %}
{% endif %}
{% if illuminance_missing %}
{% set ns.items = ns.items + ['Door monitor unavailable'] %}
{% endif %}
{% if not battery_missing and (states('sensor.downstairs_fridge_temperature_x43ts_battery') | float(-1)) <= 20 %}
{% set ns.items = ns.items + ['Low monitor battery'] %}
{% endif %}
{% if power_missing %}
{% set ns.items = ns.items + ['Power monitoring unavailable'] %}
{% elif is_state('binary_sensor.downstairs_fridge_power_draw_lost', 'on') %}
{% set ns.items = ns.items + ['Power lost - no power draw for 4 hours'] %}
{% endif %}
{% if is_state('binary_sensor.downstairs_fridge_door_open_too_long', 'on') %}
{% set ns.items = ns.items + ['Door open too long'] %}
{% elif not temperature_missing and is_state('binary_sensor.downstairs_fridge_temperature_recovery_grace', 'on') %}
{% set ns.items = ns.items + ['Temperature alert delayed - door recently open'] %}
{% elif not temperature_missing and (states('sensor.downstairs_fridge_temperature_x43ts_temperature') | float(0)) > threshold %}
{% if (states('sensor.downstairs_fridge_temperature_change_rate') | float(0)) >= 2 %}
{% set ns.items = ns.items + ['Warming rapidly - possible open door'] %}
{% else %}
{% set ns.items = ns.items + ['Temperature too high - possible loss of cooling'] %}
{% endif %}
{% endif %}
{{ ns.items }}
temperature: "{{ states('sensor.downstairs_fridge_temperature_x43ts_temperature') }}"
humidity: "{{ states('sensor.downstairs_fridge_temperature_x43ts_humidity') }}"
average_7_days: "{{ states('sensor.downstairs_fridge_temperature_average_7_days') }}"
alert_threshold: >-
{% set percentile = states('sensor.downstairs_fridge_temperature_95th_percentile_7_days') | float(none) %}
{{ [10, percentile + 0.5] | min if percentile is not none else 10 }}
change_rate: "{{ states('sensor.downstairs_fridge_temperature_change_rate') }}"
battery: "{{ states('sensor.downstairs_fridge_temperature_x43ts_battery') }}"
power: "{{ states('sensor.downstairs_fridge_gpo_power_x68pm_power') }}"
door_open: "{{ is_state('binary_sensor.downstairs_fridge_door_open', 'on') }}"
illuminance: "{{ states('sensor.downstairs_fridge_light_sensor_x58ls_illuminance') }}"
- name: "Downstairs Freezer Issue"
unique_id: downstairs_freezer_issue
device_class: problem
delay_on: "00:01:00"
state: >-
{% set temperature_missing = not has_value('sensor.downstairs_freezer_temperature_x70ts_temperature') %}
{% set battery_missing = not has_value('sensor.downstairs_freezer_temperature_x70ts_battery') %}
{% set illuminance_missing = not has_value('sensor.downstairs_freezer_light_sensor_x61ls_illuminance') %}
{% set power_missing = not has_value('sensor.downstairs_fridge_gpo_power_x68pm_power') %}
{% set percentile = states('sensor.downstairs_freezer_temperature_95th_percentile_7_days') | float(none) %}
{% set threshold = [-5, percentile + 0.5] | min if percentile is not none else -5 %}
{{
temperature_missing
or illuminance_missing
or (states('sensor.downstairs_freezer_temperature_x70ts_battery') | float(-1)) <= 20
or is_state('binary_sensor.downstairs_freezer_door_open_too_long', 'on')
or (
not temperature_missing
and not is_state('binary_sensor.downstairs_freezer_temperature_recovery_grace', 'on')
and (states('sensor.downstairs_freezer_temperature_x70ts_temperature') | float(0)) > threshold
)
or power_missing
or is_state('binary_sensor.downstairs_fridge_power_draw_lost', 'on')
}}
attributes:
appliance: "Downstairs Freezer"
issues: >-
{% set ns = namespace(items=[]) %}
{% set temperature_missing = not has_value('sensor.downstairs_freezer_temperature_x70ts_temperature') %}
{% set battery_missing = not has_value('sensor.downstairs_freezer_temperature_x70ts_battery') %}
{% set illuminance_missing = not has_value('sensor.downstairs_freezer_light_sensor_x61ls_illuminance') %}
{% set power_missing = not has_value('sensor.downstairs_fridge_gpo_power_x68pm_power') %}
{% set percentile = states('sensor.downstairs_freezer_temperature_95th_percentile_7_days') | float(none) %}
{% set threshold = [-5, percentile + 0.5] | min if percentile is not none else -5 %}
{% if temperature_missing %}
{% set ns.items = ns.items + ['Temperature monitor unavailable'] %}
{% endif %}
{% if illuminance_missing %}
{% set ns.items = ns.items + ['Door monitor unavailable'] %}
{% endif %}
{% if not battery_missing and (states('sensor.downstairs_freezer_temperature_x70ts_battery') | float(-1)) <= 20 %}
{% set ns.items = ns.items + ['Low monitor battery'] %}
{% endif %}
{% if power_missing %}
{% set ns.items = ns.items + ['Shared power monitoring unavailable'] %}
{% elif is_state('binary_sensor.downstairs_fridge_power_draw_lost', 'on') %}
{% set ns.items = ns.items + ['Shared power lost - no power draw for 4 hours'] %}
{% endif %}
{% if is_state('binary_sensor.downstairs_freezer_door_open_too_long', 'on') %}
{% set ns.items = ns.items + ['Door open too long'] %}
{% elif not temperature_missing and is_state('binary_sensor.downstairs_freezer_temperature_recovery_grace', 'on') %}
{% set ns.items = ns.items + ['Temperature alert delayed - door recently open'] %}
{% elif not temperature_missing and (states('sensor.downstairs_freezer_temperature_x70ts_temperature') | float(0)) > threshold %}
{% if (states('sensor.downstairs_freezer_temperature_change_rate') | float(0)) >= 2 %}
{% set ns.items = ns.items + ['Warming rapidly - possible open door'] %}
{% else %}
{% set ns.items = ns.items + ['Temperature too high - possible loss of cooling'] %}
{% endif %}
{% endif %}
{{ ns.items }}
temperature: "{{ states('sensor.downstairs_freezer_temperature_x70ts_temperature') }}"
humidity: "{{ states('sensor.downstairs_freezer_temperature_x70ts_humidity') }}"
average_7_days: "{{ states('sensor.downstairs_freezer_temperature_average_7_days') }}"
alert_threshold: >-
{% set percentile = states('sensor.downstairs_freezer_temperature_95th_percentile_7_days') | float(none) %}
{{ [-5, percentile + 0.5] | min if percentile is not none else -5 }}
change_rate: "{{ states('sensor.downstairs_freezer_temperature_change_rate') }}"
battery: "{{ states('sensor.downstairs_freezer_temperature_x70ts_battery') }}"
power: "{{ states('sensor.downstairs_fridge_gpo_power_x68pm_power') }}"
door_open: "{{ is_state('binary_sensor.downstairs_freezer_door_open', 'on') }}"
illuminance: "{{ states('sensor.downstairs_freezer_light_sensor_x61ls_illuminance') }}"
- name: "Drinks Freezer Issue"
unique_id: drinks_freezer_issue
device_class: problem
delay_on: "00:01:00"
state: >-
{% set temperature_missing = not has_value('sensor.drinks_freezer_environment_lcd_btt02_temperature') %}
{% set battery_missing = not has_value('sensor.drinks_freezer_environment_lcd_btt02_battery') %}
{% set power_missing = not has_value('sensor.drinks_fridge_power_x59pm_power') %}
{% set percentile = states('sensor.drinks_freezer_temperature_95th_percentile_7_days') | float(none) %}
{% set threshold = [-5, percentile + 0.5] | min if percentile is not none else -5 %}
{{
temperature_missing
or (states('sensor.drinks_freezer_environment_lcd_btt02_battery') | float(-1)) <= 20
or states('switch.drinks_fridge_power_x59pm') != 'on'
or power_missing
or is_state('binary_sensor.drinks_fridge_power_draw_lost', 'on')
or (not temperature_missing and (states('sensor.drinks_freezer_environment_lcd_btt02_temperature') | float(0)) > threshold)
}}
attributes:
appliance: "Drinks Freezer"
issues: >-
{% set ns = namespace(items=[]) %}
{% set temperature_missing = not has_value('sensor.drinks_freezer_environment_lcd_btt02_temperature') %}
{% set battery_missing = not has_value('sensor.drinks_freezer_environment_lcd_btt02_battery') %}
{% set power_missing = not has_value('sensor.drinks_fridge_power_x59pm_power') %}
{% set percentile = states('sensor.drinks_freezer_temperature_95th_percentile_7_days') | float(none) %}
{% set threshold = [-5, percentile + 0.5] | min if percentile is not none else -5 %}
{% if temperature_missing %}
{% set ns.items = ns.items + ['Temperature monitor unavailable'] %}
{% endif %}
{% if not battery_missing and (states('sensor.drinks_freezer_environment_lcd_btt02_battery') | float(-1)) <= 20 %}
{% set ns.items = ns.items + ['Low monitor battery'] %}
{% endif %}
{% if states('switch.drinks_fridge_power_x59pm') in ['unknown', 'unavailable'] or power_missing %}
{% set ns.items = ns.items + ['Power monitoring unavailable'] %}
{% elif not is_state('switch.drinks_fridge_power_x59pm', 'on') %}
{% set ns.items = ns.items + ['Power switched off'] %}
{% elif is_state('binary_sensor.drinks_fridge_power_draw_lost', 'on') %}
{% set ns.items = ns.items + ['Power lost - no power draw for 4 hours'] %}
{% endif %}
{% if not temperature_missing and (states('sensor.drinks_freezer_environment_lcd_btt02_temperature') | float(0)) > threshold %}
{% if (states('sensor.drinks_freezer_temperature_change_rate') | float(0)) >= 2 %}
{% set ns.items = ns.items + ['Warming rapidly - possible open door'] %}
{% else %}
{% set ns.items = ns.items + ['Temperature too high - possible loss of cooling'] %}
{% endif %}
{% endif %}
{{ ns.items }}
temperature: "{{ states('sensor.drinks_freezer_environment_lcd_btt02_temperature') }}"
average_7_days: "{{ states('sensor.drinks_freezer_temperature_average_7_days') }}"
alert_threshold: >-
{% set percentile = states('sensor.drinks_freezer_temperature_95th_percentile_7_days') | float(none) %}
{{ [-5, percentile + 0.5] | min if percentile is not none else -5 }}
change_rate: "{{ states('sensor.drinks_freezer_temperature_change_rate') }}"
battery: "{{ states('sensor.drinks_freezer_environment_lcd_btt02_battery') }}"
power: "{{ states('sensor.drinks_fridge_power_x59pm_power') }}"
- name: "Vertical Freezer Issue"
unique_id: vertical_freezer_issue
device_class: problem
delay_on: "00:01:00"
state: >-
{% set temperature_missing = not has_value('sensor.lcd_temp_humidity_btt03_temperature') %}
{% set battery_missing = not has_value('sensor.lcd_temp_humidity_btt03_battery') %}
{% set power_missing = not has_value('sensor.vertical_freezer_power_x60pm_power') %}
{% set percentile = states('sensor.vertical_freezer_temperature_95th_percentile_7_days') | float(none) %}
{% set threshold = [-10, percentile + 6] | min if percentile is not none else -10 %}
{{
temperature_missing
or (states('sensor.lcd_temp_humidity_btt03_battery') | float(-1)) <= 20
or states('switch.vertical_freezer_power_x60pm') != 'on'
or power_missing
or is_state('binary_sensor.vertical_freezer_power_draw_lost', 'on')
or (not temperature_missing and (states('sensor.lcd_temp_humidity_btt03_temperature') | float(0)) > threshold)
}}
attributes:
appliance: "Vertical Freezer"
issues: >-
{% set ns = namespace(items=[]) %}
{% set temperature_missing = not has_value('sensor.lcd_temp_humidity_btt03_temperature') %}
{% set battery_missing = not has_value('sensor.lcd_temp_humidity_btt03_battery') %}
{% set power_missing = not has_value('sensor.vertical_freezer_power_x60pm_power') %}
{% set percentile = states('sensor.vertical_freezer_temperature_95th_percentile_7_days') | float(none) %}
{% set threshold = [-10, percentile + 6] | min if percentile is not none else -10 %}
{% if temperature_missing %}
{% set ns.items = ns.items + ['Temperature monitor unavailable'] %}
{% endif %}
{% if not battery_missing and (states('sensor.lcd_temp_humidity_btt03_battery') | float(-1)) <= 20 %}
{% set ns.items = ns.items + ['Low monitor battery'] %}
{% endif %}
{% if states('switch.vertical_freezer_power_x60pm') in ['unknown', 'unavailable'] or power_missing %}
{% set ns.items = ns.items + ['Power monitoring unavailable'] %}
{% elif not is_state('switch.vertical_freezer_power_x60pm', 'on') %}
{% set ns.items = ns.items + ['Power switched off'] %}
{% elif is_state('binary_sensor.vertical_freezer_power_draw_lost', 'on') %}
{% set ns.items = ns.items + ['Power lost - no power draw for 4 hours'] %}
{% endif %}
{% if not temperature_missing and (states('sensor.lcd_temp_humidity_btt03_temperature') | float(0)) > threshold %}
{% if (states('sensor.vertical_freezer_temperature_change_rate') | float(0)) >= 2 %}
{% set ns.items = ns.items + ['Warming rapidly - possible open door'] %}
{% else %}
{% set ns.items = ns.items + ['Temperature too high - possible loss of cooling'] %}
{% endif %}
{% endif %}
{{ ns.items }}
temperature: "{{ states('sensor.lcd_temp_humidity_btt03_temperature') }}"
average_7_days: "{{ states('sensor.vertical_freezer_temperature_average_7_days') }}"
alert_threshold: >-
{% set percentile = states('sensor.vertical_freezer_temperature_95th_percentile_7_days') | float(none) %}
{{ [-10, percentile + 6] | min if percentile is not none else -10 }}
change_rate: "{{ states('sensor.vertical_freezer_temperature_change_rate') }}"
battery: "{{ states('sensor.lcd_temp_humidity_btt03_battery') }}"
power: "{{ states('sensor.vertical_freezer_power_x60pm_power') }}"
#:######################################################################################:#
# SUMMARY STATUS
#:######################################################################################:#
- sensor:
- name: "Fridge Freezer Monitor Status"
unique_id: fridge_freezer_monitor_status
icon: mdi:fridge-alert-outline
state: >-
{{
'Issue'
if is_state('binary_sensor.main_kitchen_fridge_door_open_too_long', 'on')
or is_state('binary_sensor.drinks_fridge_door_open_too_long', 'on')
or is_state('binary_sensor.downstairs_fridge_door_open_too_long', 'on')
or is_state('binary_sensor.downstairs_freezer_door_open_too_long', 'on')
else 'Open'
if is_state('binary_sensor.main_kitchen_fridge_door_open', 'on')
or is_state('binary_sensor.drinks_fridge_door_open', 'on')
or is_state('binary_sensor.downstairs_fridge_door_open', 'on')
or is_state('binary_sensor.downstairs_freezer_door_open', 'on')
else 'Issue'
if expand(
'binary_sensor.main_kitchen_fridge_issue',
'binary_sensor.drinks_fridge_issue',
'binary_sensor.downstairs_fridge_issue',
'binary_sensor.downstairs_freezer_issue',
'binary_sensor.wine_fridge_issue',
'binary_sensor.drinks_freezer_issue',
'binary_sensor.vertical_freezer_issue'
)
| selectattr('state', 'eq', 'on')
| list
| count > 0
else 'OK'
}}
attributes:
issue_count: >-
{{
expand(
'binary_sensor.main_kitchen_fridge_issue',
'binary_sensor.drinks_fridge_issue',
'binary_sensor.downstairs_fridge_issue',
'binary_sensor.downstairs_freezer_issue',
'binary_sensor.wine_fridge_issue',
'binary_sensor.drinks_freezer_issue',
'binary_sensor.vertical_freezer_issue'
)
| selectattr('state', 'eq', 'on')
| list
| count
}}
open_count: >-
{{
[
is_state('binary_sensor.main_kitchen_fridge_door_open', 'on'),
is_state('binary_sensor.drinks_fridge_door_open', 'on'),
is_state('binary_sensor.downstairs_fridge_door_open', 'on'),
is_state('binary_sensor.downstairs_freezer_door_open', 'on')
]
| select
| list
| count
}}
issues: >-
{% set ns = namespace(items=[]) %}
{% set monitors = [
'binary_sensor.main_kitchen_fridge_issue',
'binary_sensor.drinks_fridge_issue',
'binary_sensor.downstairs_fridge_issue',
'binary_sensor.downstairs_freezer_issue',
'binary_sensor.wine_fridge_issue',
'binary_sensor.drinks_freezer_issue',
'binary_sensor.vertical_freezer_issue'
] %}
{% for monitor in monitors if is_state(monitor, 'on') %}
{% set appliance = state_attr(monitor, 'appliance') or state_attr(monitor, 'friendly_name') %}
{% set reasons = state_attr(monitor, 'issues') or ['Issue detected'] %}
{% for reason in reasons %}
{% set ns.items = ns.items + [appliance ~ ': ' ~ reason] %}
{% endfor %}
{% endfor %}
{{ ns.items }}
+18 -4
View File
@@ -1,15 +1,16 @@
#:########################################################################################:# #:########################################################################################:#
# Package: Refrigerator Temperature MQTT Publishing # Package: Refrigerator Temperature MQTT Publishing
# File: fridge_temperatures.yaml # File: fridge_temperatures.yaml
# Version: v1.0.0 # Version: v1.2.0
# Date: 2024-09-04 # Date: 2026-08-30
# #
# Purpose: # Purpose:
# Publishes refrigerator and freezer temperature changes to household MQTT # Publishes refrigerator and freezer temperature changes to household MQTT
# status topics for use by external displays and consumers. # status topics for use by external displays and consumers.
# #
# Behaviour: # Behaviour:
# - Publishes the source sensor state whenever a valid temperature changes. # - Creates a calibrated main kitchen fridge temperature entity.
# - Publishes each valid temperature entity state whenever it changes.
# - Suppresses unknown and unavailable source states. # - Suppresses unknown and unavailable source states.
# - Covers the drinks fridge, drinks freezer, and main kitchen fridge. # - Covers the drinks fridge, drinks freezer, and main kitchen fridge.
# #
@@ -18,9 +19,22 @@
# - Home Assistant MQTT integration and broker connectivity. # - Home Assistant MQTT integration and broker connectivity.
# #
# Version History: # Version History:
# - v1.2.0 (2026-08-30): Moved the calibrated main fridge source to X65TS.
# - v1.1.0 (2026-08-17): Added the -0.84 C main fridge sensor calibration.
# - v1.0.0 (2024-09-04): Initial refrigerator temperature publishing package. # - v1.0.0 (2024-09-04): Initial refrigerator temperature publishing package.
#:########################################################################################:# #:########################################################################################:#
template:
- sensor:
- name: "Main Kitchen Fridge Temperature Calibrated"
unique_id: main_kitchen_fridge_temperature_calibrated
device_class: temperature
state_class: measurement
unit_of_measurement: "°C"
availability: "{{ has_value('sensor.main_kitchen_fridge_temperature_x65ts_temperature') }}"
state: >-
{{ (states('sensor.main_kitchen_fridge_temperature_x65ts_temperature') | float - 0.84) | round(2) }}
automation: automation:
- id: publish_fridge_temps_drinks_fridge - id: publish_fridge_temps_drinks_fridge
alias: publish_fridge_temps_drinks_fridge alias: publish_fridge_temps_drinks_fridge
@@ -56,7 +70,7 @@ automation:
alias: publish_main_kitchen_fridge_temp_zth08 alias: publish_main_kitchen_fridge_temp_zth08
trigger: trigger:
- platform: state - platform: state
entity_id: sensor.main_kitchen_fridge_temp_zth08 entity_id: sensor.main_kitchen_fridge_temperature_calibrated
not_to: not_to:
- "unavailable" - "unavailable"
- "unknown" - "unknown"
+13 -2
View File
@@ -9,9 +9,12 @@
# packages/garage_corner_light_cctv_motion.yaml # packages/garage_corner_light_cctv_motion.yaml
# #
# VERSION: # VERSION:
# V1.0 2026-06-29 # V1.1 2026-08-31
# #
# VERSION HISTORY: # VERSION HISTORY:
# V1.1 2026-08-31
# - Restrict CCTV motion activation to one hour before sunset through one hour after sunrise.
#
# V1.0 2026-06-29 # V1.0 2026-06-29
# - Initial package. # - Initial package.
# - Turns the garage external corner light on when CCTV motion MQTT payload "1" is received. # - Turns the garage external corner light on when CCTV motion MQTT payload "1" is received.
@@ -20,7 +23,8 @@
# - Adds a Home Assistant enable switch for CCTV movement triggers. # - Adds a Home Assistant enable switch for CCTV movement triggers.
# #
# PURPOSE: # PURPOSE:
# Turns on the garage external corner light from selected CCTV motion MQTT triggers. # Turns on the garage external corner light from selected CCTV motion MQTT triggers
# during the configured night window.
# #
# NOTES: # NOTES:
# - The light is: # - The light is:
@@ -31,6 +35,7 @@
# #
# - Turning OFF "Garage corner light movement triggers" prevents new CCTV triggers. # - Turning OFF "Garage corner light movement triggers" prevents new CCTV triggers.
# - If a timer is already running, it will still finish and turn the light off. # - If a timer is already running, it will still finish and turn the light off.
# - CCTV triggers run from one hour before sunset until one hour after sunrise.
# #
#:########################################################################################:# #:########################################################################################:#
# User Configuration # # User Configuration #
@@ -91,6 +96,12 @@ automation:
entity_id: input_boolean.garage_corner_light_movement_triggers entity_id: input_boolean.garage_corner_light_movement_triggers
state: "on" state: "on"
- condition: sun
after: sunset
after_offset: "-01:00:00"
before: sunrise
before_offset: "01:00:00"
action: action:
- service: switch.turn_on - service: switch.turn_on
target: target:
-63
View File
@@ -1,63 +0,0 @@
#:########################################################################################:#
# Package: Node-RED Health Checks
# File: health_checks.yaml
# Version: v1.2.0
# Date: 2025-08-25
#
# Purpose:
# Monitors the Node-RED Panda instance through independent HTTP and MQTT health
# signals and combines them into one connectivity status.
#
# Behaviour:
# - Polls the HTTP health endpoint every 30 seconds with a three-second timeout.
# - Tracks the MQTT birth/will topic with QoS 1 and a 180-second expiry.
# - Reports the combined check as available when either source is available.
# - Reports the combined check as connected when either source is connected.
#
# Dependencies:
# - Reachability of the configured Node-RED HTTP health endpoint.
# - Home Assistant MQTT integration and the Node-RED birth/will publisher.
#
# Version History:
# - v1.2.0 (2025-08-25): Reduced MQTT expiry to 180 seconds and clarified the
# combined HTTP/MQTT sensor name.
# - v1.1.0 (2025-08-22): Split HTTP and MQTT probes and added a combined sensor.
# - v1.0.0 (2025-08-21): Initial Node-RED health-check package.
#:########################################################################################:#
# ---- HTTP health probe ----
binary_sensor:
- platform: rest
name: Node-RED Panda Up (HTTP)
resource: http://192.168.3.200:1880/healthz
method: GET
device_class: connectivity
value_template: "{{ value_json.status == 'ok' }}"
timeout: 3
scan_interval: 30
# ---- MQTT Will/Birth probe ----
mqtt:
binary_sensor:
- name: "Node-RED Panda Up (MQTT)"
unique_id: "nodered_panda_up_mqtt"
state_topic: "nodered/panda/status"
payload_on: "online"
payload_off: "offline"
device_class: connectivity
qos: 1
# Optional: avoid stale 'online' if no updates arrive
expire_after: 180
# ---- Combine them (true if either is up) ----
template:
- binary_sensor:
- name: "Node-RED Panda Up (HTTP/MQTT)"
unique_id: "nodered_panda_up_combined"
device_class: connectivity
state: >
{{ is_state('binary_sensor.node_red_panda_up_http','on')
or is_state('binary_sensor.node_red_panda_up_mqtt','on') }}
availability: >
{{ states('binary_sensor.node_red_panda_up_http') not in ['unknown','unavailable']
or states('binary_sensor.node_red_panda_up_mqtt') not in ['unknown','unavailable'] }}
@@ -140,6 +140,9 @@ script:
alias: Master Bedroom Setpoint Sync From Climate alias: Master Bedroom Setpoint Sync From Climate
mode: restart mode: restart
sequence: sequence:
- condition: state
entity_id: input_boolean.flounder_master_bedroom_setpoint_pending
state: "off"
- variables: - variables:
sp: "{{ state_attr('climate.master_bedroom', 'temperature') | float(0) }}" sp: "{{ state_attr('climate.master_bedroom', 'temperature') | float(0) }}"
- service: input_number.set_value - service: input_number.set_value
+265
View File
@@ -0,0 +1,265 @@
# Capped Cloudflare throughput tests: 2 MB download and 500 kB upload per run.
command_line:
- sensor:
name: "Internet Speed Test"
unique_id: internet_speed_test
command: "/config/scripts/internet_speed_test.sh"
command_timeout: 120
scan_interval: 31536000
unit_of_measurement: "Mbit/s"
device_class: data_rate
state_class: measurement
value_template: "{{ value_json.download_mbps }}"
json_attributes:
- upload_mbps
- binary_sensor:
name: "Internet Connected"
unique_id: internet_connected
command: >-
curl --fail --silent --show-error --max-time 10 --output /dev/null
https://one.one.one.one/cdn-cgi/trace && printf "ON" || printf "OFF"
command_timeout: 15
scan_interval: 60
payload_on: "ON"
payload_off: "OFF"
device_class: connectivity
input_number:
internet_speed_tests_today:
name: "Internet Speed Tests Today"
min: 0
max: 100
step: 1
mode: box
internet_speed_tests_this_month:
name: "Internet Speed Tests This Month"
min: 0
max: 2000
step: 1
mode: box
internet_download_today_average:
name: "Internet Download Today Average"
min: 0
max: 10000
step: 0.01
mode: box
internet_upload_today_average:
name: "Internet Upload Today Average"
min: 0
max: 10000
step: 0.01
mode: box
internet_download_month_average:
name: "Internet Download Month Average"
min: 0
max: 10000
step: 0.01
mode: box
internet_upload_month_average:
name: "Internet Upload Month Average"
min: 0
max: 10000
step: 0.01
mode: box
input_boolean:
internet_speed_test_running:
name: "Internet Speed Test Running"
script:
internet_run_speed_test:
alias: "Internet: Run speed test"
sequence:
- action: input_boolean.turn_on
target:
entity_id: input_boolean.internet_speed_test_running
- variables:
test_started: "{{ as_timestamp(now()) }}"
- action: homeassistant.update_entity
target:
entity_id: sensor.internet_speed_test
# Ignore the command-line sensor's interim unavailable state.
- wait_template: >-
{{
has_value('sensor.internet_speed_test')
and as_timestamp(states.sensor.internet_speed_test.last_updated, 0) > test_started
}}
timeout:
minutes: 2
continue_on_timeout: true
- delay:
seconds: 3
- action: input_boolean.turn_off
target:
entity_id: input_boolean.internet_speed_test_running
mode: single
template:
- sensor:
- name: "Internet Upload Speed Test"
unique_id: internet_upload_speed_test
unit_of_measurement: "Mbit/s"
device_class: data_rate
state_class: measurement
availability: "{{ state_attr('sensor.internet_speed_test', 'upload_mbps') is number }}"
state: "{{ state_attr('sensor.internet_speed_test', 'upload_mbps') | float }}"
- name: "Internet Last Speed Display"
unique_id: internet_last_speed_display
state: >-
{% if is_state('input_boolean.internet_speed_test_running', 'on') %}
Testing
{% elif has_value('sensor.internet_speed_test') and has_value('sensor.internet_upload_speed_test') %}
{{ states('sensor.internet_speed_test') | float(0) | round(0) | int }}/{{ states('sensor.internet_upload_speed_test') | float(0) | round(0) | int }}
{% else %}
--
{% endif %}
- name: "Internet Download Average Today"
unique_id: internet_download_average_today
unit_of_measurement: "Mbit/s"
device_class: data_rate
state_class: measurement
availability: "{{ states('input_number.internet_speed_tests_today') | int(0) > 0 }}"
state: "{{ states('input_number.internet_download_today_average') | float(0) }}"
- name: "Internet Upload Average Today"
unique_id: internet_upload_average_today
unit_of_measurement: "Mbit/s"
device_class: data_rate
state_class: measurement
availability: "{{ states('input_number.internet_speed_tests_today') | int(0) > 0 }}"
state: "{{ states('input_number.internet_upload_today_average') | float(0) }}"
- name: "Internet Download Average This Month"
unique_id: internet_download_average_this_month
unit_of_measurement: "Mbit/s"
device_class: data_rate
state_class: measurement
availability: "{{ states('input_number.internet_speed_tests_this_month') | int(0) > 0 }}"
state: "{{ states('input_number.internet_download_month_average') | float(0) }}"
- name: "Internet Upload Average This Month"
unique_id: internet_upload_average_this_month
unit_of_measurement: "Mbit/s"
device_class: data_rate
state_class: measurement
availability: "{{ states('input_number.internet_speed_tests_this_month') | int(0) > 0 }}"
state: "{{ states('input_number.internet_upload_month_average') | float(0) }}"
sensor:
- platform: statistics
name: "Internet Download Average Last 7 Days"
unique_id: internet_download_average_last_7_days
entity_id: sensor.internet_speed_test
state_characteristic: mean
sampling_size: 400
max_age:
days: 7
- platform: statistics
name: "Internet Upload Average Last 7 Days"
unique_id: internet_upload_average_last_7_days
entity_id: sensor.internet_upload_speed_test
state_characteristic: mean
sampling_size: 400
max_age:
days: 7
automation:
- id: internet_run_speed_test
alias: "Internet: Run capped speed test"
description: "Runs at a random time between five minutes before and after each half-hour."
triggers:
- trigger: time_pattern
minutes: 25
- trigger: time_pattern
minutes: 55
conditions: []
actions:
- delay:
seconds: "{{ range(0, 601) | random }}"
- action: script.internet_run_speed_test
target:
entity_id: sensor.internet_speed_test
mode: single
- id: internet_record_speed_test
alias: "Internet: Record speed test averages"
description: "Records every successful speed test, including the startup test."
triggers:
- trigger: state
entity_id: sensor.internet_speed_test
conditions: []
actions:
# Allow the upload template sensor to update from the command-line result.
- delay:
seconds: 1
- condition: template
value_template: "{{ has_value('sensor.internet_speed_test') and has_value('sensor.internet_upload_speed_test') }}"
- action: input_number.set_value
target:
entity_id: input_number.internet_download_today_average
data:
value: >-
{{ ((states('input_number.internet_download_today_average') | float(0) * states('input_number.internet_speed_tests_today') | int(0)) + states('sensor.internet_speed_test') | float(0)) / (states('input_number.internet_speed_tests_today') | int(0) + 1) }}
- action: input_number.set_value
target:
entity_id: input_number.internet_upload_today_average
data:
value: >-
{{ ((states('input_number.internet_upload_today_average') | float(0) * states('input_number.internet_speed_tests_today') | int(0)) + states('sensor.internet_upload_speed_test') | float(0)) / (states('input_number.internet_speed_tests_today') | int(0) + 1) }}
- action: input_number.set_value
target:
entity_id: input_number.internet_download_month_average
data:
value: >-
{{ ((states('input_number.internet_download_month_average') | float(0) * states('input_number.internet_speed_tests_this_month') | int(0)) + states('sensor.internet_speed_test') | float(0)) / (states('input_number.internet_speed_tests_this_month') | int(0) + 1) }}
- action: input_number.set_value
target:
entity_id: input_number.internet_upload_month_average
data:
value: >-
{{ ((states('input_number.internet_upload_month_average') | float(0) * states('input_number.internet_speed_tests_this_month') | int(0)) + states('sensor.internet_upload_speed_test') | float(0)) / (states('input_number.internet_speed_tests_this_month') | int(0) + 1) }}
- action: input_number.set_value
target:
entity_id: input_number.internet_speed_tests_today
data:
value: "{{ states('input_number.internet_speed_tests_today') | int(0) + 1 }}"
- action: input_number.set_value
target:
entity_id: input_number.internet_speed_tests_this_month
data:
value: "{{ states('input_number.internet_speed_tests_this_month') | int(0) + 1 }}"
mode: queued
- id: internet_reset_daily_speed_averages
alias: "Internet: Reset daily speed averages"
triggers:
- trigger: time
at: "00:00:00"
conditions: []
actions:
- action: input_number.set_value
target:
entity_id:
- input_number.internet_speed_tests_today
- input_number.internet_download_today_average
- input_number.internet_upload_today_average
data:
value: 0
mode: single
- id: internet_reset_monthly_speed_averages
alias: "Internet: Reset monthly speed averages"
triggers:
- trigger: time
at: "00:00:00"
conditions:
- condition: template
value_template: "{{ now().day == 1 }}"
actions:
- action: input_number.set_value
target:
entity_id:
- input_number.internet_speed_tests_this_month
- input_number.internet_download_month_average
- input_number.internet_upload_month_average
data:
value: 0
mode: single
@@ -0,0 +1,109 @@
#:########################################################################################:#
# Jobs Tracker Appliance Completion Triggers Package #
#:########################################################################################:#
#
# TITLE:
# Jobs Tracker Appliance Completion Triggers
#
# FILE:
# jobs_tracker_appliance_completion_triggers.yaml
#
# VERSION:
# V1.1 2026-09-03
#
# PURPOSE:
# Provides time-qualified appliance door and lid triggers for Jobs Tracker completion.
#
# DEPENDS ON:
# packages/Job_Chore_Tracking/jobs_tracker_jobs.yaml V3.5 or newer.
#
# OPERATION:
# Each contact must remain open for five seconds before its corresponding Jobs Tracker
# completion route is called. Starting the dryer or washer clears only a previous Due
# unload job, so the next finished cycle creates its own new unload task.
#
# VERSION HISTORY:
# V1.1 2026-09-03
# - Clear a pending dryer or washer unload job when its next cycle starts.
#
# V1.0 2026-08-15
# - Added five-second door and lid open completion triggers for dryer and washer jobs.
#
#:########################################################################################:#
automation:
- id: jobs_tracker_empty_dryer_door_open
alias: "Jobs Tracker - Empty Dryer on Door Open"
description: "Marks Empty the Dryer complete after the dryer door is open for five seconds."
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.dryer_door_status_x34rs_contact
to: "on"
for:
seconds: 5
actions:
- action: script.jobs_tracker_route_completion
data:
completion_entity: binary_sensor.dryer_door_status_x34rs_contact
completion_to_state: "on"
- id: jobs_tracker_empty_washing_machine_lid_open
alias: "Jobs Tracker - Empty Washer on Lid Open"
description: "Marks Empty the Washer complete after the washer lid is open for five seconds."
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.washing_machine_lid_status_x39rs_contact
to: "on"
for:
seconds: 5
actions:
- action: script.jobs_tracker_route_completion
data:
completion_entity: binary_sensor.washing_machine_lid_status_x39rs_contact
completion_to_state: "on"
- id: jobs_tracker_clear_dryer_unload_on_start
alias: "Jobs Tracker - Clear Dryer Unload on Start"
description: "Clears an outstanding Empty the Dryer job when a new dryer cycle starts."
mode: single
triggers:
- trigger: state
entity_id: sensor.appliance_monitor_dryer_monitor
to: Operating
conditions:
- condition: state
entity_id: sensor.jobs_tracker_empty_dryer
state: Due
actions:
- action: script.jobs_tracker_dispatch
data:
job_id: empty_dryer
- id: jobs_tracker_clear_washer_unload_on_start
alias: "Jobs Tracker - Clear Washer Unload on Start"
description: "Clears an outstanding Empty the Washer job when a new washer cycle starts."
mode: single
triggers:
- trigger: state
entity_id: sensor.appliance_monitor_washing_machine_monitor
to: Operating
conditions:
- condition: state
entity_id: sensor.jobs_tracker_empty_washing_machine
state: Due
actions:
- action: script.jobs_tracker_dispatch
data:
job_id: empty_washing_machine
@@ -0,0 +1,65 @@
#:########################################################################################:#
# Jobs Tracker Appliance Completion Triggers Package #
#:########################################################################################:#
#
# TITLE:
# Jobs Tracker Appliance Completion Triggers
#
# FILE:
# jobs_tracker_appliance_completion_triggers.yaml
#
# VERSION:
# V1.0 2026-08-15
#
# PURPOSE:
# Provides time-qualified appliance door and lid triggers for Jobs Tracker completion.
#
# DEPENDS ON:
# packages/Job_Chore_Tracking/jobs_tracker_jobs.yaml V3.5 or newer.
#
# OPERATION:
# Each contact must remain open for five seconds before its corresponding Jobs Tracker
# completion route is called. The job registry owns the completion entity mappings.
#
# VERSION HISTORY:
# V1.0 2026-08-15
# - Added five-second door and lid open completion triggers for dryer and washer jobs.
#
#:########################################################################################:#
automation:
- id: jobs_tracker_empty_dryer_door_open
alias: "Jobs Tracker - Empty Dryer on Door Open"
description: "Marks Empty the Dryer complete after the dryer door is open for five seconds."
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.dryer_door_status_x34rs_contact
to: "on"
for:
seconds: 5
actions:
- action: script.jobs_tracker_route_completion
data:
completion_entity: binary_sensor.dryer_door_status_x34rs_contact
completion_to_state: "on"
- id: jobs_tracker_empty_washing_machine_lid_open
alias: "Jobs Tracker - Empty Washer on Lid Open"
description: "Marks Empty the Washer complete after the washer lid is open for five seconds."
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.washing_machine_lid_status_x39rs_contact
to: "on"
for:
seconds: 5
actions:
- action: script.jobs_tracker_route_completion
data:
completion_entity: binary_sensor.washing_machine_lid_status_x39rs_contact
completion_to_state: "on"
+30 -4
View File
@@ -36,7 +36,7 @@ input_number:
step: 0.1 step: 0.1
mode: box mode: box
unit_of_measurement: "s" unit_of_measurement: "s"
initial: 1.5 initial: 6
automation: automation:
- id: lounge_ir_remote_functions - id: lounge_ir_remote_functions
@@ -101,10 +101,36 @@ automation:
- alias: "Run the matching lounge command" - alias: "Run the matching lounge command"
choose: choose:
- alias: "Set both reading lights together"
conditions:
- condition: template
value_template: "{{ ir_hash == '0x79fb9e54' }}"
sequence:
- choose:
- conditions:
- condition: template
value_template: >-
{{
is_state('light.esp_lounge6chdimmer_couch_spots_left', 'on')
or is_state('light.esp_lounge6chdimmer_couch_spots_right', 'on')
}}
sequence:
- action: light.turn_off
target:
entity_id:
- light.esp_lounge6chdimmer_couch_spots_left
- light.esp_lounge6chdimmer_couch_spots_right
default:
- action: light.turn_on
target:
entity_id:
- light.esp_lounge6chdimmer_couch_spots_left
- light.esp_lounge6chdimmer_couch_spots_right
- alias: "Scene commands" - alias: "Scene commands"
conditions: conditions:
- condition: template - condition: template
value_template: "{{ ir_hash in scene_map }}" value_template: "{{ ir_hash in scene_map and ir_hash != '0x79fb9e54' }}"
sequence: sequence:
- action: script.lounge_set_scene - action: script.lounge_set_scene
data: data:
@@ -137,7 +163,7 @@ automation:
data: data:
channels: "sony_tv_lounge" channels: "sony_tv_lounge"
title: "View Road" title: "View Road"
short_announcement: "Rinnai Gas Heater" short_announcement: "Gas Fire {{ new_state_text }}"
long_announcement: "" long_announcement: ""
payload: "{{ new_state_text }}" payload: "{{ new_state_text }}"
indicator: "" indicator: ""
@@ -169,7 +195,7 @@ automation:
data: data:
channels: "sony_tv_lounge" channels: "sony_tv_lounge"
title: "View Road" title: "View Road"
short_announcement: "Surround Amplifier" short_announcement: "Surround Amp {{ new_state_text }}"
long_announcement: "" long_announcement: ""
payload: "{{ new_state_text }}" payload: "{{ new_state_text }}"
indicator: "" indicator: ""
+25
View File
@@ -65,6 +65,10 @@ input_text:
name: Lounge Last Scene name: Lounge Last Scene
initial: "All Off" initial: "All Off"
max: 40 max: 40
lounge_scene_display:
name: Lounge Scene Display
initial: "Select Scene"
max: 40
timer: timer:
lounge_override_timer: lounge_override_timer:
@@ -111,6 +115,14 @@ script:
- "Dining On/Off" - "Dining On/Off"
#- "Night Light" #- "Night Light"
display_scenes:
- "Bright"
- "TV General"
- "Balanced"
- "Movie"
- "Night Light"
- "Warm Cozy"
##################################################################### #####################################################################
# SCENE SETTINGS # SCENE SETTINGS
# Delta only - only list entities you want to change. # Delta only - only list entities you want to change.
@@ -294,6 +306,19 @@ script:
data: data:
value: "{{ sel }}" value: "{{ sel }}"
- choose:
- conditions: "{{ sel in display_scenes }}"
sequence:
- service: input_text.set_value
target: { entity_id: input_text.lounge_scene_display }
data:
value: "{{ sel }}"
default:
- service: input_text.set_value
target: { entity_id: input_text.lounge_scene_display }
data:
value: "Select Scene"
######################################################################### #########################################################################
# APPLY SELECTED SCENE # APPLY SELECTED SCENE
######################################################################### #########################################################################
+54
View File
@@ -0,0 +1,54 @@
#:########################################################################################:##
# Main House Power Peak #
#:########################################################################################:##
# Tracks the calendar-day maximum of the main three-phase active-power sensor for the
# Flounder dashboard. The helper retains its value across restarts and resets at midnight.
input_number:
main_house_power_peak_today:
name: Main House Power Peak Today
icon: mdi:flash-triangle-outline
min: 0
max: 50000
step: 1
mode: box
unit_of_measurement: W
automation:
- id: main_house_power_peak_today_track
alias: Main House Power Peak Today - Track
mode: single
triggers:
- trigger: state
entity_id: sensor.main_house_3_phase_power_monitor_main_power_total_active_power
- trigger: homeassistant
event: start
conditions:
- condition: template
value_template: >-
{{ states('sensor.main_house_3_phase_power_monitor_main_power_total_active_power') not in ['unknown', 'unavailable'] }}
- condition: template
value_template: >-
{{
states('sensor.main_house_3_phase_power_monitor_main_power_total_active_power') | float(0)
> states('input_number.main_house_power_peak_today') | float(0)
}}
actions:
- action: input_number.set_value
target:
entity_id: input_number.main_house_power_peak_today
data:
value: "{{ states('sensor.main_house_3_phase_power_monitor_main_power_total_active_power') | float(0) }}"
- id: main_house_power_peak_today_reset
alias: Main House Power Peak Today - Reset
mode: single
triggers:
- trigger: time
at: "00:00:00"
actions:
- action: input_number.set_value
target:
entity_id: input_number.main_house_power_peak_today
data:
value: 0
+10 -10
View File
@@ -54,8 +54,8 @@ automation:
max: 10 max: 10
variables: variables:
west_switch_mqtt_topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (West)" west_switch_mqtt_topic: "zigbee2mqtt/Master Bedroom Scene Switch West (X54SS)"
east_switch_mqtt_topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (East)" east_switch_mqtt_topic: "zigbee2mqtt/Master Bedroom Scene Switch East (X55SS)"
bedside_light: "switch.master_bedroom_bedside_lamp_arlec_bulb_master_bedroom_bedside_lamp_arlec_bulb_jump_fade_up_down" bedside_light: "switch.master_bedroom_bedside_lamp_arlec_bulb_master_bedroom_bedside_lamp_arlec_bulb_jump_fade_up_down"
main_bedroom_south_lights: "switch.master_bedroom_master_bedroom_main_lights_south_relay_2_master_bedroom_south_lights" main_bedroom_south_lights: "switch.master_bedroom_master_bedroom_main_lights_south_relay_2_master_bedroom_south_lights"
@@ -64,50 +64,50 @@ automation:
triggers: triggers:
# West switch actions # West switch actions
- trigger: mqtt - trigger: mqtt
topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (West)" topic: "zigbee2mqtt/Master Bedroom Scene Switch West (X54SS)"
value_template: "{{ value_json.action | default('') }}" value_template: "{{ value_json.action | default('') }}"
payload: "1_single" payload: "1_single"
id: west_top_left id: west_top_left
- trigger: mqtt - trigger: mqtt
topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (West)" topic: "zigbee2mqtt/Master Bedroom Scene Switch West (X54SS)"
value_template: "{{ value_json.action | default('') }}" value_template: "{{ value_json.action | default('') }}"
payload: "2_single" payload: "2_single"
id: west_top_right id: west_top_right
- trigger: mqtt - trigger: mqtt
topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (West)" topic: "zigbee2mqtt/Master Bedroom Scene Switch West (X54SS)"
value_template: "{{ value_json.action | default('') }}" value_template: "{{ value_json.action | default('') }}"
payload: "3_single" payload: "3_single"
id: west_bottom_left id: west_bottom_left
- trigger: mqtt - trigger: mqtt
topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (West)" topic: "zigbee2mqtt/Master Bedroom Scene Switch West (X54SS)"
value_template: "{{ value_json.action | default('') }}" value_template: "{{ value_json.action | default('') }}"
payload: "4_single" payload: "4_single"
id: west_bottom_right id: west_bottom_right
# East switch actions # East switch actions
- trigger: mqtt - trigger: mqtt
topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (East)" topic: "zigbee2mqtt/Master Bedroom Scene Switch East (X55SS)"
value_template: "{{ value_json.action | default('') }}" value_template: "{{ value_json.action | default('') }}"
payload: "1_single" payload: "1_single"
id: east_top_left id: east_top_left
- trigger: mqtt - trigger: mqtt
topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (East)" topic: "zigbee2mqtt/Master Bedroom Scene Switch East (X55SS)"
value_template: "{{ value_json.action | default('') }}" value_template: "{{ value_json.action | default('') }}"
payload: "2_single" payload: "2_single"
id: east_top_right id: east_top_right
- trigger: mqtt - trigger: mqtt
topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (East)" topic: "zigbee2mqtt/Master Bedroom Scene Switch East (X55SS)"
value_template: "{{ value_json.action | default('') }}" value_template: "{{ value_json.action | default('') }}"
payload: "3_single" payload: "3_single"
id: east_bottom_left id: east_bottom_left
- trigger: mqtt - trigger: mqtt
topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (East)" topic: "zigbee2mqtt/Master Bedroom Scene Switch East (X55SS)"
value_template: "{{ value_json.action | default('') }}" value_template: "{{ value_json.action | default('') }}"
payload: "4_single" payload: "4_single"
id: east_bottom_right id: east_bottom_right
+510
View File
@@ -0,0 +1,510 @@
#:########################################################################################:#
# Package: Network Monitoring
# File: network_monitoring.yaml
# Version: v1.3.0
# Date: 2026-08-28
#
# Purpose:
# Supplies the calculated Internet, router, MQTT, and Home Assistant health
# information for general network monitoring, including the independent
# Panda and Beaver MQTT brokers, plus Node-RED Panda.
#
# Behaviour:
# - Tracks daily and seven-day Internet speed-test peaks.
# - Calculates rolling 24-hour and seven-day WAN traffic averages and peaks.
# - Measures external HTTP latency and tracks the current public IP address.
# - Monitors the Panda MQTT broker through Home Assistant's birth topic and
# Beaver's MQTT listener through a direct TCP probe.
# - Monitors Node-RED Panda through independent HTTP and MQTT signals.
# - Retains the latest outage time for each monitored network service.
#
# Dependencies:
# - packages/internet_monitoring.yaml
# - packages/edgerouter_snmp.yaml
# - MQTT integration with the default homeassistant/status birth topic.
# - Existing Zigbee2MQTT connectivity entity.
#
# Notes:
# - WAN traffic source values are kB/s and retain that unit in statistics.
# - The 24-hour and seven-day traffic values are rolling windows.
# - Home Assistant's own last outage is represented by sensor.uptime because
# Home Assistant cannot record an event while Core itself is offline.
# - Public IP data is fetched from api.ipify.org every five minutes.
#
# Version History:
# - v1.3.0 (2026-08-28): Renamed package for general network use.
# - v1.2.0 (2026-08-28): Consolidated Node-RED Panda monitoring.
# - v1.1.0 (2026-08-28): Added Beaver MQTT and HA Core health monitoring.
# - v1.0.0 (2026-08-15): Initial monitoring and outage tracking package.
#:########################################################################################:#
command_line:
#:######################################################################################:#
# EXTERNAL NETWORK INFORMATION
#:######################################################################################:#
- sensor:
name: "Flounder External IP"
unique_id: flounder_external_ip
command: >-
curl --fail --silent --show-error --max-time 10 https://api.ipify.org
command_timeout: 15
scan_interval: 300
- sensor:
name: "Flounder Internet Response Time"
unique_id: flounder_internet_response_time
command: >-
curl --fail --silent --show-error --max-time 10 --output /dev/null
--write-out "%{time_total}" https://one.one.one.one/cdn-cgi/trace
command_timeout: 15
scan_interval: 60
unit_of_measurement: "ms"
device_class: duration
state_class: measurement
value_template: "{{ ((value | float(0)) * 1000) | round(0) }}"
- binary_sensor:
name: "Flounder Beaver MQTT Broker Connected"
unique_id: flounder_beaver_mqtt_broker_connected
command: >-
timeout 5 bash -c '>/dev/tcp/172.31.6.1/1883' && echo ON || echo OFF
command_timeout: 6
scan_interval: 60
device_class: connectivity
mqtt:
#:######################################################################################:#
# MQTT BROKER CONNECTION
#:######################################################################################:#
binary_sensor:
- name: "Flounder MQTT Broker Connected"
unique_id: flounder_mqtt_broker_connected
state_topic: "homeassistant/status"
payload_on: "online"
payload_off: "offline"
device_class: connectivity
- name: "Node-RED Panda Up (MQTT)"
unique_id: nodered_panda_up_mqtt
state_topic: "nodered/panda/status"
payload_on: "online"
payload_off: "offline"
device_class: connectivity
qos: 1
expire_after: 180
binary_sensor:
#:######################################################################################:#
# NODE-RED PANDA HTTP HEALTH
#:######################################################################################:#
- platform: rest
name: "Node-RED Panda Up (HTTP)"
resource: http://192.168.3.200:1880/healthz
method: GET
device_class: connectivity
value_template: "{{ value_json.status == 'ok' }}"
timeout: 3
scan_interval: 30
input_number:
#:######################################################################################:#
# DAILY SPEED-TEST PEAKS
#:######################################################################################:#
flounder_internet_download_peak_today:
name: "Flounder Internet Download Peak Today"
min: 0
max: 10000
step: 0.01
mode: box
flounder_internet_upload_peak_today:
name: "Flounder Internet Upload Peak Today"
min: 0
max: 10000
step: 0.01
mode: box
input_datetime:
#:######################################################################################:#
# LAST EXTERNAL IP CHANGE
#:######################################################################################:#
flounder_external_ip_last_changed:
name: "Flounder External IP Last Changed"
has_date: true
has_time: true
input_text:
#:######################################################################################:#
# LAST OUTAGE TIMES
#:######################################################################################:#
flounder_internet_last_outage:
name: "Flounder Internet Last Outage"
max: 40
flounder_edgerouter_last_outage:
name: "Flounder EdgeRouter Last Outage"
max: 40
flounder_mqtt_broker_last_outage:
name: "Flounder MQTT Broker Last Outage"
max: 40
flounder_node_red_mqtt_last_outage:
name: "Flounder Node-RED MQTT Last Outage"
max: 40
flounder_zigbee2mqtt_2_last_outage:
name: "Flounder Zigbee2MQTT 2 Last Outage"
max: 40
flounder_zigbee2mqtt_3_last_outage:
name: "Flounder Zigbee2MQTT 3 Last Outage"
max: 40
template:
#:######################################################################################:#
# POPUP STATUS AND SPEED-TEST VALUES
#:######################################################################################:#
- binary_sensor:
- name: "Flounder EdgeRouter Online"
unique_id: flounder_edgerouter_online
device_class: connectivity
state: "{{ has_value('sensor.edgerouter_uptime') }}"
- name: "Flounder HA Core Online"
unique_id: flounder_ha_core_online
device_class: connectivity
state: "{{ has_value('sensor.home_assistant_core_uptime') }}"
- name: "Node-RED Panda Up (HTTP/MQTT)"
unique_id: nodered_panda_up_combined
device_class: connectivity
state: >-
{{
is_state('binary_sensor.node_red_panda_up_http', 'on')
or is_state('binary_sensor.node_red_panda_up_mqtt', 'on')
}}
availability: >-
{{
states('binary_sensor.node_red_panda_up_http')
not in ['unknown', 'unavailable']
or states('binary_sensor.node_red_panda_up_mqtt')
not in ['unknown', 'unavailable']
}}
- sensor:
- name: "Flounder Internet Download Peak Today"
unique_id: flounder_internet_download_peak_today
unit_of_measurement: "Mbit/s"
device_class: data_rate
state_class: measurement
state: >-
{{ states('input_number.flounder_internet_download_peak_today') | float(0) }}
- name: "Flounder Internet Upload Peak Today"
unique_id: flounder_internet_upload_peak_today
unit_of_measurement: "Mbit/s"
device_class: data_rate
state_class: measurement
state: >-
{{ states('input_number.flounder_internet_upload_peak_today') | float(0) }}
sensor:
#:######################################################################################:#
# INTERNET SPEED-TEST PEAKS
#:######################################################################################:#
- platform: statistics
name: "Flounder Internet Download Peak 7 Days"
unique_id: flounder_internet_download_peak_7_days
entity_id: sensor.internet_speed_test
state_characteristic: value_max
max_age:
days: 7
precision: 2
- platform: statistics
name: "Flounder Internet Upload Peak 7 Days"
unique_id: flounder_internet_upload_peak_7_days
entity_id: sensor.internet_upload_speed_test
state_characteristic: value_max
max_age:
days: 7
precision: 2
#:######################################################################################:#
# WAN DOWNLOAD TRAFFIC STATISTICS
#:######################################################################################:#
- platform: statistics
name: "Flounder WAN Download Average 24 Hours"
unique_id: flounder_wan_download_average_24_hours
entity_id: sensor.edgerouter_wan_download_rate
state_characteristic: average_step
max_age:
hours: 24
precision: 1
- platform: statistics
name: "Flounder WAN Download Peak 24 Hours"
unique_id: flounder_wan_download_peak_24_hours
entity_id: sensor.edgerouter_wan_download_rate
state_characteristic: value_max
max_age:
hours: 24
precision: 1
- platform: statistics
name: "Flounder WAN Download Average 7 Days"
unique_id: flounder_wan_download_average_7_days
entity_id: sensor.edgerouter_wan_download_rate
state_characteristic: average_step
max_age:
days: 7
precision: 1
- platform: statistics
name: "Flounder WAN Download Peak 7 Days"
unique_id: flounder_wan_download_peak_7_days
entity_id: sensor.edgerouter_wan_download_rate
state_characteristic: value_max
max_age:
days: 7
precision: 1
#:######################################################################################:#
# WAN UPLOAD TRAFFIC STATISTICS
#:######################################################################################:#
- platform: statistics
name: "Flounder WAN Upload Average 24 Hours"
unique_id: flounder_wan_upload_average_24_hours
entity_id: sensor.edgerouter_wan_upload_rate
state_characteristic: average_step
max_age:
hours: 24
precision: 1
- platform: statistics
name: "Flounder WAN Upload Peak 24 Hours"
unique_id: flounder_wan_upload_peak_24_hours
entity_id: sensor.edgerouter_wan_upload_rate
state_characteristic: value_max
max_age:
hours: 24
precision: 1
- platform: statistics
name: "Flounder WAN Upload Average 7 Days"
unique_id: flounder_wan_upload_average_7_days
entity_id: sensor.edgerouter_wan_upload_rate
state_characteristic: average_step
max_age:
days: 7
precision: 1
- platform: statistics
name: "Flounder WAN Upload Peak 7 Days"
unique_id: flounder_wan_upload_peak_7_days
entity_id: sensor.edgerouter_wan_upload_rate
state_characteristic: value_max
max_age:
days: 7
precision: 1
#:######################################################################################:#
# EXTERNAL RESPONSE-TIME STATISTICS
#:######################################################################################:#
- platform: statistics
name: "Flounder Internet Response Time Average 24 Hours"
unique_id: flounder_internet_response_time_average_24_hours
entity_id: sensor.flounder_internet_response_time
state_characteristic: average_step
max_age:
hours: 24
precision: 0
- platform: statistics
name: "Flounder Internet Response Time Peak 24 Hours"
unique_id: flounder_internet_response_time_peak_24_hours
entity_id: sensor.flounder_internet_response_time
state_characteristic: value_max
max_age:
hours: 24
precision: 0
automation:
#:######################################################################################:#
# SPEED-TEST PEAK TRACKING
#:######################################################################################:#
- id: flounder_network_record_speed_test_peaks
alias: "Flounder Network: Record speed-test peaks"
triggers:
- trigger: state
entity_id: sensor.internet_speed_test
conditions: []
actions:
- delay:
seconds: 1
- condition: template
value_template: >-
{{
has_value('sensor.internet_speed_test')
and has_value('sensor.internet_upload_speed_test')
}}
- action: input_number.set_value
target:
entity_id: input_number.flounder_internet_download_peak_today
data:
value: >-
{{
[
states('sensor.internet_speed_test') | float(0),
states('input_number.flounder_internet_download_peak_today') | float(0)
]
| max
}}
- action: input_number.set_value
target:
entity_id: input_number.flounder_internet_upload_peak_today
data:
value: >-
{{
[
states('sensor.internet_upload_speed_test') | float(0),
states('input_number.flounder_internet_upload_peak_today') | float(0)
]
| max
}}
mode: queued
- id: flounder_network_reset_daily_speed_test_peaks
alias: "Flounder Network: Reset daily speed-test peaks"
triggers:
- trigger: time
at: "00:00:00"
conditions: []
actions:
- action: input_number.set_value
target:
entity_id:
- input_number.flounder_internet_download_peak_today
- input_number.flounder_internet_upload_peak_today
data:
value: 0
mode: single
#:######################################################################################:#
# PUBLIC IP CHANGE TRACKING
#:######################################################################################:#
- id: flounder_network_record_external_ip_change
alias: "Flounder Network: Record external IP change"
triggers:
- trigger: state
entity_id: sensor.flounder_external_ip
conditions:
- condition: template
value_template: >-
{{
trigger.to_state is not none
and trigger.to_state.state not in ['unknown', 'unavailable', 'none', '']
and (
trigger.from_state is none
or trigger.from_state.state != trigger.to_state.state
)
}}
actions:
- action: input_datetime.set_datetime
target:
entity_id: input_datetime.flounder_external_ip_last_changed
data:
timestamp: "{{ now().timestamp() }}"
mode: single
#:######################################################################################:#
# SERVICE OUTAGE TRACKING
#:######################################################################################:#
- id: flounder_network_record_service_outages
alias: "Flounder Network: Record service outages"
triggers:
- trigger: state
entity_id: binary_sensor.internet_connected
id: internet
- trigger: state
entity_id: binary_sensor.flounder_edgerouter_online
id: edgerouter
- trigger: state
entity_id: binary_sensor.flounder_mqtt_broker_connected
id: mqtt_broker
- trigger: state
entity_id: binary_sensor.flounder_beaver_mqtt_broker_connected
id: beaver_mqtt_broker
- trigger: state
entity_id: binary_sensor.node_red_panda_up_2
id: node_red_mqtt
- trigger: state
entity_id: binary_sensor.zigbee2mqtt_bridge_connection_state_2
id: zigbee2mqtt_2
- trigger: state
entity_id: binary_sensor.zigbee2mqtt_bridge_connection_state_3
id: zigbee2mqtt_3
conditions:
- condition: template
value_template: >-
{{
trigger.to_state is not none
and trigger.to_state.state in ['off', 'unavailable']
and (
trigger.from_state is none
or trigger.from_state.state not in ['off', 'unavailable']
)
}}
actions:
- choose:
- conditions: "{{ trigger.id == 'internet' }}"
sequence:
- action: input_text.set_value
target:
entity_id: input_text.flounder_internet_last_outage
data:
value: "{{ now().isoformat() }}"
- conditions: "{{ trigger.id == 'edgerouter' }}"
sequence:
- action: input_text.set_value
target:
entity_id: input_text.flounder_edgerouter_last_outage
data:
value: "{{ now().isoformat() }}"
- conditions: "{{ trigger.id == 'mqtt_broker' }}"
sequence:
- action: input_text.set_value
target:
entity_id: input_text.flounder_mqtt_broker_last_outage
data:
value: "{{ now().isoformat() }}"
- conditions: "{{ trigger.id == 'beaver_mqtt_broker' }}"
sequence:
- action: input_text.set_value
target:
entity_id: input_text.flounder_mqtt_broker_last_outage
data:
value: "{{ now().isoformat() }}"
- conditions: "{{ trigger.id == 'node_red_mqtt' }}"
sequence:
- action: input_text.set_value
target:
entity_id: input_text.flounder_node_red_mqtt_last_outage
data:
value: "{{ now().isoformat() }}"
- conditions: "{{ trigger.id == 'zigbee2mqtt_2' }}"
sequence:
- action: input_text.set_value
target:
entity_id: input_text.flounder_zigbee2mqtt_2_last_outage
data:
value: "{{ now().isoformat() }}"
- conditions: "{{ trigger.id == 'zigbee2mqtt_3' }}"
sequence:
- action: input_text.set_value
target:
entity_id: input_text.flounder_zigbee2mqtt_3_last_outage
data:
value: "{{ now().isoformat() }}"
mode: queued
@@ -3,6 +3,11 @@
################################################################################ ################################################################################
# #
# Version: # Version:
# - 1.2 - 2026-08-16
# - Removed the spare-button trigger after false presses caused unintended
# power toggles.
# - Retains occupancy-based backup control for Office power supplies.
#
# - 1.1 - 2026-07-25 # - 1.1 - 2026-07-25
# - Stripped light control (now handled by direct MQTT) # - Stripped light control (now handled by direct MQTT)
# - Retains power supply backup control only # - Retains power supply backup control only
@@ -20,16 +25,9 @@
# - Turns Office power supplies off after the Office mmWave occupancy sensor # - Turns Office power supplies off after the Office mmWave occupancy sensor
# has been clear for the configured delay period. # has been clear for the configured delay period.
# #
# - Provides a backup manual control path using the Office spare button.
# This is useful if the occupancy sensor is offline.
#
# Notes: # Notes:
# - This is treated as a BACKUP automation for POWER SUPPLIES ONLY. # - This is treated as a BACKUP automation for POWER SUPPLIES ONLY.
# - Light control is handled by direct ESPHome/MQTT (see device YAMLs). # - Light control is handled by direct ESPHome/MQTT (see device YAMLs).
# - The physical spare button is read using:
# binary_sensor.esp_officelights_button_3_spare
# - The spare relay is used as the manual master/latch state:
# switch.esp_officelights_relay_3_spare
# #
################################################################################ ################################################################################
@@ -58,9 +56,8 @@ automation:
description: > description: >
Backup automation for Office power supplies. Turns power on when Office Backup automation for Office power supplies. Turns power on when Office
mmWave occupancy or movement is detected. Turns power off after the mmWave occupancy or movement is detected. Turns power off after the
occupancy sensor is clear for the configured delay period. Also allows occupancy sensor is clear for the configured delay period. Light control
manual backup toggling using the Office spare button and spare relay. is handled by direct ESPHome/MQTT sync.
Light control is handled by direct ESPHome/MQTT sync.
mode: restart mode: restart
triggers: triggers:
########################################################################## ##########################################################################
@@ -79,19 +76,6 @@ automation:
to: "off" to: "off"
id: occupancy_clear id: occupancy_clear
##########################################################################
# Manual backup button press.
#
# The spare relay state is used as the master/latch:
# - If spare relay is OFF when the button is pressed, turn power ON.
# - If spare relay is ON when the button is pressed, turn power OFF.
##########################################################################
- trigger: state
entity_id: binary_sensor.esp_officelights_button_3_spare
from: "off"
to: "on"
id: backup_button_pressed
########################################################################## ##########################################################################
# Startup check. # Startup check.
# #
@@ -214,102 +198,3 @@ automation:
- action: switch.turn_off - action: switch.turn_off
target: target:
entity_id: switch.esp_officelights_relay_3_spare entity_id: switch.esp_officelights_relay_3_spare
######################################################################
# MANUAL BACKUP LOGIC:
# Button pressed while spare relay master is ON - turn power off.
######################################################################
- alias: "Backup button pressed - spare relay on, turn power off"
conditions:
- condition: trigger
id: backup_button_pressed
- condition: state
entity_id: switch.esp_officelights_relay_3_spare
state: "on"
sequence:
##############################################################
# Power off.
##############################################################
- alias: "Turn off Office right hand GPO if needed"
if:
- condition: state
entity_id: switch.esp_officedeskrightgpo_office_right_hand_gpo
state: "on"
then:
- action: switch.turn_off
target:
entity_id: switch.esp_officedeskrightgpo_office_right_hand_gpo
- alias: "Turn off Office ELV power supply if needed"
if:
- condition: state
entity_id: switch.esp_officeelvcontrol_office_elv_power_supply
state: "on"
then:
- action: switch.turn_off
target:
entity_id: switch.esp_officeelvcontrol_office_elv_power_supply
##############################################################
# Manual master/latch relay off last.
##############################################################
- alias: "Turn off Office spare relay master if needed"
if:
- condition: state
entity_id: switch.esp_officelights_relay_3_spare
state: "on"
then:
- action: switch.turn_off
target:
entity_id: switch.esp_officelights_relay_3_spare
######################################################################
# MANUAL BACKUP LOGIC:
# Button pressed while spare relay master is OFF - turn power on.
#
# No off timer is started from this manual press. If the occupancy
# sensor later changes to clear, the normal clear-delay logic will run.
######################################################################
- alias: "Backup button pressed - spare relay off, turn power on"
conditions:
- condition: trigger
id: backup_button_pressed
- condition: state
entity_id: switch.esp_officelights_relay_3_spare
state: "off"
sequence:
##############################################################
# Power supplies.
##############################################################
- alias: "Turn on Office ELV power supply if needed"
if:
- condition: state
entity_id: switch.esp_officeelvcontrol_office_elv_power_supply
state: "off"
then:
- action: switch.turn_on
target:
entity_id: switch.esp_officeelvcontrol_office_elv_power_supply
- alias: "Turn on Office right hand GPO if needed"
if:
- condition: state
entity_id: switch.esp_officedeskrightgpo_office_right_hand_gpo
state: "off"
then:
- action: switch.turn_on
target:
entity_id: switch.esp_officedeskrightgpo_office_right_hand_gpo
##############################################################
# Manual master/latch relay.
##############################################################
- alias: "Turn on Office spare relay master if needed"
if:
- condition: state
entity_id: switch.esp_officelights_relay_3_spare
state: "off"
then:
- action: switch.turn_on
target:
entity_id: switch.esp_officelights_relay_3_spare
+12 -6
View File
@@ -635,10 +635,13 @@ script:
entity_id: input_datetime.outdoor_lighting_number_16_last_schedule_event entity_id: input_datetime.outdoor_lighting_number_16_last_schedule_event
data: data:
datetime: "{{ (selected_event_ts | int(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true) }}" datetime: "{{ (selected_event_ts | int(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true) }}"
- action: script.outdoor_lighting_ramp_number_16 - action: script.turn_on
target:
entity_id: script.outdoor_lighting_ramp_number_16
data: data:
target_brightness: "{{ selected_target | int(0) }}" variables:
ramp_seconds: "{{ ramp_to_use | int(0) }}" target_brightness: "{{ selected_target | int(0) }}"
ramp_seconds: "{{ ramp_to_use | int(0) }}"
############################################################################# #############################################################################
# 4) Main Entrance Outside LED Lights # 4) Main Entrance Outside LED Lights
@@ -787,10 +790,13 @@ script:
entity_id: input_datetime.outdoor_lighting_main_entrance_last_schedule_event entity_id: input_datetime.outdoor_lighting_main_entrance_last_schedule_event
data: data:
datetime: "{{ (selected_event_ts | int(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true) }}" datetime: "{{ (selected_event_ts | int(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true) }}"
- action: script.outdoor_lighting_ramp_main_entrance - action: script.turn_on
target:
entity_id: script.outdoor_lighting_ramp_main_entrance
data: data:
target_brightness: "{{ selected_target | int(0) }}" variables:
ramp_seconds: "{{ ramp_to_use | int(0) }}" target_brightness: "{{ selected_target | int(0) }}"
ramp_seconds: "{{ ramp_to_use | int(0) }}"
############################################################################# #############################################################################
# Reconcile all # Reconcile all
+80 -4
View File
@@ -1,8 +1,8 @@
#:########################################################################################:# #:########################################################################################:#
# Package: Pet Feeding and Jobs Timing # Package: Pet Feeding and Jobs Timing
# File: pet_feeding_and_jobs.yaml # File: pet_feeding_and_jobs.yaml
# Version: v1.4.0 # Version: v1.5.2
# Date: 2026-07-26 # Date: 2026-09-09
# #
# Purpose: # Purpose:
# Provides legacy MQTT pet activity sensors and Jobs-derived timing sensors for # Provides legacy MQTT pet activity sensors and Jobs-derived timing sensors for
@@ -17,6 +17,11 @@
# instead of exposing a numeric sentinel value. # instead of exposing a numeric sentinel value.
# #
# Version History: # Version History:
# - v1.5.2 (2026-09-12): Ignore stale restored dog-button event states so
# reconnects cannot create false timestamps or visual feedback.
# - v1.5.1 (2026-09-09): Extended dog-button feedback intervals to 500 ms.
# - v1.5.0 (2026-09-09): Added visual feedback flashes for physical dog
# completion buttons, using the Lounge outside light relay.
# - v1.4.0 (2026-07-26): Announce four physical job completions on the Sony # - v1.4.0 (2026-07-26): Announce four physical job completions on the Sony
# lounge TV and both Pushover devices. # lounge TV and both Pushover devices.
# - v1.3.0 (2026-07-26): Restore retained dog-fed and jobs-done activity # - v1.3.0 (2026-07-26): Restore retained dog-fed and jobs-done activity
@@ -122,7 +127,12 @@ automation:
conditions: conditions:
- condition: template - condition: template
value_template: >- value_template: >-
{{ trigger.to_state.state not in ['unknown', 'unavailable'] }} {% set valid_state = trigger.to_state is not none
and trigger.to_state.state not in ['unknown', 'unavailable'] %}
{% set event_ts = as_timestamp(trigger.to_state.state, 0)
if valid_state else 0 %}
{% set event_age = as_timestamp(now()) - event_ts %}
{{ valid_state and event_ts > 0 and event_age >= 0 and event_age <= 30 }}
actions: actions:
- action: input_datetime.set_datetime - action: input_datetime.set_datetime
target: target:
@@ -145,7 +155,12 @@ automation:
conditions: conditions:
- condition: template - condition: template
value_template: >- value_template: >-
{{ trigger.to_state.state not in ['unknown', 'unavailable'] }} {% set valid_state = trigger.to_state is not none
and trigger.to_state.state not in ['unknown', 'unavailable'] %}
{% set event_ts = as_timestamp(trigger.to_state.state, 0)
if valid_state else 0 %}
{% set event_age = as_timestamp(now()) - event_ts %}
{{ valid_state and event_ts > 0 and event_age >= 0 and event_age <= 30 }}
actions: actions:
- action: input_datetime.set_datetime - action: input_datetime.set_datetime
target: target:
@@ -227,3 +242,64 @@ automation:
title: "Jobs Announcement" title: "Jobs Announcement"
short_announcement: "{{ completion_message }}" short_announcement: "{{ completion_message }}"
long_announcement: "{{ completion_message }}" long_announcement: "{{ completion_message }}"
#:######################################################################################:#
# AUTOMATION: Physical Dog-Button Visual Feedback
#:######################################################################################:#
- id: dog_button_visual_feedback
alias: "Jobs - Dog Button Visual Feedback"
description: >-
Flashes the Lounge outside light relay for one physical single-press,
preserving its original state after dog feeding or depooping is recorded.
mode: queued
max: 10
triggers:
- trigger: state
entity_id:
- event.jobs_dog_fed_button_x14bt_action
- event.jobs_dog_depooped_button_x15bt_action
conditions:
- condition: template
value_template: >-
{% set valid_state = trigger.to_state is not none
and trigger.to_state.state not in ['unknown', 'unavailable'] %}
{% set event_ts = as_timestamp(trigger.to_state.state, 0)
if valid_state else 0 %}
{% set event_age = as_timestamp(now()) - event_ts %}
{{
valid_state
and event_ts > 0
and event_age >= 0
and event_age <= 30
and trigger.to_state.attributes.event_type == 'single'
}}
actions:
- variables:
initial_state: "{{ states('switch.esp_loungesouthleftswitch_relay_2_outside_light_1') }}"
flash_count: >-
{{ 2 if trigger.entity_id == 'event.jobs_dog_fed_button_x14bt_action' else 3 }}
- repeat:
count: "{{ flash_count | int }}"
sequence:
- choose:
- conditions:
- condition: template
value_template: "{{ initial_state == 'off' }}"
sequence:
- action: switch.turn_on
target:
entity_id: switch.esp_loungesouthleftswitch_relay_2_outside_light_1
- delay: "00:00:00.500"
- action: switch.turn_off
target:
entity_id: switch.esp_loungesouthleftswitch_relay_2_outside_light_1
- delay: "00:00:00.500"
default:
- action: switch.turn_off
target:
entity_id: switch.esp_loungesouthleftswitch_relay_2_outside_light_1
- delay: "00:00:00.500"
- action: switch.turn_on
target:
entity_id: switch.esp_loungesouthleftswitch_relay_2_outside_light_1
- delay: "00:00:00.500"
+21 -15
View File
@@ -1,13 +1,12 @@
#:########################################################################################:# #:########################################################################################:#
# PACKAGE: Zorro Evening Feeding Reminder (P017-S013) # PACKAGE: Zorro Evening Feeding Reminder (P017-S013)
# FILE: packages/p017_s013_zorro_feeding_reminder.yaml # FILE: packages/p017_s013_zorro_feeding_reminder.yaml
# VERSION: v1.0.1 2026-07-26 # VERSION: v1.0.2 2026-08-12
#:########################################################################################:# #:########################################################################################:#
# PURPOSE: # PURPOSE:
# Remind to feed Zorro (evening feeding) if not completed. # Remind to feed Zorro (evening feeding) if not completed.
# LED matrix shows "Feed Zorro" every 30 min from 5pm to 10pm. # LED matrix shows "Feed Zorro" every 30 min from 5pm to 10pm.
# Voice says "Zorro has not been fed this afternoon" every 30 min from 5pm to 8pm. # Voice says "Zorro has not been fed this afternoon" every 30 min from 5pm to 8pm.
# Each announcement has a random delay of 0-10 min for natural variation.
# Clears LED matrix immediately when evening feeding is completed. # Clears LED matrix immediately when evening feeding is completed.
#:########################################################################################:# #:########################################################################################:#
# DEPENDENCIES: # DEPENDENCIES:
@@ -55,25 +54,32 @@ automation:
voice_hours: "{{ now().hour < 20 }}" voice_hours: "{{ now().hour < 20 }}"
quiet_time: "{{ is_state('input_boolean.quiet_time', 'on') }}" quiet_time: "{{ is_state('input_boolean.quiet_time', 'on') }}"
- delay:
seconds: "{{ range(0, 601) | random | int }}"
- action: script.view_road_announcement
data:
channels: "led_matrix_1, sony_tv_lounge"
title: "Zorro"
short_announcement: "Feed Zorro"
indicator: "zorro,1,15,10,6,5,0,0"
- if: - if:
- condition: template - condition: template
value_template: "{{ voice_hours and not quiet_time }}" value_template: >-
{{ states('sensor.jobs_tracker_dog_feeding_evening') != 'Complete' }}
then: then:
- action: script.view_road_announcement - action: script.view_road_announcement
data: data:
channels: "lounge_google_home_voice" channels: "led_matrix_1, sony_tv_lounge"
title: "Zorro" title: "Zorro"
short_announcement: "Zorro has not been fed this afternoon" short_announcement: "Feed Zorro"
indicator: "zorro,1,15,10,6,5,0,0"
- if:
- condition: template
value_template: >-
{{
voice_hours
and not quiet_time
and is_state('script.view_road_announcement', 'off')
}}
then:
- action: script.view_road_announcement
data:
channels: "lounge_google_home_voice"
title: "Zorro"
short_announcement: "Zorro has not been fed this afternoon"
############################################################################## ##############################################################################
# CLEAR LED MATRIX WHEN FED # CLEAR LED MATRIX WHEN FED
+16 -8
View File
@@ -1,21 +1,22 @@
#:########################################################################################:# #:########################################################################################:#
# PACKAGE: Potplant Moisture Alerts # PACKAGE: Potplant Moisture Alerts
# FILE: packages/potplant_moisture_alerts.yaml # FILE: packages/potplant_moisture_alerts.yaml
# VERSION: v1.1.0 2026-07-29 # VERSION: v1.1.1 2026-08-15
# - Preserve user-selected thresholds and master enable state across restarts. # - Update moisture sensor entity IDs after the plant devices were renamed.
# - v1.1.0 2026-07-29: Preserve user-selected thresholds and master enable state across restarts.
#:########################################################################################:# #:########################################################################################:#
# PURPOSE: # PURPOSE:
# Alert when potplant soil moisture drops below a configurable threshold. # Alert when potplant soil moisture drops below a configurable threshold.
# Displays on the Lounge LED matrix and announces via Lounge Google Home voice. # Displays on the Lounge LED matrix and announces via Lounge Google Home voice.
#:########################################################################################:# #:########################################################################################:#
# SETTINGS: # SETTINGS:
# - input_number.*_moisture_threshold: per-plant dry threshold (default 30%) # - input_number.*_moisture_threshold: per-plant dry threshold (table 25%, TV 30%)
# - input_boolean.potplant_alert_enabled: master enable toggle # - input_boolean.potplant_alert_enabled: master enable toggle
#:########################################################################################:# #:########################################################################################:#
# DEPENDENCIES: # DEPENDENCIES:
# - packages/view_road_announcement_router.yaml for script.view_road_announcement # - packages/view_road_announcement_router.yaml for script.view_road_announcement
# - sensor.table_plant_soil_moisture # - sensor.table_plant_x36md_soil_moisture
# - sensor.tv_plant_soil_moisture # - sensor.tv_plant_x37md_soil_moisture
#:########################################################################################:# #:########################################################################################:#
input_number: input_number:
@@ -24,6 +25,7 @@ input_number:
min: 0 min: 0
max: 100 max: 100
step: 5 step: 5
initial: 25
unit_of_measurement: "%" unit_of_measurement: "%"
icon: mdi:water-percent icon: mdi:water-percent
@@ -45,15 +47,21 @@ template:
- name: "Table Plant Needs Water" - name: "Table Plant Needs Water"
unique_id: table_plant_needs_water unique_id: table_plant_needs_water
icon: mdi:flower icon: mdi:flower
availability: >-
{{ not is_state('sensor.table_plant_x36md_soil_moisture', 'unavailable')
and not is_state('sensor.table_plant_x36md_soil_moisture', 'unknown') }}
state: >- state: >-
{{ (states('sensor.table_plant_soil_moisture') | float(100)) < {{ (states('sensor.table_plant_x36md_soil_moisture') | float(100)) <
(states('input_number.table_plant_moisture_threshold') | float(30)) }} (states('input_number.table_plant_moisture_threshold') | float(25)) }}
- name: "TV Plant Needs Water" - name: "TV Plant Needs Water"
unique_id: tv_plant_needs_water unique_id: tv_plant_needs_water
icon: mdi:flower icon: mdi:flower
availability: >-
{{ not is_state('sensor.tv_plant_x37md_soil_moisture', 'unavailable')
and not is_state('sensor.tv_plant_x37md_soil_moisture', 'unknown') }}
state: >- state: >-
{{ (states('sensor.tv_plant_soil_moisture') | float(100)) < {{ (states('sensor.tv_plant_x37md_soil_moisture') | float(100)) <
(states('input_number.tv_plant_moisture_threshold') | float(30)) }} (states('input_number.tv_plant_moisture_threshold') | float(30)) }}
automation: automation:
+27 -122
View File
@@ -9,42 +9,24 @@
# quiet_time_on_off_actions.yaml # quiet_time_on_off_actions.yaml
# #
# VERSION: # VERSION:
# V1.2 2026-06-20 # V1.3 2026-09-04
#
# VERSION HISTORY:
# V1.2 2026-06-20
# - Added Quiet Time OFF action to change OpenHASP panel openhasp_1
# back to page 1.
#
# V1.1 2026-06-15
# - Added separate Quiet Time ON and Quiet Time OFF action sections.
# - Added support for non-binary Quiet Time OFF actions.
# - Sets input_number.openhasp_1_backlight_level to 200 when Quiet Time ends.
#
# V1.0 2026-06-15
# - Initial standalone package split out from the main Quiet Time package.
# - Turns selected entities on/off when input_boolean.quiet_time changes.
# #
# PURPOSE: # PURPOSE:
# Runs local Home Assistant actions when Quiet Time is turned ON or OFF. # Runs local Home Assistant actions when Quiet Time is turned ON or OFF.
# #
# The main Quiet Time package should continue to own: # The main Quiet Time package owns the helper, daily reset behaviour, and MQTT
# - input_boolean.quiet_time # publishing. This package owns extra local Quiet Time actions.
# - daily reset behaviour
# - MQTT quiet_time state publishing
# #
# This package only handles extra local actions that should happen when # VERSION HISTORY:
# Quiet Time starts or ends. # V1.3 2026-09-04
# - Disables Flounder Motion Wake during Quiet Time and requests a one-off
# 10-second presence fade to Screen Off.
# - Restores Flounder Motion Wake when Quiet Time ends.
# #
# EDITING NOTES: # V1.2 2026-06-20
# - Add normal on/off entities to the relevant entity lists. # - Added Quiet Time OFF action to change OpenHASP panel openhasp_1 back to
# - Empty lists are fine and will simply do nothing. # page 1.
# - Use the "Other Quiet Time OFF Actions" section for things that are not
# simple on/off actions, such as setting input_numbers, selects, scenes,
# scripts, MQTT commands, or custom service calls.
# #
################################################################################
################################################################################ ################################################################################
# AUTOMATIONS # AUTOMATIONS
################################################################################ ################################################################################
@@ -56,56 +38,15 @@ automation:
mode: single mode: single
variables: variables:
##########################################################################
# QUIET TIME ON ACTION LISTS
##########################################################################
#
# These actions run when input_boolean.quiet_time changes to "on".
#
quiet_time_on_turn_off_entities: quiet_time_on_turn_off_entities:
- light.led_matrix_display_led_matrix_display_display - light.led_matrix_display_led_matrix_display_display
quiet_time_on_turn_on_entities: [] quiet_time_on_turn_on_entities: []
##########################################################################
# QUIET TIME OFF ACTION LISTS
##########################################################################
#
# These actions run when input_boolean.quiet_time changes to "off".
#
quiet_time_off_turn_off_entities: [] quiet_time_off_turn_off_entities: []
quiet_time_off_turn_on_entities: quiet_time_off_turn_on_entities:
- light.led_matrix_display_led_matrix_display_display - light.led_matrix_display_led_matrix_display_display
##########################################################################
# OTHER QUIET TIME OFF ACTION VALUES
##########################################################################
#
# Use this list for input_number helpers that should be set when Quiet Time
# ends.
#
# Format:
# - entity_id: input_number.some_helper
# value: 123
#
quiet_time_off_input_number_values: quiet_time_off_input_number_values:
- entity_id: input_number.openhasp_1_backlight_level - entity_id: input_number.openhasp_1_backlight_level
value: 200 value: 200
##########################################################################
# OPENHASP QUIET TIME OFF PAGE ACTION
##########################################################################
#
# Sends the OpenHASP panel back to the main page when Quiet Time ends.
#
# Current OpenHASP MQTT command topic:
# hasp/openhasp_1/command/page
#
quiet_time_off_openhasp_page_topic: "hasp/openhasp_1/command/page" quiet_time_off_openhasp_page_topic: "hasp/openhasp_1/command/page"
quiet_time_off_openhasp_page: 1 quiet_time_off_openhasp_page: 1
@@ -121,13 +62,6 @@ automation:
actions: actions:
- choose: - choose:
######################################################################
# QUIET TIME ON ACTIONS
######################################################################
#
# Runs when Quiet Time starts.
#
- conditions: - conditions:
- condition: template - condition: template
value_template: "{{ trigger.to_state.state == 'on' }}" value_template: "{{ trigger.to_state.state == 'on' }}"
@@ -138,47 +72,44 @@ automation:
- action: homeassistant.turn_off - action: homeassistant.turn_off
target: target:
entity_id: "{{ repeat.item }}" entity_id: "{{ repeat.item }}"
- repeat: - repeat:
for_each: "{{ quiet_time_on_turn_on_entities }}" for_each: "{{ quiet_time_on_turn_on_entities }}"
sequence: sequence:
- action: homeassistant.turn_on - action: homeassistant.turn_on
target: target:
entity_id: "{{ repeat.item }}" entity_id: "{{ repeat.item }}"
# Stop camera motion from extending presence, then fade the
###################################################################### # current session to the existing Screen Off base state.
# QUIET TIME OFF ACTIONS - action: switch.turn_off
###################################################################### target:
# entity_id: switch.flounder_kiosk_motion_wake
# Runs when Quiet Time ends. - wait_template: "{{ is_state('switch.flounder_kiosk_motion_wake', 'off') }}"
# timeout: "00:00:10"
continue_on_timeout: true
- action: mqtt.publish
data:
topic: "viewroad-commands/flounder/presence/end"
payload: "10"
retain: false
- conditions: - conditions:
- condition: template - condition: template
value_template: "{{ trigger.to_state.state == 'off' }}" value_template: "{{ trigger.to_state.state == 'off' }}"
sequence: sequence:
##################################################################
# Quiet Time OFF - binary/entity actions
##################################################################
- repeat: - repeat:
for_each: "{{ quiet_time_off_turn_off_entities }}" for_each: "{{ quiet_time_off_turn_off_entities }}"
sequence: sequence:
- action: homeassistant.turn_off - action: homeassistant.turn_off
target: target:
entity_id: "{{ repeat.item }}" entity_id: "{{ repeat.item }}"
- repeat: - repeat:
for_each: "{{ quiet_time_off_turn_on_entities }}" for_each: "{{ quiet_time_off_turn_on_entities }}"
sequence: sequence:
- action: homeassistant.turn_on - action: homeassistant.turn_on
target: target:
entity_id: "{{ repeat.item }}" entity_id: "{{ repeat.item }}"
- action: switch.turn_on
################################################################## target:
# Quiet Time OFF - input_number set actions entity_id: switch.flounder_kiosk_motion_wake
##################################################################
- repeat: - repeat:
for_each: "{{ quiet_time_off_input_number_values }}" for_each: "{{ quiet_time_off_input_number_values }}"
sequence: sequence:
@@ -187,34 +118,8 @@ automation:
entity_id: "{{ repeat.item.entity_id }}" entity_id: "{{ repeat.item.entity_id }}"
data: data:
value: "{{ repeat.item.value | float }}" value: "{{ repeat.item.value | float }}"
##################################################################
# Quiet Time OFF - OpenHASP page action
##################################################################
#
# Change OpenHASP panel back to page 1 when Quiet Time ends.
#
- action: mqtt.publish - action: mqtt.publish
data: data:
topic: "{{ quiet_time_off_openhasp_page_topic }}" topic: "{{ quiet_time_off_openhasp_page_topic }}"
payload: "{{ quiet_time_off_openhasp_page | int }}" payload: "{{ quiet_time_off_openhasp_page | int }}"
retain: false retain: false
##################################################################
# Quiet Time OFF - other custom actions
##################################################################
#
# Add any extra non-binary service calls here later.
#
# Examples:
#
# - action: script.some_script
#
# - action: select.select_option
# target:
# entity_id: select.some_select
# data:
# option: "Some Option"
#
##################################################################
+245
View File
@@ -0,0 +1,245 @@
################################################################################
# Quiet Time On / Off Actions Package
################################################################################
#
# TITLE:
# Quiet Time On / Off Actions
#
# FILE:
# quiet_time_on_off_actions.yaml
#
# VERSION:
# V1.3 2026-09-04
# - Disables Flounder Motion Wake during Quiet Time and requests a one-off
# 10-second presence fade to Screen Off.
# - Restores Flounder Motion Wake when Quiet Time ends.
#
# V1.2 2026-06-20
#
# VERSION HISTORY:
# V1.2 2026-06-20
# - Added Quiet Time OFF action to change OpenHASP panel openhasp_1
# back to page 1.
#
# V1.1 2026-06-15
# - Added separate Quiet Time ON and Quiet Time OFF action sections.
# - Added support for non-binary Quiet Time OFF actions.
# - Sets input_number.openhasp_1_backlight_level to 200 when Quiet Time ends.
#
# V1.0 2026-06-15
# - Initial standalone package split out from the main Quiet Time package.
# - Turns selected entities on/off when input_boolean.quiet_time changes.
#
# PURPOSE:
# Runs local Home Assistant actions when Quiet Time is turned ON or OFF.
#
# The main Quiet Time package should continue to own:
# - input_boolean.quiet_time
# - daily reset behaviour
# - MQTT quiet_time state publishing
#
# This package only handles extra local actions that should happen when
# Quiet Time starts or ends.
#
# EDITING NOTES:
# - Add normal on/off entities to the relevant entity lists.
# - Empty lists are fine and will simply do nothing.
# - Use the "Other Quiet Time OFF Actions" section for things that are not
# simple on/off actions, such as setting input_numbers, selects, scenes,
# scripts, MQTT commands, or custom service calls.
#
################################################################################
################################################################################
# AUTOMATIONS
################################################################################
automation:
- id: quiet_time_state_change_actions
alias: "Quiet Time - State Change Actions"
description: "Runs configured Home Assistant actions when Quiet Time is turned on or off."
mode: single
variables:
##########################################################################
# QUIET TIME ON ACTION LISTS
##########################################################################
#
# These actions run when input_boolean.quiet_time changes to "on".
#
quiet_time_on_turn_off_entities:
- light.led_matrix_display_led_matrix_display_display
quiet_time_on_turn_on_entities: []
##########################################################################
# QUIET TIME OFF ACTION LISTS
##########################################################################
#
# These actions run when input_boolean.quiet_time changes to "off".
#
quiet_time_off_turn_off_entities: []
quiet_time_off_turn_on_entities:
- light.led_matrix_display_led_matrix_display_display
##########################################################################
# OTHER QUIET TIME OFF ACTION VALUES
##########################################################################
#
# Use this list for input_number helpers that should be set when Quiet Time
# ends.
#
# Format:
# - entity_id: input_number.some_helper
# value: 123
#
quiet_time_off_input_number_values:
- entity_id: input_number.openhasp_1_backlight_level
value: 200
##########################################################################
# OPENHASP QUIET TIME OFF PAGE ACTION
##########################################################################
#
# Sends the OpenHASP panel back to the main page when Quiet Time ends.
#
# Current OpenHASP MQTT command topic:
# hasp/openhasp_1/command/page
#
quiet_time_off_openhasp_page_topic: "hasp/openhasp_1/command/page"
quiet_time_off_openhasp_page: 1
triggers:
- trigger: state
entity_id: input_boolean.quiet_time
from:
- "on"
- "off"
to:
- "on"
- "off"
actions:
- choose:
######################################################################
# QUIET TIME ON ACTIONS
######################################################################
#
# Runs when Quiet Time starts.
#
- conditions:
- condition: template
value_template: "{{ trigger.to_state.state == 'on' }}"
sequence:
- repeat:
for_each: "{{ quiet_time_on_turn_off_entities }}"
sequence:
- action: homeassistant.turn_off
target:
entity_id: "{{ repeat.item }}"
- repeat:
for_each: "{{ quiet_time_on_turn_on_entities }}"
sequence:
- action: homeassistant.turn_on
target:
entity_id: "{{ repeat.item }}"
# Stop camera motion from extending presence, then fade the
# current session to the existing Screen Off base state.
- action: switch.turn_off
target:
entity_id: switch.flounder_kiosk_motion_wake
- wait_template: "{{ is_state('switch.flounder_kiosk_motion_wake', 'off') }}"
timeout: "00:00:10"
continue_on_timeout: true
- action: mqtt.publish
data:
topic: "viewroad-commands/flounder/presence/end"
payload: "10"
retain: false
######################################################################
# QUIET TIME OFF ACTIONS
######################################################################
#
# Runs when Quiet Time ends.
#
- conditions:
- condition: template
value_template: "{{ trigger.to_state.state == 'off' }}"
sequence:
##################################################################
# Quiet Time OFF - binary/entity actions
##################################################################
- repeat:
for_each: "{{ quiet_time_off_turn_off_entities }}"
sequence:
- action: homeassistant.turn_off
target:
entity_id: "{{ repeat.item }}"
- repeat:
for_each: "{{ quiet_time_off_turn_on_entities }}"
sequence:
- action: homeassistant.turn_on
target:
entity_id: "{{ repeat.item }}"
- action: switch.turn_on
target:
entity_id: switch.flounder_kiosk_motion_wake
##################################################################
# Quiet Time OFF - input_number set actions
##################################################################
- repeat:
for_each: "{{ quiet_time_off_input_number_values }}"
sequence:
- action: input_number.set_value
target:
entity_id: "{{ repeat.item.entity_id }}"
data:
value: "{{ repeat.item.value | float }}"
##################################################################
# Quiet Time OFF - OpenHASP page action
##################################################################
#
# Change OpenHASP panel back to page 1 when Quiet Time ends.
#
- action: mqtt.publish
data:
topic: "{{ quiet_time_off_openhasp_page_topic }}"
payload: "{{ quiet_time_off_openhasp_page | int }}"
retain: false
##################################################################
# Quiet Time OFF - other custom actions
##################################################################
#
# Add any extra non-binary service calls here later.
#
# Examples:
#
# - action: script.some_script
#
# - action: select.select_option
# target:
# entity_id: select.some_select
# data:
# option: "Some Option"
#
##################################################################
+101
View File
@@ -0,0 +1,101 @@
#:########################################################################################:#
# TP-Link Deco Guest Wi-Fi Package #
#:########################################################################################:#
#
# TITLE:
# TP-Link Deco Guest Wi-Fi
#
# FILE:
# packages/tplink_deco_guest_wifi.yaml
#
# VERSION:
# V2.2 2026-08-24
#
# VERSION HISTORY:
# V2.2 2026-08-24
# - Replaced passive status sync with router-confirmed enforcement.
# - Repeats requests, reconciles at startup and hourly, and alerts when
# the Deco cannot confirm the requested state.
#
# V2.1 2026-08-04
# - Added the original router-status sync automation.
#
# V2.0 2026-08-04
# - Switched from Alexa voice commands to the Deco local API.
#
# PURPOSE:
# Enforces the requested TP-Link Deco guest Wi-Fi state and confirms it
# against the guest-network sensor reported by the router.
#
# DEPENDENCIES:
# - tplink_deco integration must be configured and connected to the Deco.
# - input_boolean.wifi_guest_network represents the requested guest-network
# state.
# - binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network reports
# the actual guest-network state from the router.
#
#:########################################################################################:#
automation:
- id: enforce_wifi_guest_network_state
alias: "Enforce Wifi Guest Network State"
description: >
Reconciles requested guest Wi-Fi with the Deco's reported state, repeats
the request once after 30 seconds, and alerts on an unconfirmed mismatch.
mode: restart
triggers:
- trigger: state
entity_id: input_boolean.wifi_guest_network
- trigger: state
entity_id: binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network
- trigger: homeassistant
event: start
- trigger: time_pattern
hours: "*"
minutes: "0"
conditions:
- condition: template
value_template: >
{{ states('input_boolean.wifi_guest_network') in ['on', 'off']
and states('binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network')
in ['on', 'off']
and states('input_boolean.wifi_guest_network')
!= states('binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network') }}
actions:
- repeat:
count: 2
sequence:
- choose:
- conditions:
- condition: state
entity_id: input_boolean.wifi_guest_network
state: "on"
sequence:
- action: tplink_deco.set_guest_network
data:
enable: true
- conditions:
- condition: state
entity_id: input_boolean.wifi_guest_network
state: "off"
sequence:
- action: tplink_deco.set_guest_network
data:
enable: false
- wait_template: >
{{ states('input_boolean.wifi_guest_network')
== states('binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network') }}
timeout:
seconds: 30
continue_on_timeout: true
- condition: template
value_template: >
{{ states('input_boolean.wifi_guest_network')
!= states('binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network') }}
- action: persistent_notification.create
data:
notification_id: wifi_guest_network_state_mismatch
title: "Guest Wi-Fi state not confirmed"
message: >
The Deco still reports Guest Wi-Fi as {{ states('binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network') }}
after two requests to set it {{ states('input_boolean.wifi_guest_network') }}.
+8 -5
View File
@@ -1,6 +1,10 @@
################################################################################ ################################################################################
# PACKAGE: View Road Announcement Router # PACKAGE: View Road Announcement Router
################################################################################ ################################################################################
# 1.11: Date: 2026-08-12
# - Publish ISO 8601 timestamps to the MQTT activity feed so timestamp
# sensors can consume the payload.
#
# 1.10: Date: 2026-07-24 # 1.10: Date: 2026-07-24
# - Added 2s delay before TTS speak to separate the cast chime from # - Added 2s delay before TTS speak to separate the cast chime from
# the voice announcement. # the voice announcement.
@@ -83,7 +87,7 @@
# Topic format: # Topic format:
# viewroad-status/activityfeed/<mqtt_feed> # viewroad-status/activityfeed/<mqtt_feed>
# Payload format: # Payload format:
# YYYY-MM-DD HH:MM:SS # ISO 8601 local timestamp
# #
# led_matrix_1: # led_matrix_1:
# Uses: # Uses:
@@ -655,7 +659,7 @@ script:
# Uses: # Uses:
# mqtt_feed # mqtt_feed
# #
# Publishes the current Home Assistant local time in AM/PM format. # Publishes the current Home Assistant local time as an ISO 8601 timestamp.
# #
# Topic format: # Topic format:
# viewroad-status/activityfeed/<mqtt_feed> # viewroad-status/activityfeed/<mqtt_feed>
@@ -665,7 +669,7 @@ script:
# #
# Publishes: # Publishes:
# Topic: viewroad-status/activityfeed/dogfed # Topic: viewroad-status/activityfeed/dogfed
# Payload: 12:32 PM # Payload: 2026-08-12T21:12:00+12:00
# #
########################################################################## ##########################################################################
@@ -682,8 +686,7 @@ script:
- action: mqtt.publish - action: mqtt.publish
data: data:
topic: "{{ ann_mqtt_feed_topic }}" topic: "{{ ann_mqtt_feed_topic }}"
#payload: "{{ now().strftime('%I:%M %p') | regex_replace('^0', '') }}" payload: "{{ now().isoformat() }}"
payload: "{{ now().strftime('%H:%M') }}"
qos: "{{ mqtt_view_rd_feed_mqtt_qos | int(0) }}" qos: "{{ mqtt_view_rd_feed_mqtt_qos | int(0) }}"
retain: "{{ mqtt_view_rd_feed_mqtt_retain | bool(true) }}" retain: "{{ mqtt_view_rd_feed_mqtt_retain | bool(true) }}"
+164
View File
@@ -0,0 +1,164 @@
#:########################################################################################:#
# View Road Smoke Alarm RF Package - V1.2.0 #
#:########################################################################################:#
# Receives normalized AJ-765 RF state from the decoder running on beaver. #
# #
# V1.2 performs early RF fingerprint classification, normally shortly after RF activity. #
# #
# Current RF fingerprints: #
# - detector_test : TEST button transmission from a detector #
# - remote_test : TEST transmission matching the remote capture #
# - remote_hush : HUSH/LOCATE button on the Goldair remote #
# - smoke_alarm : Experimental only - not yet considered authoritative #
# #
# Individual detector identity has NOT been found in the RF protocol. #
#:########################################################################################:#
mqtt:
- binary_sensor:
name: "RF Interconnect"
unique_id: view_road_smoke_alarm_rf_interconnect
default_entity_id: binary_sensor.smoke_alarm_rf_interconnect
state_topic: "viewroad-status/smokealarms/rf/interconnect"
payload_on: "ON"
payload_off: "OFF"
availability_topic: "viewroad-status/smokealarms/rf/availability"
payload_available: "online"
payload_not_available: "offline"
icon: mdi:radio-tower
device:
identifiers:
- view_road_smoke_alarm_rf
name: "Smoke Alarm RF Monitor"
manufacturer: "Zorruno"
model: "AJ-765 RF Monitor"
sw_version: "1.2.0"
- event:
name: "RF Event"
unique_id: view_road_smoke_alarm_rf_event
default_entity_id: event.smoke_alarm_rf_event
state_topic: "viewroad-status/smokealarms/rf/event"
event_types:
- interconnect_start
- detector_test
- remote_test
- remote_hush
- smoke_alarm
- unknown_aj765
- interconnect_end
availability_topic: "viewroad-status/smokealarms/rf/availability"
payload_available: "online"
payload_not_available: "offline"
icon: mdi:radio-tower
device:
identifiers:
- view_road_smoke_alarm_rf
- sensor:
name: "RF Current Type"
unique_id: view_road_smoke_alarm_rf_current_type
default_entity_id: sensor.smoke_alarm_rf_current_type
state_topic: "viewroad-status/smokealarms/rf/current_type"
availability_topic: "viewroad-status/smokealarms/rf/availability"
payload_available: "online"
payload_not_available: "offline"
icon: mdi:waveform
device:
identifiers:
- view_road_smoke_alarm_rf
- sensor:
name: "RF Last Type"
unique_id: view_road_smoke_alarm_rf_last_type
default_entity_id: sensor.smoke_alarm_rf_last_type
state_topic: "viewroad-status/smokealarms/rf/last_type"
entity_category: diagnostic
availability_topic: "viewroad-status/smokealarms/rf/availability"
payload_available: "online"
payload_not_available: "offline"
icon: mdi:waveform
device:
identifiers:
- view_road_smoke_alarm_rf
- sensor:
name: "RF Last Confidence"
unique_id: view_road_smoke_alarm_rf_last_confidence
default_entity_id: sensor.smoke_alarm_rf_last_confidence
state_topic: "viewroad-status/smokealarms/rf/last_confidence"
unit_of_measurement: "%"
state_class: measurement
entity_category: diagnostic
availability_topic: "viewroad-status/smokealarms/rf/availability"
payload_available: "online"
payload_not_available: "offline"
icon: mdi:percent
device:
identifiers:
- view_road_smoke_alarm_rf
- sensor:
name: "RF Last Seen"
unique_id: view_road_smoke_alarm_rf_last_seen
default_entity_id: sensor.smoke_alarm_rf_last_seen
state_topic: "viewroad-status/smokealarms/rf/last_seen"
device_class: timestamp
entity_category: diagnostic
availability_topic: "viewroad-status/smokealarms/rf/availability"
payload_available: "online"
payload_not_available: "offline"
device:
identifiers:
- view_road_smoke_alarm_rf
- sensor:
name: "RF Last Duration"
unique_id: view_road_smoke_alarm_rf_last_duration
default_entity_id: sensor.smoke_alarm_rf_last_duration
state_topic: "viewroad-status/smokealarms/rf/last_duration"
unit_of_measurement: "s"
device_class: duration
state_class: measurement
entity_category: diagnostic
availability_topic: "viewroad-status/smokealarms/rf/availability"
payload_available: "online"
payload_not_available: "offline"
device:
identifiers:
- view_road_smoke_alarm_rf
+4 -4
View File
@@ -21,12 +21,12 @@
############################################################################################ ############################################################################################
# VARIABLES (YAML ANCHORS) - HA SAFE CONTAINER # VARIABLES (YAML ANCHORS) - HA SAFE CONTAINER
# - We store anchors under a valid customize dictionary. # - We store anchors under an existing helper customization so Spook does not
# - Customizing a non-existent entity is harmless. # flag an unknown entity.
############################################################################################ ############################################################################################
homeassistant: homeassistant:
customize: customize:
sensor.xmas_tree_package_anchors: input_boolean.enable_xmas_tree_warning_light:
water_sensor_input: &water_sensor_input binary_sensor.xmas_tree_water_x32ld_water_leak water_sensor_input: &water_sensor_input binary_sensor.xmas_tree_water_x32ld_water_leak
warning_light_output: &warning_light_output switch.spare_tuya_smart_plug_zigbee_x31sp warning_light_output: &warning_light_output switch.spare_tuya_smart_plug_zigbee_x31sp
@@ -117,4 +117,4 @@ automation:
default: default:
- service: homeassistant.turn_off - service: homeassistant.turn_off
target: target:
entity_id: *warning_light_output entity_id: *warning_light_output
+120
View File
@@ -0,0 +1,120 @@
############################################################################################
# PACKAGE: Xmas Tree Water Warning Light
# FILE: xmas_tree_water_warning.yaml
#
# PURPOSE:
# - Monitors a water sensor.
# - If the sensor reads FALSE (off) AND the warning light is enabled:
# 1) Blink immediately when the sensor first goes false
# 2) Continue blinking every 5 minutes while it remains false
#
# DESIGN GUARANTEE:
# - The warning light ALWAYS ends in the same state it was in
# before the automation started.
#
# IMPORTANT:
# - Home Assistant templates cannot safely use YAML anchors inside states().
# - If you change the entity ids below, update BOTH:
# - the anchors, and
# - the two template strings in conditions/action variables.
############################################################################################
############################################################################################
# VARIABLES (YAML ANCHORS) - HA SAFE CONTAINER
# - We store anchors under a valid customize dictionary.
# - Customizing a non-existent entity is harmless.
############################################################################################
homeassistant:
customize:
sensor.xmas_tree_package_anchors:
water_sensor_input: &water_sensor_input binary_sensor.xmas_tree_water_x32ld_water_leak
warning_light_output: &warning_light_output switch.spare_tuya_smart_plug_zigbee_x31sp
############################################################################################
# ENABLE CONTROL
############################################################################################
input_boolean:
enable_xmas_tree_warning_light:
name: Enable Xmas Tree Warning Light
icon: mdi:alarm-light-outline
############################################################################################
# MIRRORED BINARY SENSOR (AS REQUESTED)
############################################################################################
template:
- binary_sensor:
- name: Enable Xmas Tree Warning Light
unique_id: enable_xmas_tree_warning_light_binary_sensor
icon: mdi:alarm-light-outline
state: "{{ is_state('input_boolean.enable_xmas_tree_warning_light', 'on') }}"
############################################################################################
# AUTOMATION: Flash warning light immediately and then every 5 minutes while sensor is FALSE
############################################################################################
automation:
- id: xmas_tree_water_warning_light_flash
alias: "Xmas Tree - Water Warning Light Flash"
description: >
If enabled and the water sensor reads false (off), blink immediately on transition
to false, and also every 5 minutes while it stays false.
mode: single
trigger:
# Immediate blink when the water sensor transitions to FALSE (off)
- platform: state
entity_id: *water_sensor_input
to: "off"
# Repeat reminder every 5 minutes while condition remains true
- platform: time_pattern
minutes: "/5"
condition:
# Only run if the warning feature is enabled
- condition: state
entity_id: input_boolean.enable_xmas_tree_warning_light
state: "on"
# Only run if water sensor is FALSE (off) and valid
- condition: template
value_template: "{{ states('binary_sensor.xmas_tree_water_x32ld_water_leak') == 'off' }}"
# Only run if warning light output is available
- condition: template
value_template: "{{ states('switch.spare_tuya_smart_plug_zigbee_x31sp') not in ['unknown','unavailable'] }}"
action:
# Capture original state so we can restore it at the end
- variables:
original_state: "{{ states('switch.spare_tuya_smart_plug_zigbee_x31sp') }}"
# Blink pattern:
# - Toggle for 1 second
# - Toggle back for 1 second
# - Repeat 5 times
- repeat:
count: 5
sequence:
- service: homeassistant.toggle
target:
entity_id: *warning_light_output
- delay:
seconds: 1
- service: homeassistant.toggle
target:
entity_id: *warning_light_output
- delay:
seconds: 1
# Restore the original state explicitly
- choose:
- conditions:
- condition: template
value_template: "{{ original_state == 'on' }}"
sequence:
- service: homeassistant.turn_on
target:
entity_id: *warning_light_output
default:
- service: homeassistant.turn_off
target:
entity_id: *warning_light_output
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Refresh Flounder's local MotionEye preview images."""
import os
from pathlib import Path
import sys
import time
from urllib.error import URLError
from urllib.request import urlopen
OUTPUTS = (
"front-door.jpg",
"driveway.jpg",
"garage.jpg",
"bike-shed.jpg",
"caravan.jpg",
)
OUTPUT_DIR = Path(os.environ.get("HASS_CONFIG", "/config")) / "www/flounder"
HASS_URL = os.environ.get("HASS_URL", "http://127.0.0.1:8123")
def download(url_path: str, destination: Path) -> bool:
url = f"{HASS_URL}{url_path}"
for attempt in range(2):
try:
with urlopen(url, timeout=4) as response:
image = response.read()
if image.startswith(b"\xff\xd8") and image.endswith(b"\xff\xd9"):
temporary = destination.with_suffix(".tmp")
temporary.write_bytes(image)
os.replace(temporary, destination)
return True
except (OSError, TimeoutError, URLError):
pass
if attempt == 0:
time.sleep(2)
return False
def main() -> int:
arguments = sys.argv[1:]
outputs = OUTPUTS
if arguments and arguments[0] == "--primary":
arguments = arguments[1:]
outputs = OUTPUTS[:3]
if len(arguments) != len(outputs):
return 2
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
failures = 0
for url_path, filename in zip(arguments, outputs, strict=True):
if not download(url_path, OUTPUT_DIR / filename):
failures += 1
time.sleep(2)
return 1 if failures else 0
if __name__ == "__main__":
raise SystemExit(main())
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
download_bytes=2000000
upload_bytes=500000
download_url="https://speed.cloudflare.com/__down?bytes=${download_bytes}"
upload_url="https://speed.cloudflare.com/__up"
download_bps=$(curl --fail --silent --show-error --location --max-time 45 \
--output /dev/null --write-out "%{speed_download}" "${download_url}")
upload_bps=$(head -c "${upload_bytes}" /dev/zero | curl --fail --silent --show-error \
--location --max-time 45 --output /dev/null --request POST \
--header "Content-Type: application/octet-stream" --data-binary @- \
--write-out "%{speed_upload}" "${upload_url}")
awk -v download_bps="${download_bps}" -v upload_bps="${upload_bps}" 'BEGIN {
printf "{\"download_mbps\":%.2f,\"upload_mbps\":%.2f}\n", \
download_bps * 8 / 1000000, upload_bps * 8 / 1000000
}'