From 0db246aefad258939f04cfd079c2d462ef791fc9 Mon Sep 17 00:00:00 2001 From: zorruno Date: Mon, 22 Jun 2026 21:38:18 +1200 Subject: [PATCH] openhasp mods and file shuffling --- esphome/.device-builder.json | 68 +- .../appliance_monitor_processor.yaml | 0 .../appliances_status_monitoring.yaml | 0 .../Job_Chore_Tracking/jobs_tracker_jobs.yaml | 809 ++++++++++++++++++ .../jobs_tracker_processor.yaml | 316 +++++++ .../central_stairway_2way_sync.yaml | 0 .../garage_lights_2way_sync.yaml | 0 .../master_bedroom_main_lights_sync.yaml | 0 .../openhasp_lounge1_autochange_pages.yaml | 0 .../openhasp_lounge1_wallplate.yaml | 504 +++++++++-- .../openhasp_lounge1_wallplate_backlight.yaml | 0 11 files changed, 1604 insertions(+), 93 deletions(-) rename packages/{ => Appliance_Monitoring}/appliance_monitor_processor.yaml (100%) rename packages/{ => Appliance_Monitoring}/appliances_status_monitoring.yaml (100%) create mode 100644 packages/Job_Chore_Tracking/jobs_tracker_jobs.yaml create mode 100644 packages/Job_Chore_Tracking/jobs_tracker_processor.yaml rename packages/{lighting_2way_sync => Lighting_2Way_Sync}/central_stairway_2way_sync.yaml (100%) rename packages/{lighting_2way_sync => Lighting_2Way_Sync}/garage_lights_2way_sync.yaml (100%) rename packages/{lighting_2way_sync => Lighting_2Way_Sync}/master_bedroom_main_lights_sync.yaml (100%) rename packages/{ => Openhasp_Displays}/openhasp_lounge1_autochange_pages.yaml (100%) rename packages/{ => Openhasp_Displays}/openhasp_lounge1_wallplate.yaml (66%) rename packages/{ => Openhasp_Displays}/openhasp_lounge1_wallplate_backlight.yaml (100%) diff --git a/esphome/.device-builder.json b/esphome/.device-builder.json index e1021e1..1406e67 100644 --- a/esphome/.device-builder.json +++ b/esphome/.device-builder.json @@ -111,7 +111,8 @@ "mac_address": "8C:AA:B5:DF:B7:DE" }, "esp-loungepelmetleds2.yaml": { - "board_id": "generic-esp8266" + "board_id": "generic-esp8266", + "mac_address": "48:E7:29:48:A4:F1" }, "esp-entmulti.yaml": { "board_id": "generic-esp32", @@ -161,7 +162,8 @@ "mac_address": "5C:CF:7F:92:35:96" }, "esp-loungecabinetleds.yaml": { - "board_id": "generic-esp8266" + "board_id": "generic-esp8266", + "mac_address": "E8:DB:84:5C:4D:34" }, "esp-6buttontest-pmb.yaml": { "board_id": "generic-esp32" @@ -174,7 +176,8 @@ "mac_address": "5C:CF:7F:B4:A7:A5" }, "esp-breakfastbarleds.yaml": { - "board_id": "generic-esp8266" + "board_id": "generic-esp8266", + "mac_address": "24:62:AB:6E:D3:94" }, "esp-laundryenv.yaml": { "board_id": "generic-esp8266", @@ -257,7 +260,8 @@ "mac_address": "2C:F4:32:4B:BC:70" }, "esp-loungebookshelfleds.yaml": { - "board_id": "generic-esp8266" + "board_id": "generic-esp8266", + "mac_address": "24:62:AB:6E:D5:3C" }, "esp-officeduallights.yaml": { "board_id": "generic-esp8266" @@ -282,7 +286,8 @@ "board_id": "d1-mini" }, "esp-downstairskitchleds.yaml": { - "board_id": "generic-esp8266" + "board_id": "generic-esp8266", + "mac_address": "E8:DB:84:49:4E:99" }, "esp-entrancelightswitch.yaml": { "board_id": "generic-esp8266", @@ -303,6 +308,56 @@ "mac_address": "88:13:BF:68:39:2C" }, "_firmware_jobs": [ + { + "job_id": "1a468db814e3", + "configuration": "esp-astroclockstepper1.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-06-22T03:29:56.540327+00:00", + "started_at": "2026-06-22T03:30:10.489873+00:00", + "completed_at": "2026-06-22T03:30:21.965677+00:00", + "exit_code": 0, + "error": null, + "port": "OTA", + "new_name": "", + "depends_on": "46b32677aa36", + "dependency_released": true, + "progress": 100, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "" + }, + { + "job_id": "46b32677aa36", + "configuration": "esp-astroclockstepper1.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-06-22T03:29:56.540282+00:00", + "started_at": "2026-06-22T03:29:56.541217+00:00", + "completed_at": "2026-06-22T03:30:10.487553+00:00", + "exit_code": 0, + "error": null, + "port": "", + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": null, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "" + }, { "job_id": "dfabed27adf5", "configuration": "esp-masterbdrmwest.yaml", @@ -2982,5 +3037,8 @@ "_board_id_user_set_migrated": true, "_dashboard_identity": { "dashboard_id": "vhamVDnWEnx-FvAa24uVv49Wsddurh84" + }, + "esp-astroclockstepper1.yaml": { + "mac_address": "24:D7:EB:D0:16:68" } } \ No newline at end of file diff --git a/packages/appliance_monitor_processor.yaml b/packages/Appliance_Monitoring/appliance_monitor_processor.yaml similarity index 100% rename from packages/appliance_monitor_processor.yaml rename to packages/Appliance_Monitoring/appliance_monitor_processor.yaml diff --git a/packages/appliances_status_monitoring.yaml b/packages/Appliance_Monitoring/appliances_status_monitoring.yaml similarity index 100% rename from packages/appliances_status_monitoring.yaml rename to packages/Appliance_Monitoring/appliances_status_monitoring.yaml diff --git a/packages/Job_Chore_Tracking/jobs_tracker_jobs.yaml b/packages/Job_Chore_Tracking/jobs_tracker_jobs.yaml new file mode 100644 index 0000000..d842d9e --- /dev/null +++ b/packages/Job_Chore_Tracking/jobs_tracker_jobs.yaml @@ -0,0 +1,809 @@ +#:########################################################################################:# +# Jobs Tracker Job List Package # +#:########################################################################################:# +# +# TITLE: +# Jobs Tracker Job List +# +# FILE: +# jobs_tracker_jobs.yaml +# +# VERSION: +# V1.4 2026-06-20 +# +# PURPOSE: +# Defines the job list, MQTT entities, completion triggers and summary output +# for the Jobs Tracker system. +# +# DEPENDS ON: +# jobs_tracker_processor.yaml V1.3.1 or newer +# +# MQTT TOPICS: +# viewroad-status/jobs_tracker/summary +# viewroad-status/jobs_tracker/dog_feeding_morning +# viewroad-status/jobs_tracker/dog_feeding_evening +# viewroad-status/jobs_tracker/dog_depooping +# viewroad-status/jobs_tracker/dishwasher_unpacked +# viewroad-status/jobs_tracker/rubbish_removed +# +# JOB STATES: +# Not Due +# Due +# Overdue +# Complete +# +# VERSION HISTORY: +# V1.4 2026-06-20 +# - Updated shared dog-fed button behaviour. +# - If either dog-feeding job is Due or Overdue, the shared physical button +# completes every dog-feeding job that is currently Due or Overdue. +# - If neither dog-feeding job is Due or Overdue, the shared physical button +# falls back to time-based early completion. +# +# V1.3 2026-06-20 +# - Added Not Due support for daily jobs before configured due time. +# - Added grouped MQTT Jobs Tracker device. +# +# NOTES: +# - due_weekday uses Python numbering: +# Monday=0, Tuesday=1, Wednesday=2, Thursday=3, +# Friday=4, Saturday=5, Sunday=6 +# +# - event.dog_fed_button_xbt14_action is shared by: +# Dog Feeding Morning +# Dog Feeding Evening +# +# - sensor.jobs_done marks both: +# Dishwasher Unpacked +# Rubbish Removed +# +#:########################################################################################:# + +#:########################################################################################:# +# MANUAL TEST BUTTONS # +#:########################################################################################:# + +input_button: + jobs_tracker_complete_dog_feeding_morning: + name: "Jobs Tracker Complete Dog Feeding Morning" + icon: mdi:dog-side + + jobs_tracker_complete_dog_feeding_evening: + name: "Jobs Tracker Complete Dog Feeding Evening" + icon: mdi:dog-side + + jobs_tracker_complete_dog_depooping: + name: "Jobs Tracker Complete Dog Depooping" + icon: mdi:emoticon-poop + + jobs_tracker_complete_dishwasher_unpacked: + name: "Jobs Tracker Complete Dishwasher Unpacked" + icon: mdi:dishwasher + + jobs_tracker_complete_rubbish_removed: + name: "Jobs Tracker Complete Rubbish Removed" + icon: mdi:trash-can + +#:########################################################################################:# +# MQTT SENSORS # +#:########################################################################################:# + +mqtt: + sensor: + #:####################################################################################:# + # Summary sensor # + #:####################################################################################:# + + - name: "Jobs Tracker - Summary" + unique_id: jobs_tracker_summary + state_topic: "viewroad-status/jobs_tracker/summary" + value_template: "{{ value_json.state | default('unknown') }}" + json_attributes_topic: "viewroad-status/jobs_tracker/summary" + icon: mdi:clipboard-list + device: + identifiers: + - viewroad_jobs_tracker + name: "Jobs Tracker" + manufacturer: "View Road Home Assistant" + model: "Shared household jobs tracker" + sw_version: "1.4" + + #:####################################################################################:# + # Individual job sensors # + #:####################################################################################:# + + - name: "Jobs Tracker - Dog Feeding Morning" + unique_id: jobs_tracker_dog_feeding_morning + state_topic: "viewroad-status/jobs_tracker/dog_feeding_morning" + value_template: "{{ value_json.state | default('unknown') }}" + json_attributes_topic: "viewroad-status/jobs_tracker/dog_feeding_morning" + icon: mdi:dog-side + device: + identifiers: + - viewroad_jobs_tracker + name: "Jobs Tracker" + manufacturer: "View Road Home Assistant" + model: "Shared household jobs tracker" + sw_version: "1.4" + + - name: "Jobs Tracker - Dog Feeding Evening" + unique_id: jobs_tracker_dog_feeding_evening + state_topic: "viewroad-status/jobs_tracker/dog_feeding_evening" + value_template: "{{ value_json.state | default('unknown') }}" + json_attributes_topic: "viewroad-status/jobs_tracker/dog_feeding_evening" + icon: mdi:dog-side + device: + identifiers: + - viewroad_jobs_tracker + name: "Jobs Tracker" + manufacturer: "View Road Home Assistant" + model: "Shared household jobs tracker" + sw_version: "1.4" + + - name: "Jobs Tracker - Dog Depooping" + unique_id: jobs_tracker_dog_depooping + state_topic: "viewroad-status/jobs_tracker/dog_depooping" + value_template: "{{ value_json.state | default('unknown') }}" + json_attributes_topic: "viewroad-status/jobs_tracker/dog_depooping" + icon: mdi:emoticon-poop + device: + identifiers: + - viewroad_jobs_tracker + name: "Jobs Tracker" + manufacturer: "View Road Home Assistant" + model: "Shared household jobs tracker" + sw_version: "1.4" + + - name: "Jobs Tracker - Dishwasher Unpacked" + unique_id: jobs_tracker_dishwasher_unpacked + state_topic: "viewroad-status/jobs_tracker/dishwasher_unpacked" + value_template: "{{ value_json.state | default('unknown') }}" + json_attributes_topic: "viewroad-status/jobs_tracker/dishwasher_unpacked" + icon: mdi:dishwasher + device: + identifiers: + - viewroad_jobs_tracker + name: "Jobs Tracker" + manufacturer: "View Road Home Assistant" + model: "Shared household jobs tracker" + sw_version: "1.4" + + - name: "Jobs Tracker - Rubbish Removed" + unique_id: jobs_tracker_rubbish_removed + state_topic: "viewroad-status/jobs_tracker/rubbish_removed" + value_template: "{{ value_json.state | default('unknown') }}" + json_attributes_topic: "viewroad-status/jobs_tracker/rubbish_removed" + icon: mdi:trash-can + device: + identifiers: + - viewroad_jobs_tracker + name: "Jobs Tracker" + manufacturer: "View Road Home Assistant" + model: "Shared household jobs tracker" + sw_version: "1.4" + +#:########################################################################################:# +# TEMPLATE SENSORS # +#:########################################################################################:# + +template: + - binary_sensor: + - name: "Job Complete - Dog Feeding Morning" + unique_id: job_complete_dog_feeding_morning + icon: mdi:dog-side + state: "{{ is_state('sensor.jobs_tracker_dog_feeding_morning', 'Complete') }}" + + - name: "Job Complete - Dog Feeding Evening" + unique_id: job_complete_dog_feeding_evening + icon: mdi:dog-side + state: "{{ is_state('sensor.jobs_tracker_dog_feeding_evening', 'Complete') }}" + + - name: "Job Complete - Dog Depooping" + unique_id: job_complete_dog_depooping + icon: mdi:emoticon-poop + state: "{{ is_state('sensor.jobs_tracker_dog_depooping', 'Complete') }}" + + - name: "Job Complete - Dishwasher Unpacked" + unique_id: job_complete_dishwasher_unpacked + icon: mdi:dishwasher + state: "{{ is_state('sensor.jobs_tracker_dishwasher_unpacked', 'Complete') }}" + + - name: "Job Complete - Rubbish Removed" + unique_id: job_complete_rubbish_removed + icon: mdi:trash-can + state: "{{ is_state('sensor.jobs_tracker_rubbish_removed', 'Complete') }}" + + - sensor: + - name: "Jobs Tracker - Complete Count" + unique_id: jobs_tracker_complete_count + icon: mdi:clipboard-check + state: >- + {{ + [ + states('sensor.jobs_tracker_dog_feeding_morning'), + states('sensor.jobs_tracker_dog_feeding_evening'), + states('sensor.jobs_tracker_dog_depooping'), + states('sensor.jobs_tracker_dishwasher_unpacked'), + states('sensor.jobs_tracker_rubbish_removed') + ] | select('eq', 'Complete') | list | count + }} + + - name: "Jobs Tracker - Not Due Count" + unique_id: jobs_tracker_not_due_count + icon: mdi:clipboard-clock-outline + state: >- + {{ + [ + states('sensor.jobs_tracker_dog_feeding_morning'), + states('sensor.jobs_tracker_dog_feeding_evening'), + states('sensor.jobs_tracker_dog_depooping'), + states('sensor.jobs_tracker_dishwasher_unpacked'), + states('sensor.jobs_tracker_rubbish_removed') + ] | select('eq', 'Not Due') | list | count + }} + + - name: "Jobs Tracker - Due Count" + unique_id: jobs_tracker_due_count + icon: mdi:clipboard-alert + state: >- + {{ + [ + states('sensor.jobs_tracker_dog_feeding_morning'), + states('sensor.jobs_tracker_dog_feeding_evening'), + states('sensor.jobs_tracker_dog_depooping'), + states('sensor.jobs_tracker_dishwasher_unpacked'), + states('sensor.jobs_tracker_rubbish_removed') + ] | select('eq', 'Due') | list | count + }} + + - name: "Jobs Tracker - Overdue Count" + unique_id: jobs_tracker_overdue_count + icon: mdi:clipboard-alert-outline + state: >- + {{ + [ + states('sensor.jobs_tracker_dog_feeding_morning'), + states('sensor.jobs_tracker_dog_feeding_evening'), + states('sensor.jobs_tracker_dog_depooping'), + states('sensor.jobs_tracker_dishwasher_unpacked'), + states('sensor.jobs_tracker_rubbish_removed') + ] | select('eq', 'Overdue') | list | count + }} + +#:########################################################################################:# +# SCRIPTS # +#:########################################################################################:# + +script: + jobs_tracker_dispatch: + alias: "Jobs Tracker - Dispatch" + description: "Runs the generic processor against either all jobs or one completed job." + mode: queued + max: 20 + + fields: + update_all: + description: "True to refresh all jobs." + example: true + + job_id: + description: "Job ID to mark complete." + example: "dog_feeding_morning" + + sequence: + - variables: + jobs_tracker_jobs: + - job_id: dog_feeding_morning + job_name: "Dog Feeding Morning" + repeat_type: daily + due_time: "06:00" + due_weekday: 0 + due_day: 1 + mqtt_topic: "viewroad-status/jobs_tracker/dog_feeding_morning" + state_entity: "sensor.jobs_tracker_dog_feeding_morning" + + - job_id: dog_feeding_evening + job_name: "Dog Feeding Evening" + repeat_type: daily + due_time: "17:00" + due_weekday: 0 + due_day: 1 + mqtt_topic: "viewroad-status/jobs_tracker/dog_feeding_evening" + state_entity: "sensor.jobs_tracker_dog_feeding_evening" + + - job_id: dog_depooping + job_name: "Dog De-pooping" + repeat_type: weekly + due_time: "08:00" + due_weekday: 4 + due_day: 1 + mqtt_topic: "viewroad-status/jobs_tracker/dog_depooping" + 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 + mqtt_topic: "viewroad-status/jobs_tracker/dishwasher_unpacked" + state_entity: "sensor.jobs_tracker_dishwasher_unpacked" + + - job_id: rubbish_removed + job_name: "Rubbish Removed" + repeat_type: daily + due_time: "15:30" + due_weekday: 0 + due_day: 1 + mqtt_topic: "viewroad-status/jobs_tracker/rubbish_removed" + state_entity: "sensor.jobs_tracker_rubbish_removed" + + - choose: + #:################################################################################:# + # Refresh all jobs # + #:################################################################################:# + + - conditions: + - condition: template + value_template: "{{ update_all | default(false) | bool }}" + sequence: + - 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 }}" + mark_complete: false + + #:##################################################################################:# + # Mark one job complete # + #:##################################################################################:# + + default: + - repeat: + for_each: "{{ jobs_tracker_jobs }}" + sequence: + - if: + - 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 }}" + mark_complete: true + + # Give the MQTT sensors a moment to update from the retained job payloads + # before building the summary payload. + - delay: "00:00:01" + + - action: script.jobs_tracker_publish_summary + + jobs_tracker_publish_summary: + alias: "Jobs Tracker - Publish Summary" + description: "Publishes retained MQTT JSON summary for all tracked jobs." + mode: queued + max: 5 + + sequence: + - variables: + complete_count: >- + {{ + [ + states('sensor.jobs_tracker_dog_feeding_morning'), + states('sensor.jobs_tracker_dog_feeding_evening'), + states('sensor.jobs_tracker_dog_depooping'), + states('sensor.jobs_tracker_dishwasher_unpacked'), + states('sensor.jobs_tracker_rubbish_removed') + ] | select('eq', 'Complete') | list | count + }} + + not_due_count: >- + {{ + [ + states('sensor.jobs_tracker_dog_feeding_morning'), + states('sensor.jobs_tracker_dog_feeding_evening'), + states('sensor.jobs_tracker_dog_depooping'), + states('sensor.jobs_tracker_dishwasher_unpacked'), + states('sensor.jobs_tracker_rubbish_removed') + ] | select('eq', 'Not Due') | list | count + }} + + due_count: >- + {{ + [ + states('sensor.jobs_tracker_dog_feeding_morning'), + states('sensor.jobs_tracker_dog_feeding_evening'), + states('sensor.jobs_tracker_dog_depooping'), + states('sensor.jobs_tracker_dishwasher_unpacked'), + states('sensor.jobs_tracker_rubbish_removed') + ] | select('eq', 'Due') | list | count + }} + + overdue_count: >- + {{ + [ + states('sensor.jobs_tracker_dog_feeding_morning'), + states('sensor.jobs_tracker_dog_feeding_evening'), + states('sensor.jobs_tracker_dog_depooping'), + states('sensor.jobs_tracker_dishwasher_unpacked'), + states('sensor.jobs_tracker_rubbish_removed') + ] | select('eq', 'Overdue') | list | count + }} + + unknown_count: >- + {{ + [ + states('sensor.jobs_tracker_dog_feeding_morning'), + states('sensor.jobs_tracker_dog_feeding_evening'), + states('sensor.jobs_tracker_dog_depooping'), + states('sensor.jobs_tracker_dishwasher_unpacked'), + states('sensor.jobs_tracker_rubbish_removed') + ] + | reject('in', ['Complete', 'Not Due', 'Due', 'Overdue']) + | list + | count + }} + + summary_state: >- + {% if overdue_count | int(0) > 0 and due_count | int(0) > 0 %} + {{ overdue_count }} Overdue, {{ due_count }} Due + {% elif overdue_count | int(0) > 0 %} + {{ overdue_count }} Overdue + {% elif due_count | int(0) > 0 %} + {{ due_count }} Due + {% elif not_due_count | int(0) > 0 and complete_count | int(0) > 0 %} + {{ complete_count }} Complete, {{ not_due_count }} Not Due + {% elif not_due_count | int(0) > 0 %} + {{ not_due_count }} Not Due + {% elif complete_count | int(0) == 5 %} + All Complete + {% elif unknown_count | int(0) > 0 %} + {{ unknown_count }} Unknown + {% else %} + Unknown + {% endif %} + + - action: mqtt.publish + data: + topic: "viewroad-status/jobs_tracker/summary" + retain: true + qos: 1 + payload: >- + {{ + { + "state": summary_state | trim, + "complete_count": complete_count | int(0), + "not_due_count": not_due_count | int(0), + "due_count": due_count | int(0), + "overdue_count": overdue_count | int(0), + "unknown_count": unknown_count | int(0), + "total_count": 5, + "jobs": [ + { + "job_id": "dog_feeding_morning", + "job_name": "Dog Feeding Morning", + "state": states('sensor.jobs_tracker_dog_feeding_morning'), + "next_due_human": state_attr('sensor.jobs_tracker_dog_feeding_morning', 'next_due_human'), + "last_completed_human": state_attr('sensor.jobs_tracker_dog_feeding_morning', 'last_completed_human'), + "minutes_since_required": state_attr('sensor.jobs_tracker_dog_feeding_morning', 'minutes_since_required'), + "hours_since_required": state_attr('sensor.jobs_tracker_dog_feeding_morning', 'hours_since_required'), + "hours_since_completed": state_attr('sensor.jobs_tracker_dog_feeding_morning', 'hours_since_completed') + }, + { + "job_id": "dog_feeding_evening", + "job_name": "Dog Feeding Evening", + "state": states('sensor.jobs_tracker_dog_feeding_evening'), + "next_due_human": state_attr('sensor.jobs_tracker_dog_feeding_evening', 'next_due_human'), + "last_completed_human": state_attr('sensor.jobs_tracker_dog_feeding_evening', 'last_completed_human'), + "minutes_since_required": state_attr('sensor.jobs_tracker_dog_feeding_evening', 'minutes_since_required'), + "hours_since_required": state_attr('sensor.jobs_tracker_dog_feeding_evening', 'hours_since_required'), + "hours_since_completed": state_attr('sensor.jobs_tracker_dog_feeding_evening', 'hours_since_completed') + }, + { + "job_id": "dog_depooping", + "job_name": "Dog De-pooping", + "state": states('sensor.jobs_tracker_dog_depooping'), + "next_due_human": state_attr('sensor.jobs_tracker_dog_depooping', 'next_due_human'), + "last_completed_human": state_attr('sensor.jobs_tracker_dog_depooping', 'last_completed_human'), + "minutes_since_required": state_attr('sensor.jobs_tracker_dog_depooping', 'minutes_since_required'), + "hours_since_required": state_attr('sensor.jobs_tracker_dog_depooping', 'hours_since_required'), + "hours_since_completed": state_attr('sensor.jobs_tracker_dog_depooping', 'hours_since_completed') + }, + { + "job_id": "dishwasher_unpacked", + "job_name": "Dishwasher Unpacked", + "state": states('sensor.jobs_tracker_dishwasher_unpacked'), + "next_due_human": state_attr('sensor.jobs_tracker_dishwasher_unpacked', 'next_due_human'), + "last_completed_human": state_attr('sensor.jobs_tracker_dishwasher_unpacked', 'last_completed_human'), + "minutes_since_required": state_attr('sensor.jobs_tracker_dishwasher_unpacked', 'minutes_since_required'), + "hours_since_required": state_attr('sensor.jobs_tracker_dishwasher_unpacked', 'hours_since_required'), + "hours_since_completed": state_attr('sensor.jobs_tracker_dishwasher_unpacked', 'hours_since_completed') + }, + { + "job_id": "rubbish_removed", + "job_name": "Rubbish Removed", + "state": states('sensor.jobs_tracker_rubbish_removed'), + "next_due_human": state_attr('sensor.jobs_tracker_rubbish_removed', 'next_due_human'), + "last_completed_human": state_attr('sensor.jobs_tracker_rubbish_removed', 'last_completed_human'), + "minutes_since_required": state_attr('sensor.jobs_tracker_rubbish_removed', 'minutes_since_required'), + "hours_since_required": state_attr('sensor.jobs_tracker_rubbish_removed', 'hours_since_required'), + "hours_since_completed": state_attr('sensor.jobs_tracker_rubbish_removed', 'hours_since_completed') + } + ], + "dog_feeding_morning": states('sensor.jobs_tracker_dog_feeding_morning'), + "dog_feeding_evening": states('sensor.jobs_tracker_dog_feeding_evening'), + "dog_depooping": states('sensor.jobs_tracker_dog_depooping'), + "dishwasher_unpacked": states('sensor.jobs_tracker_dishwasher_unpacked'), + "rubbish_removed": states('sensor.jobs_tracker_rubbish_removed'), + "last_updated_local": now().strftime('%Y-%m-%d %H:%M:%S') + } | tojson + }} + +#:########################################################################################:# +# AUTOMATIONS # +#:########################################################################################:# + +automation: + - id: jobs_tracker_refresh_all_jobs + alias: "Jobs Tracker - Refresh All Jobs" + description: "Refreshes derived due/overdue values for every job." + mode: single + + triggers: + - trigger: homeassistant + event: start + id: ha_start + + - trigger: time_pattern + minutes: "/1" + id: tick + + actions: + # Let retained MQTT job messages restore before the first refresh. + - if: + - condition: template + value_template: "{{ trigger.id == 'ha_start' }}" + then: + - delay: "00:01:00" + + - action: script.jobs_tracker_dispatch + data: + update_all: true + + - id: jobs_tracker_completion_actions + alias: "Jobs Tracker - Completion Actions" + description: "Marks jobs complete when physical/action/manual buttons are pressed." + mode: queued + max: 20 + + triggers: + #:####################################################################################:# + # Physical / action entities # + #:####################################################################################:# + + - trigger: state + entity_id: event.dog_fed_button_xbt14_action + id: dog_fed_button + + - trigger: state + entity_id: event.dog_depooped_button_xbt15_action + id: dog_depooped_button + + - trigger: state + entity_id: sensor.jobs_done + id: jobs_done_sensor + + #:####################################################################################:# + # Manual test buttons # + #:####################################################################################:# + + - trigger: state + entity_id: input_button.jobs_tracker_complete_dog_feeding_morning + id: manual_dog_feeding_morning + + - trigger: state + entity_id: input_button.jobs_tracker_complete_dog_feeding_evening + id: manual_dog_feeding_evening + + - trigger: state + entity_id: input_button.jobs_tracker_complete_dog_depooping + id: manual_dog_depooping + + - trigger: state + entity_id: input_button.jobs_tracker_complete_dishwasher_unpacked + id: manual_dishwasher_unpacked + + - trigger: state + entity_id: input_button.jobs_tracker_complete_rubbish_removed + id: manual_rubbish_removed + + conditions: + - condition: template + value_template: >- + {{ + trigger.to_state is not none + and trigger.to_state.state not in ['unknown', 'unavailable'] + }} + + actions: + - choose: + #:################################################################################:# + # Dog fed physical button # + #:################################################################################:# + # + # Shared button behaviour: + # + # If any dog feeding jobs are already Due or Overdue: + # - Complete every linked dog-feeding job that is currently Due or Overdue. + # + # If no dog feeding jobs are currently Due or Overdue: + # - Fall back to time-based behaviour so early completion still works. + # + # Examples: + # + # 07:00, morning Due, evening Not Due: + # - Completes morning only. + # + # 23:30, morning Overdue, evening Overdue: + # - Completes morning and evening. + # + # 16:30, morning Complete, evening Not Due: + # - Completes evening early. + # + #:################################################################################:# + + - conditions: + - condition: template + value_template: "{{ trigger.id == 'dog_fed_button' }}" + sequence: + - variables: + dog_feeding_morning_state: "{{ states('sensor.jobs_tracker_dog_feeding_morning') }}" + dog_feeding_evening_state: "{{ states('sensor.jobs_tracker_dog_feeding_evening') }}" + + dog_feeding_any_due_or_overdue: >- + {{ + dog_feeding_morning_state in ['Due', 'Overdue'] + or dog_feeding_evening_state in ['Due', 'Overdue'] + }} + + #:############################################################################:# + # Complete all linked dog feeding jobs that are already Due or Overdue # + #:############################################################################:# + + - if: + - condition: template + value_template: "{{ dog_feeding_morning_state in ['Due', 'Overdue'] }}" + then: + - action: script.jobs_tracker_dispatch + data: + job_id: dog_feeding_morning + + - if: + - condition: template + value_template: "{{ dog_feeding_evening_state in ['Due', 'Overdue'] }}" + then: + - action: script.jobs_tracker_dispatch + data: + job_id: dog_feeding_evening + + #:############################################################################:# + # Fallback for early completion # + #:############################################################################:# + # + # Only used when neither dog feeding job is currently Due or Overdue. + # This keeps useful behaviour such as feeding early before the due time. + # It will not overwrite an already Complete job. + + - if: + - condition: template + value_template: >- + {{ + not (dog_feeding_any_due_or_overdue | bool) + and now().hour < 12 + and dog_feeding_morning_state != 'Complete' + }} + then: + - action: script.jobs_tracker_dispatch + data: + job_id: dog_feeding_morning + + - if: + - condition: template + value_template: >- + {{ + not (dog_feeding_any_due_or_overdue | bool) + and now().hour >= 12 + and dog_feeding_evening_state != 'Complete' + }} + then: + - action: script.jobs_tracker_dispatch + data: + job_id: dog_feeding_evening + + #:################################################################################:# + # Dog de-pooped physical button # + #:################################################################################:# + + - conditions: + - condition: template + value_template: "{{ trigger.id == 'dog_depooped_button' }}" + sequence: + - action: script.jobs_tracker_dispatch + data: + job_id: dog_depooping + + #:################################################################################:# + # Shared jobs_done sensor # + #:################################################################################:# + # + # One state change marks both jobs complete. + + - conditions: + - condition: template + value_template: "{{ trigger.id == 'jobs_done_sensor' }}" + sequence: + - action: script.jobs_tracker_dispatch + data: + job_id: dishwasher_unpacked + + - action: script.jobs_tracker_dispatch + data: + job_id: rubbish_removed + + #:################################################################################:# + # Manual test buttons # + #:################################################################################:# + + - conditions: + - condition: template + value_template: "{{ trigger.id == 'manual_dog_feeding_morning' }}" + sequence: + - action: script.jobs_tracker_dispatch + data: + job_id: dog_feeding_morning + + - conditions: + - condition: template + value_template: "{{ trigger.id == 'manual_dog_feeding_evening' }}" + sequence: + - action: script.jobs_tracker_dispatch + data: + job_id: dog_feeding_evening + + - conditions: + - condition: template + value_template: "{{ trigger.id == 'manual_dog_depooping' }}" + sequence: + - action: script.jobs_tracker_dispatch + data: + job_id: dog_depooping + + - conditions: + - condition: template + value_template: "{{ trigger.id == 'manual_dishwasher_unpacked' }}" + sequence: + - action: script.jobs_tracker_dispatch + data: + job_id: dishwasher_unpacked + + - conditions: + - condition: template + value_template: "{{ trigger.id == 'manual_rubbish_removed' }}" + sequence: + - action: script.jobs_tracker_dispatch + data: + job_id: rubbish_removed diff --git a/packages/Job_Chore_Tracking/jobs_tracker_processor.yaml b/packages/Job_Chore_Tracking/jobs_tracker_processor.yaml new file mode 100644 index 0000000..ecc88d5 --- /dev/null +++ b/packages/Job_Chore_Tracking/jobs_tracker_processor.yaml @@ -0,0 +1,316 @@ +################################################################################ +# Jobs Tracker Processor Package +################################################################################ +# +# TITLE: +# Jobs Tracker Processor +# +# FILE: +# jobs_tracker_processor.yaml +# +# VERSION: +# V1.3.1 2026-06-20 +# +# CHANGELOG: +# V1.3.1: +# - Corrected script field definitions so Home Assistant reliably creates +# script.jobs_tracker_update_job. +# +# V1.3: +# - Daily jobs reset at 00:00 but do not become Due until their configured +# due time. +# - Added "Not Due" state. +# - Completion is recalculated from retained last_completed_ts after restart. +# +################################################################################ + +script: + jobs_tracker_update_job: + alias: "Jobs Tracker - Update Job" + description: "Updates one job's retained MQTT JSON state." + mode: queued + max: 20 + + fields: + job_id: + description: "Machine-safe job ID." + example: "dog_feeding_morning" + job_name: + description: "Human-readable job name." + example: "Dog Feeding Morning" + repeat_type: + description: "daily, weekly or monthly." + example: "daily" + due_time: + description: "Time the job is due, HH:MM." + example: "06:00" + due_weekday: + description: "For weekly jobs only. Monday=0, Friday=4, Sunday=6." + example: 4 + due_day: + description: "For monthly jobs only. Use 1-28." + example: 1 + mqtt_topic: + description: "MQTT topic used for retained job state." + example: "viewroad-status/jobs_tracker/dog_feeding_morning" + state_entity: + description: "MQTT sensor entity that reads this job's retained JSON." + example: "sensor.jobs_tracker_dog_feeding_morning" + mark_complete: + description: "Set true when a human has completed the job." + example: true + + sequence: + - variables: + mark_complete_bool: "{{ mark_complete | default(false) | bool }}" + repeat_type_safe: "{{ repeat_type | default('daily') | lower }}" + due_time_safe: "{{ due_time | default('00:00') }}" + due_hour: "{{ due_time_safe.split(':')[0] | int(0) }}" + due_minute: "{{ due_time_safe.split(':')[1] | int(0) }}" + due_seconds: "{{ ((due_hour | int) * 3600) + ((due_minute | int) * 60) }}" + due_weekday_safe: "{{ due_weekday | default(0) | int(0) }}" + due_day_safe: "{{ due_day | default(1) | int(1) }}" + now_ts: "{{ as_timestamp(now()) }}" + today_midnight_ts: "{{ as_timestamp(today_at('00:00')) }}" + topic_safe: "{{ mqtt_topic | default('viewroad-status/jobs_tracker/' ~ job_id) }}" + + - variables: + current_period_start_ts: >- + {% if repeat_type_safe == 'daily' %} + {{ today_midnight_ts | float(0) }} + {% elif repeat_type_safe == 'weekly' %} + {{ (today_midnight_ts | float(0)) - ((now().weekday() | int) * 86400) }} + {% elif repeat_type_safe == 'monthly' %} + {{ as_timestamp(now().replace(day=1, hour=0, minute=0, second=0, microsecond=0)) }} + {% else %} + {{ today_midnight_ts | float(0) }} + {% endif %} + + next_period_start_ts: >- + {% if repeat_type_safe == 'daily' %} + {{ (today_midnight_ts | float(0)) + 86400 }} + {% elif repeat_type_safe == 'weekly' %} + {{ ((today_midnight_ts | float(0)) - ((now().weekday() | int) * 86400)) + 604800 }} + {% elif repeat_type_safe == 'monthly' %} + {% set next_month = (now().replace(day=28) + timedelta(days=4)).replace(day=1, hour=0, minute=0, second=0, microsecond=0) %} + {{ as_timestamp(next_month) }} + {% else %} + {{ (today_midnight_ts | float(0)) + 86400 }} + {% endif %} + + - variables: + current_due_ts: >- + {% if repeat_type_safe == 'daily' %} + {{ (current_period_start_ts | float(0)) + (due_seconds | int) }} + {% elif repeat_type_safe == 'weekly' %} + {{ (current_period_start_ts | float(0)) + ((due_weekday_safe | int) * 86400) + (due_seconds | int) }} + {% elif repeat_type_safe == 'monthly' %} + {{ as_timestamp(now().replace(day=(due_day_safe | int), hour=(due_hour | int), minute=(due_minute | int), second=0, microsecond=0)) }} + {% else %} + {{ (current_period_start_ts | float(0)) + (due_seconds | int) }} + {% endif %} + + next_due_ts: >- + {% if repeat_type_safe == 'daily' %} + {{ (next_period_start_ts | float(0)) + (due_seconds | int) }} + {% elif repeat_type_safe == 'weekly' %} + {{ (next_period_start_ts | float(0)) + ((due_weekday_safe | int) * 86400) + (due_seconds | int) }} + {% elif repeat_type_safe == 'monthly' %} + {% set next_month = (now().replace(day=28) + timedelta(days=4)).replace(day=1, hour=0, minute=0, second=0, microsecond=0) %} + {{ as_timestamp(next_month.replace(day=(due_day_safe | int), hour=(due_hour | int), minute=(due_minute | int), second=0, microsecond=0)) }} + {% else %} + {{ (next_period_start_ts | float(0)) + (due_seconds | int) }} + {% endif %} + + overdue_after_minutes: >- + {% if repeat_type_safe == 'daily' %} + 120 + {% elif repeat_type_safe == 'weekly' %} + 2880 + {% elif repeat_type_safe == 'monthly' %} + 20160 + {% else %} + 120 + {% endif %} + + - variables: + stored_last_completed_ts: >- + {% set v = state_attr(state_entity, 'last_completed_ts') %} + {{ 0 if v in [none, '', 'unknown', 'unavailable'] else v | float(0) }} + + last_completed_ts: >- + {{ now_ts if mark_complete_bool else stored_last_completed_ts }} + + - variables: + complete_bool: >- + {{ (last_completed_ts | float(0)) >= (current_period_start_ts | float(0)) }} + + due_started_bool: >- + {% if repeat_type_safe == 'daily' %} + {{ (now_ts | float(0)) >= (current_due_ts | float(0)) }} + {% else %} + true + {% endif %} + + overdue_started_bool: >- + {{ (now_ts | float(0)) >= ((current_due_ts | float(0)) + ((overdue_after_minutes | int(0)) * 60)) }} + + minutes_since_required: >- + {% if (now_ts | float(0)) <= (current_due_ts | float(0)) %} + 0 + {% else %} + {{ (((now_ts | float(0)) - (current_due_ts | float(0))) / 60) | round(0) | int }} + {% endif %} + + hours_since_required: >- + {% if (now_ts | float(0)) <= (current_due_ts | float(0)) %} + 0 + {% else %} + {{ (((now_ts | float(0)) - (current_due_ts | float(0))) / 3600) | round(0) | int }} + {% endif %} + + - variables: + job_status: >- + {% if complete_bool | bool %} + Complete + {% elif not (due_started_bool | bool) %} + Not Due + {% elif overdue_started_bool | bool %} + Overdue + {% else %} + Due + {% endif %} + + display_due_ts: >- + {% if complete_bool | bool %} + {{ next_due_ts | float(0) }} + {% else %} + {{ current_due_ts | float(0) }} + {% endif %} + + overdue_minutes: >- + {% if job_status | trim == 'Overdue' %} + {{ minutes_since_required | int(0) }} + {% else %} + 0 + {% endif %} + + overdue_hours: >- + {% if job_status | trim == 'Overdue' %} + {{ hours_since_required | int(0) }} + {% else %} + 0 + {% endif %} + + hours_since_completed: >- + {% if (last_completed_ts | float(0)) > 0 %} + {{ (((now_ts | float(0)) - (last_completed_ts | float(0))) / 3600) | round(0) | int }} + {% else %} + unknown + {% 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 %} + Today @ {{ t }} + {% elif d == yesterday %} + Yesterday @ {{ t }} + {% elif days_ago < 7 %} + Last {{ ts | timestamp_custom('%A', true) }} @ {{ t }} + {% else %} + {{ days_ago }} days ago @ {{ t }} + {% endif %} + {% endif %} + + next_due_human: >- + {% set ts = display_due_ts | float(0) %} + {% 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 tomorrow = (as_timestamp(today_at('00:00')) + 86400) | timestamp_custom('%Y-%m-%d', true) %} + {% set yesterday = (as_timestamp(today_at('00:00')) - 86400) | timestamp_custom('%Y-%m-%d', true) %} + {% set due_midnight = as_timestamp(strptime(d ~ ' 00:00:00', '%Y-%m-%d %H:%M:%S')) %} + {% set days_diff = ((due_midnight - as_timestamp(today_at('00:00'))) / 86400) | round(0) | int %} + {% if d == today %} + Today @ {{ t }} + {% elif d == tomorrow %} + Tomorrow @ {{ t }} + {% elif d == yesterday %} + Yesterday @ {{ t }} + {% elif days_diff > 1 and days_diff <= 7 %} + Next {{ ts | timestamp_custom('%A', true) }} @ {{ t }} + {% elif days_diff < 0 and days_diff > -7 %} + Last {{ ts | timestamp_custom('%A', true) }} @ {{ t }} + {% elif days_diff > 7 %} + In {{ days_diff }} days @ {{ t }} + {% elif days_diff < -7 %} + {{ 0 - days_diff }} days ago @ {{ t }} + {% else %} + {{ ts | timestamp_custom('%Y-%m-%d @ %H:%M', true) }} + {% endif %} + + - action: mqtt.publish + data: + topic: "{{ topic_safe }}" + retain: true + qos: 1 + payload: >- + {{ + { + "job_id": job_id, + "job_name": job_name, + "repeat": repeat_type_safe, + "due_time": due_time_safe, + "due_weekday": due_weekday_safe | int, + "due_day": due_day_safe | int, + + "state": job_status | trim, + "complete": complete_bool | bool, + + "last_completed_ts": last_completed_ts | float(0), + "last_completed_time": last_completed_time, + "last_completed_human": last_completed_human, + + "completed_for_period_ts": current_period_start_ts | float(0) if complete_bool | bool else 0, + "completed_for_due_ts": current_period_start_ts | float(0) if complete_bool | bool else 0, + + "current_period_start_ts": current_period_start_ts | float(0), + "next_period_start_ts": next_period_start_ts | float(0), + + "current_due_ts": current_due_ts | float(0), + "next_due_ts": next_due_ts | float(0), + "display_due_ts": display_due_ts | float(0), + "next_due_human": next_due_human, + + "due_started": due_started_bool | bool, + "overdue_started": overdue_started_bool | bool, + + "minutes_since_required": minutes_since_required | int(0), + "hours_since_required": hours_since_required | int(0), + + "overdue_after_minutes": overdue_after_minutes | int(0), + "overdue_minutes": overdue_minutes | int(0), + "overdue_hours": overdue_hours | int(0), + + "hours_since_completed": hours_since_completed, + + "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/lighting_2way_sync/central_stairway_2way_sync.yaml b/packages/Lighting_2Way_Sync/central_stairway_2way_sync.yaml similarity index 100% rename from packages/lighting_2way_sync/central_stairway_2way_sync.yaml rename to packages/Lighting_2Way_Sync/central_stairway_2way_sync.yaml diff --git a/packages/lighting_2way_sync/garage_lights_2way_sync.yaml b/packages/Lighting_2Way_Sync/garage_lights_2way_sync.yaml similarity index 100% rename from packages/lighting_2way_sync/garage_lights_2way_sync.yaml rename to packages/Lighting_2Way_Sync/garage_lights_2way_sync.yaml diff --git a/packages/lighting_2way_sync/master_bedroom_main_lights_sync.yaml b/packages/Lighting_2Way_Sync/master_bedroom_main_lights_sync.yaml similarity index 100% rename from packages/lighting_2way_sync/master_bedroom_main_lights_sync.yaml rename to packages/Lighting_2Way_Sync/master_bedroom_main_lights_sync.yaml diff --git a/packages/openhasp_lounge1_autochange_pages.yaml b/packages/Openhasp_Displays/openhasp_lounge1_autochange_pages.yaml similarity index 100% rename from packages/openhasp_lounge1_autochange_pages.yaml rename to packages/Openhasp_Displays/openhasp_lounge1_autochange_pages.yaml diff --git a/packages/openhasp_lounge1_wallplate.yaml b/packages/Openhasp_Displays/openhasp_lounge1_wallplate.yaml similarity index 66% rename from packages/openhasp_lounge1_wallplate.yaml rename to packages/Openhasp_Displays/openhasp_lounge1_wallplate.yaml index 75c5ce6..6156e32 100644 --- a/packages/openhasp_lounge1_wallplate.yaml +++ b/packages/Openhasp_Displays/openhasp_lounge1_wallplate.yaml @@ -1,41 +1,51 @@ -############################################################################### +#:########################################################################################:# # PACKAGE: OpenHASP Thermostat / Gas Heater Control -# VERSION: v1.3 (2026-06-20) +# VERSION: v1.5 (2026-06-22) # NODE/TOPIC ROOT: hasp/openhasp_1/ # # PAGE 1: Gas heater / lounge environment -# - p1b1 = main temperature display -# - p1b2 = gas heater setpoint display +# - p1b1 = gas heater setpoint large display +# - p1b2 = actual lounge room temperature display # - p1b3 = gas heater setpoint down # - p1b4 = gas heater setpoint up # - p1b5 = gas heater status text # - p1b6 = gas heater operation / flame button # - p1b7 = humidity display # -# PAGE 2: Lounge climate control -# - p2b1 = main temperature display -# - p2b2 = climate.lounge setpoint display +# PAGE 2: Lounge climate heating control +# - p2b1 = climate.lounge setpoint large display +# - p2b2 = actual lounge room temperature display # - p2b3 = climate.lounge setpoint down 0.5°C # - p2b4 = climate.lounge setpoint up 0.5°C # - p2b5 = climate.lounge status text # - p2b6 = climate.lounge heat mode / sun button # - p2b7 = humidity display # -# PAGE 3: Blank placeholder +# PAGE 3: Lounge climate cooling control +# - p3b1 = climate.lounge setpoint large display +# - p3b2 = actual lounge room temperature display +# - p3b3 = climate.lounge setpoint down 0.5°C +# - p3b4 = climate.lounge setpoint up 0.5°C +# - p3b5 = climate.lounge status text +# - p3b6 = climate.lounge cool mode / snowflake button +# - p3b7 = humidity display # # NOTES: -# - p1b2/p1b7 and p2b2/p2b7 include their icons in the MQTT text payload so -# HA updates do not overwrite icons loaded from pages.jsonl. +# - The large top display is the setpoint for that device/page. +# - The middle-left temperature display is always the actual lounge room temp. # - p1b3/p1b4 are yellow. # - p1b6 is grey when OFF/BYPASS, yellow in AUTO, and red flame when firing. # - p2b3/p2b4 are pink. # - p2b6 turns climate.lounge ON and sets HVAC mode to heat. # - p2b6 is pink only when climate.lounge is in heat mode. -############################################################################### +# - p3b3/p3b4 are light blue. +# - p3b6 toggles climate.lounge OFF if already cooling; otherwise sets COOL. +# - p3b6 is light blue only when climate.lounge is in cool mode. +#:########################################################################################:# -############################################################################### +#:########################################################################################:# # MQTT ENTITIES -############################################################################### +#:########################################################################################:# mqtt: sensor: @@ -70,16 +80,16 @@ mqtt: {{ raw }} {% endif %} -############################################################################### +#:########################################################################################:# # AUTOMATIONS -############################################################################### +#:########################################################################################:# automation: # ------------------------------------------------------------------------- - # Lounge temperature display on page 1 and page 2 + # Actual lounge room temperature display on page 1, page 2, and page 3 # ------------------------------------------------------------------------- - id: hasp_lounge_temp_to_page_text - alias: "HASP: update lounge temperature displays" + alias: "HASP: update lounge actual temperature displays" mode: queued trigger: - platform: state @@ -104,10 +114,10 @@ automation: - service: script.hasp_publish_heater_status_text # ------------------------------------------------------------------------- - # Gas heater setpoint display on page 1 + # Gas heater setpoint large display on page 1 # ------------------------------------------------------------------------- - id: hasp_setpoint_to_btn_text - alias: "HASP: update gas heater setpoint (p1b2.text)" + alias: "HASP: update gas heater setpoint (p1b1.text)" mode: queued trigger: - platform: state @@ -116,7 +126,7 @@ automation: - service: script.hasp_publish_heater_setpoint # ------------------------------------------------------------------------- - # Humidity display on page 1 and page 2 + # Humidity display on page 1, page 2, and page 3 # ------------------------------------------------------------------------- - id: hasp_humidity_to_btn_text alias: "HASP: update humidity displays" @@ -191,10 +201,10 @@ automation: - service: script.hasp_apply_heat_button_state # ------------------------------------------------------------------------- - # Page 2 climate.lounge text and button state updates + # Page 2 and page 3 climate.lounge text and button state updates # ------------------------------------------------------------------------- - id: hasp_lounge_climate_to_page2 - alias: "HASP: update lounge climate page 2" + alias: "HASP: update lounge climate pages 2 and 3" mode: queued trigger: - platform: state @@ -202,6 +212,8 @@ automation: action: - service: script.hasp_push_page2_climate_texts - service: script.hasp_apply_climate_heat_button_state + - service: script.hasp_push_page3_climate_texts + - service: script.hasp_apply_climate_cool_button_state # ------------------------------------------------------------------------- # Page 2 climate.lounge setpoint down @@ -225,6 +237,7 @@ automation: {{ (current - 0.5) | round(1) }} - delay: "00:00:01" - service: script.hasp_push_page2_climate_texts + - service: script.hasp_push_page3_climate_texts # ------------------------------------------------------------------------- # Page 2 climate.lounge setpoint up @@ -248,6 +261,7 @@ automation: {{ (current + 0.5) | round(1) }} - delay: "00:00:01" - service: script.hasp_push_page2_climate_texts + - service: script.hasp_push_page3_climate_texts # ------------------------------------------------------------------------- # Page 2 climate.lounge heat mode button @@ -273,6 +287,97 @@ automation: - delay: "00:00:01" - service: script.hasp_push_page2_climate_texts - service: script.hasp_apply_climate_heat_button_state + - service: script.hasp_push_page3_climate_texts + - service: script.hasp_apply_climate_cool_button_state + + # ------------------------------------------------------------------------- + # Page 3 climate.lounge setpoint down + # ------------------------------------------------------------------------- + - id: hasp_page3_climate_down_press + alias: "HASP: p3b3 up -> lounge climate setpoint DOWN 0.5" + mode: queued + trigger: + - platform: mqtt + topic: "hasp/openhasp_1/state/p3b3" + condition: + - condition: template + value_template: "{{ trigger.payload_json.event | default('') == 'up' }}" + action: + - service: climate.set_temperature + target: + entity_id: climate.lounge + data: + temperature: > + {% set current = state_attr('climate.lounge', 'temperature') | float(20) %} + {{ (current - 0.5) | round(1) }} + - delay: "00:00:01" + - service: script.hasp_push_page2_climate_texts + - service: script.hasp_push_page3_climate_texts + + # ------------------------------------------------------------------------- + # Page 3 climate.lounge setpoint up + # ------------------------------------------------------------------------- + - id: hasp_page3_climate_up_press + alias: "HASP: p3b4 up -> lounge climate setpoint UP 0.5" + mode: queued + trigger: + - platform: mqtt + topic: "hasp/openhasp_1/state/p3b4" + condition: + - condition: template + value_template: "{{ trigger.payload_json.event | default('') == 'up' }}" + action: + - service: climate.set_temperature + target: + entity_id: climate.lounge + data: + temperature: > + {% set current = state_attr('climate.lounge', 'temperature') | float(20) %} + {{ (current + 0.5) | round(1) }} + - delay: "00:00:01" + - service: script.hasp_push_page2_climate_texts + - service: script.hasp_push_page3_climate_texts + + # ------------------------------------------------------------------------- + # Page 3 climate.lounge cool mode button + # - If already cooling, turn OFF + # - Otherwise turn ON and set COOL + # ------------------------------------------------------------------------- + - id: hasp_page3_climate_cool_press + alias: "HASP: p3b6 up -> lounge climate cool toggle" + mode: queued + trigger: + - platform: mqtt + topic: "hasp/openhasp_1/state/p3b6" + condition: + - condition: template + value_template: "{{ trigger.payload_json.event | default('') == 'up' }}" + action: + - choose: + - conditions: + - condition: state + entity_id: climate.lounge + state: "cool" + sequence: + - service: climate.turn_off + target: + entity_id: climate.lounge + + default: + - service: climate.turn_on + target: + entity_id: climate.lounge + - service: climate.set_hvac_mode + target: + entity_id: climate.lounge + data: + hvac_mode: cool + + - delay: "00:00:01" + - service: script.hasp_push_page2_climate_texts + - service: script.hasp_apply_climate_heat_button_state + - service: script.hasp_push_page3_climate_texts + - service: script.hasp_apply_climate_cool_button_state # ------------------------------------------------------------------------- # Push all screen state when Home Assistant starts @@ -300,9 +405,9 @@ automation: - delay: "00:00:05" - service: script.hasp_push_static_texts -############################################################################### +#:########################################################################################:# # SCRIPTS -############################################################################### +#:########################################################################################:# script: # ------------------------------------------------------------------------- @@ -315,6 +420,7 @@ script: - service: script.hasp_style_center_info_buttons - service: script.hasp_style_page1_control_buttons - service: script.hasp_style_page2_control_buttons + - service: script.hasp_style_page3_control_buttons - service: script.hasp_publish_lounge_temp_displays - service: script.hasp_publish_heater_status_text - service: script.hasp_publish_heater_setpoint @@ -322,61 +428,62 @@ script: - service: script.hasp_apply_heat_button_state - service: script.hasp_push_page2_climate_texts - service: script.hasp_apply_climate_heat_button_state + - service: script.hasp_push_page3_climate_texts + - service: script.hasp_apply_climate_cool_button_state # ------------------------------------------------------------------------- - # Publish lounge temperature to page 1 and page 2 + # Publish actual lounge room temperature to all middle-left displays + # p1b2 = actual room temperature + # p2b2 = actual room temperature + # p3b2 = actual room temperature # ------------------------------------------------------------------------- hasp_publish_lounge_temp_displays: - alias: "HASP: publish lounge temperature displays" - mode: queued - sequence: - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b1.text" - payload: >- - {% set raw = states('sensor.lounge_environment_lounge_environment_temperature') %} - {% set cleaned = raw - | replace('°C', '') - | replace('° C', '') - | replace('C', '') - | trim %} - {% if cleaned not in ['unknown', 'unavailable', '', 'none', 'None'] %} - {{ cleaned | float(0) | round(1) }}°C - {% else %} - --°C - {% endif %} - - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p2b1.text" - payload: >- - {% set raw = states('sensor.lounge_environment_lounge_environment_temperature') %} - {% set cleaned = raw - | replace('°C', '') - | replace('° C', '') - | replace('C', '') - | trim %} - {% if cleaned not in ['unknown', 'unavailable', '', 'none', 'None'] %} - {{ cleaned | float(0) | round(1) }}°C - {% else %} - --°C - {% endif %} - - # ------------------------------------------------------------------------- - # Publish gas heater setpoint to page 1 - # ------------------------------------------------------------------------- - hasp_publish_heater_setpoint: - alias: "HASP: publish gas heater setpoint" + alias: "HASP: publish lounge actual temperature displays" mode: queued sequence: - service: mqtt.publish data: topic: "hasp/openhasp_1/command/p1b2.text" payload: >- - {% set raw = states('sensor.rinnai_neo_gas_heater_setpoint_text') %} + {% set raw = states('sensor.lounge_environment_lounge_environment_temperature') %} {% set cleaned = raw | replace('°C', '') | replace('° C', '') + | replace('℃', '') + | replace('C', '') + | trim %} + {% if cleaned not in ['unknown', 'unavailable', '', 'none', 'None'] %} + {{ '\uE50F' }} {{ cleaned | float(0) | round(1) }}°C + {% else %} + {{ '\uE50F' }} --°C + {% endif %} + + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p2b2.text" + payload: >- + {% set raw = states('sensor.lounge_environment_lounge_environment_temperature') %} + {% set cleaned = raw + | replace('°C', '') + | replace('° C', '') + | replace('℃', '') + | replace('C', '') + | trim %} + {% if cleaned not in ['unknown', 'unavailable', '', 'none', 'None'] %} + {{ '\uE50F' }} {{ cleaned | float(0) | round(1) }}°C + {% else %} + {{ '\uE50F' }} --°C + {% endif %} + + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b2.text" + payload: >- + {% set raw = states('sensor.lounge_environment_lounge_environment_temperature') %} + {% set cleaned = raw + | replace('°C', '') + | replace('° C', '') + | replace('℃', '') | replace('C', '') | trim %} {% if cleaned not in ['unknown', 'unavailable', '', 'none', 'None'] %} @@ -386,7 +493,32 @@ script: {% endif %} # ------------------------------------------------------------------------- - # Publish humidity to page 1 and page 2 + # Publish gas heater setpoint to page 1 large top display + # p1b1 = gas heater setpoint + # ------------------------------------------------------------------------- + hasp_publish_heater_setpoint: + alias: "HASP: publish gas heater setpoint" + mode: queued + sequence: + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p1b1.text" + payload: >- + {% set raw = states('sensor.rinnai_neo_gas_heater_setpoint_text') %} + {% set cleaned = raw + | replace('°C', '') + | replace('° C', '') + | replace('℃', '') + | replace('C', '') + | trim %} + {% if cleaned not in ['unknown', 'unavailable', '', 'none', 'None'] %} + {{ '\uE238' }}{{ cleaned | float(0) | round(1) }}°C + {% else %} + {{ '\uE238' }}--°C + {% endif %} + + # ------------------------------------------------------------------------- + # Publish humidity to page 1, page 2, and page 3 # ------------------------------------------------------------------------- hasp_publish_humidity_displays: alias: "HASP: publish humidity displays" @@ -416,6 +548,18 @@ script: {{ '\uE58C' }} {{ cleaned }}% {% endif %} + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b7.text" + payload: >- + {% set raw = states('sensor.lounge_environment_humidity_mqtt') %} + {% set cleaned = raw | replace('%', '') | trim %} + {% if cleaned in ['unknown', 'unavailable', '', 'none', 'None'] %} + {{ '\uE58C' }} --% + {% else %} + {{ '\uE58C' }} {{ cleaned }}% + {% endif %} + # ------------------------------------------------------------------------- # Publish gas heater status text to page 1 bottom label # @@ -423,10 +567,7 @@ script: # - OFF -> Off # - BYPASS (Remote Only) -> Bypass (Remote Only) # - Auto but flame OFF -> Auto (Up to Temp) - # - Auto (Level 1) -> Auto + 1 flame icon - # - Auto (Level 2) -> Auto + 2 flame icons - # - Auto (Level 3) -> Auto + 3 flame icons - # - Auto (Level 4) -> Auto + 4 flame icons + # - Auto while firing -> Auto (Level 1), Auto (Level 2), etc. # ------------------------------------------------------------------------- hasp_publish_heater_status_text: alias: "HASP: publish heater status text" @@ -440,7 +581,6 @@ script: {% set op = states('switch.rinnai_neo_gas_heater_heater_operation') %} {% set flame_on = is_state('switch.rinnai_neo_gas_heater_rinnai_neo_gas_heater_activate_flame', 'on') %} {% set s = states('sensor.rinnai_neo_gas_heater_rinnai_neo_gas_heater_status_text') %} - {% set flame = '\uE238' %} {% if mode == 'OFF' %} Off @@ -449,17 +589,7 @@ script: {% elif op == 'on' and not flame_on %} Auto (Up to Temp) {% elif s not in ['unknown', 'unavailable', '', 'none', 'None'] %} - {% set matches = s | regex_findall('Level ([0-9]+)') %} - {% if matches | length > 0 %} - {% set level = matches[0] | int(0) %} - {% else %} - {% set level = 0 %} - {% endif %} - {% if level > 0 %} - Auto {{ flame * level }} - {% else %} - {{ s }} - {% endif %} + {{ s }} {% elif mode not in ['unknown', 'unavailable', '', 'none', 'None'] %} {{ mode }} {% else %} @@ -604,13 +734,13 @@ script: sequence: - service: mqtt.publish data: - topic: "hasp/openhasp_1/command/p2b2.text" + topic: "hasp/openhasp_1/command/p2b1.text" payload: >- {% set target = state_attr('climate.lounge', 'temperature') %} {% if target is not none %} - {{ '\uE50F' }} {{ target | float(0) | round(1) }}°C + {{ '\uE5A8' }}{{ target | float(0) | round(1) }}°C {% else %} - {{ '\uE50F' }} --°C + {{ '\uE5A8' }}--°C {% endif %} - service: mqtt.publish @@ -663,6 +793,74 @@ script: {{ mode_text }} {% endif %} + # ------------------------------------------------------------------------- + # Push page 3 climate.lounge setpoint and status text + # ------------------------------------------------------------------------- + hasp_push_page3_climate_texts: + alias: "HASP: push page 3 climate texts" + mode: queued + sequence: + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b1.text" + payload: >- + {% set target = state_attr('climate.lounge', 'temperature') %} + {% if target is not none %} + {{ '\uE717' }}{{ target | float(0) | round(1) }}°C + {% else %} + {{ '\uE717' }}--°C + {% endif %} + + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b5.text" + payload: >- + {% set mode = states('climate.lounge') %} + {% set mode_map = { + 'off': 'Off', + 'heat': 'Heating', + 'cool': 'Cooling', + 'heat_cool': 'Auto', + 'auto': 'Auto', + 'dry': 'Dry', + 'fan_only': 'Fan' + } %} + {% set mode_text = mode_map.get(mode, mode | replace('_', ' ') | title) %} + + {% set preset = state_attr('climate.lounge', 'preset_mode') | default('', true) | string | lower %} + {% set fan = state_attr('climate.lounge', 'fan_mode') | default('', true) | string | lower %} + {% set swing = state_attr('climate.lounge', 'swing_mode') | default('', true) | string | lower %} + + {% set eco_attr = state_attr('climate.lounge', 'eco') | default('', true) | string | lower %} + {% set powerful_attr = state_attr('climate.lounge', 'powerful') | default('', true) | string | lower %} + {% set quiet_attr = state_attr('climate.lounge', 'quiet') | default('', true) | string | lower %} + {% set nanoe_attr = state_attr('climate.lounge', 'nanoe') | default('', true) | string | lower %} + {% set nanoe_x_attr = state_attr('climate.lounge', 'nanoe_x') | default('', true) | string | lower %} + + {% set ns = namespace(extras=[]) %} + + {% if 'eco' in preset or eco_attr in ['on', 'true', '1', 'yes'] %} + {% set ns.extras = ns.extras + ['Eco'] %} + {% endif %} + + {% if 'powerful' in preset or 'boost' in preset or powerful_attr in ['on', 'true', '1', 'yes'] %} + {% set ns.extras = ns.extras + ['Powerful'] %} + {% endif %} + + {% if 'quiet' in preset or 'quiet' in fan or quiet_attr in ['on', 'true', '1', 'yes'] %} + {% set ns.extras = ns.extras + ['Quiet'] %} + {% endif %} + + {% if 'nanoe' in preset or 'nanoe' in swing or nanoe_attr in ['on', 'true', '1', 'yes'] or nanoe_x_attr in ['on', 'true', '1', 'yes'] %} + {% set ns.extras = ns.extras + ['nanoe-x'] %} + {% endif %} + + {% if ns.extras | length > 0 %} + {{ mode_text }} ({{ ns.extras | join(', ') }}) + {% else %} + {{ mode_text }} + {% endif %} + # ------------------------------------------------------------------------- # Apply page 2 climate.lounge heat button state # ------------------------------------------------------------------------- @@ -673,7 +871,7 @@ script: - service: mqtt.publish data: topic: "hasp/openhasp_1/command/p2b6.text" - payload: "\uE599" + payload: "\uE5A8" - choose: # climate.lounge is ON and in HEAT mode -> pink sun @@ -718,7 +916,61 @@ script: payload: "#2F3240" # ------------------------------------------------------------------------- - # Style page 1 and page 2 setpoint/humidity display buttons as black panels + # Apply page 3 climate.lounge cool button state + # ------------------------------------------------------------------------- + hasp_apply_climate_cool_button_state: + alias: "HASP: apply lounge climate cool button state" + mode: queued + sequence: + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b6.text" + payload: "\uE717" + + - choose: + # climate.lounge is ON and in COOL mode -> light blue snowflake + - conditions: + - condition: state + entity_id: climate.lounge + state: "cool" + sequence: + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b6.text_color" + payload: "#FFFFFF" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b6.bg_color" + payload: "#0288D1" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b6.bg_grad_color" + payload: "#4FC3F7" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b6.border_color" + payload: "#0288D1" + + default: + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b6.text_color" + payload: "#C0C0C0" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b6.bg_color" + payload: "#2F3240" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b6.bg_grad_color" + payload: "#2F3240" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b6.border_color" + payload: "#2F3240" + + # ------------------------------------------------------------------------- + # Style page 1, page 2, and page 3 actual temp/humidity displays # ------------------------------------------------------------------------- hasp_style_center_info_buttons: alias: "HASP: style centre info buttons" @@ -792,6 +1044,40 @@ script: topic: "hasp/openhasp_1/command/p2b7.border_color" payload: "#000000" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b2.text_color" + payload: "#FFFFFF" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b2.bg_color" + payload: "#000000" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b2.bg_grad_color" + payload: "#000000" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b2.border_color" + payload: "#000000" + + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b7.text_color" + payload: "#FFFFFF" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b7.bg_color" + payload: "#000000" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b7.bg_grad_color" + payload: "#000000" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b7.border_color" + payload: "#000000" + # ------------------------------------------------------------------------- # Style page 1 gas heater up/down buttons yellow # p1b6 is state-driven by hasp_apply_heat_button_state. @@ -875,3 +1161,45 @@ script: data: topic: "hasp/openhasp_1/command/p2b4.border_color" payload: "#C2185B" + + # ------------------------------------------------------------------------- + # Style page 3 climate up/down buttons light blue + # p3b6 is state-driven by hasp_apply_climate_cool_button_state. + # ------------------------------------------------------------------------- + hasp_style_page3_control_buttons: + alias: "HASP: style page 3 control buttons" + mode: queued + sequence: + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b3.text_color" + payload: "#FFFFFF" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b3.bg_color" + payload: "#0288D1" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b3.bg_grad_color" + payload: "#4FC3F7" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b3.border_color" + payload: "#0288D1" + + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b4.text_color" + payload: "#FFFFFF" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b4.bg_color" + payload: "#0288D1" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b4.bg_grad_color" + payload: "#4FC3F7" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p3b4.border_color" + payload: "#0288D1" diff --git a/packages/openhasp_lounge1_wallplate_backlight.yaml b/packages/Openhasp_Displays/openhasp_lounge1_wallplate_backlight.yaml similarity index 100% rename from packages/openhasp_lounge1_wallplate_backlight.yaml rename to packages/Openhasp_Displays/openhasp_lounge1_wallplate_backlight.yaml