Compare commits
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -1 +1 @@
|
||||
{"pid": 70, "version": 1, "ha_version": "2026.7.4", "start_ts": 1785317114.2667625}
|
||||
{"pid": 71, "version": 1, "ha_version": "2026.9.2", "start_ts": 1789211351.1286738}
|
||||
@@ -0,0 +1 @@
|
||||
a5cfb18d66d8671cf91955eaea5ad94dcf80eaef22aad2e8374ac1223680d6aa
|
||||
@@ -11,17 +11,20 @@ homeassistant:
|
||||
customize: !include customize.yaml
|
||||
packages: !include_dir_named packages
|
||||
|
||||
template: !include_dir_merge_list templates
|
||||
group: !include_dir_merge_named group/
|
||||
automation: !include_dir_merge_list automations/
|
||||
script: !include_dir_merge_named scripts/
|
||||
# Unused directory includes retained for future YAML-based configuration.
|
||||
# template: !include_dir_merge_list templates
|
||||
# group: !include_dir_merge_named group/
|
||||
# automation: !include_dir_merge_list automations/
|
||||
# script: !include_dir_merge_named scripts/
|
||||
scene: !include scenes.yaml
|
||||
sensor: !include_dir_merge_list sensor/
|
||||
# sensor: !include_dir_merge_list sensor/
|
||||
|
||||
conversation:
|
||||
# Loaded by default_config.
|
||||
# conversation:
|
||||
|
||||
logger:
|
||||
default: info
|
||||
# Home Assistant's default logging level is info.
|
||||
# logger:
|
||||
# default: info
|
||||
|
||||
zha:
|
||||
enable_quirks: true
|
||||
@@ -35,7 +38,8 @@ zha:
|
||||
ota:
|
||||
otau_directory: /config/zigpy_ota
|
||||
ikea_provider: true
|
||||
ledvance_provider: true
|
||||
salus_provider: true
|
||||
inovelli_provider: true
|
||||
thirdreality_provider: true
|
||||
# Enabled by default.
|
||||
# ledvance_provider: true
|
||||
# inovelli_provider: true
|
||||
# thirdreality_provider: true
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -8,6 +8,9 @@
|
||||
# packages/16a_ev_charging_controls.yaml
|
||||
#
|
||||
# VERSION:
|
||||
# V1.4 2026-08-12
|
||||
# - Consolidated Leaf 16A and 32A session energy tracking in this package.
|
||||
#
|
||||
# V1.3 2026-07-24
|
||||
# - Added startup catch-up to offpeak_end: when HA starts outside the
|
||||
# offpeak window in Normal mode, the relay is turned off immediately.
|
||||
@@ -27,7 +30,8 @@
|
||||
#
|
||||
# PURPOSE:
|
||||
# Controls the Leaf 16A charger relay using scheduled offpeak operation and
|
||||
# user-selectable immediate or charge-until-full modes.
|
||||
# user-selectable immediate or charge-until-full modes. Tracks completed
|
||||
# Leaf charging sessions across both the 16A and 32A chargers.
|
||||
#
|
||||
# DEPENDENCIES:
|
||||
# - switch.garage_db_control_16a_wallcharger_operation controls charger power.
|
||||
@@ -450,3 +454,171 @@ automation:
|
||||
entity_id: input_select.ev_16a_charge_mode
|
||||
data:
|
||||
option: "Normal (Offpeak)"
|
||||
|
||||
#############################################################################
|
||||
# SESSION ENERGY TRACKING
|
||||
#############################################################################
|
||||
|
||||
- id: leaf_ev_charger_track_session_energy
|
||||
alias: "Leaf EV Charger - Track Session Energy"
|
||||
description: >-
|
||||
Store the energy used by the most recently completed Leaf charge session
|
||||
on either charger.
|
||||
mode: queued
|
||||
max: 2
|
||||
|
||||
triggers:
|
||||
- trigger: homeassistant
|
||||
event: start
|
||||
id: ha_start
|
||||
- trigger: state
|
||||
entity_id: binary_sensor.garage_db_control_16a_wallcharger_charging
|
||||
to: "on"
|
||||
id: start_16a
|
||||
- trigger: state
|
||||
entity_id: binary_sensor.garage_db_control_16a_wallcharger_charging
|
||||
from: "on"
|
||||
to: "off"
|
||||
id: stop_16a
|
||||
- trigger: state
|
||||
entity_id: binary_sensor.leaf_charging
|
||||
to: "on"
|
||||
id: start_32a
|
||||
- trigger: state
|
||||
entity_id: binary_sensor.main_house_3_phase_power_monitor_32a_wallcharger_charging
|
||||
to: "off"
|
||||
id: stop_32a
|
||||
- trigger: state
|
||||
entity_id: binary_sensor.atto_3_charging
|
||||
to: "on"
|
||||
id: cancel_32a
|
||||
|
||||
conditions: []
|
||||
|
||||
actions:
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: ha_start
|
||||
- condition: state
|
||||
entity_id: binary_sensor.garage_db_control_16a_wallcharger_charging
|
||||
state: "on"
|
||||
sequence:
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.leaf_charge_session_start_energy
|
||||
data:
|
||||
value: "{{ states('sensor.garage_db_control_16a_wallcharger_total_energy') | float(0) }}"
|
||||
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: ha_start
|
||||
- condition: state
|
||||
entity_id: binary_sensor.main_house_3_phase_power_monitor_32a_wallcharger_charging
|
||||
state: "on"
|
||||
- condition: state
|
||||
entity_id: binary_sensor.atto_3_charging
|
||||
state: "off"
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ has_value('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') }}
|
||||
sequence:
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.leaf_32a_charge_session_start_energy
|
||||
data:
|
||||
value: "{{ states('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') | float(0) }}"
|
||||
- action: input_boolean.turn_on
|
||||
target:
|
||||
entity_id: input_boolean.leaf_32a_charge_session_active
|
||||
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: start_16a
|
||||
sequence:
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.leaf_charge_session_start_energy
|
||||
data:
|
||||
value: "{{ states('sensor.garage_db_control_16a_wallcharger_total_energy') | float(0) }}"
|
||||
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: stop_16a
|
||||
- condition: template
|
||||
value_template: >
|
||||
{{ states('sensor.garage_db_control_16a_wallcharger_total_energy') | float(0)
|
||||
>= states('input_number.leaf_charge_session_start_energy') | float(0) }}
|
||||
sequence:
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.leaf_last_session_energy
|
||||
data:
|
||||
value: >-
|
||||
{{ (states('sensor.garage_db_control_16a_wallcharger_total_energy') | float(0)
|
||||
- states('input_number.leaf_charge_session_start_energy') | float(0)) | round(3) }}
|
||||
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: start_32a
|
||||
- condition: state
|
||||
entity_id: binary_sensor.main_house_3_phase_power_monitor_32a_wallcharger_charging
|
||||
state: "on"
|
||||
- condition: state
|
||||
entity_id: binary_sensor.atto_3_charging
|
||||
state: "off"
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ has_value('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') }}
|
||||
sequence:
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.leaf_32a_charge_session_start_energy
|
||||
data:
|
||||
value: "{{ states('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') | float(0) }}"
|
||||
- action: input_boolean.turn_on
|
||||
target:
|
||||
entity_id: input_boolean.leaf_32a_charge_session_active
|
||||
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: stop_32a
|
||||
- condition: state
|
||||
entity_id: input_boolean.leaf_32a_charge_session_active
|
||||
state: "on"
|
||||
sequence:
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{
|
||||
has_value('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy')
|
||||
and states('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') | float(0)
|
||||
>= states('input_number.leaf_32a_charge_session_start_energy') | float(0)
|
||||
}}
|
||||
sequence:
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.leaf_last_session_energy
|
||||
data:
|
||||
value: >-
|
||||
{{
|
||||
(
|
||||
states('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') | float(0)
|
||||
- states('input_number.leaf_32a_charge_session_start_energy') | float(0)
|
||||
) | round(3)
|
||||
}}
|
||||
- action: input_boolean.turn_off
|
||||
target:
|
||||
entity_id: input_boolean.leaf_32a_charge_session_active
|
||||
|
||||
- conditions:
|
||||
- condition: trigger
|
||||
id: cancel_32a
|
||||
- condition: state
|
||||
entity_id: input_boolean.leaf_32a_charge_session_active
|
||||
state: "on"
|
||||
sequence:
|
||||
- action: input_boolean.turn_off
|
||||
target:
|
||||
entity_id: input_boolean.leaf_32a_charge_session_active
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# PACKAGE: Appliance Monitor Processor
|
||||
################################################################################
|
||||
#
|
||||
# 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,14 @@
|
||||
# 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.
|
||||
#
|
||||
# V1.6 2026-07-24
|
||||
# - Added startup delay guard to prevent false completion announcements.
|
||||
# - Added power_available field for the processor availability check.
|
||||
@@ -217,7 +225,7 @@ automation:
|
||||
operating_power_min: 10
|
||||
|
||||
announcement_enabled: true
|
||||
announcement_channels: "pushover_zorruno,pushover_kim,sony_tv_lounge,led_matrix_1"
|
||||
announcement_channels: "pushover_zorruno,pushover_kim,sony_tv_lounge,mqtt_view_rd_feed,led_matrix_1"
|
||||
short_announcement: "/s/sWashing complete"
|
||||
# long_announcement: # Generated by the processor
|
||||
announcement_payload: ""
|
||||
@@ -239,7 +247,7 @@ automation:
|
||||
operating_power_min: 10
|
||||
|
||||
announcement_enabled: true
|
||||
announcement_channels: "pushover_zorruno,pushover_kim,sony_tv_lounge,led_matrix_1"
|
||||
announcement_channels: "pushover_zorruno,pushover_kim,sony_tv_lounge,mqtt_view_rd_feed,led_matrix_1"
|
||||
short_announcement: "/s/sDryer complete"
|
||||
# long_announcement: # Generated by the processor
|
||||
announcement_payload: ""
|
||||
@@ -261,7 +269,7 @@ automation:
|
||||
operating_power_min: 30
|
||||
|
||||
announcement_enabled: true
|
||||
announcement_channels: "pushover_zorruno,pushover_kim,sony_tv_lounge,led_matrix_1"
|
||||
announcement_channels: "pushover_zorruno,pushover_kim,sony_tv_lounge,mqtt_view_rd_feed,led_matrix_1"
|
||||
short_announcement: "/sDishwasher complete"
|
||||
# long_announcement: # Generated by the processor
|
||||
announcement_payload: ""
|
||||
@@ -283,7 +291,7 @@ automation:
|
||||
operating_power_min: 30
|
||||
|
||||
announcement_enabled: true
|
||||
announcement_channels: "pushover_zorruno"
|
||||
announcement_channels: "pushover_zorruno,mqtt_view_rd_feed"
|
||||
short_announcement: "/sDownstairs Dishwasher complete"
|
||||
# long_announcement: # Generated by the processor
|
||||
announcement_payload: ""
|
||||
@@ -306,7 +314,7 @@ automation:
|
||||
operating_power_min: 100
|
||||
|
||||
announcement_enabled: true
|
||||
announcement_channels: "pushover_zorruno,pushover_kim"
|
||||
announcement_channels: "pushover_zorruno,pushover_kim,mqtt_view_rd_feed"
|
||||
short_announcement: "/s/s32A EV Charging complete"
|
||||
# long_announcement: # Generated by the processor
|
||||
announcement_payload: ""
|
||||
@@ -329,7 +337,7 @@ automation:
|
||||
operating_power_min: 100
|
||||
|
||||
announcement_enabled: true
|
||||
announcement_channels: "pushover_zorruno"
|
||||
announcement_channels: "pushover_zorruno,mqtt_view_rd_feed"
|
||||
short_announcement: "/s/s16A EV Charging complete"
|
||||
# long_announcement: # Generated by the processor
|
||||
announcement_payload: ""
|
||||
@@ -351,7 +359,7 @@ automation:
|
||||
operating_power_min: 20
|
||||
|
||||
announcement_enabled: true
|
||||
announcement_channels: "pushover_zorruno"
|
||||
announcement_channels: "pushover_zorruno,mqtt_view_rd_feed"
|
||||
short_announcement: "/s/sRyobi Charging Left"
|
||||
# long_announcement: # Generated by the processor
|
||||
announcement_payload: ""
|
||||
@@ -373,7 +381,7 @@ automation:
|
||||
operating_power_min: 20
|
||||
|
||||
announcement_enabled: true
|
||||
announcement_channels: "pushover_zorruno"
|
||||
announcement_channels: "pushover_zorruno,mqtt_view_rd_feed"
|
||||
short_announcement: "/s/sRyobi Charging Right"
|
||||
# long_announcement: # Generated by the processor
|
||||
announcement_payload: ""
|
||||
@@ -395,7 +403,7 @@ automation:
|
||||
operating_power_min: 20
|
||||
|
||||
announcement_enabled: true
|
||||
announcement_channels: "pushover_zorruno"
|
||||
announcement_channels: "pushover_zorruno,mqtt_view_rd_feed"
|
||||
short_announcement: "Pool Pump Off"
|
||||
# long_announcement: # Generated by the processor
|
||||
announcement_payload: ""
|
||||
@@ -417,7 +425,7 @@ automation:
|
||||
operating_power_min: 20
|
||||
|
||||
announcement_enabled: true
|
||||
announcement_channels: "pushover_zorruno"
|
||||
announcement_channels: "pushover_zorruno,mqtt_view_rd_feed"
|
||||
short_announcement: "3D printer finished"
|
||||
# long_announcement: # Generated by the processor
|
||||
announcement_payload: ""
|
||||
@@ -590,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 }}"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
# jobs_tracker_jobs.yaml
|
||||
#
|
||||
# VERSION:
|
||||
# V2.4 2026-07-21
|
||||
# V3.5 2026-08-15
|
||||
#
|
||||
# PURPOSE:
|
||||
# Defines the job registry, MQTT Discovery entities, completion triggers and
|
||||
@@ -20,9 +20,9 @@
|
||||
#
|
||||
# OPERATION:
|
||||
# Add and edit jobs in the JOB REGISTRY section below. Each job has its own
|
||||
# job_id, name, repeat type, due time, MQTT topic, state entity and completion
|
||||
# entity/entities. Most future job changes should only require editing that
|
||||
# registry block.
|
||||
# job_id, name, repeat type, schedule or due entity, MQTT topic, state entity
|
||||
# and completion entity/entities. Most future job changes should only require
|
||||
# editing that registry block.
|
||||
#
|
||||
# If a new job uses a completion entity that is already listed in
|
||||
# jobs_tracker_completion_entities, no trigger changes are needed. If the job
|
||||
@@ -41,6 +41,39 @@
|
||||
# Complete
|
||||
#
|
||||
# VERSION HISTORY:
|
||||
# V3.5 2026-08-15
|
||||
# - Empty the Dryer now completes when its door remains open for five seconds.
|
||||
# - Empty the Washer now completes when its lid remains open for five seconds.
|
||||
# - Removed Laundry PIR completion from the dryer and washer jobs.
|
||||
#
|
||||
# V3.3 2026-07-30
|
||||
# - Added Empty the Dryer and Empty the Washing Machine Once jobs.
|
||||
# - Laundry PIR completion sensors for dryer and washing machine.
|
||||
# - Added initial_due_behavior: ignore to prevent old cycle signals.
|
||||
# - Discovery device firmware version updated to 3.3.
|
||||
# - PIR completion entities added to the completion trigger list.
|
||||
#
|
||||
# V3.2 2026-07-30
|
||||
# - Converted Dishwasher Unpacked from daily to Once.
|
||||
# - Main dishwasher cycle_stop_ts now makes the job Due.
|
||||
# - The existing kitchen completion button still completes the job.
|
||||
# - Discovery device firmware version updated to 3.2.
|
||||
#
|
||||
# V3.1 2026-07-30
|
||||
# - Added configurable Once completion hold periods, defaulting to three hours.
|
||||
# - Added elapsed completion text and minute-level completion age attributes.
|
||||
# - Added due_attribute support for reliable attribute-driven Once jobs.
|
||||
# - Discovery device firmware version updated to 3.1.
|
||||
#
|
||||
# V3.0 2026-07-29
|
||||
# - Added event-driven Once jobs with registry-defined due and completion
|
||||
# entities.
|
||||
# - Once jobs remain Due until completed, briefly publish Complete, then
|
||||
# return to Not Due until their next due signal.
|
||||
# - Added Water TV Pot Plant and Water Table Pot Plant Once jobs.
|
||||
# - Missing retained job state now bootstraps after the startup wait.
|
||||
# - Discovery device firmware version updated to 3.0.
|
||||
#
|
||||
# V2.4 2026-07-21
|
||||
# - Startup now waits for every retained job timestamp before refreshing.
|
||||
# - Minute refreshes skip while retained MQTT state is not ready.
|
||||
@@ -91,6 +124,18 @@
|
||||
# - Dog feeding uses completion windows because both feeding jobs share the
|
||||
# same physical button.
|
||||
#
|
||||
# - Once jobs use due_entity and optional due_state. If due_state is omitted,
|
||||
# a fresh event entity timestamp or state change starts the job.
|
||||
# Set due_attribute to trigger from an increasing timestamp or numeric
|
||||
# attribute such as cycle_stop_ts.
|
||||
#
|
||||
# - Once completion entries may be simple entity IDs or mappings with an
|
||||
# entity_id and optional to_state. Once entities are evaluated by the minute
|
||||
# refresh, so adding one requires changes only in the registry.
|
||||
#
|
||||
# - completion_hold_hours controls how long a completed Once job remains
|
||||
# Complete. It defaults to 3 hours when omitted.
|
||||
#
|
||||
#:########################################################################################:#
|
||||
|
||||
script:
|
||||
@@ -139,7 +184,7 @@ script:
|
||||
completion_entities:
|
||||
- event.jobs_dog_fed_button_x14bt_action
|
||||
completion_window_start: "00:00"
|
||||
completion_window_end: "11:59"
|
||||
completion_window_end: "14:59"
|
||||
icon: mdi:dog-side
|
||||
mqtt_topic: "viewroad-status/jobs_tracker/dog_feeding_morning"
|
||||
state_entity: "sensor.jobs_tracker_dog_feeding_morning"
|
||||
@@ -152,7 +197,7 @@ script:
|
||||
due_day: 1
|
||||
completion_entities:
|
||||
- event.jobs_dog_fed_button_x14bt_action
|
||||
completion_window_start: "12:00"
|
||||
completion_window_start: "15:00"
|
||||
completion_window_end: "23:59"
|
||||
icon: mdi:dog-side
|
||||
mqtt_topic: "viewroad-status/jobs_tracker/dog_feeding_evening"
|
||||
@@ -173,21 +218,20 @@ script:
|
||||
state_entity: "sensor.jobs_tracker_dog_depooping"
|
||||
|
||||
- job_id: dishwasher_unpacked
|
||||
job_name: "Dishwasher Unpacked"
|
||||
repeat_type: daily
|
||||
due_time: "15:30"
|
||||
due_weekday: 0
|
||||
due_day: 1
|
||||
job_name: "Unpack the dishwasher"
|
||||
repeat_type: once
|
||||
due_entity: sensor.appliance_monitor_main_dishwasher_monitor
|
||||
due_state: "Finished"
|
||||
initial_due_behavior: ignore
|
||||
completion_hold_hours: 20
|
||||
completion_entities:
|
||||
- event.kitchen_jobs_kitchen_completed_buttons_button_1
|
||||
# completion_window_start: "00:00"
|
||||
# completion_window_end: "23:59"
|
||||
icon: mdi:dishwasher
|
||||
mqtt_topic: "viewroad-status/jobs_tracker/dishwasher_unpacked"
|
||||
state_entity: "sensor.jobs_tracker_dishwasher_unpacked"
|
||||
|
||||
- job_id: rubbish_removed
|
||||
job_name: "Rubbish Removed"
|
||||
job_name: "Take out rubbish"
|
||||
repeat_type: daily
|
||||
due_time: "15:30"
|
||||
due_weekday: 0
|
||||
@@ -200,6 +244,60 @@ script:
|
||||
mqtt_topic: "viewroad-status/jobs_tracker/rubbish_removed"
|
||||
state_entity: "sensor.jobs_tracker_rubbish_removed"
|
||||
|
||||
- job_id: water_tv_pot_plant
|
||||
job_name: "Water TV Pot Plant"
|
||||
repeat_type: once
|
||||
due_entity: binary_sensor.tv_plant_needs_water
|
||||
due_state: "on"
|
||||
completion_hold_hours: 3
|
||||
completion_entities:
|
||||
- entity_id: binary_sensor.tv_plant_needs_water
|
||||
to_state: "off"
|
||||
icon: mdi:watering-can
|
||||
mqtt_topic: "viewroad-status/jobs_tracker/water_tv_pot_plant"
|
||||
state_entity: "sensor.jobs_tracker_water_tv_pot_plant"
|
||||
|
||||
- job_id: water_table_pot_plant
|
||||
job_name: "Water Table Pot Plant"
|
||||
repeat_type: once
|
||||
due_entity: binary_sensor.table_plant_needs_water
|
||||
due_state: "on"
|
||||
completion_hold_hours: 3
|
||||
completion_entities:
|
||||
- entity_id: binary_sensor.table_plant_needs_water
|
||||
to_state: "off"
|
||||
icon: mdi:watering-can
|
||||
mqtt_topic: "viewroad-status/jobs_tracker/water_table_pot_plant"
|
||||
state_entity: "sensor.jobs_tracker_water_table_pot_plant"
|
||||
|
||||
- job_id: empty_dryer
|
||||
job_name: "Empty the Dryer"
|
||||
repeat_type: once
|
||||
due_entity: sensor.appliance_monitor_dryer_monitor
|
||||
due_state: "Finished"
|
||||
completion_hold_hours: 20
|
||||
completion_entities:
|
||||
- entity_id: binary_sensor.dryer_door_status_x34rs_contact
|
||||
to_state: "on"
|
||||
icon: mdi:tumble-dryer
|
||||
mqtt_topic: "viewroad-status/jobs_tracker/empty_dryer"
|
||||
state_entity: "sensor.jobs_tracker_empty_dryer"
|
||||
initial_due_behavior: ignore
|
||||
|
||||
- job_id: empty_washing_machine
|
||||
job_name: "Empty the Washer"
|
||||
repeat_type: once
|
||||
due_entity: sensor.appliance_monitor_washing_machine_monitor
|
||||
due_state: "Finished"
|
||||
completion_hold_hours: 20
|
||||
completion_entities:
|
||||
- entity_id: binary_sensor.washing_machine_lid_status_x39rs_contact
|
||||
to_state: "on"
|
||||
icon: mdi:washing-machine
|
||||
mqtt_topic: "viewroad-status/jobs_tracker/empty_washing_machine"
|
||||
state_entity: "sensor.jobs_tracker_empty_washing_machine"
|
||||
initial_due_behavior: ignore
|
||||
|
||||
#:################################################################################:#
|
||||
# COMPLETION ENTITY TRIGGER LIST #
|
||||
#:################################################################################:#
|
||||
@@ -225,19 +323,37 @@ script:
|
||||
- repeat:
|
||||
for_each: "{{ jobs_tracker_jobs }}"
|
||||
sequence:
|
||||
- action: script.jobs_tracker_update_job
|
||||
data:
|
||||
job_id: "{{ repeat.item.job_id }}"
|
||||
job_name: "{{ repeat.item.job_name }}"
|
||||
repeat_type: "{{ repeat.item.repeat_type }}"
|
||||
due_time: "{{ repeat.item.due_time }}"
|
||||
due_weekday: "{{ repeat.item.due_weekday }}"
|
||||
due_day: "{{ repeat.item.due_day }}"
|
||||
mqtt_topic: "{{ repeat.item.mqtt_topic }}"
|
||||
state_entity: "{{ repeat.item.state_entity }}"
|
||||
completion_window_start: "{{ repeat.item.completion_window_start | default('') }}"
|
||||
completion_window_end: "{{ repeat.item.completion_window_end | default('') }}"
|
||||
mark_complete: false
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ repeat.item.repeat_type == 'once' }}"
|
||||
then:
|
||||
- action: script.jobs_tracker_update_once_job
|
||||
data:
|
||||
job_id: "{{ repeat.item.job_id }}"
|
||||
job_name: "{{ repeat.item.job_name }}"
|
||||
mqtt_topic: "{{ repeat.item.mqtt_topic }}"
|
||||
state_entity: "{{ repeat.item.state_entity }}"
|
||||
due_entity: "{{ repeat.item.due_entity }}"
|
||||
due_state: "{{ repeat.item.due_state | default('') }}"
|
||||
due_attribute: "{{ repeat.item.due_attribute | default('') }}"
|
||||
completion_entities: "{{ repeat.item.completion_entities | default([]) }}"
|
||||
completion_hold_hours: "{{ repeat.item.completion_hold_hours | default(3) }}"
|
||||
initial_due_behavior: "{{ repeat.item.initial_due_behavior | default('') }}"
|
||||
mark_complete: false
|
||||
else:
|
||||
- action: script.jobs_tracker_update_job
|
||||
data:
|
||||
job_id: "{{ repeat.item.job_id }}"
|
||||
job_name: "{{ repeat.item.job_name }}"
|
||||
repeat_type: "{{ repeat.item.repeat_type }}"
|
||||
due_time: "{{ repeat.item.due_time }}"
|
||||
due_weekday: "{{ repeat.item.due_weekday }}"
|
||||
due_day: "{{ repeat.item.due_day }}"
|
||||
mqtt_topic: "{{ repeat.item.mqtt_topic }}"
|
||||
state_entity: "{{ repeat.item.state_entity }}"
|
||||
completion_window_start: "{{ repeat.item.completion_window_start | default('') }}"
|
||||
completion_window_end: "{{ repeat.item.completion_window_end | default('') }}"
|
||||
mark_complete: false
|
||||
|
||||
#:##################################################################################:#
|
||||
# Mark one job complete #
|
||||
@@ -251,20 +367,37 @@ script:
|
||||
- condition: template
|
||||
value_template: "{{ repeat.item.job_id == job_id }}"
|
||||
then:
|
||||
- action: script.jobs_tracker_update_job
|
||||
data:
|
||||
job_id: "{{ repeat.item.job_id }}"
|
||||
job_name: "{{ repeat.item.job_name }}"
|
||||
repeat_type: "{{ repeat.item.repeat_type }}"
|
||||
due_time: "{{ repeat.item.due_time }}"
|
||||
due_weekday: "{{ repeat.item.due_weekday }}"
|
||||
due_day: "{{ repeat.item.due_day }}"
|
||||
mqtt_topic: "{{ repeat.item.mqtt_topic }}"
|
||||
state_entity: "{{ repeat.item.state_entity }}"
|
||||
completion_window_start: "{{ repeat.item.completion_window_start | default('') }}"
|
||||
completion_window_end: "{{ repeat.item.completion_window_end | default('') }}"
|
||||
mark_complete: true
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ repeat.item.repeat_type == 'once' }}"
|
||||
then:
|
||||
- action: script.jobs_tracker_update_once_job
|
||||
data:
|
||||
job_id: "{{ repeat.item.job_id }}"
|
||||
job_name: "{{ repeat.item.job_name }}"
|
||||
mqtt_topic: "{{ repeat.item.mqtt_topic }}"
|
||||
state_entity: "{{ repeat.item.state_entity }}"
|
||||
due_entity: "{{ repeat.item.due_entity }}"
|
||||
due_state: "{{ repeat.item.due_state | default('') }}"
|
||||
due_attribute: "{{ repeat.item.due_attribute | default('') }}"
|
||||
completion_entities: "{{ repeat.item.completion_entities | default([]) }}"
|
||||
completion_hold_hours: "{{ repeat.item.completion_hold_hours | default(3) }}"
|
||||
initial_due_behavior: "{{ repeat.item.initial_due_behavior | default('') }}"
|
||||
mark_complete: true
|
||||
else:
|
||||
- action: script.jobs_tracker_update_job
|
||||
data:
|
||||
job_id: "{{ repeat.item.job_id }}"
|
||||
job_name: "{{ repeat.item.job_name }}"
|
||||
repeat_type: "{{ repeat.item.repeat_type }}"
|
||||
due_time: "{{ repeat.item.due_time }}"
|
||||
due_weekday: "{{ repeat.item.due_weekday }}"
|
||||
due_day: "{{ repeat.item.due_day }}"
|
||||
mqtt_topic: "{{ repeat.item.mqtt_topic }}"
|
||||
state_entity: "{{ repeat.item.state_entity }}"
|
||||
completion_window_start: "{{ repeat.item.completion_window_start | default('') }}"
|
||||
completion_window_end: "{{ repeat.item.completion_window_end | default('') }}"
|
||||
mark_complete: true
|
||||
# Give MQTT sensors a moment to update from the retained job payloads
|
||||
# before building the summary payload.
|
||||
- delay: "00:00:01"
|
||||
@@ -282,6 +415,10 @@ script:
|
||||
description: "Entity ID that triggered completion."
|
||||
example: "event.jobs_dog_fed_button_x14bt_action"
|
||||
|
||||
completion_to_state:
|
||||
description: "State reached by the completion entity."
|
||||
example: "off"
|
||||
|
||||
manual_job_id:
|
||||
description: "Job ID completed by MQTT/manual button."
|
||||
example: "dog_feeding_morning"
|
||||
@@ -290,6 +427,7 @@ script:
|
||||
- variables:
|
||||
jobs_tracker_jobs: *jobs_tracker_jobs
|
||||
completion_entity_safe: "{{ completion_entity | default('') }}"
|
||||
completion_to_state_safe: "{{ completion_to_state | default('') }}"
|
||||
manual_job_id_safe: "{{ manual_job_id | default('') }}"
|
||||
now_minutes: "{{ (now().hour * 60) + now().minute }}"
|
||||
|
||||
@@ -320,10 +458,21 @@ script:
|
||||
{% endif %}
|
||||
|
||||
physical_entity_matches: >-
|
||||
{{
|
||||
completion_entity_safe != ''
|
||||
and completion_entity_safe in completion_entities_safe
|
||||
}}
|
||||
{% set ns = namespace(matches=false) %}
|
||||
{% for completion in completion_entities_safe %}
|
||||
{% if completion is mapping %}
|
||||
{% set entity_id = completion.entity_id | default('') %}
|
||||
{% set to_state = completion.to_state | default('') %}
|
||||
{% else %}
|
||||
{% set entity_id = completion %}
|
||||
{% set to_state = '' %}
|
||||
{% endif %}
|
||||
{% if completion_entity_safe == entity_id
|
||||
and (to_state == '' or completion_to_state_safe == to_state) %}
|
||||
{% set ns.matches = true %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ ns.matches }}
|
||||
|
||||
manual_job_matches: >-
|
||||
{{
|
||||
@@ -366,7 +515,7 @@ script:
|
||||
device_name: "Jobs Tracker"
|
||||
device_manufacturer: "View Road Home Assistant"
|
||||
device_model: "Shared household jobs tracker"
|
||||
device_sw_version: "2.4"
|
||||
device_sw_version: "3.4"
|
||||
|
||||
value_template_state: >-
|
||||
{% raw %}{{ value_json.state | default('unknown') }}{% endraw %}
|
||||
@@ -508,6 +657,7 @@ script:
|
||||
"name": "Jobs Tracker - " ~ repeat.item.job_name,
|
||||
"unique_id": "jobs_tracker_" ~ repeat.item.job_id,
|
||||
"object_id": "jobs_tracker_" ~ repeat.item.job_id,
|
||||
"default_entity_id": "sensor.jobs_tracker_" ~ repeat.item.job_id,
|
||||
"state_topic": repeat.item.mqtt_topic,
|
||||
"value_template": value_template_state,
|
||||
"json_attributes_topic": repeat.item.mqtt_topic,
|
||||
@@ -533,6 +683,7 @@ script:
|
||||
"name": "Job Complete - " ~ repeat.item.job_name,
|
||||
"unique_id": "job_complete_" ~ repeat.item.job_id,
|
||||
"object_id": "job_complete_" ~ repeat.item.job_id,
|
||||
"default_entity_id": "binary_sensor.job_complete_" ~ repeat.item.job_id,
|
||||
"state_topic": repeat.item.mqtt_topic,
|
||||
"value_template": value_template_job_complete,
|
||||
"payload_on": "ON",
|
||||
@@ -559,6 +710,7 @@ script:
|
||||
"name": "Complete Job - " ~ repeat.item.job_name,
|
||||
"unique_id": "jobs_tracker_complete_" ~ repeat.item.job_id,
|
||||
"object_id": "jobs_tracker_complete_" ~ repeat.item.job_id,
|
||||
"default_entity_id": "button.jobs_tracker_complete_" ~ repeat.item.job_id,
|
||||
"command_topic": "viewroad-commands/jobs_tracker/" ~ repeat.item.job_id ~ "/complete",
|
||||
"payload_press": "PRESS",
|
||||
"icon": repeat.item.icon | default("mdi:clipboard-check-outline"),
|
||||
@@ -620,9 +772,14 @@ script:
|
||||
"job_name": job.job_name,
|
||||
"state": s,
|
||||
"repeat_type": job.repeat_type,
|
||||
"due_time": job.due_time,
|
||||
"due_weekday": job.due_weekday,
|
||||
"due_day": job.due_day,
|
||||
"due_time": job.due_time | default(""),
|
||||
"due_weekday": job.due_weekday | default(0),
|
||||
"due_day": job.due_day | default(0),
|
||||
"due_entity": job.due_entity | default(""),
|
||||
"due_state": job.due_state | default(""),
|
||||
"due_attribute": job.due_attribute | default(""),
|
||||
"initial_due_behavior": job.initial_due_behavior | default(""),
|
||||
"completion_hold_hours": job.completion_hold_hours | default(3),
|
||||
"completion_entities": job.completion_entities | default([]),
|
||||
"completion_window_start": job.completion_window_start | default(""),
|
||||
"completion_window_end": job.completion_window_end | default(""),
|
||||
@@ -636,7 +793,11 @@ script:
|
||||
"previous_missed_active": state_attr(job.state_entity, 'previous_missed_active'),
|
||||
"minutes_since_required": state_attr(job.state_entity, 'minutes_since_required'),
|
||||
"hours_since_required": state_attr(job.state_entity, 'hours_since_required'),
|
||||
"hours_since_completed": state_attr(job.state_entity, 'hours_since_completed')
|
||||
"hours_since_completed": state_attr(job.state_entity, 'hours_since_completed'),
|
||||
"minutes_since_completed": state_attr(job.state_entity, 'minutes_since_completed'),
|
||||
"completion_elapsed_human": state_attr(job.state_entity, 'completion_elapsed_human'),
|
||||
"once_pending": state_attr(job.state_entity, 'once_pending'),
|
||||
"once_due_ts": state_attr(job.state_entity, 'once_due_ts')
|
||||
}
|
||||
] %}
|
||||
{% endfor %}
|
||||
@@ -717,7 +878,7 @@ automation:
|
||||
{% endfor %}
|
||||
{{ ns.ready }}
|
||||
timeout: "00:01:30"
|
||||
continue_on_timeout: false
|
||||
continue_on_timeout: true
|
||||
|
||||
# Recalculate each job only after retained state is confirmed ready.
|
||||
- action: script.jobs_tracker_dispatch
|
||||
@@ -785,6 +946,7 @@ automation:
|
||||
- action: script.jobs_tracker_route_completion
|
||||
data:
|
||||
completion_entity: "{{ trigger.entity_id }}"
|
||||
completion_to_state: "{{ trigger.to_state.state }}"
|
||||
|
||||
- id: jobs_tracker_mqtt_manual_completion
|
||||
alias: "Jobs Tracker - MQTT Manual Completion"
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
# jobs_tracker_processor.yaml
|
||||
#
|
||||
# VERSION:
|
||||
# V1.6 2026-07-10
|
||||
# V2.5 2026-08-02
|
||||
# - A new Once due signal now refreshes the occurrence timestamp even when
|
||||
# the job is already Due, so repeated appliance cycles supersede any
|
||||
# still-uncompleted earlier occurrence.
|
||||
#
|
||||
# PURPOSE:
|
||||
# Generic processing engine for the Jobs Tracker system.
|
||||
@@ -25,6 +28,12 @@
|
||||
# restart the processor can recalculate the correct state from the last physical
|
||||
# button/action press.
|
||||
#
|
||||
# Event-driven Once jobs use the separate jobs_tracker_update_once_job script.
|
||||
# Their registry-defined due and completion entities are evaluated during the
|
||||
# Jobs Tracker minute refresh. Once jobs remain Due until completed, briefly
|
||||
# publish Complete, then return to Not Due until a new due signal arrives.
|
||||
# A newer due signal always supersedes an older pending occurrence.
|
||||
#
|
||||
# Normal operation is automatic. The job-list package refreshes all jobs on a
|
||||
# regular timer and after Home Assistant starts, and it calls this processor with
|
||||
# mark_complete: true when a physical button, event, sensor, MQTT button or other
|
||||
@@ -95,6 +104,29 @@
|
||||
# - Monthly jobs: 2 weeks after due_time
|
||||
#
|
||||
# VERSION HISTORY:
|
||||
# V2.4 2026-07-30
|
||||
# - Reformatted last_completed_human: shows "15:45" (today),
|
||||
# "Yesterday @ 15:45" or "X days ago @ 15:45".
|
||||
#
|
||||
# V2.3 2026-07-30
|
||||
# - Added initial_due_behavior: ignore field for Once jobs.
|
||||
# Records the first due signal but keeps the job Not Due.
|
||||
#
|
||||
# V2.2 2026-07-30
|
||||
# - Ignore historical event/attribute due signals that predate the most
|
||||
# recent completion while still recording them as processed.
|
||||
#
|
||||
# V2.1 2026-07-30
|
||||
# - Once jobs remain Complete for a configurable hold period.
|
||||
# - Added minute-level and human-readable completion age attributes.
|
||||
# - Added increasing numeric/timestamp attribute due signals.
|
||||
# - New due occurrences override an active completion hold.
|
||||
#
|
||||
# V2.0 2026-07-29
|
||||
# - Added the event-driven Once job processor.
|
||||
# - Added retained Once occurrence, pending and due-signal state.
|
||||
# - Added registry-defined due states and filtered completion states.
|
||||
#
|
||||
# V1.6 2026-07-10
|
||||
# - Renamed displayed state from Missed to Missed.
|
||||
# - Added optional completion_window_start and completion_window_end fields.
|
||||
@@ -474,11 +506,9 @@ script:
|
||||
{% set completed_midnight = as_timestamp(strptime(d ~ ' 00:00:00', '%Y-%m-%d %H:%M:%S')) %}
|
||||
{% set days_ago = ((as_timestamp(today_at('00:00')) - completed_midnight) / 86400) | round(0) | int %}
|
||||
{% if d == today %}
|
||||
Today @ {{ t }}
|
||||
{{ t }}
|
||||
{% elif d == yesterday %}
|
||||
Yesterday @ {{ t }}
|
||||
{% elif days_ago < 7 %}
|
||||
Last {{ ts | timestamp_custom('%A', true) }} @ {{ t }}
|
||||
{% else %}
|
||||
{{ days_ago }} days ago @ {{ t }}
|
||||
{% endif %}
|
||||
@@ -572,3 +602,332 @@ script:
|
||||
"last_updated_local": {{ ((now_ts | float(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true)) | tojson }},
|
||||
"mqtt_topic": {{ topic_safe | tojson }}
|
||||
}
|
||||
|
||||
jobs_tracker_update_once_job:
|
||||
alias: "Jobs Tracker - Update Once Job"
|
||||
description: "Updates one event-driven Once job's retained MQTT JSON state."
|
||||
mode: queued
|
||||
max: 20
|
||||
|
||||
fields:
|
||||
job_id:
|
||||
description: "Machine-safe job ID."
|
||||
example: "water_tv_pot_plant"
|
||||
job_name:
|
||||
description: "Human-readable job name."
|
||||
example: "Water TV Pot Plant"
|
||||
mqtt_topic:
|
||||
description: "MQTT topic used for retained job state."
|
||||
example: "viewroad-status/jobs_tracker/water_tv_pot_plant"
|
||||
state_entity:
|
||||
description: "MQTT sensor entity that reads this job's retained JSON."
|
||||
example: "sensor.jobs_tracker_water_tv_pot_plant"
|
||||
due_entity:
|
||||
description: "Entity that starts a new Once job occurrence."
|
||||
example: "binary_sensor.tv_plant_needs_water"
|
||||
due_state:
|
||||
description: "Optional state that starts the Once job."
|
||||
example: "on"
|
||||
due_attribute:
|
||||
description: "Optional increasing timestamp or numeric attribute that starts the job."
|
||||
example: "cycle_stop_ts"
|
||||
completion_entities:
|
||||
description: "Entities and optional target states that complete the job."
|
||||
completion_hold_hours:
|
||||
description: "Hours to remain Complete before returning to Not Due."
|
||||
example: 3
|
||||
initial_due_behavior:
|
||||
description: "ignore - records the first due signal without making the job Due. Prevents old historical cycle timestamps from triggering immediately."
|
||||
example: "ignore"
|
||||
mark_complete:
|
||||
description: "Set true for an accepted manual completion."
|
||||
example: true
|
||||
|
||||
sequence:
|
||||
- variables:
|
||||
mark_complete_bool: "{{ mark_complete | default(false) | bool }}"
|
||||
topic_safe: "{{ mqtt_topic | default('viewroad-status/jobs_tracker/' ~ job_id) }}"
|
||||
due_entity_safe: "{{ due_entity | default('') }}"
|
||||
due_state_safe: "{{ due_state | default('') }}"
|
||||
due_attribute_safe: "{{ due_attribute | default('') }}"
|
||||
completion_entities_safe: "{{ completion_entities | default([]) }}"
|
||||
completion_hold_hours_safe: "{{ completion_hold_hours | default(3) | float(3) }}"
|
||||
completion_hold_seconds: >-
|
||||
{{ (completion_hold_hours | default(3) | float(3) * 3600) | int(10800) }}
|
||||
initial_due_behavior_safe: "{{ initial_due_behavior | default('') | lower }}"
|
||||
now_ts: "{{ as_timestamp(now()) }}"
|
||||
stored_last_completed_ts: >-
|
||||
{% set value = state_attr(state_entity, 'last_completed_ts') %}
|
||||
{{ 0 if value in [none, '', 'unknown', 'unavailable'] else value | float(0) }}
|
||||
stored_once_due_ts: >-
|
||||
{% set value = state_attr(state_entity, 'once_due_ts') %}
|
||||
{{ 0 if value in [none, '', 'unknown', 'unavailable'] else value | float(0) }}
|
||||
stored_once_pending: >-
|
||||
{{ state_attr(state_entity, 'once_pending') | default(false, true) | bool }}
|
||||
stored_due_signal_active: >-
|
||||
{{ state_attr(state_entity, 'once_due_signal_active') | default(false, true) | bool }}
|
||||
stored_due_signal_ts: >-
|
||||
{% set value = state_attr(state_entity, 'once_due_signal_ts') %}
|
||||
{{ 0 if value in [none, '', 'unknown', 'unavailable'] else value | float(0) }}
|
||||
|
||||
- variables:
|
||||
due_uses_state: >-
|
||||
{{ due_state_safe not in ['', none] and due_attribute_safe in ['', none] }}
|
||||
due_uses_attribute: "{{ due_attribute_safe not in ['', none] }}"
|
||||
due_signal_active: >-
|
||||
{{
|
||||
due_uses_state | bool
|
||||
and due_entity_safe != ''
|
||||
and is_state(due_entity_safe, due_state_safe)
|
||||
}}
|
||||
current_due_signal_ts: >-
|
||||
{% if due_entity_safe == '' %}
|
||||
0
|
||||
{% elif due_uses_attribute | bool %}
|
||||
{{ state_attr(due_entity_safe, due_attribute_safe) | float(0) }}
|
||||
{% elif due_entity_safe.startswith('event.') %}
|
||||
{{ as_timestamp(states(due_entity_safe), 0) }}
|
||||
{% else %}
|
||||
{% set source = states[due_entity_safe] %}
|
||||
{{ as_timestamp(source.last_changed, 0) if source is not none else 0 }}
|
||||
{% endif %}
|
||||
due_signal_valid: >-
|
||||
{{
|
||||
due_entity_safe != ''
|
||||
and states(due_entity_safe) not in ['unknown', 'unavailable']
|
||||
and (current_due_signal_ts | float(0)) > 0
|
||||
}}
|
||||
|
||||
- variables:
|
||||
new_due_signal: >-
|
||||
{% if due_uses_state | bool %}
|
||||
{{
|
||||
due_signal_valid | bool
|
||||
and due_signal_active | bool
|
||||
and not (stored_due_signal_active | bool)
|
||||
and (initial_due_behavior_safe != 'ignore' or stored_due_signal_ts > 0)
|
||||
}}
|
||||
{% else %}
|
||||
{{
|
||||
due_signal_valid | bool
|
||||
and (current_due_signal_ts | float(0)) > (stored_due_signal_ts | float(0))
|
||||
and (current_due_signal_ts | float(0)) > (stored_last_completed_ts | float(0))
|
||||
and (initial_due_behavior_safe != 'ignore' or stored_due_signal_ts > 0)
|
||||
}}
|
||||
{% endif %}
|
||||
once_due_ts: >-
|
||||
{% if new_due_signal | bool %}
|
||||
{{ current_due_signal_ts if not (due_uses_state | bool) else now_ts }}
|
||||
{% else %}
|
||||
{{ stored_once_due_ts }}
|
||||
{% endif %}
|
||||
due_signal_active_after: >-
|
||||
{{ due_signal_active if due_uses_state | bool else false }}
|
||||
due_signal_ts_after: >-
|
||||
{{
|
||||
[stored_due_signal_ts | float(0), current_due_signal_ts | float(0)]
|
||||
| max
|
||||
}}
|
||||
pending_before_completion: >-
|
||||
{{ stored_once_pending | bool or new_due_signal | bool }}
|
||||
|
||||
- variables:
|
||||
completion_entity_matched: >-
|
||||
{% set ns = namespace(matches=false) %}
|
||||
{% if pending_before_completion | bool %}
|
||||
{% for completion in completion_entities_safe %}
|
||||
{% if completion is mapping %}
|
||||
{% set entity_id = completion.entity_id | default('') %}
|
||||
{% set to_state = completion.to_state | default('') %}
|
||||
{% else %}
|
||||
{% set entity_id = completion %}
|
||||
{% set to_state = '' %}
|
||||
{% endif %}
|
||||
{% if entity_id != '' and states(entity_id) not in ['unknown', 'unavailable'] %}
|
||||
{% if entity_id.startswith('event.') %}
|
||||
{% set signal_ts = as_timestamp(states(entity_id), 0) %}
|
||||
{% else %}
|
||||
{% set source = states[entity_id] %}
|
||||
{% set signal_ts = as_timestamp(source.last_changed, 0) if source is not none else 0 %}
|
||||
{% endif %}
|
||||
{% if signal_ts > (once_due_ts | float(0))
|
||||
and signal_ts > (stored_last_completed_ts | float(0))
|
||||
and (to_state == '' or states(entity_id) == to_state) %}
|
||||
{% set ns.matches = true %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{{ ns.matches }}
|
||||
complete_now: >-
|
||||
{{
|
||||
pending_before_completion | bool
|
||||
and (mark_complete_bool | bool or completion_entity_matched | bool)
|
||||
}}
|
||||
|
||||
- variables:
|
||||
once_pending: >-
|
||||
{{ pending_before_completion | bool and not (complete_now | bool) }}
|
||||
last_completed_ts: >-
|
||||
{{ now_ts if complete_now | bool else stored_last_completed_ts }}
|
||||
completion_belongs_to_occurrence: >-
|
||||
{{
|
||||
(once_due_ts | float(0)) > 0
|
||||
and (last_completed_ts | float(0)) >= (once_due_ts | float(0))
|
||||
}}
|
||||
completion_age_seconds: >-
|
||||
{% if (last_completed_ts | float(0)) > 0 %}
|
||||
{{ [(now_ts | float(0)) - (last_completed_ts | float(0)), 0] | max | int(0) }}
|
||||
{% else %}
|
||||
0
|
||||
{% endif %}
|
||||
completion_hold_active: >-
|
||||
{{
|
||||
not (once_pending | bool)
|
||||
and completion_belongs_to_occurrence | bool
|
||||
and (completion_age_seconds | int(0)) < (completion_hold_seconds | int(10800))
|
||||
}}
|
||||
|
||||
- variables:
|
||||
job_status: >-
|
||||
{% if once_pending | bool %}
|
||||
Due
|
||||
{% elif completion_hold_active | bool %}
|
||||
Complete
|
||||
{% else %}
|
||||
Not Due
|
||||
{% endif %}
|
||||
minutes_since_required: >-
|
||||
{% if once_pending | bool and (once_due_ts | float(0)) > 0 %}
|
||||
{{ (((now_ts | float(0)) - (once_due_ts | float(0))) / 60) | round(0) | int }}
|
||||
{% else %}
|
||||
0
|
||||
{% endif %}
|
||||
hours_since_required: >-
|
||||
{% if once_pending | bool and (once_due_ts | float(0)) > 0 %}
|
||||
{{ (((now_ts | float(0)) - (once_due_ts | float(0))) / 3600) | round(0) | int }}
|
||||
{% else %}
|
||||
0
|
||||
{% endif %}
|
||||
hours_since_completed: >-
|
||||
{% if (last_completed_ts | float(0)) > 0 %}
|
||||
{{ ((completion_age_seconds | int(0)) / 3600) | int(0) }}
|
||||
{% else %}
|
||||
unknown
|
||||
{% endif %}
|
||||
minutes_since_completed: >-
|
||||
{% if (last_completed_ts | float(0)) > 0 %}
|
||||
{{ ((completion_age_seconds | int(0)) / 60) | int(0) }}
|
||||
{% else %}
|
||||
unknown
|
||||
{% endif %}
|
||||
completion_elapsed_human: >-
|
||||
{% if (last_completed_ts | float(0)) <= 0 %}
|
||||
Never
|
||||
{% elif (completion_age_seconds | int(0)) < 60 %}
|
||||
Just now
|
||||
{% elif (completion_age_seconds | int(0)) < 3600 %}
|
||||
{% set minutes = ((completion_age_seconds | int(0)) / 60) | int(0) %}
|
||||
{{ minutes }} {{ 'min' if minutes == 1 else 'mins' }}
|
||||
{% else %}
|
||||
{% set hours = ((completion_age_seconds | int(0)) / 3600) | int(0) %}
|
||||
{{ hours }} {{ 'hour' if hours == 1 else 'hours' }}
|
||||
{% endif %}
|
||||
last_completed_time: >-
|
||||
{% if (last_completed_ts | float(0)) > 0 %}
|
||||
{{ (last_completed_ts | float(0)) | timestamp_custom('%H:%M', true) }}
|
||||
{% else %}
|
||||
Never
|
||||
{% endif %}
|
||||
last_completed_human: >-
|
||||
{% set ts = last_completed_ts | float(0) %}
|
||||
{% if ts <= 0 %}
|
||||
Never
|
||||
{% else %}
|
||||
{% set d = ts | timestamp_custom('%Y-%m-%d', true) %}
|
||||
{% set t = ts | timestamp_custom('%H:%M', true) %}
|
||||
{% set today = now().strftime('%Y-%m-%d') %}
|
||||
{% set yesterday = (as_timestamp(today_at('00:00')) - 86400) | timestamp_custom('%Y-%m-%d', true) %}
|
||||
{% set completed_midnight = as_timestamp(strptime(d ~ ' 00:00:00', '%Y-%m-%d %H:%M:%S')) %}
|
||||
{% set days_ago = ((as_timestamp(today_at('00:00')) - completed_midnight) / 86400) | round(0) | int %}
|
||||
{% if d == today %}
|
||||
{{ t }}
|
||||
{% elif d == yesterday %}
|
||||
Yesterday @ {{ t }}
|
||||
{% else %}
|
||||
{{ days_ago }} days ago @ {{ t }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
next_due_human: >-
|
||||
{{ 'Due now' if once_pending | bool else 'Waiting for trigger' }}
|
||||
completed_for_due_ts: >-
|
||||
{% if (last_completed_ts | float(0)) >= (once_due_ts | float(0))
|
||||
and (once_due_ts | float(0)) > 0 %}
|
||||
{{ once_due_ts | float(0) }}
|
||||
{% else %}
|
||||
0
|
||||
{% endif %}
|
||||
|
||||
- action: mqtt.publish
|
||||
data:
|
||||
topic: "{{ topic_safe }}"
|
||||
retain: true
|
||||
qos: 1
|
||||
payload: >-
|
||||
{{
|
||||
{
|
||||
"job_id": job_id,
|
||||
"job_name": job_name,
|
||||
"repeat": "once",
|
||||
"repeat_type": "once",
|
||||
"due_time": "",
|
||||
"due_weekday": 0,
|
||||
"due_day": 0,
|
||||
"due_entity": due_entity_safe,
|
||||
"due_state": due_state_safe,
|
||||
"due_attribute": due_attribute_safe,
|
||||
"completion_hold_hours": completion_hold_hours_safe | float(3),
|
||||
"initial_due_behavior": initial_due_behavior_safe,
|
||||
"state": job_status | trim,
|
||||
"complete": completion_hold_active | bool,
|
||||
"last_completed_ts": last_completed_ts | float(0),
|
||||
"last_completed_time": last_completed_time,
|
||||
"last_completed_human": last_completed_human,
|
||||
"previous_period_start_ts": 0,
|
||||
"previous_due_ts": 0,
|
||||
"previous_completed": false,
|
||||
"previous_missed": false,
|
||||
"previous_missed_active": false,
|
||||
"missed": false,
|
||||
"completion_window_start": "",
|
||||
"completion_window_end": "",
|
||||
"has_completion_window": false,
|
||||
"completion_window_missed": false,
|
||||
"current_completion_window_start_ts": 0,
|
||||
"current_completion_window_end_ts": 0,
|
||||
"completed_for_period_ts": completed_for_due_ts | float(0),
|
||||
"completed_for_due_ts": completed_for_due_ts | float(0),
|
||||
"current_period_start_ts": once_due_ts | float(0),
|
||||
"next_period_start_ts": 0,
|
||||
"current_due_ts": once_due_ts | float(0),
|
||||
"next_due_ts": 0,
|
||||
"display_due_ts": (once_due_ts | float(0)) if once_pending | bool else 0,
|
||||
"next_due_human": next_due_human,
|
||||
"due_started": once_pending | bool,
|
||||
"overdue_started": false,
|
||||
"minutes_since_required": minutes_since_required | int(0),
|
||||
"hours_since_required": hours_since_required | int(0),
|
||||
"overdue_after_minutes": 0,
|
||||
"overdue_minutes": 0,
|
||||
"overdue_hours": 0,
|
||||
"hours_since_completed": hours_since_completed,
|
||||
"minutes_since_completed": minutes_since_completed,
|
||||
"completion_elapsed_human": completion_elapsed_human,
|
||||
"once_pending": once_pending | bool,
|
||||
"once_due_ts": once_due_ts | float(0),
|
||||
"once_due_signal_active": due_signal_active_after | bool,
|
||||
"once_due_signal_ts": due_signal_ts_after | float(0),
|
||||
"last_updated_local": (now_ts | float(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true),
|
||||
"mqtt_topic": topic_safe
|
||||
} | tojson
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#:########################################################################################:#
|
||||
# PACKAGE: Office Lights Home Assistant Fallback Synchronization
|
||||
# FILE: Lighting_2Way_Sync/office_lights_ha_fallback_sync.yaml
|
||||
# VERSION: v1.2.0
|
||||
# DATE: 2026-09-12
|
||||
#:########################################################################################:#
|
||||
#
|
||||
# PURPOSE:
|
||||
# Provides Home Assistant-primary synchronization for the Office two-way
|
||||
# light controls. Direct MQTT peer control is the HA-down transport.
|
||||
#
|
||||
# OPERATION:
|
||||
# - Synchronizes the Office bunker and overhead control pairs in both directions.
|
||||
# - ESPHome permits direct MQTT peer synchronization only while HA's native
|
||||
# API state subscription is disconnected, preventing competing controllers.
|
||||
# - Does not reconcile state during Home Assistant startup, preventing a stale
|
||||
# restored entity state from changing a physical light.
|
||||
#
|
||||
# DEPENDENCIES:
|
||||
# - Blueprint: zorruno/two_three_way_sync.yaml
|
||||
# - esp-officelights v3.2 and esp-officeduallights v3.0 or later.
|
||||
#
|
||||
# VERSION HISTORY:
|
||||
# - v1.2.0 (2026-09-12): Enable after guarded HA-primary firmware deployment.
|
||||
# - v1.1.0 (2026-09-12): Disabled until guarded HA-primary firmware deployment.
|
||||
# - v1.0.0 (2026-08-16): Initial MQTT-down HA fallback for Office lights.
|
||||
#:########################################################################################:#
|
||||
|
||||
automation:
|
||||
#:##########################################################################:#
|
||||
# BUNKER LIGHTS
|
||||
#:##########################################################################:#
|
||||
- id: office_bunker_lights_ha_fallback_sync
|
||||
alias: "Office Bunker Lights HA Fallback Sync"
|
||||
initial_state: true
|
||||
use_blueprint:
|
||||
path: zorruno/two_three_way_sync.yaml
|
||||
input:
|
||||
members:
|
||||
- switch.office_office_main_lightswitch_3_relay_1_bunker_lights
|
||||
- light.esp_officeduallights_light_1_right_hand_bunker_light
|
||||
sync_on_start: false
|
||||
startup_delay: 5
|
||||
|
||||
#:##########################################################################:#
|
||||
# OVERHEAD LIGHTS
|
||||
#:##########################################################################:#
|
||||
- id: office_overhead_lights_ha_fallback_sync
|
||||
alias: "Office Overhead Lights HA Fallback Sync"
|
||||
initial_state: true
|
||||
use_blueprint:
|
||||
path: zorruno/two_three_way_sync.yaml
|
||||
input:
|
||||
members:
|
||||
- switch.office_office_main_lightswitch_3_relay_2_overhead_lights
|
||||
- light.esp_officeduallights_light_2_cool_white_overhead_lights
|
||||
sync_on_start: false
|
||||
startup_delay: 5
|
||||
@@ -0,0 +1,57 @@
|
||||
#:########################################################################################:#
|
||||
# PACKAGE: Office Lights Home Assistant Fallback Synchronization
|
||||
# FILE: Lighting_2Way_Sync/office_lights_ha_fallback_sync.yaml
|
||||
# VERSION: v1.1.0
|
||||
# DATE: 2026-09-12
|
||||
#:########################################################################################:#
|
||||
#
|
||||
# PURPOSE:
|
||||
# Provides the Home Assistant API transport for Office two-way light control
|
||||
# when MQTT is unavailable. Direct MQTT remains the HA-down transport.
|
||||
#
|
||||
# OPERATION:
|
||||
# - Synchronizes the Office bunker and overhead control pairs in both directions.
|
||||
# - The automations remain disabled until both ESPHome devices enforce the
|
||||
# HA-primary/MQTT-fallback authority boundary.
|
||||
# - Does not reconcile state during Home Assistant startup, preventing a stale
|
||||
# restored entity state from changing a physical light.
|
||||
#
|
||||
# DEPENDENCIES:
|
||||
# - Blueprint: zorruno/two_three_way_sync.yaml
|
||||
# - esp-officelights and esp-officeduallights native ESPHome API connections.
|
||||
#
|
||||
# VERSION HISTORY:
|
||||
# - v1.1.0 (2026-09-12): Disable until guarded HA-primary firmware is deployed.
|
||||
# - v1.0.0 (2026-08-16): Initial MQTT-down HA fallback for Office lights.
|
||||
#:########################################################################################:#
|
||||
|
||||
automation:
|
||||
#:##########################################################################:#
|
||||
# BUNKER LIGHTS
|
||||
#:##########################################################################:#
|
||||
- id: office_bunker_lights_ha_fallback_sync
|
||||
alias: "Office Bunker Lights HA Fallback Sync"
|
||||
initial_state: false
|
||||
use_blueprint:
|
||||
path: zorruno/two_three_way_sync.yaml
|
||||
input:
|
||||
members:
|
||||
- switch.office_office_main_lightswitch_3_relay_1_bunker_lights
|
||||
- light.esp_officeduallights_light_1_right_hand_bunker_light
|
||||
sync_on_start: false
|
||||
startup_delay: 5
|
||||
|
||||
#:##########################################################################:#
|
||||
# OVERHEAD LIGHTS
|
||||
#:##########################################################################:#
|
||||
- id: office_overhead_lights_ha_fallback_sync
|
||||
alias: "Office Overhead Lights HA Fallback Sync"
|
||||
initial_state: false
|
||||
use_blueprint:
|
||||
path: zorruno/two_three_way_sync.yaml
|
||||
input:
|
||||
members:
|
||||
- switch.office_office_main_lightswitch_3_relay_2_overhead_lights
|
||||
- light.esp_officeduallights_light_2_cool_white_overhead_lights
|
||||
sync_on_start: false
|
||||
startup_delay: 5
|
||||
@@ -9,9 +9,13 @@
|
||||
# packages/Pool_Controls/pool_light_operation.yaml
|
||||
#
|
||||
# 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."
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
#:########################################################################################:#
|
||||
# Alexa TP-Link Guest Wi-Fi Package #
|
||||
#:########################################################################################:#
|
||||
#
|
||||
# TITLE:
|
||||
# Alexa TP-Link Guest Wi-Fi
|
||||
#
|
||||
# FILE:
|
||||
# packages/alexa_tplink_actions.yaml
|
||||
#
|
||||
# VERSION:
|
||||
# V1.1 2026-07-20
|
||||
#
|
||||
# VERSION HISTORY:
|
||||
# V1.1 2026-07-20
|
||||
# - Added structured documentation and automation descriptions.
|
||||
#
|
||||
# V1.0
|
||||
# - Initial Alexa-mediated TP-Link guest-network controls.
|
||||
#
|
||||
# PURPOSE:
|
||||
# Enables or disables the TP-Link guest Wi-Fi network by sending custom Alexa
|
||||
# requests through the kitchen Echo Show.
|
||||
#
|
||||
# DEPENDENCIES:
|
||||
# - input_boolean.wifi_guest_network is a UI-managed helper that represents
|
||||
# the requested guest-network state.
|
||||
# - media_player.view_rd_kitchen_echo_show must be available through the Alexa
|
||||
# Media Player integration.
|
||||
# - The Alexa account must have a working, linked TP-Link skill.
|
||||
#
|
||||
# ALEXA REQUESTS:
|
||||
# - On: ask t.p.link to enable guest network
|
||||
# - Off: ask t.p.link to disable guest network
|
||||
#
|
||||
# OPERATION NOTES:
|
||||
# - The helper records requested state; it does not confirm router state.
|
||||
# - If Alexa or the TP-Link skill fails, the helper can disagree with the
|
||||
# actual guest-network state.
|
||||
# - After the helper remains on for two hours, it is turned off. That state
|
||||
# change then sends the Alexa disable request.
|
||||
# - The two-hour state-trigger period does not persist through a Home Assistant
|
||||
# restart or automation reload.
|
||||
#
|
||||
#:########################################################################################:#
|
||||
|
||||
automation:
|
||||
- alias: "Turn on the Wifi Guest Network"
|
||||
description: "Asks the linked TP-Link Alexa skill to enable the guest Wi-Fi network."
|
||||
mode: single
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: input_boolean.wifi_guest_network
|
||||
to: "on"
|
||||
actions:
|
||||
- action: media_player.play_media
|
||||
target:
|
||||
entity_id: media_player.view_rd_kitchen_echo_show
|
||||
data:
|
||||
media_content_type: custom
|
||||
media_content_id: ask t.p.link to enable guest network
|
||||
|
||||
- alias: "Turn off the Wifi Guest Network"
|
||||
description: "Asks the linked TP-Link Alexa skill to disable the guest Wi-Fi network."
|
||||
mode: single
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: input_boolean.wifi_guest_network
|
||||
to: "off"
|
||||
actions:
|
||||
- action: media_player.play_media
|
||||
target:
|
||||
entity_id: media_player.view_rd_kitchen_echo_show
|
||||
data:
|
||||
media_content_type: custom
|
||||
media_content_id: ask t.p.link to disable guest network
|
||||
|
||||
- alias: "Switch guest network off after time"
|
||||
description: "Turns off the guest-network helper after it remains on for two hours."
|
||||
mode: single
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: input_boolean.wifi_guest_network
|
||||
from: "off"
|
||||
to: "on"
|
||||
for: "02:00:00"
|
||||
conditions: []
|
||||
actions:
|
||||
- action: input_boolean.turn_off
|
||||
target:
|
||||
entity_id: input_boolean.wifi_guest_network
|
||||
@@ -0,0 +1,442 @@
|
||||
################################################################################
|
||||
# Appliance Remaining Time Predictor Package
|
||||
################################################################################
|
||||
#
|
||||
# File: appliance_remaining_time_predictor.yaml
|
||||
# Version: 1.2.0
|
||||
# Date: 2026-08-03
|
||||
#
|
||||
# Purpose:
|
||||
# - Record recent valid cycle durations for the washing machine, dryer, and
|
||||
# main dishwasher.
|
||||
# - Estimate minutes remaining while each appliance monitor is Operating.
|
||||
# - Retain the latest ten valid durations across Home Assistant restarts.
|
||||
# - Preserve the previous completed cycle's timestamps, energy, and cost while
|
||||
# a new cycle is running.
|
||||
#
|
||||
# Prediction method:
|
||||
# - Reject cycles outside appliance-specific duration limits to avoid false
|
||||
# starts and stale cycles.
|
||||
# - Keep the ten most recent valid durations.
|
||||
# - With five or more samples, discard the shortest and longest duration before
|
||||
# calculating the average. This limits the effect of unusual loads while
|
||||
# retaining recent behavior.
|
||||
# - Subtract the current cycle elapsed time from the predicted total duration.
|
||||
#
|
||||
# Version history:
|
||||
# - 1.2.0 (2026-08-06): Learn durations to the monitor's Finished transition,
|
||||
# matching the completion announcement instead of the inferred power stop.
|
||||
# - 1.1.0 (2026-08-03): Preserved last-completed cycle display values.
|
||||
# - 1.0.0 (2026-08-03): Initial three-appliance implementation.
|
||||
################################################################################
|
||||
|
||||
input_text:
|
||||
appliance_predictor_washing_machine_durations:
|
||||
name: "Appliance Predictor Washing Machine Durations"
|
||||
max: 255
|
||||
|
||||
appliance_predictor_dryer_durations:
|
||||
name: "Appliance Predictor Dryer Durations"
|
||||
max: 255
|
||||
|
||||
appliance_predictor_main_dishwasher_durations:
|
||||
name: "Appliance Predictor Main Dishwasher Durations"
|
||||
max: 255
|
||||
|
||||
appliance_predictor_washing_machine_last_completed:
|
||||
name: "Appliance Predictor Washing Machine Last Completed"
|
||||
max: 255
|
||||
|
||||
appliance_predictor_dryer_last_completed:
|
||||
name: "Appliance Predictor Dryer Last Completed"
|
||||
max: 255
|
||||
|
||||
appliance_predictor_main_dishwasher_last_completed:
|
||||
name: "Appliance Predictor Main Dishwasher Last Completed"
|
||||
max: 255
|
||||
|
||||
script:
|
||||
appliance_predictor_record_duration:
|
||||
alias: "Appliance Predictor - Record Duration"
|
||||
description: >
|
||||
Validates and appends one completed cycle duration to the selected
|
||||
appliance's retained ten-cycle history.
|
||||
mode: queued
|
||||
max: 20
|
||||
fields:
|
||||
appliance_id:
|
||||
name: "Appliance ID"
|
||||
description: "washing_machine, dryer, or main_dishwasher"
|
||||
example: washing_machine
|
||||
duration_seconds:
|
||||
name: "Duration seconds"
|
||||
description: "Completed cycle duration in seconds."
|
||||
example: 3600
|
||||
sequence:
|
||||
- variables:
|
||||
duration: "{{ duration_seconds | int(0) }}"
|
||||
history_entity: >-
|
||||
{% if appliance_id == 'washing_machine' %}
|
||||
input_text.appliance_predictor_washing_machine_durations
|
||||
{% elif appliance_id == 'dryer' %}
|
||||
input_text.appliance_predictor_dryer_durations
|
||||
{% elif appliance_id == 'main_dishwasher' %}
|
||||
input_text.appliance_predictor_main_dishwasher_durations
|
||||
{% endif %}
|
||||
minimum_duration: >-
|
||||
{{ 900 if appliance_id == 'washing_machine' else 1800 }}
|
||||
maximum_duration: >-
|
||||
{{ 14400 if appliance_id == 'washing_machine' else 21600 }}
|
||||
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{
|
||||
history_entity | trim | length > 0
|
||||
and duration >= (minimum_duration | int(0))
|
||||
and duration <= (maximum_duration | int(0))
|
||||
}}
|
||||
|
||||
- variables:
|
||||
updated_history: >-
|
||||
{% set ns = namespace(values=[]) %}
|
||||
{% for item in states(history_entity).split(',') %}
|
||||
{% set value = item | int(0) %}
|
||||
{% if value > 0 %}
|
||||
{% set ns.values = ns.values + [value] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% set ns.values = ns.values + [duration] %}
|
||||
{{ ns.values[-10:] | join(',') }}
|
||||
|
||||
- action: input_text.set_value
|
||||
target:
|
||||
entity_id: "{{ history_entity | trim }}"
|
||||
data:
|
||||
value: "{{ updated_history | trim }}"
|
||||
|
||||
automation:
|
||||
- id: appliance_predictor_record_completed_cycles
|
||||
alias: "Appliance Predictor - Record Completed Cycles"
|
||||
description: >
|
||||
Records valid cycle durations when one of the three appliance monitors
|
||||
changes to Finished.
|
||||
mode: queued
|
||||
max: 10
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id:
|
||||
- sensor.appliance_monitor_washing_machine_monitor
|
||||
- sensor.appliance_monitor_dryer_monitor
|
||||
- sensor.appliance_monitor_main_dishwasher_monitor
|
||||
to: "Finished"
|
||||
actions:
|
||||
- variables:
|
||||
appliance_id: >-
|
||||
{% if trigger.entity_id == 'sensor.appliance_monitor_washing_machine_monitor' %}
|
||||
washing_machine
|
||||
{% elif trigger.entity_id == 'sensor.appliance_monitor_dryer_monitor' %}
|
||||
dryer
|
||||
{% elif trigger.entity_id == 'sensor.appliance_monitor_main_dishwasher_monitor' %}
|
||||
main_dishwasher
|
||||
{% endif %}
|
||||
duration: >-
|
||||
{{
|
||||
as_timestamp(trigger.to_state.last_changed)
|
||||
- (trigger.to_state.attributes.cycle_start_ts | float(0))
|
||||
}}
|
||||
completed_record_entity: >-
|
||||
{% if trigger.entity_id == 'sensor.appliance_monitor_washing_machine_monitor' %}
|
||||
input_text.appliance_predictor_washing_machine_last_completed
|
||||
{% elif trigger.entity_id == 'sensor.appliance_monitor_dryer_monitor' %}
|
||||
input_text.appliance_predictor_dryer_last_completed
|
||||
{% elif trigger.entity_id == 'sensor.appliance_monitor_main_dishwasher_monitor' %}
|
||||
input_text.appliance_predictor_main_dishwasher_last_completed
|
||||
{% endif %}
|
||||
completed_record: >-
|
||||
{{
|
||||
[
|
||||
trigger.to_state.attributes.cycle_start_ts | float(0),
|
||||
trigger.to_state.attributes.cycle_stop_ts | float(0),
|
||||
trigger.to_state.attributes.cycle_energy_kwh | float(0),
|
||||
trigger.to_state.attributes.cycle_cost | float(0)
|
||||
] | join(',')
|
||||
}}
|
||||
|
||||
- action: input_text.set_value
|
||||
target:
|
||||
entity_id: "{{ completed_record_entity | trim }}"
|
||||
data:
|
||||
value: "{{ completed_record | trim }}"
|
||||
|
||||
- action: script.appliance_predictor_record_duration
|
||||
data:
|
||||
appliance_id: "{{ appliance_id | trim }}"
|
||||
duration_seconds: "{{ duration }}"
|
||||
|
||||
- id: appliance_predictor_seed_empty_histories
|
||||
alias: "Appliance Predictor - Seed Empty Histories"
|
||||
description: >
|
||||
Seeds an empty history from the retained last completed monitor cycle
|
||||
after Home Assistant starts. Existing learned histories are not changed.
|
||||
mode: single
|
||||
triggers:
|
||||
- trigger: homeassistant
|
||||
event: start
|
||||
actions:
|
||||
- delay: "00:00:30"
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ states('input_text.appliance_predictor_washing_machine_durations') | trim | length == 0 }}
|
||||
then:
|
||||
- action: script.appliance_predictor_record_duration
|
||||
data:
|
||||
appliance_id: washing_machine
|
||||
duration_seconds: >-
|
||||
{{ state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_duration_seconds') | int(0) }}
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ states('input_text.appliance_predictor_dryer_durations') | trim | length == 0 }}
|
||||
then:
|
||||
- action: script.appliance_predictor_record_duration
|
||||
data:
|
||||
appliance_id: dryer
|
||||
duration_seconds: >-
|
||||
{{ state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_duration_seconds') | int(0) }}
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ states('input_text.appliance_predictor_main_dishwasher_durations') | trim | length == 0 }}
|
||||
then:
|
||||
- action: script.appliance_predictor_record_duration
|
||||
data:
|
||||
appliance_id: main_dishwasher
|
||||
duration_seconds: >-
|
||||
{{ state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_duration_seconds') | int(0) }}
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{
|
||||
states('input_text.appliance_predictor_washing_machine_last_completed') | trim | length == 0
|
||||
and not is_state('sensor.appliance_monitor_washing_machine_monitor', 'Operating')
|
||||
}}
|
||||
then:
|
||||
- action: input_text.set_value
|
||||
target:
|
||||
entity_id: input_text.appliance_predictor_washing_machine_last_completed
|
||||
data:
|
||||
value: >-
|
||||
{{
|
||||
[
|
||||
state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_start_ts') | float(0),
|
||||
state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_stop_ts') | float(0),
|
||||
state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_energy_kwh') | float(0),
|
||||
state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_cost') | float(0)
|
||||
] | join(',')
|
||||
}}
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{
|
||||
states('input_text.appliance_predictor_dryer_last_completed') | trim | length == 0
|
||||
and not is_state('sensor.appliance_monitor_dryer_monitor', 'Operating')
|
||||
}}
|
||||
then:
|
||||
- action: input_text.set_value
|
||||
target:
|
||||
entity_id: input_text.appliance_predictor_dryer_last_completed
|
||||
data:
|
||||
value: >-
|
||||
{{
|
||||
[
|
||||
state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_start_ts') | float(0),
|
||||
state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_stop_ts') | float(0),
|
||||
state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_energy_kwh') | float(0),
|
||||
state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_cost') | float(0)
|
||||
] | join(',')
|
||||
}}
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{
|
||||
states('input_text.appliance_predictor_main_dishwasher_last_completed') | trim | length == 0
|
||||
and not is_state('sensor.appliance_monitor_main_dishwasher_monitor', 'Operating')
|
||||
}}
|
||||
then:
|
||||
- action: input_text.set_value
|
||||
target:
|
||||
entity_id: input_text.appliance_predictor_main_dishwasher_last_completed
|
||||
data:
|
||||
value: >-
|
||||
{{
|
||||
[
|
||||
state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_start_ts') | float(0),
|
||||
state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_stop_ts') | float(0),
|
||||
state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_energy_kwh') | float(0),
|
||||
state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_cost') | float(0)
|
||||
] | join(',')
|
||||
}}
|
||||
|
||||
template:
|
||||
- triggers:
|
||||
- trigger: time_pattern
|
||||
minutes: "/1"
|
||||
- trigger: state
|
||||
entity_id:
|
||||
- sensor.appliance_monitor_washing_machine_monitor
|
||||
- sensor.appliance_monitor_dryer_monitor
|
||||
- sensor.appliance_monitor_main_dishwasher_monitor
|
||||
- input_text.appliance_predictor_washing_machine_durations
|
||||
- input_text.appliance_predictor_dryer_durations
|
||||
- input_text.appliance_predictor_main_dishwasher_durations
|
||||
- input_text.appliance_predictor_washing_machine_last_completed
|
||||
- input_text.appliance_predictor_dryer_last_completed
|
||||
- input_text.appliance_predictor_main_dishwasher_last_completed
|
||||
sensor:
|
||||
- name: "Washing Machine Estimated Time Remaining"
|
||||
unique_id: washing_machine_estimated_time_remaining
|
||||
icon: mdi:timer-sand
|
||||
unit_of_measurement: "min"
|
||||
state: >-
|
||||
{% set ns = namespace(values=[]) %}
|
||||
{% for item in states('input_text.appliance_predictor_washing_machine_durations').split(',') %}
|
||||
{% set value = item | int(0) %}
|
||||
{% if value > 0 %}
|
||||
{% set ns.values = ns.values + [value] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% set ordered = ns.values | sort %}
|
||||
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
|
||||
{% set average = (samples | sum / samples | count) if samples | count > 0 else 0 %}
|
||||
{% set start = state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_start_ts') | float(0) %}
|
||||
{% set elapsed = [0, as_timestamp(now()) - start] | max if start > 0 else 0 %}
|
||||
{% set remaining = [0, (average - elapsed) / 60] | max %}
|
||||
{{ remaining | round(0, 'ceil') | int(0) if is_state('sensor.appliance_monitor_washing_machine_monitor', 'Operating') else 0 }}
|
||||
attributes:
|
||||
sample_count: >-
|
||||
{% set ns = namespace(count=0) %}
|
||||
{% for item in states('input_text.appliance_predictor_washing_machine_durations').split(',') %}
|
||||
{% if item | int(0) > 0 %}{% set ns.count = ns.count + 1 %}{% endif %}
|
||||
{% endfor %}
|
||||
{{ ns.count }}
|
||||
predicted_cycle_minutes: >-
|
||||
{% set ns = namespace(values=[]) %}
|
||||
{% for item in states('input_text.appliance_predictor_washing_machine_durations').split(',') %}
|
||||
{% set value = item | int(0) %}
|
||||
{% if value > 0 %}{% set ns.values = ns.values + [value] %}{% endif %}
|
||||
{% endfor %}
|
||||
{% set ordered = ns.values | sort %}
|
||||
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
|
||||
{{ ((samples | sum / samples | count) / 60) | round(0) | int(0) if samples | count > 0 else 0 }}
|
||||
confidence: >-
|
||||
{% set count = states('input_text.appliance_predictor_washing_machine_durations').split(',') | reject('equalto', '') | list | count %}
|
||||
{{ 'High' if count >= 7 else 'Medium' if count >= 3 else 'Low' }}
|
||||
last_completed_start_ts: >-
|
||||
{{ states('input_text.appliance_predictor_washing_machine_last_completed').split(',')[0] | float(0) }}
|
||||
last_completed_stop_ts: >-
|
||||
{{ states('input_text.appliance_predictor_washing_machine_last_completed').split(',')[1] | default(0) | float(0) }}
|
||||
last_completed_energy_kwh: >-
|
||||
{{ states('input_text.appliance_predictor_washing_machine_last_completed').split(',')[2] | default(0) | float(0) }}
|
||||
last_completed_cost: >-
|
||||
{{ states('input_text.appliance_predictor_washing_machine_last_completed').split(',')[3] | default(0) | float(0) }}
|
||||
|
||||
- name: "Dryer Estimated Time Remaining"
|
||||
unique_id: dryer_estimated_time_remaining
|
||||
icon: mdi:timer-sand
|
||||
unit_of_measurement: "min"
|
||||
state: >-
|
||||
{% set ns = namespace(values=[]) %}
|
||||
{% for item in states('input_text.appliance_predictor_dryer_durations').split(',') %}
|
||||
{% set value = item | int(0) %}
|
||||
{% if value > 0 %}
|
||||
{% set ns.values = ns.values + [value] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% set ordered = ns.values | sort %}
|
||||
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
|
||||
{% set average = (samples | sum / samples | count) if samples | count > 0 else 0 %}
|
||||
{% set start = state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_start_ts') | float(0) %}
|
||||
{% set elapsed = [0, as_timestamp(now()) - start] | max if start > 0 else 0 %}
|
||||
{% set remaining = [0, (average - elapsed) / 60] | max %}
|
||||
{{ remaining | round(0, 'ceil') | int(0) if is_state('sensor.appliance_monitor_dryer_monitor', 'Operating') else 0 }}
|
||||
attributes:
|
||||
sample_count: >-
|
||||
{% set ns = namespace(count=0) %}
|
||||
{% for item in states('input_text.appliance_predictor_dryer_durations').split(',') %}
|
||||
{% if item | int(0) > 0 %}{% set ns.count = ns.count + 1 %}{% endif %}
|
||||
{% endfor %}
|
||||
{{ ns.count }}
|
||||
predicted_cycle_minutes: >-
|
||||
{% set ns = namespace(values=[]) %}
|
||||
{% for item in states('input_text.appliance_predictor_dryer_durations').split(',') %}
|
||||
{% set value = item | int(0) %}
|
||||
{% if value > 0 %}{% set ns.values = ns.values + [value] %}{% endif %}
|
||||
{% endfor %}
|
||||
{% set ordered = ns.values | sort %}
|
||||
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
|
||||
{{ ((samples | sum / samples | count) / 60) | round(0) | int(0) if samples | count > 0 else 0 }}
|
||||
confidence: >-
|
||||
{% set count = states('input_text.appliance_predictor_dryer_durations').split(',') | reject('equalto', '') | list | count %}
|
||||
{{ 'High' if count >= 7 else 'Medium' if count >= 3 else 'Low' }}
|
||||
last_completed_start_ts: >-
|
||||
{{ states('input_text.appliance_predictor_dryer_last_completed').split(',')[0] | float(0) }}
|
||||
last_completed_stop_ts: >-
|
||||
{{ states('input_text.appliance_predictor_dryer_last_completed').split(',')[1] | default(0) | float(0) }}
|
||||
last_completed_energy_kwh: >-
|
||||
{{ states('input_text.appliance_predictor_dryer_last_completed').split(',')[2] | default(0) | float(0) }}
|
||||
last_completed_cost: >-
|
||||
{{ states('input_text.appliance_predictor_dryer_last_completed').split(',')[3] | default(0) | float(0) }}
|
||||
|
||||
- name: "Main Dishwasher Estimated Time Remaining"
|
||||
unique_id: main_dishwasher_estimated_time_remaining
|
||||
icon: mdi:timer-sand
|
||||
unit_of_measurement: "min"
|
||||
state: >-
|
||||
{% set ns = namespace(values=[]) %}
|
||||
{% for item in states('input_text.appliance_predictor_main_dishwasher_durations').split(',') %}
|
||||
{% set value = item | int(0) %}
|
||||
{% if value > 0 %}
|
||||
{% set ns.values = ns.values + [value] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% set ordered = ns.values | sort %}
|
||||
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
|
||||
{% set average = (samples | sum / samples | count) if samples | count > 0 else 0 %}
|
||||
{% set start = state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_start_ts') | float(0) %}
|
||||
{% set elapsed = [0, as_timestamp(now()) - start] | max if start > 0 else 0 %}
|
||||
{% set remaining = [0, (average - elapsed) / 60] | max %}
|
||||
{{ remaining | round(0, 'ceil') | int(0) if is_state('sensor.appliance_monitor_main_dishwasher_monitor', 'Operating') else 0 }}
|
||||
attributes:
|
||||
sample_count: >-
|
||||
{% set ns = namespace(count=0) %}
|
||||
{% for item in states('input_text.appliance_predictor_main_dishwasher_durations').split(',') %}
|
||||
{% if item | int(0) > 0 %}{% set ns.count = ns.count + 1 %}{% endif %}
|
||||
{% endfor %}
|
||||
{{ ns.count }}
|
||||
predicted_cycle_minutes: >-
|
||||
{% set ns = namespace(values=[]) %}
|
||||
{% for item in states('input_text.appliance_predictor_main_dishwasher_durations').split(',') %}
|
||||
{% set value = item | int(0) %}
|
||||
{% if value > 0 %}{% set ns.values = ns.values + [value] %}{% endif %}
|
||||
{% endfor %}
|
||||
{% set ordered = ns.values | sort %}
|
||||
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
|
||||
{{ ((samples | sum / samples | count) / 60) | round(0) | int(0) if samples | count > 0 else 0 }}
|
||||
confidence: >-
|
||||
{% set count = states('input_text.appliance_predictor_main_dishwasher_durations').split(',') | reject('equalto', '') | list | count %}
|
||||
{{ 'High' if count >= 7 else 'Medium' if count >= 3 else 'Low' }}
|
||||
last_completed_start_ts: >-
|
||||
{{ states('input_text.appliance_predictor_main_dishwasher_last_completed').split(',')[0] | float(0) }}
|
||||
last_completed_stop_ts: >-
|
||||
{{ states('input_text.appliance_predictor_main_dishwasher_last_completed').split(',')[1] | default(0) | float(0) }}
|
||||
last_completed_energy_kwh: >-
|
||||
{{ states('input_text.appliance_predictor_main_dishwasher_last_completed').split(',')[2] | default(0) | float(0) }}
|
||||
last_completed_cost: >-
|
||||
{{ states('input_text.appliance_predictor_main_dishwasher_last_completed').split(',')[3] | default(0) | float(0) }}
|
||||
@@ -1,8 +1,8 @@
|
||||
#:########################################################################################:#
|
||||
# Package: Appliance Activity Feed Sensors
|
||||
# File: appliance_status_mqttfeed.yaml
|
||||
# Version: v1.2.0
|
||||
# Date: 2026-07-20
|
||||
# Version: v1.3.0
|
||||
# Date: 2026-08-12
|
||||
#
|
||||
# Purpose:
|
||||
# Provides MQTT sensors for appliance, vehicle-charger, and tool-charger
|
||||
@@ -13,12 +13,9 @@
|
||||
# - External publishers or the announcement router must publish completion
|
||||
# payloads to the configured activity-feed topics.
|
||||
#
|
||||
# Known Limitation:
|
||||
# - sensor.washer_finished has device_class timestamp and therefore requires
|
||||
# an ISO 8601 timestamp. P013-F018 records that the current router may publish
|
||||
# a time-only payload, which is not valid for this device class.
|
||||
#
|
||||
# Version History:
|
||||
# - v1.3.0 (2026-08-12): Activity feed payloads are ISO 8601 timestamps,
|
||||
# compatible with the washer timestamp sensor.
|
||||
# - v1.2.0 (2026-07-20): Added the downstairs dishwasher completion sensor.
|
||||
# - v1.1.0 (2025-05-18): Set the washer completion sensor as a timestamp.
|
||||
# - v1.0.0 (2023-10-20): Added the initial MQTT activity-feed sensors.
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 }}"
|
||||
@@ -0,0 +1,85 @@
|
||||
# PACKAGE: Bedroom 3 Climate Controls
|
||||
#
|
||||
# Purpose: Configurable automatic heat-pump schedule for Mollie's bedroom.
|
||||
# Notes: The schedule runs only on enabled weekday and/or weekend day types.
|
||||
# Version: 2.0.0
|
||||
#
|
||||
#:########################################################################################:#
|
||||
|
||||
input_boolean:
|
||||
bedroom_3_morning_climate_enabled:
|
||||
name: "Bedroom 3 Heat Pump Auto Control"
|
||||
icon: mdi:hvac
|
||||
|
||||
bedroom_3_morning_climate_weekdays:
|
||||
name: "Bedroom 3 Heat Pump Weekdays"
|
||||
icon: mdi:calendar-week
|
||||
|
||||
bedroom_3_morning_climate_weekends:
|
||||
name: "Bedroom 3 Heat Pump Weekends"
|
||||
icon: mdi:calendar-weekend
|
||||
|
||||
input_datetime:
|
||||
bedroom_3_morning_climate_start_time:
|
||||
name: "Bedroom 3 Heat Pump On Time"
|
||||
icon: mdi:clock-start
|
||||
has_date: false
|
||||
has_time: true
|
||||
|
||||
bedroom_3_morning_climate_stop_time:
|
||||
name: "Bedroom 3 Heat Pump Off Time"
|
||||
icon: mdi:clock-end
|
||||
has_date: false
|
||||
has_time: true
|
||||
|
||||
#:########################################################################################:#
|
||||
# Automations
|
||||
#:########################################################################################:#
|
||||
|
||||
automation:
|
||||
- id: bedroom_3_morning_climate_start
|
||||
alias: "Bedroom 3 Heat Pump Auto Start"
|
||||
description: "Starts the heat pump at the selected time on enabled day types."
|
||||
triggers:
|
||||
- trigger: time_pattern
|
||||
minutes: "/1"
|
||||
conditions:
|
||||
- condition: state
|
||||
entity_id: input_boolean.bedroom_3_morning_climate_enabled
|
||||
state: "on"
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ now().strftime('%H:%M') ==
|
||||
states('input_datetime.bedroom_3_morning_climate_start_time')[:5] }}
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ (now().weekday() < 5 and
|
||||
is_state('input_boolean.bedroom_3_morning_climate_weekdays', 'on')) or
|
||||
(now().weekday() >= 5 and
|
||||
is_state('input_boolean.bedroom_3_morning_climate_weekends', 'on')) }}
|
||||
actions:
|
||||
- action: climate.turn_on
|
||||
target:
|
||||
entity_id: climate.mollie_s_room
|
||||
|
||||
- id: bedroom_3_morning_climate_stop
|
||||
alias: "Bedroom 3 Heat Pump Auto Stop"
|
||||
description: "Stops the heat pump at the selected time on enabled day types."
|
||||
triggers:
|
||||
- trigger: time_pattern
|
||||
minutes: "/1"
|
||||
conditions:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ now().strftime('%H:%M') ==
|
||||
states('input_datetime.bedroom_3_morning_climate_stop_time')[:5] }}
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ (now().weekday() < 5 and
|
||||
is_state('input_boolean.bedroom_3_morning_climate_weekdays', 'on')) or
|
||||
(now().weekday() >= 5 and
|
||||
is_state('input_boolean.bedroom_3_morning_climate_weekends', 'on')) }}
|
||||
actions:
|
||||
- action: climate.turn_off
|
||||
target:
|
||||
entity_id: climate.mollie_s_room
|
||||
@@ -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
|
||||
@@ -1,8 +1,8 @@
|
||||
#:########################################################################################:#
|
||||
# Package: CCTV Motion Feed
|
||||
# File: cctv_controls.yaml
|
||||
# Version: v1.1.0
|
||||
# Date: 2026-07-24
|
||||
# Version: v1.2.0
|
||||
# Date: 2026-08-06
|
||||
#
|
||||
# Purpose:
|
||||
# Provides a Home Assistant binary sensor for front-door motion messages
|
||||
@@ -14,6 +14,7 @@
|
||||
#
|
||||
#
|
||||
# Version History:
|
||||
# - v1.2.0 (2026-08-06): Match MotionEye HA plugin ON/OFF MQTT payloads.
|
||||
# - v1.1.0 (2026-07-24): Added explicit numeric ON/OFF payloads (P013-F039).
|
||||
# - v1.0.0 (2022-10-11): Initial CCTV front-door motion sensor package.
|
||||
#:########################################################################################:#
|
||||
@@ -23,6 +24,38 @@ mqtt:
|
||||
- unique_id: front_door_movement_camera_corner
|
||||
name: "Front Door Movement"
|
||||
state_topic: "viewroad-status/cctv/front_door_motion"
|
||||
payload_on: "1"
|
||||
payload_off: "0"
|
||||
payload_on: "ON"
|
||||
payload_off: "OFF"
|
||||
icon: mdi:exit-run
|
||||
|
||||
input_datetime:
|
||||
entranceway_last_detection:
|
||||
name: Entranceway Last Detection
|
||||
has_date: true
|
||||
has_time: true
|
||||
|
||||
template:
|
||||
- binary_sensor:
|
||||
- name: Entranceway Detection
|
||||
unique_id: entranceway_detection
|
||||
state: >-
|
||||
{{ is_state('binary_sensor.view_road_front_door_motion', 'on')
|
||||
or is_state('binary_sensor.front_door_movement', 'on') }}
|
||||
|
||||
automation:
|
||||
- id: entranceway_detection_last_seen
|
||||
alias: Entranceway Detection - Record Last Activity
|
||||
mode: restart
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: binary_sensor.view_road_front_door_motion
|
||||
to: "on"
|
||||
- platform: mqtt
|
||||
topic: viewroad-status/cctv/front_door_motion
|
||||
payload: "ON"
|
||||
action:
|
||||
- service: input_datetime.set_datetime
|
||||
target:
|
||||
entity_id: input_datetime.entranceway_last_detection
|
||||
data:
|
||||
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,44 @@
|
||||
shell_command:
|
||||
flounder_refresh_motioneye_snapshots: >-
|
||||
python3 /config/scripts/flounder_motioneye_snapshots.py
|
||||
"{{ state_attr('camera.camera2', 'entity_picture') }}"
|
||||
"{{ state_attr('camera.camera5', 'entity_picture') }}"
|
||||
"{{ state_attr('camera.camera6', 'entity_picture') }}"
|
||||
"{{ state_attr('camera.trailer', 'entity_picture') }}"
|
||||
"{{ state_attr('camera.camera4', 'entity_picture') }}"
|
||||
flounder_refresh_primary_motioneye_snapshots: >-
|
||||
python3 /config/scripts/flounder_motioneye_snapshots.py --primary
|
||||
"{{ state_attr('camera.camera2', 'entity_picture') }}"
|
||||
"{{ state_attr('camera.camera5', 'entity_picture') }}"
|
||||
"{{ state_attr('camera.camera6', 'entity_picture') }}"
|
||||
|
||||
automation:
|
||||
- id: flounder_refresh_motioneye_snapshots
|
||||
alias: "Flounder: Refresh MotionEye snapshots"
|
||||
description: >-
|
||||
Sequentially refreshes durable local CCTV previews without loading live
|
||||
streams in the kiosk browser.
|
||||
triggers:
|
||||
- trigger: time_pattern
|
||||
minutes: "*"
|
||||
id: tick
|
||||
- trigger: state
|
||||
entity_id: binary_sensor.flounder_kiosk_screen_on
|
||||
from: "off"
|
||||
to: "on"
|
||||
id: screen_on
|
||||
conditions:
|
||||
- condition: state
|
||||
entity_id: binary_sensor.flounder_kiosk_screen_on
|
||||
state: "on"
|
||||
actions:
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ trigger.id == 'tick' and now().minute % 5 == 0 }}
|
||||
sequence:
|
||||
- action: shell_command.flounder_refresh_motioneye_snapshots
|
||||
default:
|
||||
- action: shell_command.flounder_refresh_primary_motioneye_snapshots
|
||||
mode: single
|
||||
@@ -0,0 +1,374 @@
|
||||
################################################################################
|
||||
# Optimistic state helpers for the Flounder dashboard heat-pump controls.
|
||||
################################################################################
|
||||
|
||||
input_select:
|
||||
lounge_hvac_requested_mode:
|
||||
name: Lounge HVAC Requested Mode
|
||||
options:
|
||||
- none
|
||||
- "off"
|
||||
- heat
|
||||
- cool
|
||||
initial: none
|
||||
|
||||
flounder_lounge_power_request:
|
||||
name: Flounder Lounge Power Request
|
||||
options: [idle, "on", "off"]
|
||||
initial: idle
|
||||
|
||||
flounder_master_bedroom_power_request:
|
||||
name: Flounder Master Bedroom Power Request
|
||||
options: [idle, "on", "off"]
|
||||
initial: idle
|
||||
|
||||
flounder_lounge_nanoe_request:
|
||||
name: Flounder Lounge Nanoe Request
|
||||
options: [idle, "on", "off"]
|
||||
initial: idle
|
||||
|
||||
flounder_master_bedroom_nanoe_request:
|
||||
name: Flounder Master Bedroom Nanoe Request
|
||||
options: [idle, "on", "off"]
|
||||
initial: idle
|
||||
|
||||
flounder_lounge_preset_request:
|
||||
name: Flounder Lounge Preset Request
|
||||
options: [idle, none, powerful, quiet]
|
||||
initial: idle
|
||||
|
||||
flounder_master_bedroom_preset_request:
|
||||
name: Flounder Master Bedroom Preset Request
|
||||
options: [idle, none, powerful, quiet]
|
||||
initial: idle
|
||||
|
||||
input_number:
|
||||
lounge_setpoint_ui:
|
||||
name: Lounge Setpoint UI
|
||||
min: 16
|
||||
max: 30
|
||||
step: 0.5
|
||||
mode: box
|
||||
unit_of_measurement: "°C"
|
||||
icon: mdi:thermometer
|
||||
|
||||
input_boolean:
|
||||
flounder_lounge_setpoint_pending:
|
||||
name: Flounder Lounge Setpoint Pending
|
||||
|
||||
flounder_master_bedroom_setpoint_pending:
|
||||
name: Flounder Master Bedroom Setpoint Pending
|
||||
|
||||
script:
|
||||
flounder_lounge_power_toggle:
|
||||
alias: Flounder Lounge Heat Pump Power Toggle
|
||||
mode: restart
|
||||
sequence:
|
||||
- variables:
|
||||
pending: "{{ states('input_select.flounder_lounge_power_request') }}"
|
||||
requested_mode: "{{ states('input_select.lounge_hvac_requested_mode') }}"
|
||||
desired: >-
|
||||
{% set is_on = requested_mode in ['heat', 'cool'] or pending == 'on' or (pending == 'idle' and not is_state('climate.lounge', 'off')) %}
|
||||
{{ 'off' if is_on else 'on' }}
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_lounge_power_request
|
||||
data:
|
||||
option: "{{ desired }}"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.lounge_hvac_requested_mode
|
||||
data:
|
||||
option: "{{ 'off' if desired == 'off' else 'none' }}"
|
||||
- service: "climate.turn_{{ desired }}"
|
||||
target:
|
||||
entity_id: climate.lounge
|
||||
- delay: "00:00:30"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_lounge_power_request
|
||||
data:
|
||||
option: idle
|
||||
|
||||
flounder_master_bedroom_power_toggle:
|
||||
alias: Flounder Master Bedroom Heat Pump Power Toggle
|
||||
mode: restart
|
||||
sequence:
|
||||
- variables:
|
||||
pending: "{{ states('input_select.flounder_master_bedroom_power_request') }}"
|
||||
requested_mode: "{{ states('input_select.master_bedroom_hvac_requested_mode') }}"
|
||||
desired: >-
|
||||
{% set is_on = requested_mode in ['heat', 'cool', 'dry'] or pending == 'on' or (pending == 'idle' and not is_state('climate.master_bedroom', 'off')) %}
|
||||
{{ 'off' if is_on else 'on' }}
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_master_bedroom_power_request
|
||||
data:
|
||||
option: "{{ desired }}"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.master_bedroom_hvac_requested_mode
|
||||
data:
|
||||
option: "{{ 'off' if desired == 'off' else 'none' }}"
|
||||
- service: "climate.turn_{{ desired }}"
|
||||
target:
|
||||
entity_id: climate.master_bedroom
|
||||
- delay: "00:00:30"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_master_bedroom_power_request
|
||||
data:
|
||||
option: idle
|
||||
|
||||
flounder_lounge_hvac_mode:
|
||||
alias: Flounder Lounge HVAC Mode
|
||||
mode: restart
|
||||
fields:
|
||||
mode:
|
||||
description: HVAC mode to request
|
||||
sequence:
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.lounge_hvac_requested_mode
|
||||
data:
|
||||
option: "{{ mode }}"
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ mode == 'off' }}"
|
||||
sequence:
|
||||
- service: climate.turn_off
|
||||
target:
|
||||
entity_id: climate.lounge
|
||||
default:
|
||||
- service: climate.set_hvac_mode
|
||||
target:
|
||||
entity_id: climate.lounge
|
||||
data:
|
||||
hvac_mode: "{{ mode }}"
|
||||
|
||||
flounder_lounge_setpoint_adjust:
|
||||
alias: Flounder Lounge Setpoint Adjust
|
||||
mode: restart
|
||||
fields:
|
||||
step:
|
||||
description: Setpoint change in degrees C
|
||||
sequence:
|
||||
- variables:
|
||||
current_ui: "{{ states('input_number.lounge_setpoint_ui') | float(0) }}"
|
||||
current_climate: "{{ state_attr('climate.lounge', 'temperature') | float(22) }}"
|
||||
current: "{{ current_ui if current_ui >= 16 else current_climate }}"
|
||||
requested: "{{ [30, [16, current + (step | float(0))] | max] | min | round(1) }}"
|
||||
- service: input_boolean.turn_on
|
||||
target:
|
||||
entity_id: input_boolean.flounder_lounge_setpoint_pending
|
||||
- service: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.lounge_setpoint_ui
|
||||
data:
|
||||
value: "{{ requested }}"
|
||||
- service: climate.set_temperature
|
||||
target:
|
||||
entity_id: climate.lounge
|
||||
data:
|
||||
temperature: "{{ requested }}"
|
||||
- wait_template: >-
|
||||
{{ (((state_attr('climate.lounge', 'temperature') | float(0)) - (requested | float(0))) | abs) < 0.01 }}
|
||||
timeout: "00:00:30"
|
||||
continue_on_timeout: true
|
||||
- service: input_boolean.turn_off
|
||||
target:
|
||||
entity_id: input_boolean.flounder_lounge_setpoint_pending
|
||||
|
||||
flounder_master_bedroom_setpoint_adjust:
|
||||
alias: Flounder Master Bedroom Setpoint Adjust
|
||||
mode: restart
|
||||
fields:
|
||||
step:
|
||||
description: Setpoint change in degrees C
|
||||
sequence:
|
||||
- variables:
|
||||
current_ui: "{{ states('input_number.master_bedroom_setpoint_ui') | float(0) }}"
|
||||
current_climate: "{{ state_attr('climate.master_bedroom', 'temperature') | float(22) }}"
|
||||
current: "{{ current_ui if current_ui >= 16 else current_climate }}"
|
||||
requested: "{{ [30, [16, current + (step | float(0))] | max] | min | round(1) }}"
|
||||
- service: input_boolean.turn_on
|
||||
target:
|
||||
entity_id: input_boolean.flounder_master_bedroom_setpoint_pending
|
||||
- service: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.master_bedroom_setpoint_ui
|
||||
data:
|
||||
value: "{{ requested }}"
|
||||
- service: climate.set_temperature
|
||||
target:
|
||||
entity_id: climate.master_bedroom
|
||||
data:
|
||||
temperature: "{{ requested }}"
|
||||
- wait_template: >-
|
||||
{{ (((state_attr('climate.master_bedroom', 'temperature') | float(0)) - (requested | float(0))) | abs) < 0.01 }}
|
||||
timeout: "00:00:30"
|
||||
continue_on_timeout: true
|
||||
- service: input_boolean.turn_off
|
||||
target:
|
||||
entity_id: input_boolean.flounder_master_bedroom_setpoint_pending
|
||||
|
||||
flounder_lounge_nanoe_toggle:
|
||||
alias: Flounder Lounge Nanoe Toggle
|
||||
mode: restart
|
||||
sequence:
|
||||
- variables:
|
||||
pending: "{{ states('input_select.flounder_lounge_nanoe_request') }}"
|
||||
desired: >-
|
||||
{% set is_on = pending == 'on' or (pending == 'idle' and is_state('switch.lounge_nanoe', 'on')) %}
|
||||
{{ 'off' if is_on else 'on' }}
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_lounge_nanoe_request
|
||||
data:
|
||||
option: "{{ desired }}"
|
||||
- service: "switch.turn_{{ desired }}"
|
||||
target:
|
||||
entity_id: switch.lounge_nanoe
|
||||
- delay: "00:00:30"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_lounge_nanoe_request
|
||||
data:
|
||||
option: idle
|
||||
|
||||
flounder_master_bedroom_nanoe_toggle:
|
||||
alias: Flounder Master Bedroom Nanoe Toggle
|
||||
mode: restart
|
||||
sequence:
|
||||
- variables:
|
||||
pending: "{{ states('input_select.flounder_master_bedroom_nanoe_request') }}"
|
||||
desired: >-
|
||||
{% set is_on = pending == 'on' or (pending == 'idle' and is_state('switch.master_bedroom_nanoe', 'on')) %}
|
||||
{{ 'off' if is_on else 'on' }}
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_master_bedroom_nanoe_request
|
||||
data:
|
||||
option: "{{ desired }}"
|
||||
- service: "switch.turn_{{ desired }}"
|
||||
target:
|
||||
entity_id: switch.master_bedroom_nanoe
|
||||
- delay: "00:00:30"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_master_bedroom_nanoe_request
|
||||
data:
|
||||
option: idle
|
||||
|
||||
flounder_lounge_preset_toggle:
|
||||
alias: Flounder Lounge Preset Toggle
|
||||
mode: restart
|
||||
fields:
|
||||
preset:
|
||||
description: Preset to toggle
|
||||
sequence:
|
||||
- variables:
|
||||
pending: "{{ states('input_select.flounder_lounge_preset_request') }}"
|
||||
current: "{{ pending if pending != 'idle' else (state_attr('climate.lounge', 'preset_mode') or 'none') }}"
|
||||
desired: "{{ 'none' if current == preset else preset }}"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_lounge_preset_request
|
||||
data:
|
||||
option: "{{ desired }}"
|
||||
- service: climate.set_preset_mode
|
||||
target:
|
||||
entity_id: climate.lounge
|
||||
data:
|
||||
preset_mode: "{{ desired }}"
|
||||
- delay: "00:00:30"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_lounge_preset_request
|
||||
data:
|
||||
option: idle
|
||||
|
||||
flounder_master_bedroom_preset_toggle:
|
||||
alias: Flounder Master Bedroom Preset Toggle
|
||||
mode: restart
|
||||
fields:
|
||||
preset:
|
||||
description: Preset to toggle
|
||||
sequence:
|
||||
- variables:
|
||||
pending: "{{ states('input_select.flounder_master_bedroom_preset_request') }}"
|
||||
current: "{{ pending if pending != 'idle' else (state_attr('climate.master_bedroom', 'preset_mode') or 'none') }}"
|
||||
desired: "{{ 'none' if current == preset else preset }}"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_master_bedroom_preset_request
|
||||
data:
|
||||
option: "{{ desired }}"
|
||||
- service: climate.set_preset_mode
|
||||
target:
|
||||
entity_id: climate.master_bedroom
|
||||
data:
|
||||
preset_mode: "{{ desired }}"
|
||||
- delay: "00:00:30"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_master_bedroom_preset_request
|
||||
data:
|
||||
option: idle
|
||||
|
||||
automation:
|
||||
- id: flounder_lounge_setpoint_sync
|
||||
alias: Flounder Lounge Setpoint Sync
|
||||
mode: restart
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: climate.lounge
|
||||
- platform: homeassistant
|
||||
event: start
|
||||
condition:
|
||||
- condition: state
|
||||
entity_id: input_boolean.flounder_lounge_setpoint_pending
|
||||
state: "off"
|
||||
action:
|
||||
- variables:
|
||||
setpoint: "{{ state_attr('climate.lounge', 'temperature') | float(0) }}"
|
||||
- condition: template
|
||||
value_template: "{{ setpoint >= 16 }}"
|
||||
- service: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.lounge_setpoint_ui
|
||||
data:
|
||||
value: "{{ setpoint }}"
|
||||
|
||||
- id: lounge_hvac_clear_requested_mode
|
||||
alias: Lounge HVAC - Clear Requested Mode
|
||||
mode: restart
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: climate.lounge
|
||||
- platform: state
|
||||
entity_id: input_select.lounge_hvac_requested_mode
|
||||
to: ["off", heat, cool]
|
||||
condition:
|
||||
- condition: template
|
||||
value_template: "{{ states('input_select.lounge_hvac_requested_mode') != 'none' }}"
|
||||
action:
|
||||
- delay: "00:00:03"
|
||||
- choose:
|
||||
- conditions: >-
|
||||
{% set requested = states('input_select.lounge_hvac_requested_mode') %}
|
||||
{{ requested == states('climate.lounge') }}
|
||||
sequence:
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.lounge_hvac_requested_mode
|
||||
data:
|
||||
option: none
|
||||
default:
|
||||
- delay: "00:00:27"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.lounge_hvac_requested_mode
|
||||
data:
|
||||
option: none
|
||||
@@ -9,7 +9,7 @@
|
||||
# flounder_kiosk.yaml
|
||||
#
|
||||
# VERSION:
|
||||
# V2.0 - 2026-07-28
|
||||
# V2.3 - 2026-08-02
|
||||
#
|
||||
# PURPOSE:
|
||||
# Provides Home Assistant controls and status entities for the Flounder portrait
|
||||
@@ -131,6 +131,21 @@
|
||||
# - Actual Brightness reports the physical backlight percentage.
|
||||
#
|
||||
# VERSION HISTORY:
|
||||
# V2.3 - 2026-08-02
|
||||
# - Removed the Home Assistant Display Mode Restoration automation.
|
||||
# - Prevented startup and availability timing races from republishing an
|
||||
# incorrect display mode, including unexpected Screen On commands.
|
||||
# - Display-mode persistence and enforcement remain handled locally by the
|
||||
# Flounder screen-control service.
|
||||
#
|
||||
# V2.2 - 2026-07-30
|
||||
# - Enforce Screen Lock every ten minutes because the kiosk can incorrectly report
|
||||
# DPMS off after a restart while the physical panel remains dimly visible.
|
||||
#
|
||||
# V2.1 - 2026-07-30
|
||||
# - Reapply the selected display mode when the kiosk or Home Assistant restarts,
|
||||
# ensuring its physical panel, touchscreen and screensaver state are restored.
|
||||
#
|
||||
# V2.0 - 2026-07-28
|
||||
# - Replaced the separate Screen and Screensaver switches with one persistent
|
||||
# four-option Display Mode select.
|
||||
@@ -169,6 +184,22 @@
|
||||
#:########################################################################################:#
|
||||
|
||||
mqtt:
|
||||
- button:
|
||||
name: "Flounder Restart Chrome"
|
||||
unique_id: flounder_restart_chrome
|
||||
icon: mdi:google-chrome
|
||||
|
||||
device:
|
||||
identifiers:
|
||||
- flounder_kiosk
|
||||
name: "Flounder Kiosk"
|
||||
manufacturer: "Dell"
|
||||
model: "Portrait Touchscreen Kiosk"
|
||||
suggested_area: "Lounge"
|
||||
|
||||
command_topic: "viewroad-commands/flounder/chrome/restart"
|
||||
payload_press: "PRESS"
|
||||
|
||||
#:########################################################################################:#
|
||||
# Flounder Kiosk Display Mode #
|
||||
#:########################################################################################:#
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
#:########################################################################################:#
|
||||
# Package: Node-RED Health Checks
|
||||
# File: health_checks.yaml
|
||||
# Version: v1.2.0
|
||||
# Date: 2025-08-25
|
||||
#
|
||||
# Purpose:
|
||||
# Monitors the Node-RED Panda instance through independent HTTP and MQTT health
|
||||
# signals and combines them into one connectivity status.
|
||||
#
|
||||
# Behaviour:
|
||||
# - Polls the HTTP health endpoint every 30 seconds with a three-second timeout.
|
||||
# - Tracks the MQTT birth/will topic with QoS 1 and a 180-second expiry.
|
||||
# - Reports the combined check as available when either source is available.
|
||||
# - Reports the combined check as connected when either source is connected.
|
||||
#
|
||||
# Dependencies:
|
||||
# - Reachability of the configured Node-RED HTTP health endpoint.
|
||||
# - Home Assistant MQTT integration and the Node-RED birth/will publisher.
|
||||
#
|
||||
# Version History:
|
||||
# - v1.2.0 (2025-08-25): Reduced MQTT expiry to 180 seconds and clarified the
|
||||
# combined HTTP/MQTT sensor name.
|
||||
# - v1.1.0 (2025-08-22): Split HTTP and MQTT probes and added a combined sensor.
|
||||
# - v1.0.0 (2025-08-21): Initial Node-RED health-check package.
|
||||
#:########################################################################################:#
|
||||
|
||||
# ---- HTTP health probe ----
|
||||
binary_sensor:
|
||||
- platform: rest
|
||||
name: Node-RED Panda Up (HTTP)
|
||||
resource: http://192.168.3.200:1880/healthz
|
||||
method: GET
|
||||
device_class: connectivity
|
||||
value_template: "{{ value_json.status == 'ok' }}"
|
||||
timeout: 3
|
||||
scan_interval: 30
|
||||
|
||||
# ---- MQTT Will/Birth probe ----
|
||||
mqtt:
|
||||
binary_sensor:
|
||||
- name: "Node-RED Panda Up (MQTT)"
|
||||
unique_id: "nodered_panda_up_mqtt"
|
||||
state_topic: "nodered/panda/status"
|
||||
payload_on: "online"
|
||||
payload_off: "offline"
|
||||
device_class: connectivity
|
||||
qos: 1
|
||||
# Optional: avoid stale 'online' if no updates arrive
|
||||
expire_after: 180
|
||||
|
||||
# ---- Combine them (true if either is up) ----
|
||||
template:
|
||||
- binary_sensor:
|
||||
- name: "Node-RED Panda Up (HTTP/MQTT)"
|
||||
unique_id: "nodered_panda_up_combined"
|
||||
device_class: connectivity
|
||||
state: >
|
||||
{{ is_state('binary_sensor.node_red_panda_up_http','on')
|
||||
or is_state('binary_sensor.node_red_panda_up_mqtt','on') }}
|
||||
availability: >
|
||||
{{ states('binary_sensor.node_red_panda_up_http') not in ['unknown','unavailable']
|
||||
or states('binary_sensor.node_red_panda_up_mqtt') not in ['unknown','unavailable'] }}
|
||||
@@ -140,6 +140,9 @@ script:
|
||||
alias: Master Bedroom Setpoint Sync From Climate
|
||||
mode: restart
|
||||
sequence:
|
||||
- condition: state
|
||||
entity_id: input_boolean.flounder_master_bedroom_setpoint_pending
|
||||
state: "off"
|
||||
- variables:
|
||||
sp: "{{ state_attr('climate.master_bedroom', 'temperature') | float(0) }}"
|
||||
- service: input_number.set_value
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
# Capped Cloudflare throughput tests: 2 MB download and 500 kB upload per run.
|
||||
command_line:
|
||||
- sensor:
|
||||
name: "Internet Speed Test"
|
||||
unique_id: internet_speed_test
|
||||
command: "/config/scripts/internet_speed_test.sh"
|
||||
command_timeout: 120
|
||||
scan_interval: 31536000
|
||||
unit_of_measurement: "Mbit/s"
|
||||
device_class: data_rate
|
||||
state_class: measurement
|
||||
value_template: "{{ value_json.download_mbps }}"
|
||||
json_attributes:
|
||||
- upload_mbps
|
||||
|
||||
- binary_sensor:
|
||||
name: "Internet Connected"
|
||||
unique_id: internet_connected
|
||||
command: >-
|
||||
curl --fail --silent --show-error --max-time 10 --output /dev/null
|
||||
https://one.one.one.one/cdn-cgi/trace && printf "ON" || printf "OFF"
|
||||
command_timeout: 15
|
||||
scan_interval: 60
|
||||
payload_on: "ON"
|
||||
payload_off: "OFF"
|
||||
device_class: connectivity
|
||||
|
||||
input_number:
|
||||
internet_speed_tests_today:
|
||||
name: "Internet Speed Tests Today"
|
||||
min: 0
|
||||
max: 100
|
||||
step: 1
|
||||
mode: box
|
||||
internet_speed_tests_this_month:
|
||||
name: "Internet Speed Tests This Month"
|
||||
min: 0
|
||||
max: 2000
|
||||
step: 1
|
||||
mode: box
|
||||
internet_download_today_average:
|
||||
name: "Internet Download Today Average"
|
||||
min: 0
|
||||
max: 10000
|
||||
step: 0.01
|
||||
mode: box
|
||||
internet_upload_today_average:
|
||||
name: "Internet Upload Today Average"
|
||||
min: 0
|
||||
max: 10000
|
||||
step: 0.01
|
||||
mode: box
|
||||
internet_download_month_average:
|
||||
name: "Internet Download Month Average"
|
||||
min: 0
|
||||
max: 10000
|
||||
step: 0.01
|
||||
mode: box
|
||||
internet_upload_month_average:
|
||||
name: "Internet Upload Month Average"
|
||||
min: 0
|
||||
max: 10000
|
||||
step: 0.01
|
||||
mode: box
|
||||
|
||||
input_boolean:
|
||||
internet_speed_test_running:
|
||||
name: "Internet Speed Test Running"
|
||||
|
||||
script:
|
||||
internet_run_speed_test:
|
||||
alias: "Internet: Run speed test"
|
||||
sequence:
|
||||
- action: input_boolean.turn_on
|
||||
target:
|
||||
entity_id: input_boolean.internet_speed_test_running
|
||||
- variables:
|
||||
test_started: "{{ as_timestamp(now()) }}"
|
||||
- action: homeassistant.update_entity
|
||||
target:
|
||||
entity_id: sensor.internet_speed_test
|
||||
# Ignore the command-line sensor's interim unavailable state.
|
||||
- wait_template: >-
|
||||
{{
|
||||
has_value('sensor.internet_speed_test')
|
||||
and as_timestamp(states.sensor.internet_speed_test.last_updated, 0) > test_started
|
||||
}}
|
||||
timeout:
|
||||
minutes: 2
|
||||
continue_on_timeout: true
|
||||
- delay:
|
||||
seconds: 3
|
||||
- action: input_boolean.turn_off
|
||||
target:
|
||||
entity_id: input_boolean.internet_speed_test_running
|
||||
mode: single
|
||||
|
||||
template:
|
||||
- sensor:
|
||||
- name: "Internet Upload Speed Test"
|
||||
unique_id: internet_upload_speed_test
|
||||
unit_of_measurement: "Mbit/s"
|
||||
device_class: data_rate
|
||||
state_class: measurement
|
||||
availability: "{{ state_attr('sensor.internet_speed_test', 'upload_mbps') is number }}"
|
||||
state: "{{ state_attr('sensor.internet_speed_test', 'upload_mbps') | float }}"
|
||||
- name: "Internet Last Speed Display"
|
||||
unique_id: internet_last_speed_display
|
||||
state: >-
|
||||
{% if is_state('input_boolean.internet_speed_test_running', 'on') %}
|
||||
Testing
|
||||
{% elif has_value('sensor.internet_speed_test') and has_value('sensor.internet_upload_speed_test') %}
|
||||
{{ states('sensor.internet_speed_test') | float(0) | round(0) | int }}/{{ states('sensor.internet_upload_speed_test') | float(0) | round(0) | int }}
|
||||
{% else %}
|
||||
--
|
||||
{% endif %}
|
||||
- name: "Internet Download Average Today"
|
||||
unique_id: internet_download_average_today
|
||||
unit_of_measurement: "Mbit/s"
|
||||
device_class: data_rate
|
||||
state_class: measurement
|
||||
availability: "{{ states('input_number.internet_speed_tests_today') | int(0) > 0 }}"
|
||||
state: "{{ states('input_number.internet_download_today_average') | float(0) }}"
|
||||
- name: "Internet Upload Average Today"
|
||||
unique_id: internet_upload_average_today
|
||||
unit_of_measurement: "Mbit/s"
|
||||
device_class: data_rate
|
||||
state_class: measurement
|
||||
availability: "{{ states('input_number.internet_speed_tests_today') | int(0) > 0 }}"
|
||||
state: "{{ states('input_number.internet_upload_today_average') | float(0) }}"
|
||||
- name: "Internet Download Average This Month"
|
||||
unique_id: internet_download_average_this_month
|
||||
unit_of_measurement: "Mbit/s"
|
||||
device_class: data_rate
|
||||
state_class: measurement
|
||||
availability: "{{ states('input_number.internet_speed_tests_this_month') | int(0) > 0 }}"
|
||||
state: "{{ states('input_number.internet_download_month_average') | float(0) }}"
|
||||
- name: "Internet Upload Average This Month"
|
||||
unique_id: internet_upload_average_this_month
|
||||
unit_of_measurement: "Mbit/s"
|
||||
device_class: data_rate
|
||||
state_class: measurement
|
||||
availability: "{{ states('input_number.internet_speed_tests_this_month') | int(0) > 0 }}"
|
||||
state: "{{ states('input_number.internet_upload_month_average') | float(0) }}"
|
||||
|
||||
sensor:
|
||||
- platform: statistics
|
||||
name: "Internet Download Average Last 7 Days"
|
||||
unique_id: internet_download_average_last_7_days
|
||||
entity_id: sensor.internet_speed_test
|
||||
state_characteristic: mean
|
||||
sampling_size: 400
|
||||
max_age:
|
||||
days: 7
|
||||
- platform: statistics
|
||||
name: "Internet Upload Average Last 7 Days"
|
||||
unique_id: internet_upload_average_last_7_days
|
||||
entity_id: sensor.internet_upload_speed_test
|
||||
state_characteristic: mean
|
||||
sampling_size: 400
|
||||
max_age:
|
||||
days: 7
|
||||
|
||||
automation:
|
||||
- id: internet_run_speed_test
|
||||
alias: "Internet: Run capped speed test"
|
||||
description: "Runs at a random time between five minutes before and after each half-hour."
|
||||
triggers:
|
||||
- trigger: time_pattern
|
||||
minutes: 25
|
||||
- trigger: time_pattern
|
||||
minutes: 55
|
||||
conditions: []
|
||||
actions:
|
||||
- delay:
|
||||
seconds: "{{ range(0, 601) | random }}"
|
||||
- action: script.internet_run_speed_test
|
||||
target:
|
||||
entity_id: sensor.internet_speed_test
|
||||
mode: single
|
||||
|
||||
- id: internet_record_speed_test
|
||||
alias: "Internet: Record speed test averages"
|
||||
description: "Records every successful speed test, including the startup test."
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: sensor.internet_speed_test
|
||||
conditions: []
|
||||
actions:
|
||||
# Allow the upload template sensor to update from the command-line result.
|
||||
- delay:
|
||||
seconds: 1
|
||||
- condition: template
|
||||
value_template: "{{ has_value('sensor.internet_speed_test') and has_value('sensor.internet_upload_speed_test') }}"
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.internet_download_today_average
|
||||
data:
|
||||
value: >-
|
||||
{{ ((states('input_number.internet_download_today_average') | float(0) * states('input_number.internet_speed_tests_today') | int(0)) + states('sensor.internet_speed_test') | float(0)) / (states('input_number.internet_speed_tests_today') | int(0) + 1) }}
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.internet_upload_today_average
|
||||
data:
|
||||
value: >-
|
||||
{{ ((states('input_number.internet_upload_today_average') | float(0) * states('input_number.internet_speed_tests_today') | int(0)) + states('sensor.internet_upload_speed_test') | float(0)) / (states('input_number.internet_speed_tests_today') | int(0) + 1) }}
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.internet_download_month_average
|
||||
data:
|
||||
value: >-
|
||||
{{ ((states('input_number.internet_download_month_average') | float(0) * states('input_number.internet_speed_tests_this_month') | int(0)) + states('sensor.internet_speed_test') | float(0)) / (states('input_number.internet_speed_tests_this_month') | int(0) + 1) }}
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.internet_upload_month_average
|
||||
data:
|
||||
value: >-
|
||||
{{ ((states('input_number.internet_upload_month_average') | float(0) * states('input_number.internet_speed_tests_this_month') | int(0)) + states('sensor.internet_upload_speed_test') | float(0)) / (states('input_number.internet_speed_tests_this_month') | int(0) + 1) }}
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.internet_speed_tests_today
|
||||
data:
|
||||
value: "{{ states('input_number.internet_speed_tests_today') | int(0) + 1 }}"
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.internet_speed_tests_this_month
|
||||
data:
|
||||
value: "{{ states('input_number.internet_speed_tests_this_month') | int(0) + 1 }}"
|
||||
mode: queued
|
||||
|
||||
- id: internet_reset_daily_speed_averages
|
||||
alias: "Internet: Reset daily speed averages"
|
||||
triggers:
|
||||
- trigger: time
|
||||
at: "00:00:00"
|
||||
conditions: []
|
||||
actions:
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id:
|
||||
- input_number.internet_speed_tests_today
|
||||
- input_number.internet_download_today_average
|
||||
- input_number.internet_upload_today_average
|
||||
data:
|
||||
value: 0
|
||||
mode: single
|
||||
|
||||
- id: internet_reset_monthly_speed_averages
|
||||
alias: "Internet: Reset monthly speed averages"
|
||||
triggers:
|
||||
- trigger: time
|
||||
at: "00:00:00"
|
||||
conditions:
|
||||
- condition: template
|
||||
value_template: "{{ now().day == 1 }}"
|
||||
actions:
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id:
|
||||
- input_number.internet_speed_tests_this_month
|
||||
- input_number.internet_download_month_average
|
||||
- input_number.internet_upload_month_average
|
||||
data:
|
||||
value: 0
|
||||
mode: single
|
||||
@@ -0,0 +1,109 @@
|
||||
#:########################################################################################:#
|
||||
# Jobs Tracker Appliance Completion Triggers Package #
|
||||
#:########################################################################################:#
|
||||
#
|
||||
# TITLE:
|
||||
# Jobs Tracker Appliance Completion Triggers
|
||||
#
|
||||
# FILE:
|
||||
# jobs_tracker_appliance_completion_triggers.yaml
|
||||
#
|
||||
# VERSION:
|
||||
# V1.1 2026-09-03
|
||||
#
|
||||
# PURPOSE:
|
||||
# Provides time-qualified appliance door and lid triggers for Jobs Tracker completion.
|
||||
#
|
||||
# DEPENDS ON:
|
||||
# packages/Job_Chore_Tracking/jobs_tracker_jobs.yaml V3.5 or newer.
|
||||
#
|
||||
# OPERATION:
|
||||
# Each contact must remain open for five seconds before its corresponding Jobs Tracker
|
||||
# completion route is called. Starting the dryer or washer clears only a previous Due
|
||||
# unload job, so the next finished cycle creates its own new unload task.
|
||||
#
|
||||
# VERSION HISTORY:
|
||||
# V1.1 2026-09-03
|
||||
# - Clear a pending dryer or washer unload job when its next cycle starts.
|
||||
#
|
||||
# V1.0 2026-08-15
|
||||
# - Added five-second door and lid open completion triggers for dryer and washer jobs.
|
||||
#
|
||||
#:########################################################################################:#
|
||||
|
||||
automation:
|
||||
- id: jobs_tracker_empty_dryer_door_open
|
||||
alias: "Jobs Tracker - Empty Dryer on Door Open"
|
||||
description: "Marks Empty the Dryer complete after the dryer door is open for five seconds."
|
||||
mode: single
|
||||
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: binary_sensor.dryer_door_status_x34rs_contact
|
||||
to: "on"
|
||||
for:
|
||||
seconds: 5
|
||||
|
||||
actions:
|
||||
- action: script.jobs_tracker_route_completion
|
||||
data:
|
||||
completion_entity: binary_sensor.dryer_door_status_x34rs_contact
|
||||
completion_to_state: "on"
|
||||
|
||||
- id: jobs_tracker_empty_washing_machine_lid_open
|
||||
alias: "Jobs Tracker - Empty Washer on Lid Open"
|
||||
description: "Marks Empty the Washer complete after the washer lid is open for five seconds."
|
||||
mode: single
|
||||
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: binary_sensor.washing_machine_lid_status_x39rs_contact
|
||||
to: "on"
|
||||
for:
|
||||
seconds: 5
|
||||
|
||||
actions:
|
||||
- action: script.jobs_tracker_route_completion
|
||||
data:
|
||||
completion_entity: binary_sensor.washing_machine_lid_status_x39rs_contact
|
||||
completion_to_state: "on"
|
||||
|
||||
- id: jobs_tracker_clear_dryer_unload_on_start
|
||||
alias: "Jobs Tracker - Clear Dryer Unload on Start"
|
||||
description: "Clears an outstanding Empty the Dryer job when a new dryer cycle starts."
|
||||
mode: single
|
||||
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: sensor.appliance_monitor_dryer_monitor
|
||||
to: Operating
|
||||
|
||||
conditions:
|
||||
- condition: state
|
||||
entity_id: sensor.jobs_tracker_empty_dryer
|
||||
state: Due
|
||||
|
||||
actions:
|
||||
- action: script.jobs_tracker_dispatch
|
||||
data:
|
||||
job_id: empty_dryer
|
||||
|
||||
- id: jobs_tracker_clear_washer_unload_on_start
|
||||
alias: "Jobs Tracker - Clear Washer Unload on Start"
|
||||
description: "Clears an outstanding Empty the Washer job when a new washer cycle starts."
|
||||
mode: single
|
||||
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: sensor.appliance_monitor_washing_machine_monitor
|
||||
to: Operating
|
||||
|
||||
conditions:
|
||||
- condition: state
|
||||
entity_id: sensor.jobs_tracker_empty_washing_machine
|
||||
state: Due
|
||||
|
||||
actions:
|
||||
- action: script.jobs_tracker_dispatch
|
||||
data:
|
||||
job_id: empty_washing_machine
|
||||
@@ -0,0 +1,65 @@
|
||||
#:########################################################################################:#
|
||||
# Jobs Tracker Appliance Completion Triggers Package #
|
||||
#:########################################################################################:#
|
||||
#
|
||||
# TITLE:
|
||||
# Jobs Tracker Appliance Completion Triggers
|
||||
#
|
||||
# FILE:
|
||||
# jobs_tracker_appliance_completion_triggers.yaml
|
||||
#
|
||||
# VERSION:
|
||||
# V1.0 2026-08-15
|
||||
#
|
||||
# PURPOSE:
|
||||
# Provides time-qualified appliance door and lid triggers for Jobs Tracker completion.
|
||||
#
|
||||
# DEPENDS ON:
|
||||
# packages/Job_Chore_Tracking/jobs_tracker_jobs.yaml V3.5 or newer.
|
||||
#
|
||||
# OPERATION:
|
||||
# Each contact must remain open for five seconds before its corresponding Jobs Tracker
|
||||
# completion route is called. The job registry owns the completion entity mappings.
|
||||
#
|
||||
# VERSION HISTORY:
|
||||
# V1.0 2026-08-15
|
||||
# - Added five-second door and lid open completion triggers for dryer and washer jobs.
|
||||
#
|
||||
#:########################################################################################:#
|
||||
|
||||
automation:
|
||||
- id: jobs_tracker_empty_dryer_door_open
|
||||
alias: "Jobs Tracker - Empty Dryer on Door Open"
|
||||
description: "Marks Empty the Dryer complete after the dryer door is open for five seconds."
|
||||
mode: single
|
||||
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: binary_sensor.dryer_door_status_x34rs_contact
|
||||
to: "on"
|
||||
for:
|
||||
seconds: 5
|
||||
|
||||
actions:
|
||||
- action: script.jobs_tracker_route_completion
|
||||
data:
|
||||
completion_entity: binary_sensor.dryer_door_status_x34rs_contact
|
||||
completion_to_state: "on"
|
||||
|
||||
- id: jobs_tracker_empty_washing_machine_lid_open
|
||||
alias: "Jobs Tracker - Empty Washer on Lid Open"
|
||||
description: "Marks Empty the Washer complete after the washer lid is open for five seconds."
|
||||
mode: single
|
||||
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: binary_sensor.washing_machine_lid_status_x39rs_contact
|
||||
to: "on"
|
||||
for:
|
||||
seconds: 5
|
||||
|
||||
actions:
|
||||
- action: script.jobs_tracker_route_completion
|
||||
data:
|
||||
completion_entity: binary_sensor.washing_machine_lid_status_x39rs_contact
|
||||
completion_to_state: "on"
|
||||
@@ -36,7 +36,7 @@ input_number:
|
||||
step: 0.1
|
||||
mode: box
|
||||
unit_of_measurement: "s"
|
||||
initial: 1.5
|
||||
initial: 6
|
||||
|
||||
automation:
|
||||
- id: lounge_ir_remote_functions
|
||||
@@ -101,10 +101,36 @@ automation:
|
||||
|
||||
- alias: "Run the matching lounge command"
|
||||
choose:
|
||||
- alias: "Set both reading lights together"
|
||||
conditions:
|
||||
- condition: template
|
||||
value_template: "{{ ir_hash == '0x79fb9e54' }}"
|
||||
sequence:
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{
|
||||
is_state('light.esp_lounge6chdimmer_couch_spots_left', 'on')
|
||||
or is_state('light.esp_lounge6chdimmer_couch_spots_right', 'on')
|
||||
}}
|
||||
sequence:
|
||||
- action: light.turn_off
|
||||
target:
|
||||
entity_id:
|
||||
- light.esp_lounge6chdimmer_couch_spots_left
|
||||
- light.esp_lounge6chdimmer_couch_spots_right
|
||||
default:
|
||||
- action: light.turn_on
|
||||
target:
|
||||
entity_id:
|
||||
- light.esp_lounge6chdimmer_couch_spots_left
|
||||
- light.esp_lounge6chdimmer_couch_spots_right
|
||||
|
||||
- alias: "Scene commands"
|
||||
conditions:
|
||||
- condition: template
|
||||
value_template: "{{ ir_hash in scene_map }}"
|
||||
value_template: "{{ ir_hash in scene_map and ir_hash != '0x79fb9e54' }}"
|
||||
sequence:
|
||||
- action: script.lounge_set_scene
|
||||
data:
|
||||
@@ -137,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: ""
|
||||
@@ -169,7 +195,7 @@ automation:
|
||||
data:
|
||||
channels: "sony_tv_lounge"
|
||||
title: "View Road"
|
||||
short_announcement: "Surround Amplifier"
|
||||
short_announcement: "Surround Amp {{ new_state_text }}"
|
||||
long_announcement: ""
|
||||
payload: "{{ new_state_text }}"
|
||||
indicator: ""
|
||||
|
||||
@@ -65,6 +65,10 @@ input_text:
|
||||
name: Lounge Last Scene
|
||||
initial: "All Off"
|
||||
max: 40
|
||||
lounge_scene_display:
|
||||
name: Lounge Scene Display
|
||||
initial: "Select Scene"
|
||||
max: 40
|
||||
|
||||
timer:
|
||||
lounge_override_timer:
|
||||
@@ -111,6 +115,14 @@ script:
|
||||
- "Dining On/Off"
|
||||
#- "Night Light"
|
||||
|
||||
display_scenes:
|
||||
- "Bright"
|
||||
- "TV General"
|
||||
- "Balanced"
|
||||
- "Movie"
|
||||
- "Night Light"
|
||||
- "Warm Cozy"
|
||||
|
||||
#####################################################################
|
||||
# SCENE SETTINGS
|
||||
# Delta only - only list entities you want to change.
|
||||
@@ -294,6 +306,19 @@ script:
|
||||
data:
|
||||
value: "{{ sel }}"
|
||||
|
||||
- choose:
|
||||
- conditions: "{{ sel in display_scenes }}"
|
||||
sequence:
|
||||
- service: input_text.set_value
|
||||
target: { entity_id: input_text.lounge_scene_display }
|
||||
data:
|
||||
value: "{{ sel }}"
|
||||
default:
|
||||
- service: input_text.set_value
|
||||
target: { entity_id: input_text.lounge_scene_display }
|
||||
data:
|
||||
value: "Select Scene"
|
||||
|
||||
#########################################################################
|
||||
# APPLY SELECTED SCENE
|
||||
#########################################################################
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#:########################################################################################:##
|
||||
# Main House Power Peak #
|
||||
#:########################################################################################:##
|
||||
# Tracks the calendar-day maximum of the main three-phase active-power sensor for the
|
||||
# Flounder dashboard. The helper retains its value across restarts and resets at midnight.
|
||||
|
||||
input_number:
|
||||
main_house_power_peak_today:
|
||||
name: Main House Power Peak Today
|
||||
icon: mdi:flash-triangle-outline
|
||||
min: 0
|
||||
max: 50000
|
||||
step: 1
|
||||
mode: box
|
||||
unit_of_measurement: W
|
||||
|
||||
automation:
|
||||
- id: main_house_power_peak_today_track
|
||||
alias: Main House Power Peak Today - Track
|
||||
mode: single
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: sensor.main_house_3_phase_power_monitor_main_power_total_active_power
|
||||
- trigger: homeassistant
|
||||
event: start
|
||||
conditions:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ states('sensor.main_house_3_phase_power_monitor_main_power_total_active_power') not in ['unknown', 'unavailable'] }}
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{
|
||||
states('sensor.main_house_3_phase_power_monitor_main_power_total_active_power') | float(0)
|
||||
> states('input_number.main_house_power_peak_today') | float(0)
|
||||
}}
|
||||
actions:
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.main_house_power_peak_today
|
||||
data:
|
||||
value: "{{ states('sensor.main_house_3_phase_power_monitor_main_power_total_active_power') | float(0) }}"
|
||||
|
||||
- id: main_house_power_peak_today_reset
|
||||
alias: Main House Power Peak Today - Reset
|
||||
mode: single
|
||||
triggers:
|
||||
- trigger: time
|
||||
at: "00:00:00"
|
||||
actions:
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.main_house_power_peak_today
|
||||
data:
|
||||
value: 0
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
#:########################################################################################:#
|
||||
# PACKAGE: Zorro Evening Feeding Reminder (P017-S013)
|
||||
# FILE: packages/p017_s013_zorro_feeding_reminder.yaml
|
||||
# VERSION: v1.0.1 2026-07-26
|
||||
# VERSION: v1.0.2 2026-08-12
|
||||
#:########################################################################################:#
|
||||
# PURPOSE:
|
||||
# Remind to feed Zorro (evening feeding) if not completed.
|
||||
# LED matrix shows "Feed Zorro" every 30 min from 5pm to 10pm.
|
||||
# Voice says "Zorro has not been fed this afternoon" every 30 min from 5pm to 8pm.
|
||||
# Each announcement has a random delay of 0-10 min for natural variation.
|
||||
# Clears LED matrix immediately when evening feeding is completed.
|
||||
#:########################################################################################:#
|
||||
# DEPENDENCIES:
|
||||
@@ -55,25 +54,32 @@ automation:
|
||||
voice_hours: "{{ now().hour < 20 }}"
|
||||
quiet_time: "{{ is_state('input_boolean.quiet_time', 'on') }}"
|
||||
|
||||
- delay:
|
||||
seconds: "{{ range(0, 601) | random | int }}"
|
||||
|
||||
- action: script.view_road_announcement
|
||||
data:
|
||||
channels: "led_matrix_1, sony_tv_lounge"
|
||||
title: "Zorro"
|
||||
short_announcement: "Feed Zorro"
|
||||
indicator: "zorro,1,15,10,6,5,0,0"
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ voice_hours and not quiet_time }}"
|
||||
value_template: >-
|
||||
{{ states('sensor.jobs_tracker_dog_feeding_evening') != 'Complete' }}
|
||||
then:
|
||||
- action: script.view_road_announcement
|
||||
data:
|
||||
channels: "lounge_google_home_voice"
|
||||
channels: "led_matrix_1, sony_tv_lounge"
|
||||
title: "Zorro"
|
||||
short_announcement: "Zorro has not been fed this afternoon"
|
||||
short_announcement: "Feed Zorro"
|
||||
indicator: "zorro,1,15,10,6,5,0,0"
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{
|
||||
voice_hours
|
||||
and not quiet_time
|
||||
and is_state('script.view_road_announcement', 'off')
|
||||
}}
|
||||
then:
|
||||
- action: script.view_road_announcement
|
||||
data:
|
||||
channels: "lounge_google_home_voice"
|
||||
title: "Zorro"
|
||||
short_announcement: "Zorro has not been fed this afternoon"
|
||||
|
||||
##############################################################################
|
||||
# CLEAR LED MATRIX WHEN FED
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
#:########################################################################################:#
|
||||
# PACKAGE: Potplant Moisture Alerts
|
||||
# FILE: packages/potplant_moisture_alerts.yaml
|
||||
# VERSION: v1.1.0 2026-07-29
|
||||
# - Preserve user-selected thresholds and master enable state across restarts.
|
||||
# VERSION: v1.1.1 2026-08-15
|
||||
# - Update moisture sensor entity IDs after the plant devices were renamed.
|
||||
# - v1.1.0 2026-07-29: Preserve user-selected thresholds and master enable state across restarts.
|
||||
#:########################################################################################:#
|
||||
# PURPOSE:
|
||||
# Alert when potplant soil moisture drops below a configurable threshold.
|
||||
# Displays on the Lounge LED matrix and announces via Lounge Google Home voice.
|
||||
#:########################################################################################:#
|
||||
# SETTINGS:
|
||||
# - input_number.*_moisture_threshold: per-plant dry threshold (default 30%)
|
||||
# - input_number.*_moisture_threshold: per-plant dry threshold (table 25%, TV 30%)
|
||||
# - input_boolean.potplant_alert_enabled: master enable toggle
|
||||
#:########################################################################################:#
|
||||
# DEPENDENCIES:
|
||||
# - packages/view_road_announcement_router.yaml for script.view_road_announcement
|
||||
# - sensor.table_plant_soil_moisture
|
||||
# - sensor.tv_plant_soil_moisture
|
||||
# - sensor.table_plant_x36md_soil_moisture
|
||||
# - sensor.tv_plant_x37md_soil_moisture
|
||||
#:########################################################################################:#
|
||||
|
||||
input_number:
|
||||
@@ -24,6 +25,7 @@ input_number:
|
||||
min: 0
|
||||
max: 100
|
||||
step: 5
|
||||
initial: 25
|
||||
unit_of_measurement: "%"
|
||||
icon: mdi:water-percent
|
||||
|
||||
@@ -45,15 +47,21 @@ template:
|
||||
- name: "Table Plant Needs Water"
|
||||
unique_id: table_plant_needs_water
|
||||
icon: mdi:flower
|
||||
availability: >-
|
||||
{{ not is_state('sensor.table_plant_x36md_soil_moisture', 'unavailable')
|
||||
and not is_state('sensor.table_plant_x36md_soil_moisture', 'unknown') }}
|
||||
state: >-
|
||||
{{ (states('sensor.table_plant_soil_moisture') | float(100)) <
|
||||
(states('input_number.table_plant_moisture_threshold') | float(30)) }}
|
||||
{{ (states('sensor.table_plant_x36md_soil_moisture') | float(100)) <
|
||||
(states('input_number.table_plant_moisture_threshold') | float(25)) }}
|
||||
|
||||
- name: "TV Plant Needs Water"
|
||||
unique_id: tv_plant_needs_water
|
||||
icon: mdi:flower
|
||||
availability: >-
|
||||
{{ not is_state('sensor.tv_plant_x37md_soil_moisture', 'unavailable')
|
||||
and not is_state('sensor.tv_plant_x37md_soil_moisture', 'unknown') }}
|
||||
state: >-
|
||||
{{ (states('sensor.tv_plant_soil_moisture') | float(100)) <
|
||||
{{ (states('sensor.tv_plant_x37md_soil_moisture') | float(100)) <
|
||||
(states('input_number.tv_plant_moisture_threshold') | float(30)) }}
|
||||
|
||||
automation:
|
||||
|
||||
@@ -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"
|
||||
#
|
||||
##################################################################
|
||||
|
||||
@@ -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"
|
||||
#
|
||||
##################################################################
|
||||
@@ -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') }}.
|
||||
@@ -1,6 +1,10 @@
|
||||
################################################################################
|
||||
# PACKAGE: View Road Announcement Router
|
||||
################################################################################
|
||||
# 1.11: Date: 2026-08-12
|
||||
# - Publish ISO 8601 timestamps to the MQTT activity feed so timestamp
|
||||
# sensors can consume the payload.
|
||||
#
|
||||
# 1.10: Date: 2026-07-24
|
||||
# - Added 2s delay before TTS speak to separate the cast chime from
|
||||
# the voice announcement.
|
||||
@@ -83,7 +87,7 @@
|
||||
# Topic format:
|
||||
# viewroad-status/activityfeed/<mqtt_feed>
|
||||
# Payload format:
|
||||
# YYYY-MM-DD HH:MM:SS
|
||||
# ISO 8601 local timestamp
|
||||
#
|
||||
# led_matrix_1:
|
||||
# Uses:
|
||||
@@ -655,7 +659,7 @@ script:
|
||||
# Uses:
|
||||
# mqtt_feed
|
||||
#
|
||||
# Publishes the current Home Assistant local time in AM/PM format.
|
||||
# Publishes the current Home Assistant local time as an ISO 8601 timestamp.
|
||||
#
|
||||
# Topic format:
|
||||
# viewroad-status/activityfeed/<mqtt_feed>
|
||||
@@ -665,7 +669,7 @@ script:
|
||||
#
|
||||
# Publishes:
|
||||
# Topic: viewroad-status/activityfeed/dogfed
|
||||
# Payload: 12:32 PM
|
||||
# Payload: 2026-08-12T21:12:00+12:00
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
@@ -682,8 +686,7 @@ script:
|
||||
- action: mqtt.publish
|
||||
data:
|
||||
topic: "{{ ann_mqtt_feed_topic }}"
|
||||
#payload: "{{ now().strftime('%I:%M %p') | regex_replace('^0', '') }}"
|
||||
payload: "{{ now().strftime('%H:%M') }}"
|
||||
payload: "{{ now().isoformat() }}"
|
||||
qos: "{{ mqtt_view_rd_feed_mqtt_qos | int(0) }}"
|
||||
retain: "{{ mqtt_view_rd_feed_mqtt_retain | bool(true) }}"
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Refresh Flounder's local MotionEye preview images."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import time
|
||||
from urllib.error import URLError
|
||||
from urllib.request import urlopen
|
||||
|
||||
|
||||
OUTPUTS = (
|
||||
"front-door.jpg",
|
||||
"driveway.jpg",
|
||||
"garage.jpg",
|
||||
"bike-shed.jpg",
|
||||
"caravan.jpg",
|
||||
)
|
||||
OUTPUT_DIR = Path(os.environ.get("HASS_CONFIG", "/config")) / "www/flounder"
|
||||
HASS_URL = os.environ.get("HASS_URL", "http://127.0.0.1:8123")
|
||||
|
||||
|
||||
def download(url_path: str, destination: Path) -> bool:
|
||||
url = f"{HASS_URL}{url_path}"
|
||||
for attempt in range(2):
|
||||
try:
|
||||
with urlopen(url, timeout=4) as response:
|
||||
image = response.read()
|
||||
if image.startswith(b"\xff\xd8") and image.endswith(b"\xff\xd9"):
|
||||
temporary = destination.with_suffix(".tmp")
|
||||
temporary.write_bytes(image)
|
||||
os.replace(temporary, destination)
|
||||
return True
|
||||
except (OSError, TimeoutError, URLError):
|
||||
pass
|
||||
if attempt == 0:
|
||||
time.sleep(2)
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
arguments = sys.argv[1:]
|
||||
outputs = OUTPUTS
|
||||
if arguments and arguments[0] == "--primary":
|
||||
arguments = arguments[1:]
|
||||
outputs = OUTPUTS[:3]
|
||||
|
||||
if len(arguments) != len(outputs):
|
||||
return 2
|
||||
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
failures = 0
|
||||
for url_path, filename in zip(arguments, outputs, strict=True):
|
||||
if not download(url_path, OUTPUT_DIR / filename):
|
||||
failures += 1
|
||||
time.sleep(2)
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
download_bytes=2000000
|
||||
upload_bytes=500000
|
||||
download_url="https://speed.cloudflare.com/__down?bytes=${download_bytes}"
|
||||
upload_url="https://speed.cloudflare.com/__up"
|
||||
|
||||
download_bps=$(curl --fail --silent --show-error --location --max-time 45 \
|
||||
--output /dev/null --write-out "%{speed_download}" "${download_url}")
|
||||
upload_bps=$(head -c "${upload_bytes}" /dev/zero | curl --fail --silent --show-error \
|
||||
--location --max-time 45 --output /dev/null --request POST \
|
||||
--header "Content-Type: application/octet-stream" --data-binary @- \
|
||||
--write-out "%{speed_upload}" "${upload_url}")
|
||||
|
||||
awk -v download_bps="${download_bps}" -v upload_bps="${upload_bps}" 'BEGIN {
|
||||
printf "{\"download_mbps\":%.2f,\"upload_mbps\":%.2f}\n", \
|
||||
download_bps * 8 / 1000000, upload_bps * 8 / 1000000
|
||||
}'
|
||||