From dbb0a6a89c09e4f5d4c97f1a6143f1b6084380ae Mon Sep 17 00:00:00 2001 From: zorruno Date: Sat, 15 Aug 2026 14:58:19 +1200 Subject: [PATCH] various tidyups --- .../integrations/byd_vehicle/dark_icon.png | Bin 8304 -> 17173 bytes .ha_run.lock | 2 +- .opencode_agents_md.sha256 | 1 + automations.yaml | 0 configuration.yaml | 26 +- esphome/.device-builder.json | 1079 +++++++++-------- mqtt_dump.txt | 10 + packages/16a_ev_charging_controls.yaml | 174 ++- .../appliances_status_monitoring.yaml | 23 +- .../Job_Chore_Tracking/jobs_tracker_jobs.yaml | 264 +++- .../jobs_tracker_processor.yaml | 367 +++++- packages/alexa_tplink_actions.yaml | 98 +- .../appliance_remaining_time_predictor.yaml | 442 +++++++ packages/appliance_status_mqttfeed.yaml | 11 +- packages/bedroom_3_controls.yaml | 75 ++ packages/cctv_controls.yaml | 41 +- packages/flounder_cctv_snapshots.yaml | 44 + packages/flounder_heat_pump_optimistic.yaml | 374 ++++++ packages/flounder_kiosk.yaml | 33 +- .../heat_pump_master_bedroom_helpers.yaml | 3 + packages/internet_monitoring.yaml | 265 ++++ ...tracker_appliance_completion_triggers.yaml | 65 + packages/lounge_ir_sensor_actions.yaml | 32 +- packages/lounge_lighting_scenes.yaml | 25 + packages/main_house_power_peak.yaml | 54 + packages/pet_feeding_reminder.yaml | 36 +- packages/potplant_moisture_alerts.yaml | 24 +- packages/view_road_announcement_router.yaml | 13 +- scripts/flounder_motioneye_snapshots.py | 61 + scripts/internet_speed_test.sh | 19 + 30 files changed, 2977 insertions(+), 684 deletions(-) create mode 100644 .opencode_agents_md.sha256 create mode 100644 automations.yaml create mode 100644 mqtt_dump.txt create mode 100644 packages/appliance_remaining_time_predictor.yaml create mode 100644 packages/bedroom_3_controls.yaml create mode 100644 packages/flounder_cctv_snapshots.yaml create mode 100644 packages/flounder_heat_pump_optimistic.yaml create mode 100644 packages/internet_monitoring.yaml create mode 100644 packages/jobs_tracker_appliance_completion_triggers.yaml create mode 100644 packages/main_house_power_peak.yaml create mode 100644 scripts/flounder_motioneye_snapshots.py create mode 100755 scripts/internet_speed_test.sh diff --git a/.cache/brands/integrations/byd_vehicle/dark_icon.png b/.cache/brands/integrations/byd_vehicle/dark_icon.png index a31c107ca8f43200d92ed83d7eb2ac126652e64c..a86a688099220452f8b8c03a0c15ca362d807355 100644 GIT binary patch literal 17173 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 literal 8304 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 + 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 diff --git a/packages/Appliance_Monitoring/appliances_status_monitoring.yaml b/packages/Appliance_Monitoring/appliances_status_monitoring.yaml index b06d801..a8f2110 100644 --- a/packages/Appliance_Monitoring/appliances_status_monitoring.yaml +++ b/packages/Appliance_Monitoring/appliances_status_monitoring.yaml @@ -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: "" diff --git a/packages/Job_Chore_Tracking/jobs_tracker_jobs.yaml b/packages/Job_Chore_Tracking/jobs_tracker_jobs.yaml index f12b0b1..9b26804 100644 --- a/packages/Job_Chore_Tracking/jobs_tracker_jobs.yaml +++ b/packages/Job_Chore_Tracking/jobs_tracker_jobs.yaml @@ -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" diff --git a/packages/Job_Chore_Tracking/jobs_tracker_processor.yaml b/packages/Job_Chore_Tracking/jobs_tracker_processor.yaml index fe549ef..f30538b 100644 --- a/packages/Job_Chore_Tracking/jobs_tracker_processor.yaml +++ b/packages/Job_Chore_Tracking/jobs_tracker_processor.yaml @@ -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 + }} diff --git a/packages/alexa_tplink_actions.yaml b/packages/alexa_tplink_actions.yaml index a36f524..82c266c 100644 --- a/packages/alexa_tplink_actions.yaml +++ b/packages/alexa_tplink_actions.yaml @@ -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 diff --git a/packages/appliance_remaining_time_predictor.yaml b/packages/appliance_remaining_time_predictor.yaml new file mode 100644 index 0000000..56c9a73 --- /dev/null +++ b/packages/appliance_remaining_time_predictor.yaml @@ -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) }} diff --git a/packages/appliance_status_mqttfeed.yaml b/packages/appliance_status_mqttfeed.yaml index bd8cc1a..3904be8 100644 --- a/packages/appliance_status_mqttfeed.yaml +++ b/packages/appliance_status_mqttfeed.yaml @@ -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. diff --git a/packages/bedroom_3_controls.yaml b/packages/bedroom_3_controls.yaml new file mode 100644 index 0000000..3ec3e37 --- /dev/null +++ b/packages/bedroom_3_controls.yaml @@ -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 diff --git a/packages/cctv_controls.yaml b/packages/cctv_controls.yaml index 84c386e..3acc125 100644 --- a/packages/cctv_controls.yaml +++ b/packages/cctv_controls.yaml @@ -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') }}" diff --git a/packages/flounder_cctv_snapshots.yaml b/packages/flounder_cctv_snapshots.yaml new file mode 100644 index 0000000..5c8988c --- /dev/null +++ b/packages/flounder_cctv_snapshots.yaml @@ -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 diff --git a/packages/flounder_heat_pump_optimistic.yaml b/packages/flounder_heat_pump_optimistic.yaml new file mode 100644 index 0000000..82187b3 --- /dev/null +++ b/packages/flounder_heat_pump_optimistic.yaml @@ -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 diff --git a/packages/flounder_kiosk.yaml b/packages/flounder_kiosk.yaml index e38f68c..81fcccf 100644 --- a/packages/flounder_kiosk.yaml +++ b/packages/flounder_kiosk.yaml @@ -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 # #:########################################################################################:# diff --git a/packages/heat_pump_master_bedroom_helpers.yaml b/packages/heat_pump_master_bedroom_helpers.yaml index 0ce47a5..e782b29 100644 --- a/packages/heat_pump_master_bedroom_helpers.yaml +++ b/packages/heat_pump_master_bedroom_helpers.yaml @@ -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 diff --git a/packages/internet_monitoring.yaml b/packages/internet_monitoring.yaml new file mode 100644 index 0000000..cad2b02 --- /dev/null +++ b/packages/internet_monitoring.yaml @@ -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 diff --git a/packages/jobs_tracker_appliance_completion_triggers.yaml b/packages/jobs_tracker_appliance_completion_triggers.yaml new file mode 100644 index 0000000..1b2c6f1 --- /dev/null +++ b/packages/jobs_tracker_appliance_completion_triggers.yaml @@ -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" diff --git a/packages/lounge_ir_sensor_actions.yaml b/packages/lounge_ir_sensor_actions.yaml index cb892b8..1b2e4d0 100644 --- a/packages/lounge_ir_sensor_actions.yaml +++ b/packages/lounge_ir_sensor_actions.yaml @@ -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: "" diff --git a/packages/lounge_lighting_scenes.yaml b/packages/lounge_lighting_scenes.yaml index 70a3c01..9bd9ccd 100644 --- a/packages/lounge_lighting_scenes.yaml +++ b/packages/lounge_lighting_scenes.yaml @@ -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 ######################################################################### diff --git a/packages/main_house_power_peak.yaml b/packages/main_house_power_peak.yaml new file mode 100644 index 0000000..53495ea --- /dev/null +++ b/packages/main_house_power_peak.yaml @@ -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 diff --git a/packages/pet_feeding_reminder.yaml b/packages/pet_feeding_reminder.yaml index d14de82..a9bc2b0 100644 --- a/packages/pet_feeding_reminder.yaml +++ b/packages/pet_feeding_reminder.yaml @@ -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 diff --git a/packages/potplant_moisture_alerts.yaml b/packages/potplant_moisture_alerts.yaml index 52ec106..c372126 100644 --- a/packages/potplant_moisture_alerts.yaml +++ b/packages/potplant_moisture_alerts.yaml @@ -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: diff --git a/packages/view_road_announcement_router.yaml b/packages/view_road_announcement_router.yaml index f46d83f..77fcd09 100644 --- a/packages/view_road_announcement_router.yaml +++ b/packages/view_road_announcement_router.yaml @@ -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/ # 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/ @@ -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) }}" diff --git a/scripts/flounder_motioneye_snapshots.py b/scripts/flounder_motioneye_snapshots.py new file mode 100644 index 0000000..3a64311 --- /dev/null +++ b/scripts/flounder_motioneye_snapshots.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Refresh Flounder's local MotionEye preview images.""" + +import os +from pathlib import Path +import sys +import time +from urllib.error import URLError +from urllib.request import urlopen + + +OUTPUTS = ( + "front-door.jpg", + "driveway.jpg", + "garage.jpg", + "bike-shed.jpg", + "caravan.jpg", +) +OUTPUT_DIR = Path(os.environ.get("HASS_CONFIG", "/config")) / "www/flounder" +HASS_URL = os.environ.get("HASS_URL", "http://127.0.0.1:8123") + + +def download(url_path: str, destination: Path) -> bool: + url = f"{HASS_URL}{url_path}" + for attempt in range(2): + try: + with urlopen(url, timeout=4) as response: + image = response.read() + if image.startswith(b"\xff\xd8") and image.endswith(b"\xff\xd9"): + temporary = destination.with_suffix(".tmp") + temporary.write_bytes(image) + os.replace(temporary, destination) + return True + except (OSError, TimeoutError, URLError): + pass + if attempt == 0: + time.sleep(2) + return False + + +def main() -> int: + arguments = sys.argv[1:] + outputs = OUTPUTS + if arguments and arguments[0] == "--primary": + arguments = arguments[1:] + outputs = OUTPUTS[:3] + + if len(arguments) != len(outputs): + return 2 + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + failures = 0 + for url_path, filename in zip(arguments, outputs, strict=True): + if not download(url_path, OUTPUT_DIR / filename): + failures += 1 + time.sleep(2) + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/internet_speed_test.sh b/scripts/internet_speed_test.sh new file mode 100755 index 0000000..ac3d114 --- /dev/null +++ b/scripts/internet_speed_test.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +download_bytes=2000000 +upload_bytes=500000 +download_url="https://speed.cloudflare.com/__down?bytes=${download_bytes}" +upload_url="https://speed.cloudflare.com/__up" + +download_bps=$(curl --fail --silent --show-error --location --max-time 45 \ + --output /dev/null --write-out "%{speed_download}" "${download_url}") +upload_bps=$(head -c "${upload_bytes}" /dev/zero | curl --fail --silent --show-error \ + --location --max-time 45 --output /dev/null --request POST \ + --header "Content-Type: application/octet-stream" --data-binary @- \ + --write-out "%{speed_upload}" "${upload_url}") + +awk -v download_bps="${download_bps}" -v upload_bps="${upload_bps}" 'BEGIN { + printf "{\"download_mbps\":%.2f,\"upload_mbps\":%.2f}\n", \ + download_bps * 8 / 1000000, upload_bps * 8 / 1000000 +}'