diff --git a/.cache/brands/integrations/byd_vehicle/dark_icon.png b/.cache/brands/integrations/byd_vehicle/dark_icon.png index a31c107..a86a688 100644 Binary files a/.cache/brands/integrations/byd_vehicle/dark_icon.png and b/.cache/brands/integrations/byd_vehicle/dark_icon.png differ diff --git a/.ha_run.lock b/.ha_run.lock index 569b7b6..352bf00 100644 --- a/.ha_run.lock +++ b/.ha_run.lock @@ -1 +1 @@ -{"pid": 70, "version": 1, "ha_version": "2026.7.4", "start_ts": 1785317114.2667625} \ No newline at end of file +{"pid": 70, "version": 1, "ha_version": "2026.8.1", "start_ts": 1786755431.7882934} \ No newline at end of file diff --git a/.opencode_agents_md.sha256 b/.opencode_agents_md.sha256 new file mode 100644 index 0000000..f29d14a --- /dev/null +++ b/.opencode_agents_md.sha256 @@ -0,0 +1 @@ +a5cfb18d66d8671cf91955eaea5ad94dcf80eaef22aad2e8374ac1223680d6aa diff --git a/automations.yaml b/automations.yaml new file mode 100644 index 0000000..e69de29 diff --git a/configuration.yaml b/configuration.yaml index e2f34e2..09f7676 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -11,17 +11,20 @@ homeassistant: customize: !include customize.yaml packages: !include_dir_named packages -template: !include_dir_merge_list templates -group: !include_dir_merge_named group/ -automation: !include_dir_merge_list automations/ -script: !include_dir_merge_named scripts/ +# Unused directory includes retained for future YAML-based configuration. +# template: !include_dir_merge_list templates +# group: !include_dir_merge_named group/ +# automation: !include_dir_merge_list automations/ +# script: !include_dir_merge_named scripts/ scene: !include scenes.yaml -sensor: !include_dir_merge_list sensor/ +# sensor: !include_dir_merge_list sensor/ -conversation: +# Loaded by default_config. +# conversation: -logger: - default: info +# Home Assistant's default logging level is info. +# logger: +# default: info zha: enable_quirks: true @@ -35,7 +38,8 @@ zha: ota: otau_directory: /config/zigpy_ota ikea_provider: true - ledvance_provider: true salus_provider: true - inovelli_provider: true - thirdreality_provider: true + # Enabled by default. + # ledvance_provider: true + # inovelli_provider: true + # thirdreality_provider: true diff --git a/esphome/.device-builder.json b/esphome/.device-builder.json index 9423905..203cfef 100644 --- a/esphome/.device-builder.json +++ b/esphome/.device-builder.json @@ -317,13 +317,13 @@ }, "_firmware_jobs": [ { - "job_id": "706c4470b8b9", + "job_id": "a473e1ff91a1", "configuration": "esp-bedside-panel.yaml", "job_type": "upload", "status": "completed", - "created_at": "2026-07-28T06:21:22.025321+00:00", - "started_at": "2026-07-28T06:23:48.530789+00:00", - "completed_at": "2026-07-28T06:24:00.005953+00:00", + "created_at": "2026-08-15T02:02:52.316971+00:00", + "started_at": "2026-08-15T02:14:19.408959+00:00", + "completed_at": "2026-08-15T02:14:50.774825+00:00", "compile_started_at": null, "compile_ended_at": null, "exit_code": 0, @@ -332,7 +332,7 @@ "flash_bootloader": false, "is_deferred_install": false, "new_name": "", - "depends_on": "cb56c852095e", + "depends_on": "2fbca35e3c95", "dependency_released": true, "progress": 100, "remote_peer": "", @@ -348,15 +348,15 @@ "failure_reason": "" }, { - "job_id": "cb56c852095e", + "job_id": "2fbca35e3c95", "configuration": "esp-bedside-panel.yaml", "job_type": "compile", "status": "completed", - "created_at": "2026-07-28T06:21:22.025281+00:00", - "started_at": "2026-07-28T06:22:22.419275+00:00", - "completed_at": "2026-07-28T06:23:48.527793+00:00", - "compile_started_at": "2026-07-28T06:22:35.704539+00:00", - "compile_ended_at": "2026-07-28T06:23:47.137682+00:00", + "created_at": "2026-08-15T02:02:52.316913+00:00", + "started_at": "2026-08-15T02:02:52.317974+00:00", + "completed_at": "2026-08-15T02:14:19.407163+00:00", + "compile_started_at": "2026-08-15T02:05:17.332991+00:00", + "compile_ended_at": "2026-08-15T02:14:07.196719+00:00", "exit_code": 0, "error": null, "port": "", @@ -365,27 +365,27 @@ "new_name": "", "depends_on": "", "dependency_released": false, - "progress": 79, + "progress": 99, "remote_peer": "", "remote_job_id": "", "remote_peer_label": "", "device_name": "", "device_friendly_name": "", - "source": "local", - "source_pin_sha256": "", - "source_label": "", - "source_esphome_version": "", + "source": "remote", + "source_pin_sha256": "11faf1169a008873b779b7223309fd7d8def200d1548f7b62955f71a67796a12", + "source_label": "Panda ESPHome Builder", + "source_esphome_version": "2026.7.2", "target_esphome_version": "", "failure_reason": "" }, { - "job_id": "68c4dce9b4d3", - "configuration": "esp-3dprinterpow.yaml", + "job_id": "05a1d03fd4c9", + "configuration": "esp-centralstairs-top.yaml", "job_type": "upload", "status": "completed", - "created_at": "2026-07-28T06:16:53.772447+00:00", - "started_at": "2026-07-28T06:30:52.000444+00:00", - "completed_at": "2026-07-28T06:31:09.366305+00:00", + "created_at": "2026-08-12T06:11:24.834926+00:00", + "started_at": "2026-08-12T06:23:25.526769+00:00", + "completed_at": "2026-08-12T06:23:34.982995+00:00", "compile_started_at": null, "compile_ended_at": null, "exit_code": 0, @@ -394,7 +394,7 @@ "flash_bootloader": false, "is_deferred_install": false, "new_name": "", - "depends_on": "8177dc9179ac", + "depends_on": "eea70cae5b4b", "dependency_released": true, "progress": 100, "remote_peer": "", @@ -410,15 +410,480 @@ "failure_reason": "" }, { - "job_id": "8177dc9179ac", + "job_id": "eea70cae5b4b", + "configuration": "esp-centralstairs-top.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-08-12T06:11:24.834880+00:00", + "started_at": "2026-08-12T06:21:21.949657+00:00", + "completed_at": "2026-08-12T06:23:25.524993+00:00", + "compile_started_at": "2026-08-12T06:21:24.590651+00:00", + "compile_ended_at": "2026-08-12T06:23:24.055776+00:00", + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "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": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "c4a2e6f3bb36", + "configuration": "esp-centralstairs-bottom.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-08-12T06:11:22.149090+00:00", + "started_at": "2026-08-12T06:21:21.943720+00:00", + "completed_at": "2026-08-12T06:21:28.496577+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "730061c59987", + "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": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "730061c59987", + "configuration": "esp-centralstairs-bottom.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-08-12T06:11:22.149049+00:00", + "started_at": "2026-08-12T06:19:18.498381+00:00", + "completed_at": "2026-08-12T06:21:21.941025+00:00", + "compile_started_at": "2026-08-12T06:19:21.073513+00:00", + "compile_ended_at": "2026-08-12T06:21:20.478540+00:00", + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "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": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "a736d9dcc153", + "configuration": "esp-bedrm2lights.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-08-12T06:11:13.845297+00:00", + "started_at": "2026-08-12T06:19:18.488204+00:00", + "completed_at": "2026-08-12T06:19:28.042254+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "4d5614fb7667", + "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": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "4d5614fb7667", + "configuration": "esp-bedrm2lights.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-08-12T06:11:13.845258+00:00", + "started_at": "2026-08-12T06:17:14.541724+00:00", + "completed_at": "2026-08-12T06:19:18.485658+00:00", + "compile_started_at": "2026-08-12T06:17:17.164050+00:00", + "compile_ended_at": "2026-08-12T06:19:16.998559+00:00", + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "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": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "ebf22c4ec1e4", + "configuration": "esp-bedrm2fanswitch.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-08-12T06:11:07.484185+00:00", + "started_at": "2026-08-12T06:17:14.532716+00:00", + "completed_at": "2026-08-12T06:17:24.415493+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "e670a4e9aaf3", + "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": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "e670a4e9aaf3", + "configuration": "esp-bedrm2fanswitch.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-08-12T06:11:07.484146+00:00", + "started_at": "2026-08-12T06:15:10.126960+00:00", + "completed_at": "2026-08-12T06:17:14.530452+00:00", + "compile_started_at": "2026-08-12T06:15:12.830985+00:00", + "compile_ended_at": "2026-08-12T06:17:13.061744+00:00", + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "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": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "f404874c5865", + "configuration": "esp-bedrm1airfilter.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-08-12T06:10:58.768261+00:00", + "started_at": "2026-08-12T06:15:10.119533+00:00", + "completed_at": "2026-08-12T06:15:18.715219+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "0b236905f726", + "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": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "0b236905f726", + "configuration": "esp-bedrm1airfilter.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-08-12T06:10:58.768219+00:00", + "started_at": "2026-08-12T06:13:02.290161+00:00", + "completed_at": "2026-08-12T06:15:10.117093+00:00", + "compile_started_at": "2026-08-12T06:13:04.764246+00:00", + "compile_ended_at": "2026-08-12T06:15:08.655342+00:00", + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "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": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "43ff758be814", + "configuration": "esp-arcademachine1.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-08-12T06:10:53.375603+00:00", + "started_at": "2026-08-12T06:13:02.233373+00:00", + "completed_at": "2026-08-12T06:13:12.365411+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "af52dcc80c5c", + "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": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "af52dcc80c5c", + "configuration": "esp-arcademachine1.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-08-12T06:10:53.375529+00:00", + "started_at": "2026-08-12T06:10:53.388288+00:00", + "completed_at": "2026-08-12T06:13:02.231522+00:00", + "compile_started_at": "2026-08-12T06:10:56.525770+00:00", + "compile_ended_at": "2026-08-12T06:13:00.758596+00:00", + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "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": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "7b199c64b0e9", + "configuration": "esp-3dprinterpow.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-08-12T06:10:41.162002+00:00", + "started_at": "2026-08-12T06:23:26.985687+00:00", + "completed_at": "2026-08-12T06:23:40.457621+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "09a303016889", + "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": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "09a303016889", "configuration": "esp-3dprinterpow.yaml", "job_type": "compile", "status": "completed", - "created_at": "2026-07-28T06:16:53.772409+00:00", - "started_at": "2026-07-28T06:16:53.775344+00:00", - "completed_at": "2026-07-28T06:30:51.997065+00:00", - "compile_started_at": "2026-07-28T06:18:04.677684+00:00", - "compile_ended_at": "2026-07-28T06:30:46.680217+00:00", + "created_at": "2026-08-12T06:10:41.161848+00:00", + "started_at": "2026-08-12T06:10:41.163624+00:00", + "completed_at": "2026-08-12T06:23:26.981545+00:00", + "compile_started_at": "2026-08-12T06:12:01.760774+00:00", + "compile_ended_at": "2026-08-12T06:23:23.398615+00:00", + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": null, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "remote", + "source_pin_sha256": "11faf1169a008873b779b7223309fd7d8def200d1548f7b62955f71a67796a12", + "source_label": "Panda ESPHome Builder", + "source_esphome_version": "2026.7.2", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "504d1e6ab453", + "configuration": "esp-reterminal-e1001.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-08-02T04:40:59.135722+00:00", + "started_at": "2026-08-02T04:40:59.136484+00:00", + "completed_at": "2026-08-02T04:41:23.522874+00:00", + "compile_started_at": "2026-08-02T04:41:11.876316+00:00", + "compile_ended_at": "2026-08-02T04:41:14.205320+00:00", + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": 93, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "remote", + "source_pin_sha256": "11faf1169a008873b779b7223309fd7d8def200d1548f7b62955f71a67796a12", + "source_label": "Panda ESPHome Builder", + "source_esphome_version": "2026.7.2", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "5632451d7d90", + "configuration": "esp-reterminal-e1001.yaml", + "job_type": "upload", + "status": "failed", + "created_at": "2026-08-02T04:29:21.758006+00:00", + "started_at": "2026-08-02T04:40:32.968152+00:00", + "completed_at": "2026-08-02T04:40:36.606158+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 1, + "error": null, + "port": "OTA", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "727bf11ecacb", + "dependency_released": true, + "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": "", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "5fd0b20d888b", + "configuration": "esp-spare1-athomv1.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-07-29T12:23:58.391860+00:00", + "started_at": "2026-07-29T12:23:58.392632+00:00", + "completed_at": "2026-07-29T12:24:16.337866+00:00", + "compile_started_at": "2026-07-29T12:24:05.586368+00:00", + "compile_ended_at": "2026-07-29T12:24:15.246869+00:00", "exit_code": 0, "error": null, "port": "", @@ -2548,68 +3013,6 @@ "target_esphome_version": "", "failure_reason": "" }, - { - "job_id": "4a1e0e0a3d89", - "configuration": "esp-bedrm1airfilter.yaml", - "job_type": "upload", - "status": "completed", - "created_at": "2026-07-25T11:52:36.073257+00:00", - "started_at": "2026-07-25T12:14:35.982225+00:00", - "completed_at": "2026-07-25T12:14:46.432606+00:00", - "compile_started_at": null, - "compile_ended_at": null, - "exit_code": 0, - "error": null, - "port": "OTA", - "flash_bootloader": false, - "is_deferred_install": false, - "new_name": "", - "depends_on": "be00645ee3bc", - "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": "", - "target_esphome_version": "", - "failure_reason": "" - }, - { - "job_id": "be00645ee3bc", - "configuration": "esp-bedrm1airfilter.yaml", - "job_type": "compile", - "status": "completed", - "created_at": "2026-07-25T11:52:36.073214+00:00", - "started_at": "2026-07-25T12:11:46.929221+00:00", - "completed_at": "2026-07-25T12:14:35.980171+00:00", - "compile_started_at": "2026-07-25T12:11:52.304144+00:00", - "compile_ended_at": "2026-07-25T12:14:34.430826+00:00", - "exit_code": 0, - "error": null, - "port": "", - "flash_bootloader": false, - "is_deferred_install": false, - "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": "", - "target_esphome_version": "", - "failure_reason": "" - }, { "job_id": "a97cec78925a", "configuration": "esp-laundrylights.yaml", @@ -2672,68 +3075,6 @@ "target_esphome_version": "", "failure_reason": "" }, - { - "job_id": "695629d609b0", - "configuration": "esp-arcademachine1.yaml", - "job_type": "upload", - "status": "completed", - "created_at": "2026-07-25T11:41:29.623671+00:00", - "started_at": "2026-07-25T12:09:05.834004+00:00", - "completed_at": "2026-07-25T12:09:16.863344+00:00", - "compile_started_at": null, - "compile_ended_at": null, - "exit_code": 0, - "error": null, - "port": "OTA", - "flash_bootloader": false, - "is_deferred_install": false, - "new_name": "", - "depends_on": "16e51b9b69b9", - "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": "", - "target_esphome_version": "", - "failure_reason": "" - }, - { - "job_id": "16e51b9b69b9", - "configuration": "esp-arcademachine1.yaml", - "job_type": "compile", - "status": "completed", - "created_at": "2026-07-25T11:41:29.623627+00:00", - "started_at": "2026-07-25T12:06:08.265488+00:00", - "completed_at": "2026-07-25T12:09:05.831853+00:00", - "compile_started_at": "2026-07-25T12:06:14.136134+00:00", - "compile_ended_at": "2026-07-25T12:09:04.258675+00:00", - "exit_code": 0, - "error": null, - "port": "", - "flash_bootloader": false, - "is_deferred_install": false, - "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": "", - "target_esphome_version": "", - "failure_reason": "" - }, { "job_id": "92fa14b996f8", "configuration": "esp-garagelights.yaml", @@ -4098,68 +4439,6 @@ "target_esphome_version": "", "failure_reason": "" }, - { - "job_id": "2ef1bc33032b", - "configuration": "esp-bedrm2lights.yaml", - "job_type": "upload", - "status": "completed", - "created_at": "2026-07-25T10:25:31.542986+00:00", - "started_at": "2026-07-25T10:43:06.702642+00:00", - "completed_at": "2026-07-25T10:43:17.410697+00:00", - "compile_started_at": null, - "compile_ended_at": null, - "exit_code": 0, - "error": null, - "port": "OTA", - "flash_bootloader": false, - "is_deferred_install": false, - "new_name": "", - "depends_on": "e25e4bd9600e", - "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": "", - "target_esphome_version": "", - "failure_reason": "" - }, - { - "job_id": "e25e4bd9600e", - "configuration": "esp-bedrm2lights.yaml", - "job_type": "compile", - "status": "completed", - "created_at": "2026-07-25T10:25:31.542943+00:00", - "started_at": "2026-07-25T10:40:18.306987+00:00", - "completed_at": "2026-07-25T10:43:06.700119+00:00", - "compile_started_at": "2026-07-25T10:40:23.916825+00:00", - "compile_ended_at": "2026-07-25T10:43:05.075052+00:00", - "exit_code": 0, - "error": null, - "port": "", - "flash_bootloader": false, - "is_deferred_install": false, - "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": "", - "target_esphome_version": "", - "failure_reason": "" - }, { "job_id": "4433db694627", "configuration": "esp-downstbathswitch.yaml", @@ -4284,192 +4563,6 @@ "target_esphome_version": "", "failure_reason": "" }, - { - "job_id": "ddeee9c081ba", - "configuration": "esp-centralstairs-top.yaml", - "job_type": "upload", - "status": "completed", - "created_at": "2026-07-25T10:19:50.198091+00:00", - "started_at": "2026-07-25T10:31:51.368161+00:00", - "completed_at": "2026-07-25T10:32:01.559333+00:00", - "compile_started_at": null, - "compile_ended_at": null, - "exit_code": 0, - "error": null, - "port": "OTA", - "flash_bootloader": false, - "is_deferred_install": false, - "new_name": "", - "depends_on": "a7a110fce24f", - "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": "", - "target_esphome_version": "", - "failure_reason": "" - }, - { - "job_id": "a7a110fce24f", - "configuration": "esp-centralstairs-top.yaml", - "job_type": "compile", - "status": "completed", - "created_at": "2026-07-25T10:19:50.198043+00:00", - "started_at": "2026-07-25T10:29:02.898343+00:00", - "completed_at": "2026-07-25T10:31:51.365311+00:00", - "compile_started_at": "2026-07-25T10:29:08.900409+00:00", - "compile_ended_at": "2026-07-25T10:31:49.694162+00:00", - "exit_code": 0, - "error": null, - "port": "", - "flash_bootloader": false, - "is_deferred_install": false, - "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": "", - "target_esphome_version": "", - "failure_reason": "" - }, - { - "job_id": "95d955c189b0", - "configuration": "esp-centralstairs-bottom.yaml", - "job_type": "upload", - "status": "completed", - "created_at": "2026-07-25T10:19:42.704512+00:00", - "started_at": "2026-07-25T10:29:02.884705+00:00", - "completed_at": "2026-07-25T10:29:13.623409+00:00", - "compile_started_at": null, - "compile_ended_at": null, - "exit_code": 0, - "error": null, - "port": "OTA", - "flash_bootloader": false, - "is_deferred_install": false, - "new_name": "", - "depends_on": "8f882ee50eae", - "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": "", - "target_esphome_version": "", - "failure_reason": "" - }, - { - "job_id": "8f882ee50eae", - "configuration": "esp-centralstairs-bottom.yaml", - "job_type": "compile", - "status": "completed", - "created_at": "2026-07-25T10:19:42.704473+00:00", - "started_at": "2026-07-25T10:26:12.507379+00:00", - "completed_at": "2026-07-25T10:29:02.882708+00:00", - "compile_started_at": "2026-07-25T10:26:18.530932+00:00", - "compile_ended_at": "2026-07-25T10:29:01.132112+00:00", - "exit_code": 0, - "error": null, - "port": "", - "flash_bootloader": false, - "is_deferred_install": false, - "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": "", - "target_esphome_version": "", - "failure_reason": "" - }, - { - "job_id": "331e47f2f399", - "configuration": "esp-bedrm2fanswitch.yaml", - "job_type": "upload", - "status": "completed", - "created_at": "2026-07-25T10:19:28.261620+00:00", - "started_at": "2026-07-25T10:26:12.499990+00:00", - "completed_at": "2026-07-25T10:26:25.940168+00:00", - "compile_started_at": null, - "compile_ended_at": null, - "exit_code": 0, - "error": null, - "port": "OTA", - "flash_bootloader": false, - "is_deferred_install": false, - "new_name": "", - "depends_on": "f8d1777a586e", - "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": "", - "target_esphome_version": "", - "failure_reason": "" - }, - { - "job_id": "f8d1777a586e", - "configuration": "esp-bedrm2fanswitch.yaml", - "job_type": "compile", - "status": "completed", - "created_at": "2026-07-25T10:19:28.261574+00:00", - "started_at": "2026-07-25T10:23:06.376588+00:00", - "completed_at": "2026-07-25T10:26:12.497240+00:00", - "compile_started_at": "2026-07-25T10:23:12.193003+00:00", - "compile_ended_at": "2026-07-25T10:26:10.655295+00:00", - "exit_code": 0, - "error": null, - "port": "", - "flash_bootloader": false, - "is_deferred_install": false, - "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": "", - "target_esphome_version": "", - "failure_reason": "" - }, { "job_id": "5426487d5ac9", "configuration": "esp-bedrm2ceilingfan.yaml", @@ -4966,68 +5059,6 @@ "target_esphome_version": "", "failure_reason": "" }, - { - "job_id": "73f5241b3ffb", - "configuration": "esp-reterminal-e1001.yaml", - "job_type": "upload", - "status": "completed", - "created_at": "2026-07-20T05:17:58.469210+00:00", - "started_at": "2026-07-20T05:22:52.972291+00:00", - "completed_at": "2026-07-20T05:23:13.584838+00:00", - "compile_started_at": null, - "compile_ended_at": null, - "exit_code": 0, - "error": null, - "port": "OTA", - "flash_bootloader": false, - "is_deferred_install": false, - "new_name": "", - "depends_on": "8f5082f38895", - "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": "", - "target_esphome_version": "", - "failure_reason": "" - }, - { - "job_id": "8f5082f38895", - "configuration": "esp-reterminal-e1001.yaml", - "job_type": "compile", - "status": "completed", - "created_at": "2026-07-20T05:17:58.469169+00:00", - "started_at": "2026-07-20T05:17:58.470685+00:00", - "completed_at": "2026-07-20T05:22:52.968819+00:00", - "compile_started_at": "2026-07-20T05:18:19.282003+00:00", - "compile_ended_at": null, - "exit_code": 0, - "error": null, - "port": "", - "flash_bootloader": false, - "is_deferred_install": false, - "new_name": "", - "depends_on": "", - "dependency_released": false, - "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": "", - "target_esphome_version": "", - "failure_reason": "" - }, { "job_id": "c84bad09993e", "configuration": "esp-espnow_test_transmitter1.yaml", @@ -7756,6 +7787,68 @@ "target_esphome_version": "", "failure_reason": "" }, + { + "job_id": "04048b2f1ea3", + "configuration": "esp-reterminal-e1001.yaml", + "job_type": "clean", + "status": "completed", + "created_at": "2026-07-30T22:58:24.613142+00:00", + "started_at": "2026-07-30T22:58:26.750624+00:00", + "completed_at": "2026-07-30T22:58:35.858447+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": null, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "remote", + "source_pin_sha256": "11faf1169a008873b779b7223309fd7d8def200d1548f7b62955f71a67796a12", + "source_label": "Panda ESPHome Builder", + "source_esphome_version": "2026.7.2", + "target_esphome_version": "", + "failure_reason": "" + }, + { + "job_id": "df9f9c37129f", + "configuration": "esp-reterminal-e1001.yaml", + "job_type": "clean", + "status": "completed", + "created_at": "2026-07-30T22:58:24.596150+00:00", + "started_at": "2026-07-30T22:58:24.597383+00:00", + "completed_at": "2026-07-30T22:58:26.735105+00:00", + "compile_started_at": null, + "compile_ended_at": null, + "exit_code": 0, + "error": null, + "port": "", + "flash_bootloader": false, + "is_deferred_install": false, + "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": "", + "target_esphome_version": "", + "failure_reason": "" + }, { "job_id": "8bab31ed17f3", "configuration": "esp-masterbdrmwest.yaml", @@ -7848,68 +7941,6 @@ "source_esphome_version": "", "target_esphome_version": "", "failure_reason": "" - }, - { - "job_id": "3143f8f29109", - "configuration": "esp-officeusbhubpower.yaml", - "job_type": "clean", - "status": "completed", - "created_at": "2026-07-26T08:32:07.941160+00:00", - "started_at": "2026-07-26T08:33:15.068288+00:00", - "completed_at": "2026-07-26T08:33:15.955828+00:00", - "compile_started_at": null, - "compile_ended_at": null, - "exit_code": 0, - "error": null, - "port": "", - "flash_bootloader": false, - "is_deferred_install": false, - "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": "", - "target_esphome_version": "", - "failure_reason": "" - }, - { - "job_id": "ee2b5195d050", - "configuration": "esp-loungewestswitch.yaml", - "job_type": "clean", - "status": "cancelled", - "created_at": "2026-07-26T08:31:11.168499+00:00", - "started_at": null, - "completed_at": "2026-07-26T08:33:09.163540+00:00", - "compile_started_at": null, - "compile_ended_at": null, - "exit_code": null, - "error": null, - "port": "", - "flash_bootloader": false, - "is_deferred_install": false, - "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": "", - "target_esphome_version": "", - "failure_reason": "" } ], "esp-nspanel2lounge copy.yaml": { diff --git a/mqtt_dump.txt b/mqtt_dump.txt new file mode 100644 index 0000000..a188b6a --- /dev/null +++ b/mqtt_dump.txt @@ -0,0 +1,10 @@ +viewroad-status/jobs_tracker/dog_feeding_morning,{ "job_id": "dog_feeding_morning", "job_name": "Dog Feeding Morning", "repeat": "daily", "due_time": "06:00", "due_weekday": 0, "due_day": 1, "state": "Missed", "complete": false, "last_completed_ts": 1786144419.148793, "last_completed_time": "11:13", "last_completed_human": "Yesterday @ 11:13", "previous_period_start_ts": 1786104000.0, "previous_due_ts": 1786125600.0, "previous_completed": true, "previous_missed": false, "previous_missed_active": false, "missed": true, "completion_window_start": "00:00", "completion_window_end": "11:59", "has_completion_window": true, "completion_window_missed": true, "current_completion_window_start_ts": 1786190400.0, "current_completion_window_end_ts": 1786233599.0, "completed_for_period_ts": 0.0, "completed_for_due_ts": 0.0, "current_period_start_ts": 1786190400.0, "next_period_start_ts": 1786276800.0, "current_due_ts": 1786212000.0, "next_due_ts": 1786298400.0, "display_due_ts": 1786212000.0, "next_due_human": "Today @ 06:00", "due_started": true, "overdue_started": true, "minutes_since_required": 603, "hours_since_required": 10, "overdue_after_minutes": 120, "overdue_minutes": 0, "overdue_hours": 0, "hours_since_completed": 29, "last_updated_local": "2026-08-09 16:03:00", "mqtt_topic": "viewroad-status/jobs_tracker/dog_feeding_morning"} +viewroad-status/jobs_tracker/dog_feeding_evening,{ "job_id": "dog_feeding_evening", "job_name": "Dog Feeding Evening", "repeat": "daily", "due_time": "17:00", "due_weekday": 0, "due_day": 1, "state": "Complete", "complete": true, "last_completed_ts": 1786241366.470674, "last_completed_time": "14:09", "last_completed_human": "14:09", "previous_period_start_ts": 1786104000.0, "previous_due_ts": 1786165200.0, "previous_completed": false, "previous_missed": true, "previous_missed_active": false, "missed": false, "completion_window_start": "12:00", "completion_window_end": "23:59", "has_completion_window": true, "completion_window_missed": false, "current_completion_window_start_ts": 1786233600.0, "current_completion_window_end_ts": 1786276799.0, "completed_for_period_ts": 1786190400.0, "completed_for_due_ts": 1786190400.0, "current_period_start_ts": 1786190400.0, "next_period_start_ts": 1786276800.0, "current_due_ts": 1786251600.0, "next_due_ts": 1786338000.0, "display_due_ts": 1786338000.0, "next_due_human": "Tomorrow @ 17:00", "due_started": false, "overdue_started": false, "minutes_since_required": 0, "hours_since_required": 0, "overdue_after_minutes": 120, "overdue_minutes": 0, "overdue_hours": 0, "hours_since_completed": 2, "last_updated_local": "2026-08-09 16:03:00", "mqtt_topic": "viewroad-status/jobs_tracker/dog_feeding_evening"} +viewroad-status/jobs_tracker/dog_depooping,{ "job_id": "dog_depooping", "job_name": "Dog De-pooping", "repeat": "weekly", "due_time": "08:00", "due_weekday": 4, "due_day": 1, "state": "Complete", "complete": true, "last_completed_ts": 1786144418.015302, "last_completed_time": "11:13", "last_completed_human": "Yesterday @ 11:13", "previous_period_start_ts": 1785067200.0, "previous_due_ts": 1785441600.0, "previous_completed": false, "previous_missed": true, "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.0, "current_completion_window_end_ts": 0.0, "completed_for_period_ts": 1785672000.0, "completed_for_due_ts": 1785672000.0, "current_period_start_ts": 1785672000.0, "next_period_start_ts": 1786276800.0, "current_due_ts": 1786046400.0, "next_due_ts": 1786651200.0, "display_due_ts": 1786651200.0, "next_due_human": "Next Friday @ 08:00", "due_started": true, "overdue_started": true, "minutes_since_required": 3363, "hours_since_required": 56, "overdue_after_minutes": 2880, "overdue_minutes": 0, "overdue_hours": 0, "hours_since_completed": 29, "last_updated_local": "2026-08-09 16:03:00", "mqtt_topic": "viewroad-status/jobs_tracker/dog_depooping"} +viewroad-status/jobs_tracker/dishwasher_unpacked,{"complete": false, "completed_for_due_ts": 0.0, "completed_for_period_ts": 0.0, "completion_elapsed_human": "68 hours", "completion_hold_hours": 20.0, "completion_window_end": "", "completion_window_missed": false, "completion_window_start": "", "current_completion_window_end_ts": 0, "current_completion_window_start_ts": 0, "current_due_ts": 1786247100.469644, "current_period_start_ts": 1786247100.469644, "display_due_ts": 1786247100.469644, "due_attribute": "", "due_day": 0, "due_entity": "sensor.appliance_monitor_main_dishwasher_monitor", "due_started": true, "due_state": "Finished", "due_time": "", "due_weekday": 0, "has_completion_window": false, "hours_since_completed": 68, "hours_since_required": 0, "initial_due_behavior": "ignore", "job_id": "dishwasher_unpacked", "job_name": "Unpack the dishwasher", "last_completed_human": "3 days ago @ 19:17", "last_completed_time": "19:17", "last_completed_ts": 1786000653.020492, "last_updated_local": "2026-08-09 16:03:00", "minutes_since_completed": 4125, "minutes_since_required": 18, "missed": false, "mqtt_topic": "viewroad-status/jobs_tracker/dishwasher_unpacked", "next_due_human": "Due now", "next_due_ts": 0, "next_period_start_ts": 0, "once_due_signal_active": false, "once_due_signal_ts": 1786247220.902285, "once_due_ts": 1786247100.469644, "once_pending": true, "overdue_after_minutes": 0, "overdue_hours": 0, "overdue_minutes": 0, "overdue_started": false, "previous_completed": false, "previous_due_ts": 0, "previous_missed": false, "previous_missed_active": false, "previous_period_start_ts": 0, "repeat": "once", "repeat_type": "once", "state": "Due"} +viewroad-status/jobs_tracker/rubbish_removed,{ "job_id": "rubbish_removed", "job_name": "Take out rubbish", "repeat": "daily", "due_time": "15:30", "due_weekday": 0, "due_day": 1, "state": "Due", "complete": false, "last_completed_ts": 1786156238.738474, "last_completed_time": "14:30", "last_completed_human": "Yesterday @ 14:30", "previous_period_start_ts": 1786104000.0, "previous_due_ts": 1786159800.0, "previous_completed": true, "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.0, "current_completion_window_end_ts": 0.0, "completed_for_period_ts": 0.0, "completed_for_due_ts": 0.0, "current_period_start_ts": 1786190400.0, "next_period_start_ts": 1786276800.0, "current_due_ts": 1786246200.0, "next_due_ts": 1786332600.0, "display_due_ts": 1786246200.0, "next_due_human": "Today @ 15:30", "due_started": true, "overdue_started": false, "minutes_since_required": 33, "hours_since_required": 1, "overdue_after_minutes": 120, "overdue_minutes": 0, "overdue_hours": 0, "hours_since_completed": 26, "last_updated_local": "2026-08-09 16:03:00", "mqtt_topic": "viewroad-status/jobs_tracker/rubbish_removed"} +viewroad-status/jobs_tracker/summary,{"complete_count": 3, "due_count": 2, "jobs": [{"completion_elapsed_human": null, "completion_entities": ["event.jobs_dog_fed_button_x14bt_action"], "completion_hold_hours": 3, "completion_window_end": "11:59", "completion_window_missed": true, "completion_window_start": "00:00", "due_attribute": "", "due_day": 1, "due_entity": "", "due_state": "", "due_time": "06:00", "due_weekday": 0, "hours_since_completed": 29, "hours_since_required": 10, "initial_due_behavior": "", "job_id": "dog_feeding_morning", "job_name": "Dog Feeding Morning", "last_completed_human": "Yesterday @ 11:13", "minutes_since_completed": null, "minutes_since_required": 603, "missed": true, "mqtt_topic": "viewroad-status/jobs_tracker/dog_feeding_morning", "next_due_human": "Today @ 06:00", "once_due_ts": null, "once_pending": null, "previous_missed": false, "previous_missed_active": false, "repeat_type": "daily", "state": "Missed", "state_entity": "sensor.jobs_tracker_dog_feeding_morning"}, {"completion_elapsed_human": null, "completion_entities": ["event.jobs_dog_fed_button_x14bt_action"], "completion_hold_hours": 3, "completion_window_end": "23:59", "completion_window_missed": false, "completion_window_start": "12:00", "due_attribute": "", "due_day": 1, "due_entity": "", "due_state": "", "due_time": "17:00", "due_weekday": 0, "hours_since_completed": 2, "hours_since_required": 0, "initial_due_behavior": "", "job_id": "dog_feeding_evening", "job_name": "Dog Feeding Evening", "last_completed_human": "14:09", "minutes_since_completed": null, "minutes_since_required": 0, "missed": false, "mqtt_topic": "viewroad-status/jobs_tracker/dog_feeding_evening", "next_due_human": "Tomorrow @ 17:00", "once_due_ts": null, "once_pending": null, "previous_missed": true, "previous_missed_active": false, "repeat_type": "daily", "state": "Complete", "state_entity": "sensor.jobs_tracker_dog_feeding_evening"}, {"completion_elapsed_human": null, "completion_entities": ["event.jobs_dog_depooped_button_x15bt_action"], "completion_hold_hours": 3, "completion_window_end": "", "completion_window_missed": false, "completion_window_start": "", "due_attribute": "", "due_day": 1, "due_entity": "", "due_state": "", "due_time": "08:00", "due_weekday": 4, "hours_since_completed": 29, "hours_since_required": 56, "initial_due_behavior": "", "job_id": "dog_depooping", "job_name": "Dog De-pooping", "last_completed_human": "Yesterday @ 11:13", "minutes_since_completed": null, "minutes_since_required": 3363, "missed": false, "mqtt_topic": "viewroad-status/jobs_tracker/dog_depooping", "next_due_human": "Next Friday @ 08:00", "once_due_ts": null, "once_pending": null, "previous_missed": true, "previous_missed_active": false, "repeat_type": "weekly", "state": "Complete", "state_entity": "sensor.jobs_tracker_dog_depooping"}, {"completion_elapsed_human": "68 hours", "completion_entities": ["event.kitchen_jobs_kitchen_completed_buttons_button_1"], "completion_hold_hours": 20, "completion_window_end": "", "completion_window_missed": false, "completion_window_start": "", "due_attribute": "", "due_day": 0, "due_entity": "sensor.appliance_monitor_main_dishwasher_monitor", "due_state": "Finished", "due_time": "", "due_weekday": 0, "hours_since_completed": 68, "hours_since_required": 0, "initial_due_behavior": "ignore", "job_id": "dishwasher_unpacked", "job_name": "Unpack the dishwasher", "last_completed_human": "3 days ago @ 19:17", "minutes_since_completed": 4125, "minutes_since_required": 18, "missed": false, "mqtt_topic": "viewroad-status/jobs_tracker/dishwasher_unpacked", "next_due_human": "Due now", "once_due_ts": 1786247100.469644, "once_pending": true, "previous_missed": false, "previous_missed_active": false, "repeat_type": "once", "state": "Due", "state_entity": "sensor.jobs_tracker_dishwasher_unpacked"}, {"completion_elapsed_human": null, "completion_entities": ["event.kitchen_jobs_kitchen_completed_buttons_button_2"], "completion_hold_hours": 3, "completion_window_end": "", "completion_window_missed": false, "completion_window_start": "", "due_attribute": "", "due_day": 1, "due_entity": "", "due_state": "", "due_time": "15:30", "due_weekday": 0, "hours_since_completed": 26, "hours_since_required": 1, "initial_due_behavior": "", "job_id": "rubbish_removed", "job_name": "Take out rubbish", "last_completed_human": "Yesterday @ 14:30", "minutes_since_completed": null, "minutes_since_required": 33, "missed": false, "mqtt_topic": "viewroad-status/jobs_tracker/rubbish_removed", "next_due_human": "Today @ 15:30", "once_due_ts": null, "once_pending": null, "previous_missed": false, "previous_missed_active": false, "repeat_type": "daily", "state": "Due", "state_entity": "sensor.jobs_tracker_rubbish_removed"}, {"completion_elapsed_human": "Never", "completion_entities": [{"entity_id": "binary_sensor.tv_plant_needs_water", "to_state": "off"}], "completion_hold_hours": 3, "completion_window_end": "", "completion_window_missed": false, "completion_window_start": "", "due_attribute": "", "due_day": 0, "due_entity": "binary_sensor.tv_plant_needs_water", "due_state": "on", "due_time": "", "due_weekday": 0, "hours_since_completed": "unknown", "hours_since_required": 0, "initial_due_behavior": "", "job_id": "water_tv_pot_plant", "job_name": "Water TV Pot Plant", "last_completed_human": "Never", "minutes_since_completed": "unknown", "minutes_since_required": 0, "missed": false, "mqtt_topic": "viewroad-status/jobs_tracker/water_tv_pot_plant", "next_due_human": "Waiting for trigger", "once_due_ts": 0.0, "once_pending": false, "previous_missed": false, "previous_missed_active": false, "repeat_type": "once", "state": "Not Due", "state_entity": "sensor.jobs_tracker_water_tv_pot_plant"}, {"completion_elapsed_human": "90 hours", "completion_entities": [{"entity_id": "binary_sensor.table_plant_needs_water", "to_state": "off"}], "completion_hold_hours": 3, "completion_window_end": "", "completion_window_missed": false, "completion_window_start": "", "due_attribute": "", "due_day": 0, "due_entity": "binary_sensor.table_plant_needs_water", "due_state": "on", "due_time": "", "due_weekday": 0, "hours_since_completed": 90, "hours_since_required": 0, "initial_due_behavior": "", "job_id": "water_table_pot_plant", "job_name": "Water Table Pot Plant", "last_completed_human": "4 days ago @ 21:49", "minutes_since_completed": 5413, "minutes_since_required": 0, "missed": false, "mqtt_topic": "viewroad-status/jobs_tracker/water_table_pot_plant", "next_due_human": "Waiting for trigger", "once_due_ts": 1785834240.604805, "once_pending": false, "previous_missed": false, "previous_missed_active": false, "repeat_type": "once", "state": "Not Due", "state_entity": "sensor.jobs_tracker_water_table_pot_plant"}, {"completion_elapsed_human": "49 hours", "completion_entities": ["binary_sensor.esp_laundryenv_laundry_pir", "binary_sensor.laundry_ceiling_pir_x03ir_occupancy"], "completion_hold_hours": 20, "completion_window_end": "", "completion_window_missed": false, "completion_window_start": "", "due_attribute": "", "due_day": 0, "due_entity": "sensor.appliance_monitor_dryer_monitor", "due_state": "Finished", "due_time": "", "due_weekday": 0, "hours_since_completed": 49, "hours_since_required": 0, "initial_due_behavior": "ignore", "job_id": "empty_dryer", "job_name": "Empty the Dryer", "last_completed_human": "2 days ago @ 14:42", "minutes_since_completed": 2960, "minutes_since_required": 0, "missed": false, "mqtt_topic": "viewroad-status/jobs_tracker/empty_dryer", "next_due_human": "Waiting for trigger", "once_due_ts": 1786048140.755156, "once_pending": false, "previous_missed": false, "previous_missed_active": false, "repeat_type": "once", "state": "Not Due", "state_entity": "sensor.jobs_tracker_empty_dryer"}, {"completion_elapsed_human": "13 mins", "completion_entities": ["binary_sensor.esp_laundryenv_laundry_pir", "binary_sensor.laundry_ceiling_pir_x03ir_occupancy"], "completion_hold_hours": 20, "completion_window_end": "", "completion_window_missed": false, "completion_window_start": "", "due_attribute": "", "due_day": 0, "due_entity": "sensor.appliance_monitor_washing_machine_monitor", "due_state": "Finished", "due_time": "", "due_weekday": 0, "hours_since_completed": 0, "hours_since_required": 0, "initial_due_behavior": "ignore", "job_id": "empty_washing_machine", "job_name": "Empty the Washer", "last_completed_human": "15:49", "minutes_since_completed": 13, "minutes_since_required": 0, "missed": false, "mqtt_topic": "viewroad-status/jobs_tracker/empty_washing_machine", "next_due_human": "Waiting for trigger", "once_due_ts": 1786247220.447973, "once_pending": false, "previous_missed": false, "previous_missed_active": false, "repeat_type": "once", "state": "Complete", "state_entity": "sensor.jobs_tracker_empty_washing_machine"}], "last_updated_local": "2026-08-09 16:03:01", "missed_count": 1, "not_due_count": 3, "overdue_count": 0, "previous_missed_count": 1, "state": "2 Due, 1 Missed, 3 Not Due", "total_count": 9, "unknown_count": 0} +viewroad-status/jobs_tracker/water_tv_pot_plant,{"complete": false, "completed_for_due_ts": 0.0, "completed_for_period_ts": 0.0, "completion_elapsed_human": "Never", "completion_hold_hours": 3.0, "completion_window_end": "", "completion_window_missed": false, "completion_window_start": "", "current_completion_window_end_ts": 0, "current_completion_window_start_ts": 0, "current_due_ts": 0.0, "current_period_start_ts": 0.0, "display_due_ts": 0, "due_attribute": "", "due_day": 0, "due_entity": "binary_sensor.tv_plant_needs_water", "due_started": false, "due_state": "on", "due_time": "", "due_weekday": 0, "has_completion_window": false, "hours_since_completed": "unknown", "hours_since_required": 0, "initial_due_behavior": "", "job_id": "water_tv_pot_plant", "job_name": "Water TV Pot Plant", "last_completed_human": "Never", "last_completed_time": "Never", "last_completed_ts": 0.0, "last_updated_local": "2026-08-09 16:03:00", "minutes_since_completed": "unknown", "minutes_since_required": 0, "missed": false, "mqtt_topic": "viewroad-status/jobs_tracker/water_tv_pot_plant", "next_due_human": "Waiting for trigger", "next_due_ts": 0, "next_period_start_ts": 0, "once_due_signal_active": false, "once_due_signal_ts": 1786246919.991207, "once_due_ts": 0.0, "once_pending": false, "overdue_after_minutes": 0, "overdue_hours": 0, "overdue_minutes": 0, "overdue_started": false, "previous_completed": false, "previous_due_ts": 0, "previous_missed": false, "previous_missed_active": false, "previous_period_start_ts": 0, "repeat": "once", "repeat_type": "once", "state": "Not Due"} +viewroad-status/jobs_tracker/water_table_pot_plant,{"complete": false, "completed_for_due_ts": 1785834240.604805, "completed_for_period_ts": 1785834240.604805, "completion_elapsed_human": "90 hours", "completion_hold_hours": 3.0, "completion_window_end": "", "completion_window_missed": false, "completion_window_start": "", "current_completion_window_end_ts": 0, "current_completion_window_start_ts": 0, "current_due_ts": 1785834240.604805, "current_period_start_ts": 1785834240.604805, "display_due_ts": 0, "due_attribute": "", "due_day": 0, "due_entity": "binary_sensor.table_plant_needs_water", "due_started": false, "due_state": "on", "due_time": "", "due_weekday": 0, "has_completion_window": false, "hours_since_completed": 90, "hours_since_required": 0, "initial_due_behavior": "", "job_id": "water_table_pot_plant", "job_name": "Water Table Pot Plant", "last_completed_human": "4 days ago @ 21:49", "last_completed_time": "21:49", "last_completed_ts": 1785923340.924547, "last_updated_local": "2026-08-09 16:03:00", "minutes_since_completed": 5413, "minutes_since_required": 0, "missed": false, "mqtt_topic": "viewroad-status/jobs_tracker/water_table_pot_plant", "next_due_human": "Waiting for trigger", "next_due_ts": 0, "next_period_start_ts": 0, "once_due_signal_active": false, "once_due_signal_ts": 1786247055.542165, "once_due_ts": 1785834240.604805, "once_pending": false, "overdue_after_minutes": 0, "overdue_hours": 0, "overdue_minutes": 0, "overdue_started": false, "previous_completed": false, "previous_due_ts": 0, "previous_missed": false, "previous_missed_active": false, "previous_period_start_ts": 0, "repeat": "once", "repeat_type": "once", "state": "Not Due"} +viewroad-status/jobs_tracker/empty_dryer,{"complete": false, "completed_for_due_ts": 1786048140.755156, "completed_for_period_ts": 1786048140.755156, "completion_elapsed_human": "49 hours", "completion_hold_hours": 20.0, "completion_window_end": "", "completion_window_missed": false, "completion_window_start": "", "current_completion_window_end_ts": 0, "current_completion_window_start_ts": 0, "current_due_ts": 1786048140.755156, "current_period_start_ts": 1786048140.755156, "display_due_ts": 0, "due_attribute": "", "due_day": 0, "due_entity": "sensor.appliance_monitor_dryer_monitor", "due_started": false, "due_state": "Finished", "due_time": "", "due_weekday": 0, "has_completion_window": false, "hours_since_completed": 49, "hours_since_required": 0, "initial_due_behavior": "ignore", "job_id": "empty_dryer", "job_name": "Empty the Dryer", "last_completed_human": "2 days ago @ 14:42", "last_completed_time": "14:42", "last_completed_ts": 1786070552.197665, "last_updated_local": "2026-08-09 16:03:00", "minutes_since_completed": 2960, "minutes_since_required": 0, "missed": false, "mqtt_topic": "viewroad-status/jobs_tracker/empty_dryer", "next_due_human": "Waiting for trigger", "next_due_ts": 0, "next_period_start_ts": 0, "once_due_signal_active": false, "once_due_signal_ts": 1786246911.901469, "once_due_ts": 1786048140.755156, "once_pending": false, "overdue_after_minutes": 0, "overdue_hours": 0, "overdue_minutes": 0, "overdue_started": false, "previous_completed": false, "previous_due_ts": 0, "previous_missed": false, "previous_missed_active": false, "previous_period_start_ts": 0, "repeat": "once", "repeat_type": "once", "state": "Not Due"} +viewroad-status/jobs_tracker/empty_washing_machine,{"complete": true, "completed_for_due_ts": 1786247220.447973, "completed_for_period_ts": 1786247220.447973, "completion_elapsed_human": "13 mins", "completion_hold_hours": 20.0, "completion_window_end": "", "completion_window_missed": false, "completion_window_start": "", "current_completion_window_end_ts": 0, "current_completion_window_start_ts": 0, "current_due_ts": 1786247220.447973, "current_period_start_ts": 1786247220.447973, "display_due_ts": 0, "due_attribute": "", "due_day": 0, "due_entity": "sensor.appliance_monitor_washing_machine_monitor", "due_started": false, "due_state": "Finished", "due_time": "", "due_weekday": 0, "has_completion_window": false, "hours_since_completed": 0, "hours_since_required": 0, "initial_due_behavior": "ignore", "job_id": "empty_washing_machine", "job_name": "Empty the Washer", "last_completed_human": "15:49", "last_completed_time": "15:49", "last_completed_ts": 1786247387.396981, "last_updated_local": "2026-08-09 16:03:00", "minutes_since_completed": 13, "minutes_since_required": 0, "missed": false, "mqtt_topic": "viewroad-status/jobs_tracker/empty_washing_machine", "next_due_human": "Waiting for trigger", "next_due_ts": 0, "next_period_start_ts": 0, "once_due_signal_active": false, "once_due_signal_ts": 1786247760.267239, "once_due_ts": 1786247220.447973, "once_pending": false, "overdue_after_minutes": 0, "overdue_hours": 0, "overdue_minutes": 0, "overdue_started": false, "previous_completed": false, "previous_due_ts": 0, "previous_missed": false, "previous_missed_active": false, "previous_period_start_ts": 0, "repeat": "once", "repeat_type": "once", "state": "Complete"} diff --git a/packages/16a_ev_charging_controls.yaml b/packages/16a_ev_charging_controls.yaml index 1a2d4d6..8f37f28 100644 --- a/packages/16a_ev_charging_controls.yaml +++ b/packages/16a_ev_charging_controls.yaml @@ -8,6 +8,9 @@ # packages/16a_ev_charging_controls.yaml # # VERSION: +# V1.4 2026-08-12 +# - Consolidated Leaf 16A and 32A session energy tracking in this package. +# # V1.3 2026-07-24 # - Added startup catch-up to offpeak_end: when HA starts outside the # offpeak window in Normal mode, the relay is turned off immediately. @@ -27,7 +30,8 @@ # # PURPOSE: # Controls the Leaf 16A charger relay using scheduled offpeak operation and -# user-selectable immediate or charge-until-full modes. +# user-selectable immediate or charge-until-full modes. Tracks completed +# Leaf charging sessions across both the 16A and 32A chargers. # # DEPENDENCIES: # - switch.garage_db_control_16a_wallcharger_operation controls charger power. @@ -450,3 +454,171 @@ automation: entity_id: input_select.ev_16a_charge_mode data: option: "Normal (Offpeak)" + + ############################################################################# + # SESSION ENERGY TRACKING + ############################################################################# + + - id: leaf_ev_charger_track_session_energy + alias: "Leaf EV Charger - Track Session Energy" + description: >- + Store the energy used by the most recently completed Leaf charge session + on either charger. + mode: queued + max: 2 + + triggers: + - trigger: homeassistant + event: start + id: ha_start + - trigger: state + entity_id: binary_sensor.garage_db_control_16a_wallcharger_charging + to: "on" + id: start_16a + - trigger: state + entity_id: binary_sensor.garage_db_control_16a_wallcharger_charging + from: "on" + to: "off" + id: stop_16a + - trigger: state + entity_id: binary_sensor.leaf_charging + to: "on" + id: start_32a + - trigger: state + entity_id: binary_sensor.main_house_3_phase_power_monitor_32a_wallcharger_charging + to: "off" + id: stop_32a + - trigger: state + entity_id: binary_sensor.atto_3_charging + to: "on" + id: cancel_32a + + conditions: [] + + actions: + - choose: + - conditions: + - condition: trigger + id: ha_start + - condition: state + entity_id: binary_sensor.garage_db_control_16a_wallcharger_charging + state: "on" + sequence: + - action: input_number.set_value + target: + entity_id: input_number.leaf_charge_session_start_energy + data: + value: "{{ states('sensor.garage_db_control_16a_wallcharger_total_energy') | float(0) }}" + + - conditions: + - condition: trigger + id: ha_start + - condition: state + entity_id: binary_sensor.main_house_3_phase_power_monitor_32a_wallcharger_charging + state: "on" + - condition: state + entity_id: binary_sensor.atto_3_charging + state: "off" + - condition: template + value_template: >- + {{ has_value('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') }} + sequence: + - action: input_number.set_value + target: + entity_id: input_number.leaf_32a_charge_session_start_energy + data: + value: "{{ states('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') | float(0) }}" + - action: input_boolean.turn_on + target: + entity_id: input_boolean.leaf_32a_charge_session_active + + - conditions: + - condition: trigger + id: start_16a + sequence: + - action: input_number.set_value + target: + entity_id: input_number.leaf_charge_session_start_energy + data: + value: "{{ states('sensor.garage_db_control_16a_wallcharger_total_energy') | float(0) }}" + + - conditions: + - condition: trigger + id: stop_16a + - condition: template + value_template: > + {{ states('sensor.garage_db_control_16a_wallcharger_total_energy') | float(0) + >= states('input_number.leaf_charge_session_start_energy') | float(0) }} + sequence: + - action: input_number.set_value + target: + entity_id: input_number.leaf_last_session_energy + data: + value: >- + {{ (states('sensor.garage_db_control_16a_wallcharger_total_energy') | float(0) + - states('input_number.leaf_charge_session_start_energy') | float(0)) | round(3) }} + + - conditions: + - condition: trigger + id: start_32a + - condition: state + entity_id: binary_sensor.main_house_3_phase_power_monitor_32a_wallcharger_charging + state: "on" + - condition: state + entity_id: binary_sensor.atto_3_charging + state: "off" + - condition: template + value_template: >- + {{ has_value('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') }} + sequence: + - action: input_number.set_value + target: + entity_id: input_number.leaf_32a_charge_session_start_energy + data: + value: "{{ states('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') | float(0) }}" + - action: input_boolean.turn_on + target: + entity_id: input_boolean.leaf_32a_charge_session_active + + - conditions: + - condition: trigger + id: stop_32a + - condition: state + entity_id: input_boolean.leaf_32a_charge_session_active + state: "on" + sequence: + - choose: + - conditions: + - condition: template + value_template: >- + {{ + has_value('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') + and states('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') | float(0) + >= states('input_number.leaf_32a_charge_session_start_energy') | float(0) + }} + sequence: + - action: input_number.set_value + target: + entity_id: input_number.leaf_last_session_energy + data: + value: >- + {{ + ( + states('sensor.main_house_3_phase_power_monitor_32a_wallcharger_total_energy') | float(0) + - states('input_number.leaf_32a_charge_session_start_energy') | float(0) + ) | round(3) + }} + - action: input_boolean.turn_off + target: + entity_id: input_boolean.leaf_32a_charge_session_active + + - conditions: + - condition: trigger + id: cancel_32a + - condition: state + entity_id: input_boolean.leaf_32a_charge_session_active + state: "on" + sequence: + - action: input_boolean.turn_off + target: + entity_id: input_boolean.leaf_32a_charge_session_active 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 +}'