various tidyups
This commit is contained in:
@@ -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
|
||||
}}
|
||||
|
||||
@@ -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) }}
|
||||
@@ -1,8 +1,8 @@
|
||||
#:########################################################################################:#
|
||||
# Package: Appliance Activity Feed Sensors
|
||||
# File: appliance_status_mqttfeed.yaml
|
||||
# Version: v1.2.0
|
||||
# Date: 2026-07-20
|
||||
# Version: v1.3.0
|
||||
# Date: 2026-08-12
|
||||
#
|
||||
# Purpose:
|
||||
# Provides MQTT sensors for appliance, vehicle-charger, and tool-charger
|
||||
@@ -13,12 +13,9 @@
|
||||
# - External publishers or the announcement router must publish completion
|
||||
# payloads to the configured activity-feed topics.
|
||||
#
|
||||
# Known Limitation:
|
||||
# - sensor.washer_finished has device_class timestamp and therefore requires
|
||||
# an ISO 8601 timestamp. P013-F018 records that the current router may publish
|
||||
# a time-only payload, which is not valid for this device class.
|
||||
#
|
||||
# Version History:
|
||||
# - v1.3.0 (2026-08-12): Activity feed payloads are ISO 8601 timestamps,
|
||||
# compatible with the washer timestamp sensor.
|
||||
# - v1.2.0 (2026-07-20): Added the downstairs dishwasher completion sensor.
|
||||
# - v1.1.0 (2025-05-18): Set the washer completion sensor as a timestamp.
|
||||
# - v1.0.0 (2023-10-20): Added the initial MQTT activity-feed sensors.
|
||||
|
||||
@@ -0,0 +1,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
|
||||
@@ -1,8 +1,8 @@
|
||||
#:########################################################################################:#
|
||||
# Package: CCTV Motion Feed
|
||||
# File: cctv_controls.yaml
|
||||
# Version: v1.1.0
|
||||
# Date: 2026-07-24
|
||||
# Version: v1.2.0
|
||||
# Date: 2026-08-06
|
||||
#
|
||||
# Purpose:
|
||||
# Provides a Home Assistant binary sensor for front-door motion messages
|
||||
@@ -14,6 +14,7 @@
|
||||
#
|
||||
#
|
||||
# Version History:
|
||||
# - v1.2.0 (2026-08-06): Match MotionEye HA plugin ON/OFF MQTT payloads.
|
||||
# - v1.1.0 (2026-07-24): Added explicit numeric ON/OFF payloads (P013-F039).
|
||||
# - v1.0.0 (2022-10-11): Initial CCTV front-door motion sensor package.
|
||||
#:########################################################################################:#
|
||||
@@ -23,6 +24,38 @@ mqtt:
|
||||
- unique_id: front_door_movement_camera_corner
|
||||
name: "Front Door Movement"
|
||||
state_topic: "viewroad-status/cctv/front_door_motion"
|
||||
payload_on: "1"
|
||||
payload_off: "0"
|
||||
payload_on: "ON"
|
||||
payload_off: "OFF"
|
||||
icon: mdi:exit-run
|
||||
|
||||
input_datetime:
|
||||
entranceway_last_detection:
|
||||
name: Entranceway Last Detection
|
||||
has_date: true
|
||||
has_time: true
|
||||
|
||||
template:
|
||||
- binary_sensor:
|
||||
- name: Entranceway Detection
|
||||
unique_id: entranceway_detection
|
||||
state: >-
|
||||
{{ is_state('binary_sensor.view_road_front_door_motion', 'on')
|
||||
or is_state('binary_sensor.front_door_movement', 'on') }}
|
||||
|
||||
automation:
|
||||
- id: entranceway_detection_last_seen
|
||||
alias: Entranceway Detection - Record Last Activity
|
||||
mode: restart
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: binary_sensor.view_road_front_door_motion
|
||||
to: "on"
|
||||
- platform: mqtt
|
||||
topic: viewroad-status/cctv/front_door_motion
|
||||
payload: "ON"
|
||||
action:
|
||||
- service: input_datetime.set_datetime
|
||||
target:
|
||||
entity_id: input_datetime.entranceway_last_detection
|
||||
data:
|
||||
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
shell_command:
|
||||
flounder_refresh_motioneye_snapshots: >-
|
||||
python3 /config/scripts/flounder_motioneye_snapshots.py
|
||||
"{{ state_attr('camera.camera2', 'entity_picture') }}"
|
||||
"{{ state_attr('camera.camera5', 'entity_picture') }}"
|
||||
"{{ state_attr('camera.camera6', 'entity_picture') }}"
|
||||
"{{ state_attr('camera.trailer', 'entity_picture') }}"
|
||||
"{{ state_attr('camera.camera4', 'entity_picture') }}"
|
||||
flounder_refresh_primary_motioneye_snapshots: >-
|
||||
python3 /config/scripts/flounder_motioneye_snapshots.py --primary
|
||||
"{{ state_attr('camera.camera2', 'entity_picture') }}"
|
||||
"{{ state_attr('camera.camera5', 'entity_picture') }}"
|
||||
"{{ state_attr('camera.camera6', 'entity_picture') }}"
|
||||
|
||||
automation:
|
||||
- id: flounder_refresh_motioneye_snapshots
|
||||
alias: "Flounder: Refresh MotionEye snapshots"
|
||||
description: >-
|
||||
Sequentially refreshes durable local CCTV previews without loading live
|
||||
streams in the kiosk browser.
|
||||
triggers:
|
||||
- trigger: time_pattern
|
||||
minutes: "*"
|
||||
id: tick
|
||||
- trigger: state
|
||||
entity_id: binary_sensor.flounder_kiosk_screen_on
|
||||
from: "off"
|
||||
to: "on"
|
||||
id: screen_on
|
||||
conditions:
|
||||
- condition: state
|
||||
entity_id: binary_sensor.flounder_kiosk_screen_on
|
||||
state: "on"
|
||||
actions:
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ trigger.id == 'tick' and now().minute % 5 == 0 }}
|
||||
sequence:
|
||||
- action: shell_command.flounder_refresh_motioneye_snapshots
|
||||
default:
|
||||
- action: shell_command.flounder_refresh_primary_motioneye_snapshots
|
||||
mode: single
|
||||
@@ -0,0 +1,374 @@
|
||||
################################################################################
|
||||
# Optimistic state helpers for the Flounder dashboard heat-pump controls.
|
||||
################################################################################
|
||||
|
||||
input_select:
|
||||
lounge_hvac_requested_mode:
|
||||
name: Lounge HVAC Requested Mode
|
||||
options:
|
||||
- none
|
||||
- "off"
|
||||
- heat
|
||||
- cool
|
||||
initial: none
|
||||
|
||||
flounder_lounge_power_request:
|
||||
name: Flounder Lounge Power Request
|
||||
options: [idle, "on", "off"]
|
||||
initial: idle
|
||||
|
||||
flounder_master_bedroom_power_request:
|
||||
name: Flounder Master Bedroom Power Request
|
||||
options: [idle, "on", "off"]
|
||||
initial: idle
|
||||
|
||||
flounder_lounge_nanoe_request:
|
||||
name: Flounder Lounge Nanoe Request
|
||||
options: [idle, "on", "off"]
|
||||
initial: idle
|
||||
|
||||
flounder_master_bedroom_nanoe_request:
|
||||
name: Flounder Master Bedroom Nanoe Request
|
||||
options: [idle, "on", "off"]
|
||||
initial: idle
|
||||
|
||||
flounder_lounge_preset_request:
|
||||
name: Flounder Lounge Preset Request
|
||||
options: [idle, none, powerful, quiet]
|
||||
initial: idle
|
||||
|
||||
flounder_master_bedroom_preset_request:
|
||||
name: Flounder Master Bedroom Preset Request
|
||||
options: [idle, none, powerful, quiet]
|
||||
initial: idle
|
||||
|
||||
input_number:
|
||||
lounge_setpoint_ui:
|
||||
name: Lounge Setpoint UI
|
||||
min: 16
|
||||
max: 30
|
||||
step: 0.5
|
||||
mode: box
|
||||
unit_of_measurement: "°C"
|
||||
icon: mdi:thermometer
|
||||
|
||||
input_boolean:
|
||||
flounder_lounge_setpoint_pending:
|
||||
name: Flounder Lounge Setpoint Pending
|
||||
|
||||
flounder_master_bedroom_setpoint_pending:
|
||||
name: Flounder Master Bedroom Setpoint Pending
|
||||
|
||||
script:
|
||||
flounder_lounge_power_toggle:
|
||||
alias: Flounder Lounge Heat Pump Power Toggle
|
||||
mode: restart
|
||||
sequence:
|
||||
- variables:
|
||||
pending: "{{ states('input_select.flounder_lounge_power_request') }}"
|
||||
requested_mode: "{{ states('input_select.lounge_hvac_requested_mode') }}"
|
||||
desired: >-
|
||||
{% set is_on = requested_mode in ['heat', 'cool'] or pending == 'on' or (pending == 'idle' and not is_state('climate.lounge', 'off')) %}
|
||||
{{ 'off' if is_on else 'on' }}
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_lounge_power_request
|
||||
data:
|
||||
option: "{{ desired }}"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.lounge_hvac_requested_mode
|
||||
data:
|
||||
option: "{{ 'off' if desired == 'off' else 'none' }}"
|
||||
- service: "climate.turn_{{ desired }}"
|
||||
target:
|
||||
entity_id: climate.lounge
|
||||
- delay: "00:00:30"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_lounge_power_request
|
||||
data:
|
||||
option: idle
|
||||
|
||||
flounder_master_bedroom_power_toggle:
|
||||
alias: Flounder Master Bedroom Heat Pump Power Toggle
|
||||
mode: restart
|
||||
sequence:
|
||||
- variables:
|
||||
pending: "{{ states('input_select.flounder_master_bedroom_power_request') }}"
|
||||
requested_mode: "{{ states('input_select.master_bedroom_hvac_requested_mode') }}"
|
||||
desired: >-
|
||||
{% set is_on = requested_mode in ['heat', 'cool', 'dry'] or pending == 'on' or (pending == 'idle' and not is_state('climate.master_bedroom', 'off')) %}
|
||||
{{ 'off' if is_on else 'on' }}
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_master_bedroom_power_request
|
||||
data:
|
||||
option: "{{ desired }}"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.master_bedroom_hvac_requested_mode
|
||||
data:
|
||||
option: "{{ 'off' if desired == 'off' else 'none' }}"
|
||||
- service: "climate.turn_{{ desired }}"
|
||||
target:
|
||||
entity_id: climate.master_bedroom
|
||||
- delay: "00:00:30"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_master_bedroom_power_request
|
||||
data:
|
||||
option: idle
|
||||
|
||||
flounder_lounge_hvac_mode:
|
||||
alias: Flounder Lounge HVAC Mode
|
||||
mode: restart
|
||||
fields:
|
||||
mode:
|
||||
description: HVAC mode to request
|
||||
sequence:
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.lounge_hvac_requested_mode
|
||||
data:
|
||||
option: "{{ mode }}"
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: "{{ mode == 'off' }}"
|
||||
sequence:
|
||||
- service: climate.turn_off
|
||||
target:
|
||||
entity_id: climate.lounge
|
||||
default:
|
||||
- service: climate.set_hvac_mode
|
||||
target:
|
||||
entity_id: climate.lounge
|
||||
data:
|
||||
hvac_mode: "{{ mode }}"
|
||||
|
||||
flounder_lounge_setpoint_adjust:
|
||||
alias: Flounder Lounge Setpoint Adjust
|
||||
mode: restart
|
||||
fields:
|
||||
step:
|
||||
description: Setpoint change in degrees C
|
||||
sequence:
|
||||
- variables:
|
||||
current_ui: "{{ states('input_number.lounge_setpoint_ui') | float(0) }}"
|
||||
current_climate: "{{ state_attr('climate.lounge', 'temperature') | float(22) }}"
|
||||
current: "{{ current_ui if current_ui >= 16 else current_climate }}"
|
||||
requested: "{{ [30, [16, current + (step | float(0))] | max] | min | round(1) }}"
|
||||
- service: input_boolean.turn_on
|
||||
target:
|
||||
entity_id: input_boolean.flounder_lounge_setpoint_pending
|
||||
- service: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.lounge_setpoint_ui
|
||||
data:
|
||||
value: "{{ requested }}"
|
||||
- service: climate.set_temperature
|
||||
target:
|
||||
entity_id: climate.lounge
|
||||
data:
|
||||
temperature: "{{ requested }}"
|
||||
- wait_template: >-
|
||||
{{ (((state_attr('climate.lounge', 'temperature') | float(0)) - (requested | float(0))) | abs) < 0.01 }}
|
||||
timeout: "00:00:30"
|
||||
continue_on_timeout: true
|
||||
- service: input_boolean.turn_off
|
||||
target:
|
||||
entity_id: input_boolean.flounder_lounge_setpoint_pending
|
||||
|
||||
flounder_master_bedroom_setpoint_adjust:
|
||||
alias: Flounder Master Bedroom Setpoint Adjust
|
||||
mode: restart
|
||||
fields:
|
||||
step:
|
||||
description: Setpoint change in degrees C
|
||||
sequence:
|
||||
- variables:
|
||||
current_ui: "{{ states('input_number.master_bedroom_setpoint_ui') | float(0) }}"
|
||||
current_climate: "{{ state_attr('climate.master_bedroom', 'temperature') | float(22) }}"
|
||||
current: "{{ current_ui if current_ui >= 16 else current_climate }}"
|
||||
requested: "{{ [30, [16, current + (step | float(0))] | max] | min | round(1) }}"
|
||||
- service: input_boolean.turn_on
|
||||
target:
|
||||
entity_id: input_boolean.flounder_master_bedroom_setpoint_pending
|
||||
- service: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.master_bedroom_setpoint_ui
|
||||
data:
|
||||
value: "{{ requested }}"
|
||||
- service: climate.set_temperature
|
||||
target:
|
||||
entity_id: climate.master_bedroom
|
||||
data:
|
||||
temperature: "{{ requested }}"
|
||||
- wait_template: >-
|
||||
{{ (((state_attr('climate.master_bedroom', 'temperature') | float(0)) - (requested | float(0))) | abs) < 0.01 }}
|
||||
timeout: "00:00:30"
|
||||
continue_on_timeout: true
|
||||
- service: input_boolean.turn_off
|
||||
target:
|
||||
entity_id: input_boolean.flounder_master_bedroom_setpoint_pending
|
||||
|
||||
flounder_lounge_nanoe_toggle:
|
||||
alias: Flounder Lounge Nanoe Toggle
|
||||
mode: restart
|
||||
sequence:
|
||||
- variables:
|
||||
pending: "{{ states('input_select.flounder_lounge_nanoe_request') }}"
|
||||
desired: >-
|
||||
{% set is_on = pending == 'on' or (pending == 'idle' and is_state('switch.lounge_nanoe', 'on')) %}
|
||||
{{ 'off' if is_on else 'on' }}
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_lounge_nanoe_request
|
||||
data:
|
||||
option: "{{ desired }}"
|
||||
- service: "switch.turn_{{ desired }}"
|
||||
target:
|
||||
entity_id: switch.lounge_nanoe
|
||||
- delay: "00:00:30"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_lounge_nanoe_request
|
||||
data:
|
||||
option: idle
|
||||
|
||||
flounder_master_bedroom_nanoe_toggle:
|
||||
alias: Flounder Master Bedroom Nanoe Toggle
|
||||
mode: restart
|
||||
sequence:
|
||||
- variables:
|
||||
pending: "{{ states('input_select.flounder_master_bedroom_nanoe_request') }}"
|
||||
desired: >-
|
||||
{% set is_on = pending == 'on' or (pending == 'idle' and is_state('switch.master_bedroom_nanoe', 'on')) %}
|
||||
{{ 'off' if is_on else 'on' }}
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_master_bedroom_nanoe_request
|
||||
data:
|
||||
option: "{{ desired }}"
|
||||
- service: "switch.turn_{{ desired }}"
|
||||
target:
|
||||
entity_id: switch.master_bedroom_nanoe
|
||||
- delay: "00:00:30"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_master_bedroom_nanoe_request
|
||||
data:
|
||||
option: idle
|
||||
|
||||
flounder_lounge_preset_toggle:
|
||||
alias: Flounder Lounge Preset Toggle
|
||||
mode: restart
|
||||
fields:
|
||||
preset:
|
||||
description: Preset to toggle
|
||||
sequence:
|
||||
- variables:
|
||||
pending: "{{ states('input_select.flounder_lounge_preset_request') }}"
|
||||
current: "{{ pending if pending != 'idle' else (state_attr('climate.lounge', 'preset_mode') or 'none') }}"
|
||||
desired: "{{ 'none' if current == preset else preset }}"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_lounge_preset_request
|
||||
data:
|
||||
option: "{{ desired }}"
|
||||
- service: climate.set_preset_mode
|
||||
target:
|
||||
entity_id: climate.lounge
|
||||
data:
|
||||
preset_mode: "{{ desired }}"
|
||||
- delay: "00:00:30"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_lounge_preset_request
|
||||
data:
|
||||
option: idle
|
||||
|
||||
flounder_master_bedroom_preset_toggle:
|
||||
alias: Flounder Master Bedroom Preset Toggle
|
||||
mode: restart
|
||||
fields:
|
||||
preset:
|
||||
description: Preset to toggle
|
||||
sequence:
|
||||
- variables:
|
||||
pending: "{{ states('input_select.flounder_master_bedroom_preset_request') }}"
|
||||
current: "{{ pending if pending != 'idle' else (state_attr('climate.master_bedroom', 'preset_mode') or 'none') }}"
|
||||
desired: "{{ 'none' if current == preset else preset }}"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_master_bedroom_preset_request
|
||||
data:
|
||||
option: "{{ desired }}"
|
||||
- service: climate.set_preset_mode
|
||||
target:
|
||||
entity_id: climate.master_bedroom
|
||||
data:
|
||||
preset_mode: "{{ desired }}"
|
||||
- delay: "00:00:30"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.flounder_master_bedroom_preset_request
|
||||
data:
|
||||
option: idle
|
||||
|
||||
automation:
|
||||
- id: flounder_lounge_setpoint_sync
|
||||
alias: Flounder Lounge Setpoint Sync
|
||||
mode: restart
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: climate.lounge
|
||||
- platform: homeassistant
|
||||
event: start
|
||||
condition:
|
||||
- condition: state
|
||||
entity_id: input_boolean.flounder_lounge_setpoint_pending
|
||||
state: "off"
|
||||
action:
|
||||
- variables:
|
||||
setpoint: "{{ state_attr('climate.lounge', 'temperature') | float(0) }}"
|
||||
- condition: template
|
||||
value_template: "{{ setpoint >= 16 }}"
|
||||
- service: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.lounge_setpoint_ui
|
||||
data:
|
||||
value: "{{ setpoint }}"
|
||||
|
||||
- id: lounge_hvac_clear_requested_mode
|
||||
alias: Lounge HVAC - Clear Requested Mode
|
||||
mode: restart
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: climate.lounge
|
||||
- platform: state
|
||||
entity_id: input_select.lounge_hvac_requested_mode
|
||||
to: ["off", heat, cool]
|
||||
condition:
|
||||
- condition: template
|
||||
value_template: "{{ states('input_select.lounge_hvac_requested_mode') != 'none' }}"
|
||||
action:
|
||||
- delay: "00:00:03"
|
||||
- choose:
|
||||
- conditions: >-
|
||||
{% set requested = states('input_select.lounge_hvac_requested_mode') %}
|
||||
{{ requested == states('climate.lounge') }}
|
||||
sequence:
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.lounge_hvac_requested_mode
|
||||
data:
|
||||
option: none
|
||||
default:
|
||||
- delay: "00:00:27"
|
||||
- service: input_select.select_option
|
||||
target:
|
||||
entity_id: input_select.lounge_hvac_requested_mode
|
||||
data:
|
||||
option: none
|
||||
@@ -9,7 +9,7 @@
|
||||
# flounder_kiosk.yaml
|
||||
#
|
||||
# VERSION:
|
||||
# V2.0 - 2026-07-28
|
||||
# V2.3 - 2026-08-02
|
||||
#
|
||||
# PURPOSE:
|
||||
# Provides Home Assistant controls and status entities for the Flounder portrait
|
||||
@@ -131,6 +131,21 @@
|
||||
# - Actual Brightness reports the physical backlight percentage.
|
||||
#
|
||||
# VERSION HISTORY:
|
||||
# V2.3 - 2026-08-02
|
||||
# - Removed the Home Assistant Display Mode Restoration automation.
|
||||
# - Prevented startup and availability timing races from republishing an
|
||||
# incorrect display mode, including unexpected Screen On commands.
|
||||
# - Display-mode persistence and enforcement remain handled locally by the
|
||||
# Flounder screen-control service.
|
||||
#
|
||||
# V2.2 - 2026-07-30
|
||||
# - Enforce Screen Lock every ten minutes because the kiosk can incorrectly report
|
||||
# DPMS off after a restart while the physical panel remains dimly visible.
|
||||
#
|
||||
# V2.1 - 2026-07-30
|
||||
# - Reapply the selected display mode when the kiosk or Home Assistant restarts,
|
||||
# ensuring its physical panel, touchscreen and screensaver state are restored.
|
||||
#
|
||||
# V2.0 - 2026-07-28
|
||||
# - Replaced the separate Screen and Screensaver switches with one persistent
|
||||
# four-option Display Mode select.
|
||||
@@ -169,6 +184,22 @@
|
||||
#:########################################################################################:#
|
||||
|
||||
mqtt:
|
||||
- button:
|
||||
name: "Flounder Restart Chrome"
|
||||
unique_id: flounder_restart_chrome
|
||||
icon: mdi:google-chrome
|
||||
|
||||
device:
|
||||
identifiers:
|
||||
- flounder_kiosk
|
||||
name: "Flounder Kiosk"
|
||||
manufacturer: "Dell"
|
||||
model: "Portrait Touchscreen Kiosk"
|
||||
suggested_area: "Lounge"
|
||||
|
||||
command_topic: "viewroad-commands/flounder/chrome/restart"
|
||||
payload_press: "PRESS"
|
||||
|
||||
#:########################################################################################:#
|
||||
# Flounder Kiosk Display Mode #
|
||||
#:########################################################################################:#
|
||||
|
||||
@@ -140,6 +140,9 @@ script:
|
||||
alias: Master Bedroom Setpoint Sync From Climate
|
||||
mode: restart
|
||||
sequence:
|
||||
- condition: state
|
||||
entity_id: input_boolean.flounder_master_bedroom_setpoint_pending
|
||||
state: "off"
|
||||
- variables:
|
||||
sp: "{{ state_attr('climate.master_bedroom', 'temperature') | float(0) }}"
|
||||
- service: input_number.set_value
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
# Capped Cloudflare throughput tests: 2 MB download and 500 kB upload per run.
|
||||
command_line:
|
||||
- sensor:
|
||||
name: "Internet Speed Test"
|
||||
unique_id: internet_speed_test
|
||||
command: "/config/scripts/internet_speed_test.sh"
|
||||
command_timeout: 120
|
||||
scan_interval: 31536000
|
||||
unit_of_measurement: "Mbit/s"
|
||||
device_class: data_rate
|
||||
state_class: measurement
|
||||
value_template: "{{ value_json.download_mbps }}"
|
||||
json_attributes:
|
||||
- upload_mbps
|
||||
|
||||
- binary_sensor:
|
||||
name: "Internet Connected"
|
||||
unique_id: internet_connected
|
||||
command: >-
|
||||
curl --fail --silent --show-error --max-time 10 --output /dev/null
|
||||
https://one.one.one.one/cdn-cgi/trace && printf "ON" || printf "OFF"
|
||||
command_timeout: 15
|
||||
scan_interval: 60
|
||||
payload_on: "ON"
|
||||
payload_off: "OFF"
|
||||
device_class: connectivity
|
||||
|
||||
input_number:
|
||||
internet_speed_tests_today:
|
||||
name: "Internet Speed Tests Today"
|
||||
min: 0
|
||||
max: 100
|
||||
step: 1
|
||||
mode: box
|
||||
internet_speed_tests_this_month:
|
||||
name: "Internet Speed Tests This Month"
|
||||
min: 0
|
||||
max: 2000
|
||||
step: 1
|
||||
mode: box
|
||||
internet_download_today_average:
|
||||
name: "Internet Download Today Average"
|
||||
min: 0
|
||||
max: 10000
|
||||
step: 0.01
|
||||
mode: box
|
||||
internet_upload_today_average:
|
||||
name: "Internet Upload Today Average"
|
||||
min: 0
|
||||
max: 10000
|
||||
step: 0.01
|
||||
mode: box
|
||||
internet_download_month_average:
|
||||
name: "Internet Download Month Average"
|
||||
min: 0
|
||||
max: 10000
|
||||
step: 0.01
|
||||
mode: box
|
||||
internet_upload_month_average:
|
||||
name: "Internet Upload Month Average"
|
||||
min: 0
|
||||
max: 10000
|
||||
step: 0.01
|
||||
mode: box
|
||||
|
||||
input_boolean:
|
||||
internet_speed_test_running:
|
||||
name: "Internet Speed Test Running"
|
||||
|
||||
script:
|
||||
internet_run_speed_test:
|
||||
alias: "Internet: Run speed test"
|
||||
sequence:
|
||||
- action: input_boolean.turn_on
|
||||
target:
|
||||
entity_id: input_boolean.internet_speed_test_running
|
||||
- variables:
|
||||
test_started: "{{ as_timestamp(now()) }}"
|
||||
- action: homeassistant.update_entity
|
||||
target:
|
||||
entity_id: sensor.internet_speed_test
|
||||
# Ignore the command-line sensor's interim unavailable state.
|
||||
- wait_template: >-
|
||||
{{
|
||||
has_value('sensor.internet_speed_test')
|
||||
and as_timestamp(states.sensor.internet_speed_test.last_updated, 0) > test_started
|
||||
}}
|
||||
timeout:
|
||||
minutes: 2
|
||||
continue_on_timeout: true
|
||||
- delay:
|
||||
seconds: 3
|
||||
- action: input_boolean.turn_off
|
||||
target:
|
||||
entity_id: input_boolean.internet_speed_test_running
|
||||
mode: single
|
||||
|
||||
template:
|
||||
- sensor:
|
||||
- name: "Internet Upload Speed Test"
|
||||
unique_id: internet_upload_speed_test
|
||||
unit_of_measurement: "Mbit/s"
|
||||
device_class: data_rate
|
||||
state_class: measurement
|
||||
availability: "{{ state_attr('sensor.internet_speed_test', 'upload_mbps') is number }}"
|
||||
state: "{{ state_attr('sensor.internet_speed_test', 'upload_mbps') | float }}"
|
||||
- name: "Internet Last Speed Display"
|
||||
unique_id: internet_last_speed_display
|
||||
state: >-
|
||||
{% if is_state('input_boolean.internet_speed_test_running', 'on') %}
|
||||
Testing
|
||||
{% elif has_value('sensor.internet_speed_test') and has_value('sensor.internet_upload_speed_test') %}
|
||||
{{ states('sensor.internet_speed_test') | float(0) | round(0) | int }}/{{ states('sensor.internet_upload_speed_test') | float(0) | round(0) | int }}
|
||||
{% else %}
|
||||
--
|
||||
{% endif %}
|
||||
- name: "Internet Download Average Today"
|
||||
unique_id: internet_download_average_today
|
||||
unit_of_measurement: "Mbit/s"
|
||||
device_class: data_rate
|
||||
state_class: measurement
|
||||
availability: "{{ states('input_number.internet_speed_tests_today') | int(0) > 0 }}"
|
||||
state: "{{ states('input_number.internet_download_today_average') | float(0) }}"
|
||||
- name: "Internet Upload Average Today"
|
||||
unique_id: internet_upload_average_today
|
||||
unit_of_measurement: "Mbit/s"
|
||||
device_class: data_rate
|
||||
state_class: measurement
|
||||
availability: "{{ states('input_number.internet_speed_tests_today') | int(0) > 0 }}"
|
||||
state: "{{ states('input_number.internet_upload_today_average') | float(0) }}"
|
||||
- name: "Internet Download Average This Month"
|
||||
unique_id: internet_download_average_this_month
|
||||
unit_of_measurement: "Mbit/s"
|
||||
device_class: data_rate
|
||||
state_class: measurement
|
||||
availability: "{{ states('input_number.internet_speed_tests_this_month') | int(0) > 0 }}"
|
||||
state: "{{ states('input_number.internet_download_month_average') | float(0) }}"
|
||||
- name: "Internet Upload Average This Month"
|
||||
unique_id: internet_upload_average_this_month
|
||||
unit_of_measurement: "Mbit/s"
|
||||
device_class: data_rate
|
||||
state_class: measurement
|
||||
availability: "{{ states('input_number.internet_speed_tests_this_month') | int(0) > 0 }}"
|
||||
state: "{{ states('input_number.internet_upload_month_average') | float(0) }}"
|
||||
|
||||
sensor:
|
||||
- platform: statistics
|
||||
name: "Internet Download Average Last 7 Days"
|
||||
unique_id: internet_download_average_last_7_days
|
||||
entity_id: sensor.internet_speed_test
|
||||
state_characteristic: mean
|
||||
sampling_size: 400
|
||||
max_age:
|
||||
days: 7
|
||||
- platform: statistics
|
||||
name: "Internet Upload Average Last 7 Days"
|
||||
unique_id: internet_upload_average_last_7_days
|
||||
entity_id: sensor.internet_upload_speed_test
|
||||
state_characteristic: mean
|
||||
sampling_size: 400
|
||||
max_age:
|
||||
days: 7
|
||||
|
||||
automation:
|
||||
- id: internet_run_speed_test
|
||||
alias: "Internet: Run capped speed test"
|
||||
description: "Runs at a random time between five minutes before and after each half-hour."
|
||||
triggers:
|
||||
- trigger: time_pattern
|
||||
minutes: 25
|
||||
- trigger: time_pattern
|
||||
minutes: 55
|
||||
conditions: []
|
||||
actions:
|
||||
- delay:
|
||||
seconds: "{{ range(0, 601) | random }}"
|
||||
- action: script.internet_run_speed_test
|
||||
target:
|
||||
entity_id: sensor.internet_speed_test
|
||||
mode: single
|
||||
|
||||
- id: internet_record_speed_test
|
||||
alias: "Internet: Record speed test averages"
|
||||
description: "Records every successful speed test, including the startup test."
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: sensor.internet_speed_test
|
||||
conditions: []
|
||||
actions:
|
||||
# Allow the upload template sensor to update from the command-line result.
|
||||
- delay:
|
||||
seconds: 1
|
||||
- condition: template
|
||||
value_template: "{{ has_value('sensor.internet_speed_test') and has_value('sensor.internet_upload_speed_test') }}"
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.internet_download_today_average
|
||||
data:
|
||||
value: >-
|
||||
{{ ((states('input_number.internet_download_today_average') | float(0) * states('input_number.internet_speed_tests_today') | int(0)) + states('sensor.internet_speed_test') | float(0)) / (states('input_number.internet_speed_tests_today') | int(0) + 1) }}
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.internet_upload_today_average
|
||||
data:
|
||||
value: >-
|
||||
{{ ((states('input_number.internet_upload_today_average') | float(0) * states('input_number.internet_speed_tests_today') | int(0)) + states('sensor.internet_upload_speed_test') | float(0)) / (states('input_number.internet_speed_tests_today') | int(0) + 1) }}
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.internet_download_month_average
|
||||
data:
|
||||
value: >-
|
||||
{{ ((states('input_number.internet_download_month_average') | float(0) * states('input_number.internet_speed_tests_this_month') | int(0)) + states('sensor.internet_speed_test') | float(0)) / (states('input_number.internet_speed_tests_this_month') | int(0) + 1) }}
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.internet_upload_month_average
|
||||
data:
|
||||
value: >-
|
||||
{{ ((states('input_number.internet_upload_month_average') | float(0) * states('input_number.internet_speed_tests_this_month') | int(0)) + states('sensor.internet_upload_speed_test') | float(0)) / (states('input_number.internet_speed_tests_this_month') | int(0) + 1) }}
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.internet_speed_tests_today
|
||||
data:
|
||||
value: "{{ states('input_number.internet_speed_tests_today') | int(0) + 1 }}"
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.internet_speed_tests_this_month
|
||||
data:
|
||||
value: "{{ states('input_number.internet_speed_tests_this_month') | int(0) + 1 }}"
|
||||
mode: queued
|
||||
|
||||
- id: internet_reset_daily_speed_averages
|
||||
alias: "Internet: Reset daily speed averages"
|
||||
triggers:
|
||||
- trigger: time
|
||||
at: "00:00:00"
|
||||
conditions: []
|
||||
actions:
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id:
|
||||
- input_number.internet_speed_tests_today
|
||||
- input_number.internet_download_today_average
|
||||
- input_number.internet_upload_today_average
|
||||
data:
|
||||
value: 0
|
||||
mode: single
|
||||
|
||||
- id: internet_reset_monthly_speed_averages
|
||||
alias: "Internet: Reset monthly speed averages"
|
||||
triggers:
|
||||
- trigger: time
|
||||
at: "00:00:00"
|
||||
conditions:
|
||||
- condition: template
|
||||
value_template: "{{ now().day == 1 }}"
|
||||
actions:
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id:
|
||||
- input_number.internet_speed_tests_this_month
|
||||
- input_number.internet_download_month_average
|
||||
- input_number.internet_upload_month_average
|
||||
data:
|
||||
value: 0
|
||||
mode: single
|
||||
@@ -0,0 +1,65 @@
|
||||
#:########################################################################################:#
|
||||
# Jobs Tracker Appliance Completion Triggers Package #
|
||||
#:########################################################################################:#
|
||||
#
|
||||
# TITLE:
|
||||
# Jobs Tracker Appliance Completion Triggers
|
||||
#
|
||||
# FILE:
|
||||
# jobs_tracker_appliance_completion_triggers.yaml
|
||||
#
|
||||
# VERSION:
|
||||
# V1.0 2026-08-15
|
||||
#
|
||||
# PURPOSE:
|
||||
# Provides time-qualified appliance door and lid triggers for Jobs Tracker completion.
|
||||
#
|
||||
# DEPENDS ON:
|
||||
# packages/Job_Chore_Tracking/jobs_tracker_jobs.yaml V3.5 or newer.
|
||||
#
|
||||
# OPERATION:
|
||||
# Each contact must remain open for five seconds before its corresponding Jobs Tracker
|
||||
# completion route is called. The job registry owns the completion entity mappings.
|
||||
#
|
||||
# VERSION HISTORY:
|
||||
# V1.0 2026-08-15
|
||||
# - Added five-second door and lid open completion triggers for dryer and washer jobs.
|
||||
#
|
||||
#:########################################################################################:#
|
||||
|
||||
automation:
|
||||
- id: jobs_tracker_empty_dryer_door_open
|
||||
alias: "Jobs Tracker - Empty Dryer on Door Open"
|
||||
description: "Marks Empty the Dryer complete after the dryer door is open for five seconds."
|
||||
mode: single
|
||||
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: binary_sensor.dryer_door_status_x34rs_contact
|
||||
to: "on"
|
||||
for:
|
||||
seconds: 5
|
||||
|
||||
actions:
|
||||
- action: script.jobs_tracker_route_completion
|
||||
data:
|
||||
completion_entity: binary_sensor.dryer_door_status_x34rs_contact
|
||||
completion_to_state: "on"
|
||||
|
||||
- id: jobs_tracker_empty_washing_machine_lid_open
|
||||
alias: "Jobs Tracker - Empty Washer on Lid Open"
|
||||
description: "Marks Empty the Washer complete after the washer lid is open for five seconds."
|
||||
mode: single
|
||||
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: binary_sensor.washing_machine_lid_status_x39rs_contact
|
||||
to: "on"
|
||||
for:
|
||||
seconds: 5
|
||||
|
||||
actions:
|
||||
- action: script.jobs_tracker_route_completion
|
||||
data:
|
||||
completion_entity: binary_sensor.washing_machine_lid_status_x39rs_contact
|
||||
completion_to_state: "on"
|
||||
@@ -36,7 +36,7 @@ input_number:
|
||||
step: 0.1
|
||||
mode: box
|
||||
unit_of_measurement: "s"
|
||||
initial: 1.5
|
||||
initial: 6
|
||||
|
||||
automation:
|
||||
- id: lounge_ir_remote_functions
|
||||
@@ -101,10 +101,36 @@ automation:
|
||||
|
||||
- alias: "Run the matching lounge command"
|
||||
choose:
|
||||
- alias: "Set both reading lights together"
|
||||
conditions:
|
||||
- condition: template
|
||||
value_template: "{{ ir_hash == '0x79fb9e54' }}"
|
||||
sequence:
|
||||
- choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{
|
||||
is_state('light.esp_lounge6chdimmer_couch_spots_left', 'on')
|
||||
or is_state('light.esp_lounge6chdimmer_couch_spots_right', 'on')
|
||||
}}
|
||||
sequence:
|
||||
- action: light.turn_off
|
||||
target:
|
||||
entity_id:
|
||||
- light.esp_lounge6chdimmer_couch_spots_left
|
||||
- light.esp_lounge6chdimmer_couch_spots_right
|
||||
default:
|
||||
- action: light.turn_on
|
||||
target:
|
||||
entity_id:
|
||||
- light.esp_lounge6chdimmer_couch_spots_left
|
||||
- light.esp_lounge6chdimmer_couch_spots_right
|
||||
|
||||
- alias: "Scene commands"
|
||||
conditions:
|
||||
- condition: template
|
||||
value_template: "{{ ir_hash in scene_map }}"
|
||||
value_template: "{{ ir_hash in scene_map and ir_hash != '0x79fb9e54' }}"
|
||||
sequence:
|
||||
- action: script.lounge_set_scene
|
||||
data:
|
||||
@@ -169,7 +195,7 @@ automation:
|
||||
data:
|
||||
channels: "sony_tv_lounge"
|
||||
title: "View Road"
|
||||
short_announcement: "Surround Amplifier"
|
||||
short_announcement: "Surround Amp {{ new_state_text }}"
|
||||
long_announcement: ""
|
||||
payload: "{{ new_state_text }}"
|
||||
indicator: ""
|
||||
|
||||
@@ -65,6 +65,10 @@ input_text:
|
||||
name: Lounge Last Scene
|
||||
initial: "All Off"
|
||||
max: 40
|
||||
lounge_scene_display:
|
||||
name: Lounge Scene Display
|
||||
initial: "Select Scene"
|
||||
max: 40
|
||||
|
||||
timer:
|
||||
lounge_override_timer:
|
||||
@@ -111,6 +115,14 @@ script:
|
||||
- "Dining On/Off"
|
||||
#- "Night Light"
|
||||
|
||||
display_scenes:
|
||||
- "Bright"
|
||||
- "TV General"
|
||||
- "Balanced"
|
||||
- "Movie"
|
||||
- "Night Light"
|
||||
- "Warm Cozy"
|
||||
|
||||
#####################################################################
|
||||
# SCENE SETTINGS
|
||||
# Delta only - only list entities you want to change.
|
||||
@@ -294,6 +306,19 @@ script:
|
||||
data:
|
||||
value: "{{ sel }}"
|
||||
|
||||
- choose:
|
||||
- conditions: "{{ sel in display_scenes }}"
|
||||
sequence:
|
||||
- service: input_text.set_value
|
||||
target: { entity_id: input_text.lounge_scene_display }
|
||||
data:
|
||||
value: "{{ sel }}"
|
||||
default:
|
||||
- service: input_text.set_value
|
||||
target: { entity_id: input_text.lounge_scene_display }
|
||||
data:
|
||||
value: "Select Scene"
|
||||
|
||||
#########################################################################
|
||||
# APPLY SELECTED SCENE
|
||||
#########################################################################
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#:########################################################################################:##
|
||||
# Main House Power Peak #
|
||||
#:########################################################################################:##
|
||||
# Tracks the calendar-day maximum of the main three-phase active-power sensor for the
|
||||
# Flounder dashboard. The helper retains its value across restarts and resets at midnight.
|
||||
|
||||
input_number:
|
||||
main_house_power_peak_today:
|
||||
name: Main House Power Peak Today
|
||||
icon: mdi:flash-triangle-outline
|
||||
min: 0
|
||||
max: 50000
|
||||
step: 1
|
||||
mode: box
|
||||
unit_of_measurement: W
|
||||
|
||||
automation:
|
||||
- id: main_house_power_peak_today_track
|
||||
alias: Main House Power Peak Today - Track
|
||||
mode: single
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: sensor.main_house_3_phase_power_monitor_main_power_total_active_power
|
||||
- trigger: homeassistant
|
||||
event: start
|
||||
conditions:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ states('sensor.main_house_3_phase_power_monitor_main_power_total_active_power') not in ['unknown', 'unavailable'] }}
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{
|
||||
states('sensor.main_house_3_phase_power_monitor_main_power_total_active_power') | float(0)
|
||||
> states('input_number.main_house_power_peak_today') | float(0)
|
||||
}}
|
||||
actions:
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.main_house_power_peak_today
|
||||
data:
|
||||
value: "{{ states('sensor.main_house_3_phase_power_monitor_main_power_total_active_power') | float(0) }}"
|
||||
|
||||
- id: main_house_power_peak_today_reset
|
||||
alias: Main House Power Peak Today - Reset
|
||||
mode: single
|
||||
triggers:
|
||||
- trigger: time
|
||||
at: "00:00:00"
|
||||
actions:
|
||||
- action: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.main_house_power_peak_today
|
||||
data:
|
||||
value: 0
|
||||
@@ -1,13 +1,12 @@
|
||||
#:########################################################################################:#
|
||||
# PACKAGE: Zorro Evening Feeding Reminder (P017-S013)
|
||||
# FILE: packages/p017_s013_zorro_feeding_reminder.yaml
|
||||
# VERSION: v1.0.1 2026-07-26
|
||||
# VERSION: v1.0.2 2026-08-12
|
||||
#:########################################################################################:#
|
||||
# PURPOSE:
|
||||
# Remind to feed Zorro (evening feeding) if not completed.
|
||||
# LED matrix shows "Feed Zorro" every 30 min from 5pm to 10pm.
|
||||
# Voice says "Zorro has not been fed this afternoon" every 30 min from 5pm to 8pm.
|
||||
# Each announcement has a random delay of 0-10 min for natural variation.
|
||||
# Clears LED matrix immediately when evening feeding is completed.
|
||||
#:########################################################################################:#
|
||||
# DEPENDENCIES:
|
||||
@@ -55,25 +54,32 @@ automation:
|
||||
voice_hours: "{{ now().hour < 20 }}"
|
||||
quiet_time: "{{ is_state('input_boolean.quiet_time', 'on') }}"
|
||||
|
||||
- delay:
|
||||
seconds: "{{ range(0, 601) | random | int }}"
|
||||
|
||||
- action: script.view_road_announcement
|
||||
data:
|
||||
channels: "led_matrix_1, sony_tv_lounge"
|
||||
title: "Zorro"
|
||||
short_announcement: "Feed Zorro"
|
||||
indicator: "zorro,1,15,10,6,5,0,0"
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ voice_hours and not quiet_time }}"
|
||||
value_template: >-
|
||||
{{ states('sensor.jobs_tracker_dog_feeding_evening') != 'Complete' }}
|
||||
then:
|
||||
- action: script.view_road_announcement
|
||||
data:
|
||||
channels: "lounge_google_home_voice"
|
||||
channels: "led_matrix_1, sony_tv_lounge"
|
||||
title: "Zorro"
|
||||
short_announcement: "Zorro has not been fed this afternoon"
|
||||
short_announcement: "Feed Zorro"
|
||||
indicator: "zorro,1,15,10,6,5,0,0"
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{
|
||||
voice_hours
|
||||
and not quiet_time
|
||||
and is_state('script.view_road_announcement', 'off')
|
||||
}}
|
||||
then:
|
||||
- action: script.view_road_announcement
|
||||
data:
|
||||
channels: "lounge_google_home_voice"
|
||||
title: "Zorro"
|
||||
short_announcement: "Zorro has not been fed this afternoon"
|
||||
|
||||
##############################################################################
|
||||
# CLEAR LED MATRIX WHEN FED
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
#:########################################################################################:#
|
||||
# PACKAGE: Potplant Moisture Alerts
|
||||
# FILE: packages/potplant_moisture_alerts.yaml
|
||||
# VERSION: v1.1.0 2026-07-29
|
||||
# - Preserve user-selected thresholds and master enable state across restarts.
|
||||
# VERSION: v1.1.1 2026-08-15
|
||||
# - Update moisture sensor entity IDs after the plant devices were renamed.
|
||||
# - v1.1.0 2026-07-29: Preserve user-selected thresholds and master enable state across restarts.
|
||||
#:########################################################################################:#
|
||||
# PURPOSE:
|
||||
# Alert when potplant soil moisture drops below a configurable threshold.
|
||||
# Displays on the Lounge LED matrix and announces via Lounge Google Home voice.
|
||||
#:########################################################################################:#
|
||||
# SETTINGS:
|
||||
# - input_number.*_moisture_threshold: per-plant dry threshold (default 30%)
|
||||
# - input_number.*_moisture_threshold: per-plant dry threshold (table 25%, TV 30%)
|
||||
# - input_boolean.potplant_alert_enabled: master enable toggle
|
||||
#:########################################################################################:#
|
||||
# DEPENDENCIES:
|
||||
# - packages/view_road_announcement_router.yaml for script.view_road_announcement
|
||||
# - sensor.table_plant_soil_moisture
|
||||
# - sensor.tv_plant_soil_moisture
|
||||
# - sensor.table_plant_x36md_soil_moisture
|
||||
# - sensor.tv_plant_x37md_soil_moisture
|
||||
#:########################################################################################:#
|
||||
|
||||
input_number:
|
||||
@@ -24,6 +25,7 @@ input_number:
|
||||
min: 0
|
||||
max: 100
|
||||
step: 5
|
||||
initial: 25
|
||||
unit_of_measurement: "%"
|
||||
icon: mdi:water-percent
|
||||
|
||||
@@ -45,15 +47,21 @@ template:
|
||||
- name: "Table Plant Needs Water"
|
||||
unique_id: table_plant_needs_water
|
||||
icon: mdi:flower
|
||||
availability: >-
|
||||
{{ not is_state('sensor.table_plant_x36md_soil_moisture', 'unavailable')
|
||||
and not is_state('sensor.table_plant_x36md_soil_moisture', 'unknown') }}
|
||||
state: >-
|
||||
{{ (states('sensor.table_plant_soil_moisture') | float(100)) <
|
||||
(states('input_number.table_plant_moisture_threshold') | float(30)) }}
|
||||
{{ (states('sensor.table_plant_x36md_soil_moisture') | float(100)) <
|
||||
(states('input_number.table_plant_moisture_threshold') | float(25)) }}
|
||||
|
||||
- name: "TV Plant Needs Water"
|
||||
unique_id: tv_plant_needs_water
|
||||
icon: mdi:flower
|
||||
availability: >-
|
||||
{{ not is_state('sensor.tv_plant_x37md_soil_moisture', 'unavailable')
|
||||
and not is_state('sensor.tv_plant_x37md_soil_moisture', 'unknown') }}
|
||||
state: >-
|
||||
{{ (states('sensor.tv_plant_soil_moisture') | float(100)) <
|
||||
{{ (states('sensor.tv_plant_x37md_soil_moisture') | float(100)) <
|
||||
(states('input_number.tv_plant_moisture_threshold') | float(30)) }}
|
||||
|
||||
automation:
|
||||
|
||||
@@ -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) }}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user