Compare commits

...
13 Commits
54 changed files with 9243 additions and 4673 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: 17 KiB

After

Width:  |  Height:  |  Size: 8.1 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.8.1", "start_ts": 1786755431.7882934}
{"pid": 71, "version": 1, "ha_version": "2026.9.2", "start_ts": 1789211351.1286738}
+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.
+3999 -3937
View File
File diff suppressed because it is too large Load Diff
+137 -31
View File
@@ -6,6 +6,9 @@
# 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
# V1.53 2026-07-31 Expanded Jobs Summary to nine active jobs in a 3x4 grid with three spare slots
# V1.52 2026-07-28 Centralised battery publishing and removed continuously evaluated battery template lambdas
# V1.51 2026-07-28 Removed unsupported update_interval from the template USB-power binary sensor
@@ -246,7 +249,6 @@ substitutions:
# 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"
@@ -919,20 +921,6 @@ number:
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
name: "${friendly_name} Dark Lux Threshold"
@@ -1239,6 +1227,36 @@ sensor:
}
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
- platform: homeassistant
id: moon_phase
@@ -1916,6 +1934,12 @@ globals:
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.
- id: security_page_active
type: bool
@@ -3140,6 +3164,83 @@ script:
# 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
mode: restart
then:
- if:
condition:
lambda: |-
return id(display_blackout_active);
then:
- lvgl.label.update:
id: clock_time_label
text_color: 0x4A4742
- lvgl.label.update:
id: clock_date_label
text_color: 0x383632
- lvgl.widget.update:
id: clock_battery_icon_label
hidden: true
- lvgl.widget.update:
id: clock_battery_percent_label
hidden: true
- lvgl.widget.update:
id: clock_blackout_details_mask
hidden: false
else:
- lvgl.label.update:
id: clock_time_label
text_color: 0xE4DDD2
- lvgl.label.update:
id: clock_date_label
text_color: 0xA8A198
- lvgl.widget.update:
id: clock_battery_icon_label
hidden: false
- lvgl.widget.update:
id: clock_battery_percent_label
hidden: false
- lvgl.widget.update:
id: clock_blackout_details_mask
hidden: true
- id: apply_active_backlight
mode: restart
then:
@@ -3151,20 +3252,14 @@ script:
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;
}
@@ -3178,7 +3273,7 @@ script:
float brightness_percent;
if (!isnan(lux) && lux < blackout_threshold) {
if (id(display_blackout_active)) {
brightness_percent =
id(display_blackout_brightness).state;
id(ambient_light_band_text).publish_state("Blackout");
@@ -3206,6 +3301,8 @@ script:
return brightness_percent / 100.0f;
transition_length: 150ms
- script.execute: apply_clock_blackout_palette
- id: apply_dim_backlight
mode: restart
then:
@@ -3217,20 +3314,14 @@ script:
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;
}
@@ -3244,7 +3335,7 @@ script:
float brightness_percent;
if (!isnan(lux) && lux < blackout_threshold) {
if (id(display_blackout_active)) {
brightness_percent =
id(display_blackout_brightness).state;
id(ambient_light_band_text).publish_state("Blackout");
@@ -3272,6 +3363,8 @@ script:
return brightness_percent / 100.0f;
transition_length: 700ms
- script.execute: apply_clock_blackout_palette
#:######################################################################################:#
# BOTTOM NAVIGATION LIVE TIME
#:######################################################################################:#
@@ -12819,3 +12912,16 @@ lvgl:
text_font: font_small
text_color: 0x887799
clickable: false
# Blackout mode leaves only the time and date visible.
- obj:
id: clock_blackout_details_mask
x: 0
y: 430
width: 1024
height: 170
bg_color: 0x020304
bg_opa: COVER
border_width: 0
clickable: false
hidden: true
+47 -55
View File
@@ -6,6 +6,10 @@
# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-officeduallights.yaml
#:########################################################################################:#
# 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).
# on_connect republishes retained status on MQTT reconnect. on_message
# subscribes to main lightswitch command topics for offline HA-independent
@@ -91,7 +95,7 @@ substitutions:
# Project Naming
project_name: "Sonoff Technologies.Sonoff Dual R1"
project_version: "v2.8"
project_version: "v3.0"
# Passwords and Secrets
api_key: !secret esp-api_key
@@ -124,12 +128,6 @@ substitutions:
mqtt_mainlight_function_overhead: "office-main-lights-right"
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_command_on: "On"
mqtt_command_off: "Off"
@@ -237,24 +235,27 @@ mqtt:
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:
- topic: "${mqtt_local_command1_topic}"
payload: "On"
then:
- 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}"
payload: "Off"
then:
- 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}"
then:
@@ -286,13 +287,21 @@ mqtt:
payload: "On"
then:
- 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}"
payload: "Off"
then:
- 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}"
then:
@@ -320,16 +329,19 @@ mqtt:
id(relay_2_timer_script)->execute(timer_seconds * 1000);
# Mirror remote bunker command from esp-officelights onto light_1, but only if a real
# state change is needed. Do not republish the command from here. Relay status is
# published by the relay callback below.
# MQTT peer synchronization is the HA-down fallback. When HA is connected it is the
# sole synchronizer, so remote MQTT commands must not change either physical relay.
- topic: "${mqtt_mainlight_command_bunker}"
payload: "${mqtt_command_on}"
then:
- script.stop: relay_1_timer_script
- if:
condition:
lambda: "return !id(relay_1).state;"
and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return !id(relay_1).state;"
then:
- light.turn_on: light_1
@@ -339,19 +351,25 @@ mqtt:
- script.stop: relay_1_timer_script
- if:
condition:
lambda: "return id(relay_1).state;"
and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return id(relay_1).state;"
then:
- 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}"
payload: "${mqtt_command_on}"
then:
- script.stop: relay_2_timer_script
- if:
condition:
lambda: "return !id(relay_2).state;"
and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return !id(relay_2).state;"
then:
- light.turn_on: light_2
@@ -361,29 +379,11 @@ mqtt:
- script.stop: relay_2_timer_script
- if:
condition:
lambda: "return id(relay_2).state;"
then:
- light.turn_off: light_2
# Mirror shared overhead status from esp-officelights onto light_2. This handles the
# 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;"
and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return id(relay_2).state;"
then:
- light.turn_off: light_2
@@ -498,10 +498,6 @@ switch:
topic: "${mqtt_local_status2_topic}"
payload: "On"
retain: true
- mqtt.publish:
topic: "${mqtt_shared_overhead_status_topic}"
payload: "On"
retain: true
- if:
condition:
switch.is_off: relay_1
@@ -514,10 +510,6 @@ switch:
topic: "${mqtt_local_status2_topic}"
payload: "Off"
retain: true
- mqtt.publish:
topic: "${mqtt_shared_overhead_status_topic}"
payload: "Off"
retain: true
- if:
condition:
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
#:########################################################################################:#
# 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
# HA control now publish command topics to officeduallights; relay callbacks
# publish status only to prevent command ping-pong/chattering. 2-way sync with
@@ -41,11 +45,11 @@
# Overhead Lights (Button 2):
# - Command topic: ${mqtt_this_device_command_topic_overhead}
# Payloads: ${mqtt_command_on}, ${mqtt_command_off}, or 1 to 3600 (timer seconds)
# - Status topic: ${mqtt_this_device_status_topic_overhead}
# Payloads mirror the overhead virtual relay state and are published retained.
# - Status topic: ${mqtt_dual_status_overhead_topic}
# 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.
# 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.
#:########################################################################################:#
# OFFLINE NOTES:
@@ -79,7 +83,7 @@ substitutions:
# Project Naming
project_name: "Zemismart Technologies.KS-811 Triple"
project_version: "v3.0"
project_version: "v3.2"
# Passwords & Secrets
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 LOCAL Controls - Overhead Lights
# Command topics: this device publishes here when Button 2 or HA toggles overhead.
# Status topics: this device publishes retained status here on virtual relay state changes.
# esp-officeduallights subscribes to mirror remote overhead state.
# Commands from Button 2 or HA are sent to the Sonoff Dual, which is the
# sole physical relay and status authority for this circuit.
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_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_command_on: "On"
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_1_name: "Bunker Lights" # Physical relay (GPIO13) connected to bunker light load
switch_2_name: "Overhead Lights" # Virtual mirror of esp-officeduallights overhead state
@@ -217,86 +215,62 @@ mqtt:
payload: !lambda |-
return id(Relay_1).state ? "${mqtt_command_on}" : "${mqtt_command_off}";
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:
# Mirror remote overhead command onto virtual Relay_2, but only if a real state change
# is needed. Do not republish the command from here. Relay status is published by the
# relay callback below.
# MQTT peer synchronization is the HA-down fallback. When HA is connected it is the
# sole synchronizer, so remote MQTT state must not change this mirror.
- topic: "${mqtt_this_device_command_topic_overhead}"
payload: "${mqtt_command_on}"
then:
- script.stop: overhead_timer_script
- if:
condition:
lambda: "return !id(Relay_2).state;"
and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return !id(Relay_2).state;"
then:
- switch.turn_on: Relay_2
- topic: "${mqtt_this_device_command_topic_overhead}"
payload: "${mqtt_command_off}"
then:
- script.stop: overhead_timer_script
- if:
condition:
lambda: "return id(Relay_2).state;"
and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return id(Relay_2).state;"
then:
- switch.turn_off: Relay_2
- topic: "${mqtt_this_device_command_topic_overhead}"
then:
- lambda: |-
if (x.empty()) {
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}"
# Mirror the Sonoff Dual's physical overhead relay status onto virtual Relay_2.
# The main lightswitch never publishes this status, so a reconnect cannot create
# a feedback loop.
- topic: "${mqtt_dual_status_overhead_topic}"
payload: "${mqtt_command_on}"
then:
- if:
condition:
lambda: "return !id(Relay_2).state;"
and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return !id(Relay_2).state;"
then:
- switch.turn_on: Relay_2
- topic: "${mqtt_shared_overhead_status_topic}"
- topic: "${mqtt_dual_status_overhead_topic}"
payload: "${mqtt_command_off}"
then:
- if:
condition:
lambda: "return id(Relay_2).state;"
and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return id(Relay_2).state;"
then:
- switch.turn_off: Relay_2
@@ -308,7 +282,11 @@ mqtt:
then:
- if:
condition:
lambda: "return !id(Relay_1).state;"
and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return !id(Relay_1).state;"
then:
- switch.turn_on: Relay_1
@@ -317,7 +295,11 @@ mqtt:
then:
- if:
condition:
lambda: "return id(Relay_1).state;"
and:
- not:
api.connected:
state_subscription_only: true
- lambda: "return id(Relay_1).state;"
then:
- switch.turn_off: Relay_1
@@ -384,10 +366,16 @@ script:
lambda: "return !id(Relay_1).state;"
then:
- switch.turn_on: Relay_1
- mqtt.publish:
topic: "${mqtt_this_device_command_topic_bunker}"
payload: "${mqtt_command_on}"
retain: false
- if:
condition:
not:
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.
- id: office_bunker_off
@@ -398,10 +386,16 @@ script:
lambda: "return id(Relay_1).state;"
then:
- switch.turn_off: Relay_1
- mqtt.publish:
topic: "${mqtt_this_device_command_topic_bunker}"
payload: "${mqtt_command_off}"
retain: false
- if:
condition:
not:
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
# ON vs OFF for the bunker.
@@ -426,10 +420,16 @@ script:
lambda: "return !id(Relay_2).state;"
then:
- switch.turn_on: Relay_2
- mqtt.publish:
topic: "${mqtt_this_device_command_topic_overhead}"
payload: "${mqtt_command_on}"
retain: false
- if:
condition:
not:
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.
- id: office_overhead_off
@@ -440,10 +440,16 @@ script:
lambda: "return id(Relay_2).state;"
then:
- switch.turn_off: Relay_2
- mqtt.publish:
topic: "${mqtt_this_device_command_topic_overhead}"
payload: "${mqtt_command_off}"
retain: false
- if:
condition:
not:
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.
- id: office_overhead_toggle
@@ -468,16 +474,6 @@ script:
- delay: !lambda "return timer_duration_ms;"
- 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
# https://esphome.io/components/binary_sensor/
@@ -566,32 +562,14 @@ switch:
payload: "${mqtt_command_off}"
retain: true
# Virtual Relay 2 (Overhead Lights mirror). No physical load connected. Mirrors the state
# of esp-officeduallights overhead light via MQTT status subscription.
# Virtual Relay 2 (Overhead Lights mirror). No physical load connected. Mirrors the
# Sonoff Dual's physical overhead relay via its one-way MQTT status topic.
- platform: gpio
name: "Internal Relay 2: ${switch_2_name}"
internal: true
pin: GPIO12
id: Relay_2
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
- platform: gpio
+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;
@@ -2,8 +2,8 @@
# PACKAGE: Appliance Monitor Processor
################################################################################
#
# Version: 1.5
# Date: 2026-07-26
# Version: 1.7
# Date: 2026-08-17
#
# Purpose:
# Shared appliance power monitoring processor.
@@ -29,7 +29,7 @@
# operating_power_min: 12
# paid_tariff_start: "00: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
# max_sample_gap_seconds: 900
# min_cycle_notify_seconds: 300
@@ -41,6 +41,14 @@
# Finished
#
# 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:
# - Standardized Sony TV and Pushover appliance notification titles as
# "Appliance Notification".
@@ -248,13 +256,21 @@ script:
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_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_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_power_available: "{{ power_available | default(true, true) | bool(true) }}"
am_power_available: "{{ power_available | default(true) | bool(true) }}"
##########################################################################
# ANNOUNCEMENT DEFAULTS AND PASSED VALUES
@@ -537,7 +553,7 @@ script:
am_tariff_rate: >-
{% if am_is_paid_interval | bool(false) %}
{{ am_paid_tariff_cost | float(0.2023) }}
{{ am_paid_tariff_cost | float(0.33235) }}
{% else %}
{{ am_offpeak_tariff_cost | float(0.0) }}
{% endif %}
@@ -679,7 +695,7 @@ script:
{% endif %}
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) }}
am_cycle_offpeak_savings_formatted: >-
@@ -6,7 +6,7 @@
# appliances_status_monitoring.yaml
#
# VERSION:
# V1.5 2026-07-11
# V1.8 2026-08-15
#
# PURPOSE:
# Appliance status monitoring package for multiple appliances.
@@ -51,6 +51,11 @@
# matrix_indicator.
#
# 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.
#
@@ -593,28 +598,32 @@ automation:
am_power_available: >-
{{ states(repeat.item.power_entity) not in ['unknown', 'unavailable'] }}
- 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 }}"
- if:
- condition: template
value_template: "{{ am_power_available | bool(false) }}"
then:
- 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_available: "{{ am_power_available }}"
monitor_sensor: "{{ am_monitor_sensor }}"
state_topic: "{{ am_state_topic }}"
power_w: "{{ am_power_w }}"
power_available: "{{ am_power_available }}"
monitor_sensor: "{{ am_monitor_sensor }}"
state_topic: "{{ am_state_topic }}"
debug_flow: "{{ am_debug_flow }}"
resolution: "{{ am_resolution }}"
standby_power: "{{ am_standby_power }}"
operating_power_min: "{{ am_operating_power_min }}"
min_cycle_notify_seconds: "{{ am_min_cycle_notify_seconds }}"
debug_flow: "{{ am_debug_flow }}"
resolution: "{{ am_resolution }}"
standby_power: "{{ am_standby_power }}"
operating_power_min: "{{ am_operating_power_min }}"
min_cycle_notify_seconds: "{{ am_min_cycle_notify_seconds }}"
announcement_enabled: "{{ am_announcement_enabled }}"
announcement_channels: "{{ am_announcement_channels }}"
title: "{{ am_title }}"
short_announcement: "{{ am_short_announcement }}"
long_announcement: "{{ am_long_announcement }}"
announcement_payload: "{{ am_announcement_payload }}"
matrix_indicator: "{{ am_matrix_indicator }}"
mqtt_feed: "{{ am_mqtt_feed }}"
announcement_enabled: "{{ am_announcement_enabled }}"
announcement_channels: "{{ am_announcement_channels }}"
title: "{{ am_title }}"
short_announcement: "{{ am_short_announcement }}"
long_announcement: "{{ am_long_announcement }}"
announcement_payload: "{{ am_announcement_payload }}"
matrix_indicator: "{{ am_matrix_indicator }}"
mqtt_feed: "{{ am_mqtt_feed }}"
@@ -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
#
# VERSION:
# V1.2 2026-07-20
# V1.3 2026-09-06
#
# 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
# - Corrected the documented package filename.
# - Documented the relationship with the native ESPHome controls.
@@ -151,6 +155,40 @@ automation:
target:
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
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."
-111
View File
@@ -1,111 +0,0 @@
#:########################################################################################:#
# TP-Link Deco Guest Wi-Fi Package #
#:########################################################################################:#
#
# TITLE:
# TP-Link Deco Guest Wi-Fi
#
# FILE:
# packages/alexa_tplink_actions.yaml
#
# VERSION:
# V2.1 2026-08-04
#
# VERSION HISTORY:
# V2.1 2026-08-04
# - Added sync automation so input_boolean.wifi_guest_network tracks the
# router's actual guest Wi-Fi status via the new guest-network
# binary sensor from the integration.
#
# V2.0 2026-08-04
# - Switched from Alexa voice commands to direct Deco local API control.
# - Uses tplink_deco.set_guest_network service for reliable on/off.
#
# 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 via the Deco's local
# API, using the tplink_deco custom integration's set_guest_network service.
#
# DEPENDENCIES:
# - tplink_deco integration must be configured and connected to the Deco.
# - input_boolean.wifi_guest_network is a UI-managed helper that 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 and keeps the helper in
# sync.
#
# OPERATION NOTES:
# - The helper records requested state; the sync automation below then
# reconciles it against the router's actual state so the helper tracks
# the real guest-network status.
# - If the Deco API call fails, the helper can disagree with the
# actual guest-network state.
#
#:########################################################################################:#
automation:
- alias: "Turn on the Wifi Guest Network"
description: "Enables the TP-Link Deco guest Wi-Fi network via local API."
mode: single
triggers:
- trigger: state
entity_id: input_boolean.wifi_guest_network
to: "on"
actions:
- action: tplink_deco.set_guest_network
data:
enable: true
- alias: "Turn off the Wifi Guest Network"
description: "Disables the TP-Link Deco guest Wi-Fi network via local API."
mode: single
triggers:
- trigger: state
entity_id: input_boolean.wifi_guest_network
to: "off"
actions:
- action: tplink_deco.set_guest_network
data:
enable: false
- alias: "Sync Wifi Guest Network button with actual status"
description: >
Reconciles input_boolean.wifi_guest_network against the router's actual
guest Wi-Fi state reported by the guest-network binary sensor, so the
button reflects real status (including changes made outside HA).
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network
to: "on"
- trigger: state
entity_id: binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network
to: "off"
conditions:
- condition: template
value_template: >
{{ states('input_boolean.wifi_guest_network')
!= states('binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network') }}
actions:
- choose:
- conditions:
- condition: template
value_template: >
{{ states('binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network') == 'on' }}
sequence:
- action: input_boolean.turn_on
target:
entity_id: input_boolean.wifi_guest_network
- conditions:
- condition: template
value_template: >
{{ states('binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network') == 'off' }}
sequence:
- action: input_boolean.turn_off
target:
entity_id: input_boolean.wifi_guest_network
+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 }}"
+61 -51
View File
@@ -1,75 +1,85 @@
##########################################################################################
# Bedroom 3 Morning Climate
##########################################################################################
# PACKAGE: Bedroom 3 Climate Controls
#
# Turns on the Bedroom 3 heat pump at the selected morning time without
# changing its current HVAC mode, temperature, or other settings, then turns
# it off at 08:30.
# 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 Morning Climate
icon: mdi:weather-sunny
initial: true
name: "Bedroom 3 Heat Pump Auto Control"
icon: mdi:hvac
input_select:
bedroom_3_morning_climate_time:
name: Bedroom 3 Morning Climate Time
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
initial: "06:45"
options:
- "04:30"
- "04:45"
- "05:00"
- "05:15"
- "05:30"
- "05:45"
- "06:00"
- "06:15"
- "06:30"
- "06:45"
- "07:00"
- "07:15"
- "07:30"
- "07:45"
- "08:00"
- "08:15"
- "08:30"
- "08:45"
- "09:00"
- "09:15"
- "09:30"
- "09:45"
- "10:00"
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 Morning Climate Start
description: >-
Turns on the Bedroom 3 heat pump at the selected time when enabled, then
turns it off at 08:30.
trigger:
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: "/15"
condition:
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_select.bedroom_3_morning_climate_time') }}
action:
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
- wait_template: >-
{{ now().hour > 8 or (now().hour == 8 and now().minute >= 30) }}
- 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
+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)
+12 -3
View File
@@ -2,6 +2,11 @@
# PACKAGE: Free Power Time Automation #
#:########################################################################################:#
# 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
# - Added startup catch-up to end_actions: when HA starts outside the
# free power window, the end actions (chargers off, dehumidifier off)
@@ -321,11 +326,15 @@ automation:
triggers:
- trigger: time
at: input_datetime.free_power_end_time
id: scheduled_end
- trigger: homeassistant
event: start
id: startup
conditions:
- condition: state
entity_id: binary_sensor.free_power_time_active
state: "off"
# The scheduled end must not race the template sensor's minute update.
# Startup catch-up still runs only outside the free power window.
- condition: template
value_template: >-
{{ trigger.id == 'scheduled_end' or is_state('binary_sensor.free_power_time_active', 'off') }}
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
# File: fridge_temperatures.yaml
# Version: v1.0.0
# Date: 2024-09-04
# Version: v1.2.0
# Date: 2026-08-30
#
# Purpose:
# Publishes refrigerator and freezer temperature changes to household MQTT
# status topics for use by external displays and consumers.
#
# 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.
# - Covers the drinks fridge, drinks freezer, and main kitchen fridge.
#
@@ -18,9 +19,22 @@
# - Home Assistant MQTT integration and broker connectivity.
#
# 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.
#:########################################################################################:#
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:
- id: publish_fridge_temps_drinks_fridge
alias: publish_fridge_temps_drinks_fridge
@@ -56,7 +70,7 @@ automation:
alias: publish_main_kitchen_fridge_temp_zth08
trigger:
- platform: state
entity_id: sensor.main_kitchen_fridge_temp_zth08
entity_id: sensor.main_kitchen_fridge_temperature_calibrated
not_to:
- "unavailable"
- "unknown"
+13 -2
View File
@@ -9,9 +9,12 @@
# packages/garage_corner_light_cctv_motion.yaml
#
# VERSION:
# V1.0 2026-06-29
# V1.1 2026-08-31
#
# 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
# - Initial package.
# - 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.
#
# 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:
# - The light is:
@@ -31,6 +35,7 @@
#
# - 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.
# - CCTV triggers run from one hour before sunset until one hour after sunrise.
#
#:########################################################################################:#
# User Configuration #
@@ -91,6 +96,12 @@ automation:
entity_id: input_boolean.garage_corner_light_movement_triggers
state: "on"
- condition: sun
after: sunset
after_offset: "-01:00:00"
before: sunrise
before_offset: "01:00:00"
action:
- service: switch.turn_on
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'] }}
@@ -9,7 +9,7 @@
# jobs_tracker_appliance_completion_triggers.yaml
#
# VERSION:
# V1.0 2026-08-15
# V1.1 2026-09-03
#
# PURPOSE:
# Provides time-qualified appliance door and lid triggers for Jobs Tracker completion.
@@ -19,9 +19,13 @@
#
# 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.
# 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.
#
@@ -63,3 +67,43 @@ automation:
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"
+1 -1
View File
@@ -163,7 +163,7 @@ automation:
data:
channels: "sony_tv_lounge"
title: "View Road"
short_announcement: "Rinnai Gas Heater"
short_announcement: "Gas Fire {{ new_state_text }}"
long_announcement: ""
payload: "{{ new_state_text }}"
indicator: ""
+10 -10
View File
@@ -54,8 +54,8 @@ automation:
max: 10
variables:
west_switch_mqtt_topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (West)"
east_switch_mqtt_topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (East)"
west_switch_mqtt_topic: "zigbee2mqtt/Master Bedroom Scene Switch West (X54SS)"
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"
main_bedroom_south_lights: "switch.master_bedroom_master_bedroom_main_lights_south_relay_2_master_bedroom_south_lights"
@@ -64,50 +64,50 @@ automation:
triggers:
# West switch actions
- trigger: mqtt
topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (West)"
topic: "zigbee2mqtt/Master Bedroom Scene Switch West (X54SS)"
value_template: "{{ value_json.action | default('') }}"
payload: "1_single"
id: west_top_left
- trigger: mqtt
topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (West)"
topic: "zigbee2mqtt/Master Bedroom Scene Switch West (X54SS)"
value_template: "{{ value_json.action | default('') }}"
payload: "2_single"
id: west_top_right
- trigger: mqtt
topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (West)"
topic: "zigbee2mqtt/Master Bedroom Scene Switch West (X54SS)"
value_template: "{{ value_json.action | default('') }}"
payload: "3_single"
id: west_bottom_left
- trigger: mqtt
topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (West)"
topic: "zigbee2mqtt/Master Bedroom Scene Switch West (X54SS)"
value_template: "{{ value_json.action | default('') }}"
payload: "4_single"
id: west_bottom_right
# East switch actions
- trigger: mqtt
topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (East)"
topic: "zigbee2mqtt/Master Bedroom Scene Switch East (X55SS)"
value_template: "{{ value_json.action | default('') }}"
payload: "1_single"
id: east_top_left
- trigger: mqtt
topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (East)"
topic: "zigbee2mqtt/Master Bedroom Scene Switch East (X55SS)"
value_template: "{{ value_json.action | default('') }}"
payload: "2_single"
id: east_top_right
- trigger: mqtt
topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (East)"
topic: "zigbee2mqtt/Master Bedroom Scene Switch East (X55SS)"
value_template: "{{ value_json.action | default('') }}"
payload: "3_single"
id: east_bottom_left
- trigger: mqtt
topic: "zigbee2mqtt/Master Bedroom 4 Button Switch (East)"
topic: "zigbee2mqtt/Master Bedroom Scene Switch East (X55SS)"
value_template: "{{ value_json.action | default('') }}"
payload: "4_single"
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:
# - 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
# - Stripped light control (now handled by direct MQTT)
# - Retains power supply backup control only
@@ -20,16 +25,9 @@
# - Turns Office power supplies off after the Office mmWave occupancy sensor
# 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:
# - This is treated as a BACKUP automation for POWER SUPPLIES ONLY.
# - 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: >
Backup automation for Office power supplies. Turns power on when Office
mmWave occupancy or movement is detected. Turns power off after the
occupancy sensor is clear for the configured delay period. Also allows
manual backup toggling using the Office spare button and spare relay.
Light control is handled by direct ESPHome/MQTT sync.
occupancy sensor is clear for the configured delay period. Light control
is handled by direct ESPHome/MQTT sync.
mode: restart
triggers:
##########################################################################
@@ -79,19 +76,6 @@ automation:
to: "off"
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.
#
@@ -214,102 +198,3 @@ automation:
- action: switch.turn_off
target:
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
data:
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:
target_brightness: "{{ selected_target | int(0) }}"
ramp_seconds: "{{ ramp_to_use | int(0) }}"
variables:
target_brightness: "{{ selected_target | int(0) }}"
ramp_seconds: "{{ ramp_to_use | int(0) }}"
#############################################################################
# 4) Main Entrance Outside LED Lights
@@ -787,10 +790,13 @@ script:
entity_id: input_datetime.outdoor_lighting_main_entrance_last_schedule_event
data:
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:
target_brightness: "{{ selected_target | int(0) }}"
ramp_seconds: "{{ ramp_to_use | int(0) }}"
variables:
target_brightness: "{{ selected_target | int(0) }}"
ramp_seconds: "{{ ramp_to_use | int(0) }}"
#############################################################################
# Reconcile all
+80 -4
View File
@@ -1,8 +1,8 @@
#:########################################################################################:#
# Package: Pet Feeding and Jobs Timing
# File: pet_feeding_and_jobs.yaml
# Version: v1.4.0
# Date: 2026-07-26
# Version: v1.5.2
# Date: 2026-09-09
#
# Purpose:
# Provides legacy MQTT pet activity sensors and Jobs-derived timing sensors for
@@ -17,6 +17,11 @@
# instead of exposing a numeric sentinel value.
#
# 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
# lounge TV and both Pushover devices.
# - v1.3.0 (2026-07-26): Restore retained dog-fed and jobs-done activity
@@ -122,7 +127,12 @@ automation:
conditions:
- condition: 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:
- action: input_datetime.set_datetime
target:
@@ -145,7 +155,12 @@ automation:
conditions:
- condition: 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:
- action: input_datetime.set_datetime
target:
@@ -227,3 +242,64 @@ automation:
title: "Jobs Announcement"
short_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"
+27 -122
View File
@@ -9,42 +9,24 @@
# quiet_time_on_off_actions.yaml
#
# VERSION:
# 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.
# V1.3 2026-09-04
#
# 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
# The main Quiet Time package owns the helper, daily reset behaviour, and MQTT
# publishing. This package owns extra local Quiet Time actions.
#
# This package only handles extra local actions that should happen when
# Quiet Time starts or ends.
# VERSION HISTORY:
# 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:
# - 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.
# V1.2 2026-06-20
# - Added Quiet Time OFF action to change OpenHASP panel openhasp_1 back to
# page 1.
#
################################################################################
################################################################################
# AUTOMATIONS
################################################################################
@@ -56,56 +38,15 @@ automation:
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
@@ -121,13 +62,6 @@ automation:
actions:
- choose:
######################################################################
# QUIET TIME ON ACTIONS
######################################################################
#
# Runs when Quiet Time starts.
#
- conditions:
- condition: template
value_template: "{{ trigger.to_state.state == 'on' }}"
@@ -138,47 +72,44 @@ automation:
- 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 }}"
######################################################################
# QUIET TIME OFF ACTIONS
######################################################################
#
# Runs when Quiet Time ends.
#
# 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
- 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 }}"
##################################################################
# Quiet Time OFF - input_number set actions
##################################################################
- action: switch.turn_on
target:
entity_id: switch.flounder_kiosk_motion_wake
- repeat:
for_each: "{{ quiet_time_off_input_number_values }}"
sequence:
@@ -187,34 +118,8 @@ automation:
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"
#
##################################################################
+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') }}.
+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
# - We store anchors under a valid customize dictionary.
# - Customizing a non-existent entity is harmless.
# - We store anchors under an existing helper customization so Spook does not
# flag an unknown entity.
############################################################################################
homeassistant:
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
warning_light_output: &warning_light_output switch.spare_tuya_smart_plug_zigbee_x31sp
@@ -117,4 +117,4 @@ automation:
default:
- service: homeassistant.turn_off
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