Compare commits

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

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 17 KiB

+1 -1
View File
@@ -1 +1 @@
{"pid": 70, "version": 1, "ha_version": "2026.7.4", "start_ts": 1785317114.2667625}
{"pid": 70, "version": 1, "ha_version": "2026.8.1", "start_ts": 1786755431.7882934}
+1
View File
@@ -0,0 +1 @@
a5cfb18d66d8671cf91955eaea5ad94dcf80eaef22aad2e8374ac1223680d6aa
View File
+15 -11
View File
@@ -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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+10
View File
File diff suppressed because one or more lines are too long
+173 -1
View File
@@ -8,6 +8,9 @@
# packages/16a_ev_charging_controls.yaml
#
# 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
@@ -51,6 +51,9 @@
# matrix_indicator.
#
# VERSION HISTORY:
# 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 +220,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 +242,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 +264,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 +286,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 +309,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 +332,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 +354,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 +376,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 +398,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 +420,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: ""
@@ -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
}}
+59 -39
View File
@@ -1,17 +1,26 @@
#:########################################################################################:#
# Alexa TP-Link Guest Wi-Fi Package #
# TP-Link Deco Guest Wi-Fi Package #
#:########################################################################################:#
#
# TITLE:
# Alexa TP-Link Guest Wi-Fi
# TP-Link Deco Guest Wi-Fi
#
# FILE:
# packages/alexa_tplink_actions.yaml
#
# VERSION:
# V1.1 2026-07-20
# V2.1 2026-08-04
#
# VERSION HISTORY:
# V2.1 2026-08-04
# - Added sync automation so input_boolean.wifi_guest_network tracks the
# router's actual guest Wi-Fi status via the new guest-network
# binary sensor from the integration.
#
# V2.0 2026-08-04
# - Switched from Alexa voice commands to direct Deco local API control.
# - Uses tplink_deco.set_guest_network service for reliable on/off.
#
# V1.1 2026-07-20
# - Added structured documentation and automation descriptions.
#
@@ -19,73 +28,84 @@
# - 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.
# Enables or disables the TP-Link guest Wi-Fi network via the Deco's local
# API, using the tplink_deco custom integration's set_guest_network service.
#
# DEPENDENCIES:
# - tplink_deco integration must be configured and connected to the Deco.
# - input_boolean.wifi_guest_network is a UI-managed helper that represents
# the requested guest-network state.
# - 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
# - binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network reports
# the actual guest-network state from the router and keeps the helper in
# sync.
#
# OPERATION NOTES:
# - The helper records requested state; it does not confirm router state.
# - If Alexa or the TP-Link skill fails, the helper can disagree with the
# - The helper records requested state; the sync automation below then
# reconciles it against the router's actual state so the helper tracks
# the real guest-network status.
# - If the Deco API call fails, the helper can disagree with the
# actual guest-network state.
# - 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."
description: "Enables the TP-Link Deco guest Wi-Fi network via local API."
mode: single
triggers:
- trigger: state
entity_id: input_boolean.wifi_guest_network
to: "on"
actions:
- action: media_player.play_media
target:
entity_id: media_player.view_rd_kitchen_echo_show
- action: tplink_deco.set_guest_network
data:
media_content_type: custom
media_content_id: ask t.p.link to enable guest network
enable: true
- alias: "Turn off the Wifi Guest Network"
description: "Asks the linked TP-Link Alexa skill to disable the guest Wi-Fi network."
description: "Disables the TP-Link Deco guest Wi-Fi network via local API."
mode: single
triggers:
- trigger: state
entity_id: input_boolean.wifi_guest_network
to: "off"
actions:
- action: media_player.play_media
target:
entity_id: media_player.view_rd_kitchen_echo_show
- action: tplink_deco.set_guest_network
data:
media_content_type: custom
media_content_id: ask t.p.link to disable guest network
enable: false
- alias: "Switch guest network off after time"
description: "Turns off the guest-network helper after it remains on for two hours."
- alias: "Sync Wifi Guest Network button with actual status"
description: >
Reconciles input_boolean.wifi_guest_network against the router's actual
guest Wi-Fi state reported by the guest-network binary sensor, so the
button reflects real status (including changes made outside HA).
mode: single
triggers:
- trigger: state
entity_id: input_boolean.wifi_guest_network
from: "off"
entity_id: binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network
to: "on"
for: "02:00:00"
conditions: []
- trigger: state
entity_id: binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network
to: "off"
conditions:
- condition: template
value_template: >
{{ states('input_boolean.wifi_guest_network')
!= states('binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network') }}
actions:
- action: input_boolean.turn_off
target:
entity_id: input_boolean.wifi_guest_network
- choose:
- conditions:
- condition: template
value_template: >
{{ states('binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network') == 'on' }}
sequence:
- action: input_boolean.turn_on
target:
entity_id: input_boolean.wifi_guest_network
- conditions:
- condition: template
value_template: >
{{ states('binary_sensor.data_cupboard_new_sundamage_12_deco_guest_network') == 'off' }}
sequence:
- action: input_boolean.turn_off
target:
entity_id: input_boolean.wifi_guest_network
@@ -0,0 +1,442 @@
################################################################################
# Appliance Remaining Time Predictor Package
################################################################################
#
# File: appliance_remaining_time_predictor.yaml
# Version: 1.2.0
# Date: 2026-08-03
#
# Purpose:
# - Record recent valid cycle durations for the washing machine, dryer, and
# main dishwasher.
# - Estimate minutes remaining while each appliance monitor is Operating.
# - Retain the latest ten valid durations across Home Assistant restarts.
# - Preserve the previous completed cycle's timestamps, energy, and cost while
# a new cycle is running.
#
# Prediction method:
# - Reject cycles outside appliance-specific duration limits to avoid false
# starts and stale cycles.
# - Keep the ten most recent valid durations.
# - With five or more samples, discard the shortest and longest duration before
# calculating the average. This limits the effect of unusual loads while
# retaining recent behavior.
# - Subtract the current cycle elapsed time from the predicted total duration.
#
# Version history:
# - 1.2.0 (2026-08-06): Learn durations to the monitor's Finished transition,
# matching the completion announcement instead of the inferred power stop.
# - 1.1.0 (2026-08-03): Preserved last-completed cycle display values.
# - 1.0.0 (2026-08-03): Initial three-appliance implementation.
################################################################################
input_text:
appliance_predictor_washing_machine_durations:
name: "Appliance Predictor Washing Machine Durations"
max: 255
appliance_predictor_dryer_durations:
name: "Appliance Predictor Dryer Durations"
max: 255
appliance_predictor_main_dishwasher_durations:
name: "Appliance Predictor Main Dishwasher Durations"
max: 255
appliance_predictor_washing_machine_last_completed:
name: "Appliance Predictor Washing Machine Last Completed"
max: 255
appliance_predictor_dryer_last_completed:
name: "Appliance Predictor Dryer Last Completed"
max: 255
appliance_predictor_main_dishwasher_last_completed:
name: "Appliance Predictor Main Dishwasher Last Completed"
max: 255
script:
appliance_predictor_record_duration:
alias: "Appliance Predictor - Record Duration"
description: >
Validates and appends one completed cycle duration to the selected
appliance's retained ten-cycle history.
mode: queued
max: 20
fields:
appliance_id:
name: "Appliance ID"
description: "washing_machine, dryer, or main_dishwasher"
example: washing_machine
duration_seconds:
name: "Duration seconds"
description: "Completed cycle duration in seconds."
example: 3600
sequence:
- variables:
duration: "{{ duration_seconds | int(0) }}"
history_entity: >-
{% if appliance_id == 'washing_machine' %}
input_text.appliance_predictor_washing_machine_durations
{% elif appliance_id == 'dryer' %}
input_text.appliance_predictor_dryer_durations
{% elif appliance_id == 'main_dishwasher' %}
input_text.appliance_predictor_main_dishwasher_durations
{% endif %}
minimum_duration: >-
{{ 900 if appliance_id == 'washing_machine' else 1800 }}
maximum_duration: >-
{{ 14400 if appliance_id == 'washing_machine' else 21600 }}
- condition: template
value_template: >-
{{
history_entity | trim | length > 0
and duration >= (minimum_duration | int(0))
and duration <= (maximum_duration | int(0))
}}
- variables:
updated_history: >-
{% set ns = namespace(values=[]) %}
{% for item in states(history_entity).split(',') %}
{% set value = item | int(0) %}
{% if value > 0 %}
{% set ns.values = ns.values + [value] %}
{% endif %}
{% endfor %}
{% set ns.values = ns.values + [duration] %}
{{ ns.values[-10:] | join(',') }}
- action: input_text.set_value
target:
entity_id: "{{ history_entity | trim }}"
data:
value: "{{ updated_history | trim }}"
automation:
- id: appliance_predictor_record_completed_cycles
alias: "Appliance Predictor - Record Completed Cycles"
description: >
Records valid cycle durations when one of the three appliance monitors
changes to Finished.
mode: queued
max: 10
triggers:
- trigger: state
entity_id:
- sensor.appliance_monitor_washing_machine_monitor
- sensor.appliance_monitor_dryer_monitor
- sensor.appliance_monitor_main_dishwasher_monitor
to: "Finished"
actions:
- variables:
appliance_id: >-
{% if trigger.entity_id == 'sensor.appliance_monitor_washing_machine_monitor' %}
washing_machine
{% elif trigger.entity_id == 'sensor.appliance_monitor_dryer_monitor' %}
dryer
{% elif trigger.entity_id == 'sensor.appliance_monitor_main_dishwasher_monitor' %}
main_dishwasher
{% endif %}
duration: >-
{{
as_timestamp(trigger.to_state.last_changed)
- (trigger.to_state.attributes.cycle_start_ts | float(0))
}}
completed_record_entity: >-
{% if trigger.entity_id == 'sensor.appliance_monitor_washing_machine_monitor' %}
input_text.appliance_predictor_washing_machine_last_completed
{% elif trigger.entity_id == 'sensor.appliance_monitor_dryer_monitor' %}
input_text.appliance_predictor_dryer_last_completed
{% elif trigger.entity_id == 'sensor.appliance_monitor_main_dishwasher_monitor' %}
input_text.appliance_predictor_main_dishwasher_last_completed
{% endif %}
completed_record: >-
{{
[
trigger.to_state.attributes.cycle_start_ts | float(0),
trigger.to_state.attributes.cycle_stop_ts | float(0),
trigger.to_state.attributes.cycle_energy_kwh | float(0),
trigger.to_state.attributes.cycle_cost | float(0)
] | join(',')
}}
- action: input_text.set_value
target:
entity_id: "{{ completed_record_entity | trim }}"
data:
value: "{{ completed_record | trim }}"
- action: script.appliance_predictor_record_duration
data:
appliance_id: "{{ appliance_id | trim }}"
duration_seconds: "{{ duration }}"
- id: appliance_predictor_seed_empty_histories
alias: "Appliance Predictor - Seed Empty Histories"
description: >
Seeds an empty history from the retained last completed monitor cycle
after Home Assistant starts. Existing learned histories are not changed.
mode: single
triggers:
- trigger: homeassistant
event: start
actions:
- delay: "00:00:30"
- if:
- condition: template
value_template: >-
{{ states('input_text.appliance_predictor_washing_machine_durations') | trim | length == 0 }}
then:
- action: script.appliance_predictor_record_duration
data:
appliance_id: washing_machine
duration_seconds: >-
{{ state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_duration_seconds') | int(0) }}
- if:
- condition: template
value_template: >-
{{ states('input_text.appliance_predictor_dryer_durations') | trim | length == 0 }}
then:
- action: script.appliance_predictor_record_duration
data:
appliance_id: dryer
duration_seconds: >-
{{ state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_duration_seconds') | int(0) }}
- if:
- condition: template
value_template: >-
{{ states('input_text.appliance_predictor_main_dishwasher_durations') | trim | length == 0 }}
then:
- action: script.appliance_predictor_record_duration
data:
appliance_id: main_dishwasher
duration_seconds: >-
{{ state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_duration_seconds') | int(0) }}
- if:
- condition: template
value_template: >-
{{
states('input_text.appliance_predictor_washing_machine_last_completed') | trim | length == 0
and not is_state('sensor.appliance_monitor_washing_machine_monitor', 'Operating')
}}
then:
- action: input_text.set_value
target:
entity_id: input_text.appliance_predictor_washing_machine_last_completed
data:
value: >-
{{
[
state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_start_ts') | float(0),
state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_stop_ts') | float(0),
state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_energy_kwh') | float(0),
state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_cost') | float(0)
] | join(',')
}}
- if:
- condition: template
value_template: >-
{{
states('input_text.appliance_predictor_dryer_last_completed') | trim | length == 0
and not is_state('sensor.appliance_monitor_dryer_monitor', 'Operating')
}}
then:
- action: input_text.set_value
target:
entity_id: input_text.appliance_predictor_dryer_last_completed
data:
value: >-
{{
[
state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_start_ts') | float(0),
state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_stop_ts') | float(0),
state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_energy_kwh') | float(0),
state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_cost') | float(0)
] | join(',')
}}
- if:
- condition: template
value_template: >-
{{
states('input_text.appliance_predictor_main_dishwasher_last_completed') | trim | length == 0
and not is_state('sensor.appliance_monitor_main_dishwasher_monitor', 'Operating')
}}
then:
- action: input_text.set_value
target:
entity_id: input_text.appliance_predictor_main_dishwasher_last_completed
data:
value: >-
{{
[
state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_start_ts') | float(0),
state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_stop_ts') | float(0),
state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_energy_kwh') | float(0),
state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_cost') | float(0)
] | join(',')
}}
template:
- triggers:
- trigger: time_pattern
minutes: "/1"
- trigger: state
entity_id:
- sensor.appliance_monitor_washing_machine_monitor
- sensor.appliance_monitor_dryer_monitor
- sensor.appliance_monitor_main_dishwasher_monitor
- input_text.appliance_predictor_washing_machine_durations
- input_text.appliance_predictor_dryer_durations
- input_text.appliance_predictor_main_dishwasher_durations
- input_text.appliance_predictor_washing_machine_last_completed
- input_text.appliance_predictor_dryer_last_completed
- input_text.appliance_predictor_main_dishwasher_last_completed
sensor:
- name: "Washing Machine Estimated Time Remaining"
unique_id: washing_machine_estimated_time_remaining
icon: mdi:timer-sand
unit_of_measurement: "min"
state: >-
{% set ns = namespace(values=[]) %}
{% for item in states('input_text.appliance_predictor_washing_machine_durations').split(',') %}
{% set value = item | int(0) %}
{% if value > 0 %}
{% set ns.values = ns.values + [value] %}
{% endif %}
{% endfor %}
{% set ordered = ns.values | sort %}
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
{% set average = (samples | sum / samples | count) if samples | count > 0 else 0 %}
{% set start = state_attr('sensor.appliance_monitor_washing_machine_monitor', 'cycle_start_ts') | float(0) %}
{% set elapsed = [0, as_timestamp(now()) - start] | max if start > 0 else 0 %}
{% set remaining = [0, (average - elapsed) / 60] | max %}
{{ remaining | round(0, 'ceil') | int(0) if is_state('sensor.appliance_monitor_washing_machine_monitor', 'Operating') else 0 }}
attributes:
sample_count: >-
{% set ns = namespace(count=0) %}
{% for item in states('input_text.appliance_predictor_washing_machine_durations').split(',') %}
{% if item | int(0) > 0 %}{% set ns.count = ns.count + 1 %}{% endif %}
{% endfor %}
{{ ns.count }}
predicted_cycle_minutes: >-
{% set ns = namespace(values=[]) %}
{% for item in states('input_text.appliance_predictor_washing_machine_durations').split(',') %}
{% set value = item | int(0) %}
{% if value > 0 %}{% set ns.values = ns.values + [value] %}{% endif %}
{% endfor %}
{% set ordered = ns.values | sort %}
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
{{ ((samples | sum / samples | count) / 60) | round(0) | int(0) if samples | count > 0 else 0 }}
confidence: >-
{% set count = states('input_text.appliance_predictor_washing_machine_durations').split(',') | reject('equalto', '') | list | count %}
{{ 'High' if count >= 7 else 'Medium' if count >= 3 else 'Low' }}
last_completed_start_ts: >-
{{ states('input_text.appliance_predictor_washing_machine_last_completed').split(',')[0] | float(0) }}
last_completed_stop_ts: >-
{{ states('input_text.appliance_predictor_washing_machine_last_completed').split(',')[1] | default(0) | float(0) }}
last_completed_energy_kwh: >-
{{ states('input_text.appliance_predictor_washing_machine_last_completed').split(',')[2] | default(0) | float(0) }}
last_completed_cost: >-
{{ states('input_text.appliance_predictor_washing_machine_last_completed').split(',')[3] | default(0) | float(0) }}
- name: "Dryer Estimated Time Remaining"
unique_id: dryer_estimated_time_remaining
icon: mdi:timer-sand
unit_of_measurement: "min"
state: >-
{% set ns = namespace(values=[]) %}
{% for item in states('input_text.appliance_predictor_dryer_durations').split(',') %}
{% set value = item | int(0) %}
{% if value > 0 %}
{% set ns.values = ns.values + [value] %}
{% endif %}
{% endfor %}
{% set ordered = ns.values | sort %}
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
{% set average = (samples | sum / samples | count) if samples | count > 0 else 0 %}
{% set start = state_attr('sensor.appliance_monitor_dryer_monitor', 'cycle_start_ts') | float(0) %}
{% set elapsed = [0, as_timestamp(now()) - start] | max if start > 0 else 0 %}
{% set remaining = [0, (average - elapsed) / 60] | max %}
{{ remaining | round(0, 'ceil') | int(0) if is_state('sensor.appliance_monitor_dryer_monitor', 'Operating') else 0 }}
attributes:
sample_count: >-
{% set ns = namespace(count=0) %}
{% for item in states('input_text.appliance_predictor_dryer_durations').split(',') %}
{% if item | int(0) > 0 %}{% set ns.count = ns.count + 1 %}{% endif %}
{% endfor %}
{{ ns.count }}
predicted_cycle_minutes: >-
{% set ns = namespace(values=[]) %}
{% for item in states('input_text.appliance_predictor_dryer_durations').split(',') %}
{% set value = item | int(0) %}
{% if value > 0 %}{% set ns.values = ns.values + [value] %}{% endif %}
{% endfor %}
{% set ordered = ns.values | sort %}
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
{{ ((samples | sum / samples | count) / 60) | round(0) | int(0) if samples | count > 0 else 0 }}
confidence: >-
{% set count = states('input_text.appliance_predictor_dryer_durations').split(',') | reject('equalto', '') | list | count %}
{{ 'High' if count >= 7 else 'Medium' if count >= 3 else 'Low' }}
last_completed_start_ts: >-
{{ states('input_text.appliance_predictor_dryer_last_completed').split(',')[0] | float(0) }}
last_completed_stop_ts: >-
{{ states('input_text.appliance_predictor_dryer_last_completed').split(',')[1] | default(0) | float(0) }}
last_completed_energy_kwh: >-
{{ states('input_text.appliance_predictor_dryer_last_completed').split(',')[2] | default(0) | float(0) }}
last_completed_cost: >-
{{ states('input_text.appliance_predictor_dryer_last_completed').split(',')[3] | default(0) | float(0) }}
- name: "Main Dishwasher Estimated Time Remaining"
unique_id: main_dishwasher_estimated_time_remaining
icon: mdi:timer-sand
unit_of_measurement: "min"
state: >-
{% set ns = namespace(values=[]) %}
{% for item in states('input_text.appliance_predictor_main_dishwasher_durations').split(',') %}
{% set value = item | int(0) %}
{% if value > 0 %}
{% set ns.values = ns.values + [value] %}
{% endif %}
{% endfor %}
{% set ordered = ns.values | sort %}
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
{% set average = (samples | sum / samples | count) if samples | count > 0 else 0 %}
{% set start = state_attr('sensor.appliance_monitor_main_dishwasher_monitor', 'cycle_start_ts') | float(0) %}
{% set elapsed = [0, as_timestamp(now()) - start] | max if start > 0 else 0 %}
{% set remaining = [0, (average - elapsed) / 60] | max %}
{{ remaining | round(0, 'ceil') | int(0) if is_state('sensor.appliance_monitor_main_dishwasher_monitor', 'Operating') else 0 }}
attributes:
sample_count: >-
{% set ns = namespace(count=0) %}
{% for item in states('input_text.appliance_predictor_main_dishwasher_durations').split(',') %}
{% if item | int(0) > 0 %}{% set ns.count = ns.count + 1 %}{% endif %}
{% endfor %}
{{ ns.count }}
predicted_cycle_minutes: >-
{% set ns = namespace(values=[]) %}
{% for item in states('input_text.appliance_predictor_main_dishwasher_durations').split(',') %}
{% set value = item | int(0) %}
{% if value > 0 %}{% set ns.values = ns.values + [value] %}{% endif %}
{% endfor %}
{% set ordered = ns.values | sort %}
{% set samples = ordered[1:-1] if ordered | count >= 5 else ordered %}
{{ ((samples | sum / samples | count) / 60) | round(0) | int(0) if samples | count > 0 else 0 }}
confidence: >-
{% set count = states('input_text.appliance_predictor_main_dishwasher_durations').split(',') | reject('equalto', '') | list | count %}
{{ 'High' if count >= 7 else 'Medium' if count >= 3 else 'Low' }}
last_completed_start_ts: >-
{{ states('input_text.appliance_predictor_main_dishwasher_last_completed').split(',')[0] | float(0) }}
last_completed_stop_ts: >-
{{ states('input_text.appliance_predictor_main_dishwasher_last_completed').split(',')[1] | default(0) | float(0) }}
last_completed_energy_kwh: >-
{{ states('input_text.appliance_predictor_main_dishwasher_last_completed').split(',')[2] | default(0) | float(0) }}
last_completed_cost: >-
{{ states('input_text.appliance_predictor_main_dishwasher_last_completed').split(',')[3] | default(0) | float(0) }}
+4 -7
View File
@@ -1,8 +1,8 @@
#:########################################################################################:#
# Package: Appliance Activity Feed Sensors
# 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.
+75
View File
@@ -0,0 +1,75 @@
##########################################################################################
# Bedroom 3 Morning Climate
##########################################################################################
#
# Turns on the Bedroom 3 heat pump at the selected morning time without
# changing its current HVAC mode, temperature, or other settings, then turns
# it off at 08:30.
#
##########################################################################################
---
input_boolean:
bedroom_3_morning_climate_enabled:
name: Bedroom 3 Morning Climate
icon: mdi:weather-sunny
initial: true
input_select:
bedroom_3_morning_climate_time:
name: Bedroom 3 Morning Climate Time
icon: mdi:clock-start
initial: "06:45"
options:
- "04:30"
- "04:45"
- "05:00"
- "05:15"
- "05:30"
- "05:45"
- "06:00"
- "06:15"
- "06:30"
- "06:45"
- "07:00"
- "07:15"
- "07:30"
- "07:45"
- "08:00"
- "08:15"
- "08:30"
- "08:45"
- "09:00"
- "09:15"
- "09:30"
- "09:45"
- "10:00"
automation:
- id: bedroom_3_morning_climate_start
alias: Bedroom 3 Morning Climate Start
description: >-
Turns on the Bedroom 3 heat pump at the selected time when enabled, then
turns it off at 08:30.
trigger:
- trigger: time_pattern
minutes: "/15"
condition:
- condition: state
entity_id: input_boolean.bedroom_3_morning_climate_enabled
state: "on"
- condition: template
value_template: >-
{{ now().strftime('%H:%M') ==
states('input_select.bedroom_3_morning_climate_time') }}
action:
- action: climate.turn_on
target:
entity_id: climate.mollie_s_room
- wait_template: >-
{{ now().hour > 8 or (now().hour == 8 and now().minute >= 30) }}
- action: climate.turn_off
target:
entity_id: climate.mollie_s_room
+37 -4
View File
@@ -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') }}"
+44
View File
@@ -0,0 +1,44 @@
shell_command:
flounder_refresh_motioneye_snapshots: >-
python3 /config/scripts/flounder_motioneye_snapshots.py
"{{ state_attr('camera.camera2', 'entity_picture') }}"
"{{ state_attr('camera.camera5', 'entity_picture') }}"
"{{ state_attr('camera.camera6', 'entity_picture') }}"
"{{ state_attr('camera.trailer', 'entity_picture') }}"
"{{ state_attr('camera.camera4', 'entity_picture') }}"
flounder_refresh_primary_motioneye_snapshots: >-
python3 /config/scripts/flounder_motioneye_snapshots.py --primary
"{{ state_attr('camera.camera2', 'entity_picture') }}"
"{{ state_attr('camera.camera5', 'entity_picture') }}"
"{{ state_attr('camera.camera6', 'entity_picture') }}"
automation:
- id: flounder_refresh_motioneye_snapshots
alias: "Flounder: Refresh MotionEye snapshots"
description: >-
Sequentially refreshes durable local CCTV previews without loading live
streams in the kiosk browser.
triggers:
- trigger: time_pattern
minutes: "*"
id: tick
- trigger: state
entity_id: binary_sensor.flounder_kiosk_screen_on
from: "off"
to: "on"
id: screen_on
conditions:
- condition: state
entity_id: binary_sensor.flounder_kiosk_screen_on
state: "on"
actions:
- choose:
- conditions:
- condition: template
value_template: >-
{{ trigger.id == 'tick' and now().minute % 5 == 0 }}
sequence:
- action: shell_command.flounder_refresh_motioneye_snapshots
default:
- action: shell_command.flounder_refresh_primary_motioneye_snapshots
mode: single
+374
View File
@@ -0,0 +1,374 @@
################################################################################
# Optimistic state helpers for the Flounder dashboard heat-pump controls.
################################################################################
input_select:
lounge_hvac_requested_mode:
name: Lounge HVAC Requested Mode
options:
- none
- "off"
- heat
- cool
initial: none
flounder_lounge_power_request:
name: Flounder Lounge Power Request
options: [idle, "on", "off"]
initial: idle
flounder_master_bedroom_power_request:
name: Flounder Master Bedroom Power Request
options: [idle, "on", "off"]
initial: idle
flounder_lounge_nanoe_request:
name: Flounder Lounge Nanoe Request
options: [idle, "on", "off"]
initial: idle
flounder_master_bedroom_nanoe_request:
name: Flounder Master Bedroom Nanoe Request
options: [idle, "on", "off"]
initial: idle
flounder_lounge_preset_request:
name: Flounder Lounge Preset Request
options: [idle, none, powerful, quiet]
initial: idle
flounder_master_bedroom_preset_request:
name: Flounder Master Bedroom Preset Request
options: [idle, none, powerful, quiet]
initial: idle
input_number:
lounge_setpoint_ui:
name: Lounge Setpoint UI
min: 16
max: 30
step: 0.5
mode: box
unit_of_measurement: "°C"
icon: mdi:thermometer
input_boolean:
flounder_lounge_setpoint_pending:
name: Flounder Lounge Setpoint Pending
flounder_master_bedroom_setpoint_pending:
name: Flounder Master Bedroom Setpoint Pending
script:
flounder_lounge_power_toggle:
alias: Flounder Lounge Heat Pump Power Toggle
mode: restart
sequence:
- variables:
pending: "{{ states('input_select.flounder_lounge_power_request') }}"
requested_mode: "{{ states('input_select.lounge_hvac_requested_mode') }}"
desired: >-
{% set is_on = requested_mode in ['heat', 'cool'] or pending == 'on' or (pending == 'idle' and not is_state('climate.lounge', 'off')) %}
{{ 'off' if is_on else 'on' }}
- service: input_select.select_option
target:
entity_id: input_select.flounder_lounge_power_request
data:
option: "{{ desired }}"
- service: input_select.select_option
target:
entity_id: input_select.lounge_hvac_requested_mode
data:
option: "{{ 'off' if desired == 'off' else 'none' }}"
- service: "climate.turn_{{ desired }}"
target:
entity_id: climate.lounge
- delay: "00:00:30"
- service: input_select.select_option
target:
entity_id: input_select.flounder_lounge_power_request
data:
option: idle
flounder_master_bedroom_power_toggle:
alias: Flounder Master Bedroom Heat Pump Power Toggle
mode: restart
sequence:
- variables:
pending: "{{ states('input_select.flounder_master_bedroom_power_request') }}"
requested_mode: "{{ states('input_select.master_bedroom_hvac_requested_mode') }}"
desired: >-
{% set is_on = requested_mode in ['heat', 'cool', 'dry'] or pending == 'on' or (pending == 'idle' and not is_state('climate.master_bedroom', 'off')) %}
{{ 'off' if is_on else 'on' }}
- service: input_select.select_option
target:
entity_id: input_select.flounder_master_bedroom_power_request
data:
option: "{{ desired }}"
- service: input_select.select_option
target:
entity_id: input_select.master_bedroom_hvac_requested_mode
data:
option: "{{ 'off' if desired == 'off' else 'none' }}"
- service: "climate.turn_{{ desired }}"
target:
entity_id: climate.master_bedroom
- delay: "00:00:30"
- service: input_select.select_option
target:
entity_id: input_select.flounder_master_bedroom_power_request
data:
option: idle
flounder_lounge_hvac_mode:
alias: Flounder Lounge HVAC Mode
mode: restart
fields:
mode:
description: HVAC mode to request
sequence:
- service: input_select.select_option
target:
entity_id: input_select.lounge_hvac_requested_mode
data:
option: "{{ mode }}"
- choose:
- conditions:
- condition: template
value_template: "{{ mode == 'off' }}"
sequence:
- service: climate.turn_off
target:
entity_id: climate.lounge
default:
- service: climate.set_hvac_mode
target:
entity_id: climate.lounge
data:
hvac_mode: "{{ mode }}"
flounder_lounge_setpoint_adjust:
alias: Flounder Lounge Setpoint Adjust
mode: restart
fields:
step:
description: Setpoint change in degrees C
sequence:
- variables:
current_ui: "{{ states('input_number.lounge_setpoint_ui') | float(0) }}"
current_climate: "{{ state_attr('climate.lounge', 'temperature') | float(22) }}"
current: "{{ current_ui if current_ui >= 16 else current_climate }}"
requested: "{{ [30, [16, current + (step | float(0))] | max] | min | round(1) }}"
- service: input_boolean.turn_on
target:
entity_id: input_boolean.flounder_lounge_setpoint_pending
- service: input_number.set_value
target:
entity_id: input_number.lounge_setpoint_ui
data:
value: "{{ requested }}"
- service: climate.set_temperature
target:
entity_id: climate.lounge
data:
temperature: "{{ requested }}"
- wait_template: >-
{{ (((state_attr('climate.lounge', 'temperature') | float(0)) - (requested | float(0))) | abs) < 0.01 }}
timeout: "00:00:30"
continue_on_timeout: true
- service: input_boolean.turn_off
target:
entity_id: input_boolean.flounder_lounge_setpoint_pending
flounder_master_bedroom_setpoint_adjust:
alias: Flounder Master Bedroom Setpoint Adjust
mode: restart
fields:
step:
description: Setpoint change in degrees C
sequence:
- variables:
current_ui: "{{ states('input_number.master_bedroom_setpoint_ui') | float(0) }}"
current_climate: "{{ state_attr('climate.master_bedroom', 'temperature') | float(22) }}"
current: "{{ current_ui if current_ui >= 16 else current_climate }}"
requested: "{{ [30, [16, current + (step | float(0))] | max] | min | round(1) }}"
- service: input_boolean.turn_on
target:
entity_id: input_boolean.flounder_master_bedroom_setpoint_pending
- service: input_number.set_value
target:
entity_id: input_number.master_bedroom_setpoint_ui
data:
value: "{{ requested }}"
- service: climate.set_temperature
target:
entity_id: climate.master_bedroom
data:
temperature: "{{ requested }}"
- wait_template: >-
{{ (((state_attr('climate.master_bedroom', 'temperature') | float(0)) - (requested | float(0))) | abs) < 0.01 }}
timeout: "00:00:30"
continue_on_timeout: true
- service: input_boolean.turn_off
target:
entity_id: input_boolean.flounder_master_bedroom_setpoint_pending
flounder_lounge_nanoe_toggle:
alias: Flounder Lounge Nanoe Toggle
mode: restart
sequence:
- variables:
pending: "{{ states('input_select.flounder_lounge_nanoe_request') }}"
desired: >-
{% set is_on = pending == 'on' or (pending == 'idle' and is_state('switch.lounge_nanoe', 'on')) %}
{{ 'off' if is_on else 'on' }}
- service: input_select.select_option
target:
entity_id: input_select.flounder_lounge_nanoe_request
data:
option: "{{ desired }}"
- service: "switch.turn_{{ desired }}"
target:
entity_id: switch.lounge_nanoe
- delay: "00:00:30"
- service: input_select.select_option
target:
entity_id: input_select.flounder_lounge_nanoe_request
data:
option: idle
flounder_master_bedroom_nanoe_toggle:
alias: Flounder Master Bedroom Nanoe Toggle
mode: restart
sequence:
- variables:
pending: "{{ states('input_select.flounder_master_bedroom_nanoe_request') }}"
desired: >-
{% set is_on = pending == 'on' or (pending == 'idle' and is_state('switch.master_bedroom_nanoe', 'on')) %}
{{ 'off' if is_on else 'on' }}
- service: input_select.select_option
target:
entity_id: input_select.flounder_master_bedroom_nanoe_request
data:
option: "{{ desired }}"
- service: "switch.turn_{{ desired }}"
target:
entity_id: switch.master_bedroom_nanoe
- delay: "00:00:30"
- service: input_select.select_option
target:
entity_id: input_select.flounder_master_bedroom_nanoe_request
data:
option: idle
flounder_lounge_preset_toggle:
alias: Flounder Lounge Preset Toggle
mode: restart
fields:
preset:
description: Preset to toggle
sequence:
- variables:
pending: "{{ states('input_select.flounder_lounge_preset_request') }}"
current: "{{ pending if pending != 'idle' else (state_attr('climate.lounge', 'preset_mode') or 'none') }}"
desired: "{{ 'none' if current == preset else preset }}"
- service: input_select.select_option
target:
entity_id: input_select.flounder_lounge_preset_request
data:
option: "{{ desired }}"
- service: climate.set_preset_mode
target:
entity_id: climate.lounge
data:
preset_mode: "{{ desired }}"
- delay: "00:00:30"
- service: input_select.select_option
target:
entity_id: input_select.flounder_lounge_preset_request
data:
option: idle
flounder_master_bedroom_preset_toggle:
alias: Flounder Master Bedroom Preset Toggle
mode: restart
fields:
preset:
description: Preset to toggle
sequence:
- variables:
pending: "{{ states('input_select.flounder_master_bedroom_preset_request') }}"
current: "{{ pending if pending != 'idle' else (state_attr('climate.master_bedroom', 'preset_mode') or 'none') }}"
desired: "{{ 'none' if current == preset else preset }}"
- service: input_select.select_option
target:
entity_id: input_select.flounder_master_bedroom_preset_request
data:
option: "{{ desired }}"
- service: climate.set_preset_mode
target:
entity_id: climate.master_bedroom
data:
preset_mode: "{{ desired }}"
- delay: "00:00:30"
- service: input_select.select_option
target:
entity_id: input_select.flounder_master_bedroom_preset_request
data:
option: idle
automation:
- id: flounder_lounge_setpoint_sync
alias: Flounder Lounge Setpoint Sync
mode: restart
trigger:
- platform: state
entity_id: climate.lounge
- platform: homeassistant
event: start
condition:
- condition: state
entity_id: input_boolean.flounder_lounge_setpoint_pending
state: "off"
action:
- variables:
setpoint: "{{ state_attr('climate.lounge', 'temperature') | float(0) }}"
- condition: template
value_template: "{{ setpoint >= 16 }}"
- service: input_number.set_value
target:
entity_id: input_number.lounge_setpoint_ui
data:
value: "{{ setpoint }}"
- id: lounge_hvac_clear_requested_mode
alias: Lounge HVAC - Clear Requested Mode
mode: restart
trigger:
- platform: state
entity_id: climate.lounge
- platform: state
entity_id: input_select.lounge_hvac_requested_mode
to: ["off", heat, cool]
condition:
- condition: template
value_template: "{{ states('input_select.lounge_hvac_requested_mode') != 'none' }}"
action:
- delay: "00:00:03"
- choose:
- conditions: >-
{% set requested = states('input_select.lounge_hvac_requested_mode') %}
{{ requested == states('climate.lounge') }}
sequence:
- service: input_select.select_option
target:
entity_id: input_select.lounge_hvac_requested_mode
data:
option: none
default:
- delay: "00:00:27"
- service: input_select.select_option
target:
entity_id: input_select.lounge_hvac_requested_mode
data:
option: none
+32 -1
View File
@@ -9,7 +9,7 @@
# flounder_kiosk.yaml
#
# 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 #
#:########################################################################################:#
@@ -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
+265
View File
@@ -0,0 +1,265 @@
# Capped Cloudflare throughput tests: 2 MB download and 500 kB upload per run.
command_line:
- sensor:
name: "Internet Speed Test"
unique_id: internet_speed_test
command: "/config/scripts/internet_speed_test.sh"
command_timeout: 120
scan_interval: 31536000
unit_of_measurement: "Mbit/s"
device_class: data_rate
state_class: measurement
value_template: "{{ value_json.download_mbps }}"
json_attributes:
- upload_mbps
- binary_sensor:
name: "Internet Connected"
unique_id: internet_connected
command: >-
curl --fail --silent --show-error --max-time 10 --output /dev/null
https://one.one.one.one/cdn-cgi/trace && printf "ON" || printf "OFF"
command_timeout: 15
scan_interval: 60
payload_on: "ON"
payload_off: "OFF"
device_class: connectivity
input_number:
internet_speed_tests_today:
name: "Internet Speed Tests Today"
min: 0
max: 100
step: 1
mode: box
internet_speed_tests_this_month:
name: "Internet Speed Tests This Month"
min: 0
max: 2000
step: 1
mode: box
internet_download_today_average:
name: "Internet Download Today Average"
min: 0
max: 10000
step: 0.01
mode: box
internet_upload_today_average:
name: "Internet Upload Today Average"
min: 0
max: 10000
step: 0.01
mode: box
internet_download_month_average:
name: "Internet Download Month Average"
min: 0
max: 10000
step: 0.01
mode: box
internet_upload_month_average:
name: "Internet Upload Month Average"
min: 0
max: 10000
step: 0.01
mode: box
input_boolean:
internet_speed_test_running:
name: "Internet Speed Test Running"
script:
internet_run_speed_test:
alias: "Internet: Run speed test"
sequence:
- action: input_boolean.turn_on
target:
entity_id: input_boolean.internet_speed_test_running
- variables:
test_started: "{{ as_timestamp(now()) }}"
- action: homeassistant.update_entity
target:
entity_id: sensor.internet_speed_test
# Ignore the command-line sensor's interim unavailable state.
- wait_template: >-
{{
has_value('sensor.internet_speed_test')
and as_timestamp(states.sensor.internet_speed_test.last_updated, 0) > test_started
}}
timeout:
minutes: 2
continue_on_timeout: true
- delay:
seconds: 3
- action: input_boolean.turn_off
target:
entity_id: input_boolean.internet_speed_test_running
mode: single
template:
- sensor:
- name: "Internet Upload Speed Test"
unique_id: internet_upload_speed_test
unit_of_measurement: "Mbit/s"
device_class: data_rate
state_class: measurement
availability: "{{ state_attr('sensor.internet_speed_test', 'upload_mbps') is number }}"
state: "{{ state_attr('sensor.internet_speed_test', 'upload_mbps') | float }}"
- name: "Internet Last Speed Display"
unique_id: internet_last_speed_display
state: >-
{% if is_state('input_boolean.internet_speed_test_running', 'on') %}
Testing
{% elif has_value('sensor.internet_speed_test') and has_value('sensor.internet_upload_speed_test') %}
{{ states('sensor.internet_speed_test') | float(0) | round(0) | int }}/{{ states('sensor.internet_upload_speed_test') | float(0) | round(0) | int }}
{% else %}
--
{% endif %}
- name: "Internet Download Average Today"
unique_id: internet_download_average_today
unit_of_measurement: "Mbit/s"
device_class: data_rate
state_class: measurement
availability: "{{ states('input_number.internet_speed_tests_today') | int(0) > 0 }}"
state: "{{ states('input_number.internet_download_today_average') | float(0) }}"
- name: "Internet Upload Average Today"
unique_id: internet_upload_average_today
unit_of_measurement: "Mbit/s"
device_class: data_rate
state_class: measurement
availability: "{{ states('input_number.internet_speed_tests_today') | int(0) > 0 }}"
state: "{{ states('input_number.internet_upload_today_average') | float(0) }}"
- name: "Internet Download Average This Month"
unique_id: internet_download_average_this_month
unit_of_measurement: "Mbit/s"
device_class: data_rate
state_class: measurement
availability: "{{ states('input_number.internet_speed_tests_this_month') | int(0) > 0 }}"
state: "{{ states('input_number.internet_download_month_average') | float(0) }}"
- name: "Internet Upload Average This Month"
unique_id: internet_upload_average_this_month
unit_of_measurement: "Mbit/s"
device_class: data_rate
state_class: measurement
availability: "{{ states('input_number.internet_speed_tests_this_month') | int(0) > 0 }}"
state: "{{ states('input_number.internet_upload_month_average') | float(0) }}"
sensor:
- platform: statistics
name: "Internet Download Average Last 7 Days"
unique_id: internet_download_average_last_7_days
entity_id: sensor.internet_speed_test
state_characteristic: mean
sampling_size: 400
max_age:
days: 7
- platform: statistics
name: "Internet Upload Average Last 7 Days"
unique_id: internet_upload_average_last_7_days
entity_id: sensor.internet_upload_speed_test
state_characteristic: mean
sampling_size: 400
max_age:
days: 7
automation:
- id: internet_run_speed_test
alias: "Internet: Run capped speed test"
description: "Runs at a random time between five minutes before and after each half-hour."
triggers:
- trigger: time_pattern
minutes: 25
- trigger: time_pattern
minutes: 55
conditions: []
actions:
- delay:
seconds: "{{ range(0, 601) | random }}"
- action: script.internet_run_speed_test
target:
entity_id: sensor.internet_speed_test
mode: single
- id: internet_record_speed_test
alias: "Internet: Record speed test averages"
description: "Records every successful speed test, including the startup test."
triggers:
- trigger: state
entity_id: sensor.internet_speed_test
conditions: []
actions:
# Allow the upload template sensor to update from the command-line result.
- delay:
seconds: 1
- condition: template
value_template: "{{ has_value('sensor.internet_speed_test') and has_value('sensor.internet_upload_speed_test') }}"
- action: input_number.set_value
target:
entity_id: input_number.internet_download_today_average
data:
value: >-
{{ ((states('input_number.internet_download_today_average') | float(0) * states('input_number.internet_speed_tests_today') | int(0)) + states('sensor.internet_speed_test') | float(0)) / (states('input_number.internet_speed_tests_today') | int(0) + 1) }}
- action: input_number.set_value
target:
entity_id: input_number.internet_upload_today_average
data:
value: >-
{{ ((states('input_number.internet_upload_today_average') | float(0) * states('input_number.internet_speed_tests_today') | int(0)) + states('sensor.internet_upload_speed_test') | float(0)) / (states('input_number.internet_speed_tests_today') | int(0) + 1) }}
- action: input_number.set_value
target:
entity_id: input_number.internet_download_month_average
data:
value: >-
{{ ((states('input_number.internet_download_month_average') | float(0) * states('input_number.internet_speed_tests_this_month') | int(0)) + states('sensor.internet_speed_test') | float(0)) / (states('input_number.internet_speed_tests_this_month') | int(0) + 1) }}
- action: input_number.set_value
target:
entity_id: input_number.internet_upload_month_average
data:
value: >-
{{ ((states('input_number.internet_upload_month_average') | float(0) * states('input_number.internet_speed_tests_this_month') | int(0)) + states('sensor.internet_upload_speed_test') | float(0)) / (states('input_number.internet_speed_tests_this_month') | int(0) + 1) }}
- action: input_number.set_value
target:
entity_id: input_number.internet_speed_tests_today
data:
value: "{{ states('input_number.internet_speed_tests_today') | int(0) + 1 }}"
- action: input_number.set_value
target:
entity_id: input_number.internet_speed_tests_this_month
data:
value: "{{ states('input_number.internet_speed_tests_this_month') | int(0) + 1 }}"
mode: queued
- id: internet_reset_daily_speed_averages
alias: "Internet: Reset daily speed averages"
triggers:
- trigger: time
at: "00:00:00"
conditions: []
actions:
- action: input_number.set_value
target:
entity_id:
- input_number.internet_speed_tests_today
- input_number.internet_download_today_average
- input_number.internet_upload_today_average
data:
value: 0
mode: single
- id: internet_reset_monthly_speed_averages
alias: "Internet: Reset monthly speed averages"
triggers:
- trigger: time
at: "00:00:00"
conditions:
- condition: template
value_template: "{{ now().day == 1 }}"
actions:
- action: input_number.set_value
target:
entity_id:
- input_number.internet_speed_tests_this_month
- input_number.internet_download_month_average
- input_number.internet_upload_month_average
data:
value: 0
mode: single
@@ -0,0 +1,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"
+29 -3
View File
@@ -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:
@@ -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: ""
+25
View File
@@ -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
#########################################################################
+54
View File
@@ -0,0 +1,54 @@
#:########################################################################################:##
# Main House Power Peak #
#:########################################################################################:##
# Tracks the calendar-day maximum of the main three-phase active-power sensor for the
# Flounder dashboard. The helper retains its value across restarts and resets at midnight.
input_number:
main_house_power_peak_today:
name: Main House Power Peak Today
icon: mdi:flash-triangle-outline
min: 0
max: 50000
step: 1
mode: box
unit_of_measurement: W
automation:
- id: main_house_power_peak_today_track
alias: Main House Power Peak Today - Track
mode: single
triggers:
- trigger: state
entity_id: sensor.main_house_3_phase_power_monitor_main_power_total_active_power
- trigger: homeassistant
event: start
conditions:
- condition: template
value_template: >-
{{ states('sensor.main_house_3_phase_power_monitor_main_power_total_active_power') not in ['unknown', 'unavailable'] }}
- condition: template
value_template: >-
{{
states('sensor.main_house_3_phase_power_monitor_main_power_total_active_power') | float(0)
> states('input_number.main_house_power_peak_today') | float(0)
}}
actions:
- action: input_number.set_value
target:
entity_id: input_number.main_house_power_peak_today
data:
value: "{{ states('sensor.main_house_3_phase_power_monitor_main_power_total_active_power') | float(0) }}"
- id: main_house_power_peak_today_reset
alias: Main House Power Peak Today - Reset
mode: single
triggers:
- trigger: time
at: "00:00:00"
actions:
- action: input_number.set_value
target:
entity_id: input_number.main_house_power_peak_today
data:
value: 0
+21 -15
View File
@@ -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
+16 -8
View File
@@ -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:
+8 -5
View File
@@ -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) }}"
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Refresh Flounder's local MotionEye preview images."""
import os
from pathlib import Path
import sys
import time
from urllib.error import URLError
from urllib.request import urlopen
OUTPUTS = (
"front-door.jpg",
"driveway.jpg",
"garage.jpg",
"bike-shed.jpg",
"caravan.jpg",
)
OUTPUT_DIR = Path(os.environ.get("HASS_CONFIG", "/config")) / "www/flounder"
HASS_URL = os.environ.get("HASS_URL", "http://127.0.0.1:8123")
def download(url_path: str, destination: Path) -> bool:
url = f"{HASS_URL}{url_path}"
for attempt in range(2):
try:
with urlopen(url, timeout=4) as response:
image = response.read()
if image.startswith(b"\xff\xd8") and image.endswith(b"\xff\xd9"):
temporary = destination.with_suffix(".tmp")
temporary.write_bytes(image)
os.replace(temporary, destination)
return True
except (OSError, TimeoutError, URLError):
pass
if attempt == 0:
time.sleep(2)
return False
def main() -> int:
arguments = sys.argv[1:]
outputs = OUTPUTS
if arguments and arguments[0] == "--primary":
arguments = arguments[1:]
outputs = OUTPUTS[:3]
if len(arguments) != len(outputs):
return 2
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
failures = 0
for url_path, filename in zip(arguments, outputs, strict=True):
if not download(url_path, OUTPUT_DIR / filename):
failures += 1
time.sleep(2)
return 1 if failures else 0
if __name__ == "__main__":
raise SystemExit(main())
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
download_bytes=2000000
upload_bytes=500000
download_url="https://speed.cloudflare.com/__down?bytes=${download_bytes}"
upload_url="https://speed.cloudflare.com/__up"
download_bps=$(curl --fail --silent --show-error --location --max-time 45 \
--output /dev/null --write-out "%{speed_download}" "${download_url}")
upload_bps=$(head -c "${upload_bytes}" /dev/zero | curl --fail --silent --show-error \
--location --max-time 45 --output /dev/null --request POST \
--header "Content-Type: application/octet-stream" --data-binary @- \
--write-out "%{speed_upload}" "${upload_url}")
awk -v download_bps="${download_bps}" -v upload_bps="${upload_bps}" 'BEGIN {
printf "{\"download_mbps\":%.2f,\"upload_mbps\":%.2f}\n", \
download_bps * 8 / 1000000, upload_bps * 8 / 1000000
}'