various tidyups
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
# jobs_tracker_jobs.yaml
|
||||
#
|
||||
# VERSION:
|
||||
# V2.4 2026-07-21
|
||||
# V3.5 2026-08-15
|
||||
#
|
||||
# PURPOSE:
|
||||
# Defines the job registry, MQTT Discovery entities, completion triggers and
|
||||
@@ -20,9 +20,9 @@
|
||||
#
|
||||
# OPERATION:
|
||||
# Add and edit jobs in the JOB REGISTRY section below. Each job has its own
|
||||
# job_id, name, repeat type, due time, MQTT topic, state entity and completion
|
||||
# entity/entities. Most future job changes should only require editing that
|
||||
# registry block.
|
||||
# job_id, name, repeat type, schedule or due entity, MQTT topic, state entity
|
||||
# and completion entity/entities. Most future job changes should only require
|
||||
# editing that registry block.
|
||||
#
|
||||
# If a new job uses a completion entity that is already listed in
|
||||
# jobs_tracker_completion_entities, no trigger changes are needed. If the job
|
||||
@@ -41,6 +41,39 @@
|
||||
# Complete
|
||||
#
|
||||
# VERSION HISTORY:
|
||||
# V3.5 2026-08-15
|
||||
# - Empty the Dryer now completes when its door remains open for five seconds.
|
||||
# - Empty the Washer now completes when its lid remains open for five seconds.
|
||||
# - Removed Laundry PIR completion from the dryer and washer jobs.
|
||||
#
|
||||
# V3.3 2026-07-30
|
||||
# - Added Empty the Dryer and Empty the Washing Machine Once jobs.
|
||||
# - Laundry PIR completion sensors for dryer and washing machine.
|
||||
# - Added initial_due_behavior: ignore to prevent old cycle signals.
|
||||
# - Discovery device firmware version updated to 3.3.
|
||||
# - PIR completion entities added to the completion trigger list.
|
||||
#
|
||||
# V3.2 2026-07-30
|
||||
# - Converted Dishwasher Unpacked from daily to Once.
|
||||
# - Main dishwasher cycle_stop_ts now makes the job Due.
|
||||
# - The existing kitchen completion button still completes the job.
|
||||
# - Discovery device firmware version updated to 3.2.
|
||||
#
|
||||
# V3.1 2026-07-30
|
||||
# - Added configurable Once completion hold periods, defaulting to three hours.
|
||||
# - Added elapsed completion text and minute-level completion age attributes.
|
||||
# - Added due_attribute support for reliable attribute-driven Once jobs.
|
||||
# - Discovery device firmware version updated to 3.1.
|
||||
#
|
||||
# V3.0 2026-07-29
|
||||
# - Added event-driven Once jobs with registry-defined due and completion
|
||||
# entities.
|
||||
# - Once jobs remain Due until completed, briefly publish Complete, then
|
||||
# return to Not Due until their next due signal.
|
||||
# - Added Water TV Pot Plant and Water Table Pot Plant Once jobs.
|
||||
# - Missing retained job state now bootstraps after the startup wait.
|
||||
# - Discovery device firmware version updated to 3.0.
|
||||
#
|
||||
# V2.4 2026-07-21
|
||||
# - Startup now waits for every retained job timestamp before refreshing.
|
||||
# - Minute refreshes skip while retained MQTT state is not ready.
|
||||
@@ -91,6 +124,18 @@
|
||||
# - Dog feeding uses completion windows because both feeding jobs share the
|
||||
# same physical button.
|
||||
#
|
||||
# - Once jobs use due_entity and optional due_state. If due_state is omitted,
|
||||
# a fresh event entity timestamp or state change starts the job.
|
||||
# Set due_attribute to trigger from an increasing timestamp or numeric
|
||||
# attribute such as cycle_stop_ts.
|
||||
#
|
||||
# - Once completion entries may be simple entity IDs or mappings with an
|
||||
# entity_id and optional to_state. Once entities are evaluated by the minute
|
||||
# refresh, so adding one requires changes only in the registry.
|
||||
#
|
||||
# - completion_hold_hours controls how long a completed Once job remains
|
||||
# Complete. It defaults to 3 hours when omitted.
|
||||
#
|
||||
#:########################################################################################:#
|
||||
|
||||
script:
|
||||
@@ -139,7 +184,7 @@ script:
|
||||
completion_entities:
|
||||
- event.jobs_dog_fed_button_x14bt_action
|
||||
completion_window_start: "00:00"
|
||||
completion_window_end: "11:59"
|
||||
completion_window_end: "14:59"
|
||||
icon: mdi:dog-side
|
||||
mqtt_topic: "viewroad-status/jobs_tracker/dog_feeding_morning"
|
||||
state_entity: "sensor.jobs_tracker_dog_feeding_morning"
|
||||
@@ -152,7 +197,7 @@ script:
|
||||
due_day: 1
|
||||
completion_entities:
|
||||
- event.jobs_dog_fed_button_x14bt_action
|
||||
completion_window_start: "12:00"
|
||||
completion_window_start: "15:00"
|
||||
completion_window_end: "23:59"
|
||||
icon: mdi:dog-side
|
||||
mqtt_topic: "viewroad-status/jobs_tracker/dog_feeding_evening"
|
||||
@@ -173,21 +218,20 @@ script:
|
||||
state_entity: "sensor.jobs_tracker_dog_depooping"
|
||||
|
||||
- job_id: dishwasher_unpacked
|
||||
job_name: "Dishwasher Unpacked"
|
||||
repeat_type: daily
|
||||
due_time: "15:30"
|
||||
due_weekday: 0
|
||||
due_day: 1
|
||||
job_name: "Unpack the dishwasher"
|
||||
repeat_type: once
|
||||
due_entity: sensor.appliance_monitor_main_dishwasher_monitor
|
||||
due_state: "Finished"
|
||||
initial_due_behavior: ignore
|
||||
completion_hold_hours: 20
|
||||
completion_entities:
|
||||
- event.kitchen_jobs_kitchen_completed_buttons_button_1
|
||||
# completion_window_start: "00:00"
|
||||
# completion_window_end: "23:59"
|
||||
icon: mdi:dishwasher
|
||||
mqtt_topic: "viewroad-status/jobs_tracker/dishwasher_unpacked"
|
||||
state_entity: "sensor.jobs_tracker_dishwasher_unpacked"
|
||||
|
||||
- job_id: rubbish_removed
|
||||
job_name: "Rubbish Removed"
|
||||
job_name: "Take out rubbish"
|
||||
repeat_type: daily
|
||||
due_time: "15:30"
|
||||
due_weekday: 0
|
||||
@@ -200,6 +244,60 @@ script:
|
||||
mqtt_topic: "viewroad-status/jobs_tracker/rubbish_removed"
|
||||
state_entity: "sensor.jobs_tracker_rubbish_removed"
|
||||
|
||||
- job_id: water_tv_pot_plant
|
||||
job_name: "Water TV Pot Plant"
|
||||
repeat_type: once
|
||||
due_entity: binary_sensor.tv_plant_needs_water
|
||||
due_state: "on"
|
||||
completion_hold_hours: 3
|
||||
completion_entities:
|
||||
- entity_id: binary_sensor.tv_plant_needs_water
|
||||
to_state: "off"
|
||||
icon: mdi:watering-can
|
||||
mqtt_topic: "viewroad-status/jobs_tracker/water_tv_pot_plant"
|
||||
state_entity: "sensor.jobs_tracker_water_tv_pot_plant"
|
||||
|
||||
- job_id: water_table_pot_plant
|
||||
job_name: "Water Table Pot Plant"
|
||||
repeat_type: once
|
||||
due_entity: binary_sensor.table_plant_needs_water
|
||||
due_state: "on"
|
||||
completion_hold_hours: 3
|
||||
completion_entities:
|
||||
- entity_id: binary_sensor.table_plant_needs_water
|
||||
to_state: "off"
|
||||
icon: mdi:watering-can
|
||||
mqtt_topic: "viewroad-status/jobs_tracker/water_table_pot_plant"
|
||||
state_entity: "sensor.jobs_tracker_water_table_pot_plant"
|
||||
|
||||
- job_id: empty_dryer
|
||||
job_name: "Empty the Dryer"
|
||||
repeat_type: once
|
||||
due_entity: sensor.appliance_monitor_dryer_monitor
|
||||
due_state: "Finished"
|
||||
completion_hold_hours: 20
|
||||
completion_entities:
|
||||
- entity_id: binary_sensor.dryer_door_status_x34rs_contact
|
||||
to_state: "on"
|
||||
icon: mdi:tumble-dryer
|
||||
mqtt_topic: "viewroad-status/jobs_tracker/empty_dryer"
|
||||
state_entity: "sensor.jobs_tracker_empty_dryer"
|
||||
initial_due_behavior: ignore
|
||||
|
||||
- job_id: empty_washing_machine
|
||||
job_name: "Empty the Washer"
|
||||
repeat_type: once
|
||||
due_entity: sensor.appliance_monitor_washing_machine_monitor
|
||||
due_state: "Finished"
|
||||
completion_hold_hours: 20
|
||||
completion_entities:
|
||||
- entity_id: binary_sensor.washing_machine_lid_status_x39rs_contact
|
||||
to_state: "on"
|
||||
icon: mdi:washing-machine
|
||||
mqtt_topic: "viewroad-status/jobs_tracker/empty_washing_machine"
|
||||
state_entity: "sensor.jobs_tracker_empty_washing_machine"
|
||||
initial_due_behavior: ignore
|
||||
|
||||
#:################################################################################:#
|
||||
# COMPLETION ENTITY TRIGGER LIST #
|
||||
#:################################################################################:#
|
||||
@@ -225,19 +323,37 @@ script:
|
||||
- repeat:
|
||||
for_each: "{{ jobs_tracker_jobs }}"
|
||||
sequence:
|
||||
- action: script.jobs_tracker_update_job
|
||||
data:
|
||||
job_id: "{{ repeat.item.job_id }}"
|
||||
job_name: "{{ repeat.item.job_name }}"
|
||||
repeat_type: "{{ repeat.item.repeat_type }}"
|
||||
due_time: "{{ repeat.item.due_time }}"
|
||||
due_weekday: "{{ repeat.item.due_weekday }}"
|
||||
due_day: "{{ repeat.item.due_day }}"
|
||||
mqtt_topic: "{{ repeat.item.mqtt_topic }}"
|
||||
state_entity: "{{ repeat.item.state_entity }}"
|
||||
completion_window_start: "{{ repeat.item.completion_window_start | default('') }}"
|
||||
completion_window_end: "{{ repeat.item.completion_window_end | default('') }}"
|
||||
mark_complete: false
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ repeat.item.repeat_type == 'once' }}"
|
||||
then:
|
||||
- action: script.jobs_tracker_update_once_job
|
||||
data:
|
||||
job_id: "{{ repeat.item.job_id }}"
|
||||
job_name: "{{ repeat.item.job_name }}"
|
||||
mqtt_topic: "{{ repeat.item.mqtt_topic }}"
|
||||
state_entity: "{{ repeat.item.state_entity }}"
|
||||
due_entity: "{{ repeat.item.due_entity }}"
|
||||
due_state: "{{ repeat.item.due_state | default('') }}"
|
||||
due_attribute: "{{ repeat.item.due_attribute | default('') }}"
|
||||
completion_entities: "{{ repeat.item.completion_entities | default([]) }}"
|
||||
completion_hold_hours: "{{ repeat.item.completion_hold_hours | default(3) }}"
|
||||
initial_due_behavior: "{{ repeat.item.initial_due_behavior | default('') }}"
|
||||
mark_complete: false
|
||||
else:
|
||||
- action: script.jobs_tracker_update_job
|
||||
data:
|
||||
job_id: "{{ repeat.item.job_id }}"
|
||||
job_name: "{{ repeat.item.job_name }}"
|
||||
repeat_type: "{{ repeat.item.repeat_type }}"
|
||||
due_time: "{{ repeat.item.due_time }}"
|
||||
due_weekday: "{{ repeat.item.due_weekday }}"
|
||||
due_day: "{{ repeat.item.due_day }}"
|
||||
mqtt_topic: "{{ repeat.item.mqtt_topic }}"
|
||||
state_entity: "{{ repeat.item.state_entity }}"
|
||||
completion_window_start: "{{ repeat.item.completion_window_start | default('') }}"
|
||||
completion_window_end: "{{ repeat.item.completion_window_end | default('') }}"
|
||||
mark_complete: false
|
||||
|
||||
#:##################################################################################:#
|
||||
# Mark one job complete #
|
||||
@@ -251,20 +367,37 @@ script:
|
||||
- condition: template
|
||||
value_template: "{{ repeat.item.job_id == job_id }}"
|
||||
then:
|
||||
- action: script.jobs_tracker_update_job
|
||||
data:
|
||||
job_id: "{{ repeat.item.job_id }}"
|
||||
job_name: "{{ repeat.item.job_name }}"
|
||||
repeat_type: "{{ repeat.item.repeat_type }}"
|
||||
due_time: "{{ repeat.item.due_time }}"
|
||||
due_weekday: "{{ repeat.item.due_weekday }}"
|
||||
due_day: "{{ repeat.item.due_day }}"
|
||||
mqtt_topic: "{{ repeat.item.mqtt_topic }}"
|
||||
state_entity: "{{ repeat.item.state_entity }}"
|
||||
completion_window_start: "{{ repeat.item.completion_window_start | default('') }}"
|
||||
completion_window_end: "{{ repeat.item.completion_window_end | default('') }}"
|
||||
mark_complete: true
|
||||
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ repeat.item.repeat_type == 'once' }}"
|
||||
then:
|
||||
- action: script.jobs_tracker_update_once_job
|
||||
data:
|
||||
job_id: "{{ repeat.item.job_id }}"
|
||||
job_name: "{{ repeat.item.job_name }}"
|
||||
mqtt_topic: "{{ repeat.item.mqtt_topic }}"
|
||||
state_entity: "{{ repeat.item.state_entity }}"
|
||||
due_entity: "{{ repeat.item.due_entity }}"
|
||||
due_state: "{{ repeat.item.due_state | default('') }}"
|
||||
due_attribute: "{{ repeat.item.due_attribute | default('') }}"
|
||||
completion_entities: "{{ repeat.item.completion_entities | default([]) }}"
|
||||
completion_hold_hours: "{{ repeat.item.completion_hold_hours | default(3) }}"
|
||||
initial_due_behavior: "{{ repeat.item.initial_due_behavior | default('') }}"
|
||||
mark_complete: true
|
||||
else:
|
||||
- action: script.jobs_tracker_update_job
|
||||
data:
|
||||
job_id: "{{ repeat.item.job_id }}"
|
||||
job_name: "{{ repeat.item.job_name }}"
|
||||
repeat_type: "{{ repeat.item.repeat_type }}"
|
||||
due_time: "{{ repeat.item.due_time }}"
|
||||
due_weekday: "{{ repeat.item.due_weekday }}"
|
||||
due_day: "{{ repeat.item.due_day }}"
|
||||
mqtt_topic: "{{ repeat.item.mqtt_topic }}"
|
||||
state_entity: "{{ repeat.item.state_entity }}"
|
||||
completion_window_start: "{{ repeat.item.completion_window_start | default('') }}"
|
||||
completion_window_end: "{{ repeat.item.completion_window_end | default('') }}"
|
||||
mark_complete: true
|
||||
# Give MQTT sensors a moment to update from the retained job payloads
|
||||
# before building the summary payload.
|
||||
- delay: "00:00:01"
|
||||
@@ -282,6 +415,10 @@ script:
|
||||
description: "Entity ID that triggered completion."
|
||||
example: "event.jobs_dog_fed_button_x14bt_action"
|
||||
|
||||
completion_to_state:
|
||||
description: "State reached by the completion entity."
|
||||
example: "off"
|
||||
|
||||
manual_job_id:
|
||||
description: "Job ID completed by MQTT/manual button."
|
||||
example: "dog_feeding_morning"
|
||||
@@ -290,6 +427,7 @@ script:
|
||||
- variables:
|
||||
jobs_tracker_jobs: *jobs_tracker_jobs
|
||||
completion_entity_safe: "{{ completion_entity | default('') }}"
|
||||
completion_to_state_safe: "{{ completion_to_state | default('') }}"
|
||||
manual_job_id_safe: "{{ manual_job_id | default('') }}"
|
||||
now_minutes: "{{ (now().hour * 60) + now().minute }}"
|
||||
|
||||
@@ -320,10 +458,21 @@ script:
|
||||
{% endif %}
|
||||
|
||||
physical_entity_matches: >-
|
||||
{{
|
||||
completion_entity_safe != ''
|
||||
and completion_entity_safe in completion_entities_safe
|
||||
}}
|
||||
{% set ns = namespace(matches=false) %}
|
||||
{% for completion in completion_entities_safe %}
|
||||
{% if completion is mapping %}
|
||||
{% set entity_id = completion.entity_id | default('') %}
|
||||
{% set to_state = completion.to_state | default('') %}
|
||||
{% else %}
|
||||
{% set entity_id = completion %}
|
||||
{% set to_state = '' %}
|
||||
{% endif %}
|
||||
{% if completion_entity_safe == entity_id
|
||||
and (to_state == '' or completion_to_state_safe == to_state) %}
|
||||
{% set ns.matches = true %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ ns.matches }}
|
||||
|
||||
manual_job_matches: >-
|
||||
{{
|
||||
@@ -366,7 +515,7 @@ script:
|
||||
device_name: "Jobs Tracker"
|
||||
device_manufacturer: "View Road Home Assistant"
|
||||
device_model: "Shared household jobs tracker"
|
||||
device_sw_version: "2.4"
|
||||
device_sw_version: "3.4"
|
||||
|
||||
value_template_state: >-
|
||||
{% raw %}{{ value_json.state | default('unknown') }}{% endraw %}
|
||||
@@ -508,6 +657,7 @@ script:
|
||||
"name": "Jobs Tracker - " ~ repeat.item.job_name,
|
||||
"unique_id": "jobs_tracker_" ~ repeat.item.job_id,
|
||||
"object_id": "jobs_tracker_" ~ repeat.item.job_id,
|
||||
"default_entity_id": "sensor.jobs_tracker_" ~ repeat.item.job_id,
|
||||
"state_topic": repeat.item.mqtt_topic,
|
||||
"value_template": value_template_state,
|
||||
"json_attributes_topic": repeat.item.mqtt_topic,
|
||||
@@ -533,6 +683,7 @@ script:
|
||||
"name": "Job Complete - " ~ repeat.item.job_name,
|
||||
"unique_id": "job_complete_" ~ repeat.item.job_id,
|
||||
"object_id": "job_complete_" ~ repeat.item.job_id,
|
||||
"default_entity_id": "binary_sensor.job_complete_" ~ repeat.item.job_id,
|
||||
"state_topic": repeat.item.mqtt_topic,
|
||||
"value_template": value_template_job_complete,
|
||||
"payload_on": "ON",
|
||||
@@ -559,6 +710,7 @@ script:
|
||||
"name": "Complete Job - " ~ repeat.item.job_name,
|
||||
"unique_id": "jobs_tracker_complete_" ~ repeat.item.job_id,
|
||||
"object_id": "jobs_tracker_complete_" ~ repeat.item.job_id,
|
||||
"default_entity_id": "button.jobs_tracker_complete_" ~ repeat.item.job_id,
|
||||
"command_topic": "viewroad-commands/jobs_tracker/" ~ repeat.item.job_id ~ "/complete",
|
||||
"payload_press": "PRESS",
|
||||
"icon": repeat.item.icon | default("mdi:clipboard-check-outline"),
|
||||
@@ -620,9 +772,14 @@ script:
|
||||
"job_name": job.job_name,
|
||||
"state": s,
|
||||
"repeat_type": job.repeat_type,
|
||||
"due_time": job.due_time,
|
||||
"due_weekday": job.due_weekday,
|
||||
"due_day": job.due_day,
|
||||
"due_time": job.due_time | default(""),
|
||||
"due_weekday": job.due_weekday | default(0),
|
||||
"due_day": job.due_day | default(0),
|
||||
"due_entity": job.due_entity | default(""),
|
||||
"due_state": job.due_state | default(""),
|
||||
"due_attribute": job.due_attribute | default(""),
|
||||
"initial_due_behavior": job.initial_due_behavior | default(""),
|
||||
"completion_hold_hours": job.completion_hold_hours | default(3),
|
||||
"completion_entities": job.completion_entities | default([]),
|
||||
"completion_window_start": job.completion_window_start | default(""),
|
||||
"completion_window_end": job.completion_window_end | default(""),
|
||||
@@ -636,7 +793,11 @@ script:
|
||||
"previous_missed_active": state_attr(job.state_entity, 'previous_missed_active'),
|
||||
"minutes_since_required": state_attr(job.state_entity, 'minutes_since_required'),
|
||||
"hours_since_required": state_attr(job.state_entity, 'hours_since_required'),
|
||||
"hours_since_completed": state_attr(job.state_entity, 'hours_since_completed')
|
||||
"hours_since_completed": state_attr(job.state_entity, 'hours_since_completed'),
|
||||
"minutes_since_completed": state_attr(job.state_entity, 'minutes_since_completed'),
|
||||
"completion_elapsed_human": state_attr(job.state_entity, 'completion_elapsed_human'),
|
||||
"once_pending": state_attr(job.state_entity, 'once_pending'),
|
||||
"once_due_ts": state_attr(job.state_entity, 'once_due_ts')
|
||||
}
|
||||
] %}
|
||||
{% endfor %}
|
||||
@@ -717,7 +878,7 @@ automation:
|
||||
{% endfor %}
|
||||
{{ ns.ready }}
|
||||
timeout: "00:01:30"
|
||||
continue_on_timeout: false
|
||||
continue_on_timeout: true
|
||||
|
||||
# Recalculate each job only after retained state is confirmed ready.
|
||||
- action: script.jobs_tracker_dispatch
|
||||
@@ -785,6 +946,7 @@ automation:
|
||||
- action: script.jobs_tracker_route_completion
|
||||
data:
|
||||
completion_entity: "{{ trigger.entity_id }}"
|
||||
completion_to_state: "{{ trigger.to_state.state }}"
|
||||
|
||||
- id: jobs_tracker_mqtt_manual_completion
|
||||
alias: "Jobs Tracker - MQTT Manual Completion"
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
# jobs_tracker_processor.yaml
|
||||
#
|
||||
# VERSION:
|
||||
# V1.6 2026-07-10
|
||||
# V2.5 2026-08-02
|
||||
# - A new Once due signal now refreshes the occurrence timestamp even when
|
||||
# the job is already Due, so repeated appliance cycles supersede any
|
||||
# still-uncompleted earlier occurrence.
|
||||
#
|
||||
# PURPOSE:
|
||||
# Generic processing engine for the Jobs Tracker system.
|
||||
@@ -25,6 +28,12 @@
|
||||
# restart the processor can recalculate the correct state from the last physical
|
||||
# button/action press.
|
||||
#
|
||||
# Event-driven Once jobs use the separate jobs_tracker_update_once_job script.
|
||||
# Their registry-defined due and completion entities are evaluated during the
|
||||
# Jobs Tracker minute refresh. Once jobs remain Due until completed, briefly
|
||||
# publish Complete, then return to Not Due until a new due signal arrives.
|
||||
# A newer due signal always supersedes an older pending occurrence.
|
||||
#
|
||||
# Normal operation is automatic. The job-list package refreshes all jobs on a
|
||||
# regular timer and after Home Assistant starts, and it calls this processor with
|
||||
# mark_complete: true when a physical button, event, sensor, MQTT button or other
|
||||
@@ -95,6 +104,29 @@
|
||||
# - Monthly jobs: 2 weeks after due_time
|
||||
#
|
||||
# VERSION HISTORY:
|
||||
# V2.4 2026-07-30
|
||||
# - Reformatted last_completed_human: shows "15:45" (today),
|
||||
# "Yesterday @ 15:45" or "X days ago @ 15:45".
|
||||
#
|
||||
# V2.3 2026-07-30
|
||||
# - Added initial_due_behavior: ignore field for Once jobs.
|
||||
# Records the first due signal but keeps the job Not Due.
|
||||
#
|
||||
# V2.2 2026-07-30
|
||||
# - Ignore historical event/attribute due signals that predate the most
|
||||
# recent completion while still recording them as processed.
|
||||
#
|
||||
# V2.1 2026-07-30
|
||||
# - Once jobs remain Complete for a configurable hold period.
|
||||
# - Added minute-level and human-readable completion age attributes.
|
||||
# - Added increasing numeric/timestamp attribute due signals.
|
||||
# - New due occurrences override an active completion hold.
|
||||
#
|
||||
# V2.0 2026-07-29
|
||||
# - Added the event-driven Once job processor.
|
||||
# - Added retained Once occurrence, pending and due-signal state.
|
||||
# - Added registry-defined due states and filtered completion states.
|
||||
#
|
||||
# V1.6 2026-07-10
|
||||
# - Renamed displayed state from Missed to Missed.
|
||||
# - Added optional completion_window_start and completion_window_end fields.
|
||||
@@ -474,11 +506,9 @@ script:
|
||||
{% set completed_midnight = as_timestamp(strptime(d ~ ' 00:00:00', '%Y-%m-%d %H:%M:%S')) %}
|
||||
{% set days_ago = ((as_timestamp(today_at('00:00')) - completed_midnight) / 86400) | round(0) | int %}
|
||||
{% if d == today %}
|
||||
Today @ {{ t }}
|
||||
{{ t }}
|
||||
{% elif d == yesterday %}
|
||||
Yesterday @ {{ t }}
|
||||
{% elif days_ago < 7 %}
|
||||
Last {{ ts | timestamp_custom('%A', true) }} @ {{ t }}
|
||||
{% else %}
|
||||
{{ days_ago }} days ago @ {{ t }}
|
||||
{% endif %}
|
||||
@@ -572,3 +602,332 @@ script:
|
||||
"last_updated_local": {{ ((now_ts | float(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true)) | tojson }},
|
||||
"mqtt_topic": {{ topic_safe | tojson }}
|
||||
}
|
||||
|
||||
jobs_tracker_update_once_job:
|
||||
alias: "Jobs Tracker - Update Once Job"
|
||||
description: "Updates one event-driven Once job's retained MQTT JSON state."
|
||||
mode: queued
|
||||
max: 20
|
||||
|
||||
fields:
|
||||
job_id:
|
||||
description: "Machine-safe job ID."
|
||||
example: "water_tv_pot_plant"
|
||||
job_name:
|
||||
description: "Human-readable job name."
|
||||
example: "Water TV Pot Plant"
|
||||
mqtt_topic:
|
||||
description: "MQTT topic used for retained job state."
|
||||
example: "viewroad-status/jobs_tracker/water_tv_pot_plant"
|
||||
state_entity:
|
||||
description: "MQTT sensor entity that reads this job's retained JSON."
|
||||
example: "sensor.jobs_tracker_water_tv_pot_plant"
|
||||
due_entity:
|
||||
description: "Entity that starts a new Once job occurrence."
|
||||
example: "binary_sensor.tv_plant_needs_water"
|
||||
due_state:
|
||||
description: "Optional state that starts the Once job."
|
||||
example: "on"
|
||||
due_attribute:
|
||||
description: "Optional increasing timestamp or numeric attribute that starts the job."
|
||||
example: "cycle_stop_ts"
|
||||
completion_entities:
|
||||
description: "Entities and optional target states that complete the job."
|
||||
completion_hold_hours:
|
||||
description: "Hours to remain Complete before returning to Not Due."
|
||||
example: 3
|
||||
initial_due_behavior:
|
||||
description: "ignore - records the first due signal without making the job Due. Prevents old historical cycle timestamps from triggering immediately."
|
||||
example: "ignore"
|
||||
mark_complete:
|
||||
description: "Set true for an accepted manual completion."
|
||||
example: true
|
||||
|
||||
sequence:
|
||||
- variables:
|
||||
mark_complete_bool: "{{ mark_complete | default(false) | bool }}"
|
||||
topic_safe: "{{ mqtt_topic | default('viewroad-status/jobs_tracker/' ~ job_id) }}"
|
||||
due_entity_safe: "{{ due_entity | default('') }}"
|
||||
due_state_safe: "{{ due_state | default('') }}"
|
||||
due_attribute_safe: "{{ due_attribute | default('') }}"
|
||||
completion_entities_safe: "{{ completion_entities | default([]) }}"
|
||||
completion_hold_hours_safe: "{{ completion_hold_hours | default(3) | float(3) }}"
|
||||
completion_hold_seconds: >-
|
||||
{{ (completion_hold_hours | default(3) | float(3) * 3600) | int(10800) }}
|
||||
initial_due_behavior_safe: "{{ initial_due_behavior | default('') | lower }}"
|
||||
now_ts: "{{ as_timestamp(now()) }}"
|
||||
stored_last_completed_ts: >-
|
||||
{% set value = state_attr(state_entity, 'last_completed_ts') %}
|
||||
{{ 0 if value in [none, '', 'unknown', 'unavailable'] else value | float(0) }}
|
||||
stored_once_due_ts: >-
|
||||
{% set value = state_attr(state_entity, 'once_due_ts') %}
|
||||
{{ 0 if value in [none, '', 'unknown', 'unavailable'] else value | float(0) }}
|
||||
stored_once_pending: >-
|
||||
{{ state_attr(state_entity, 'once_pending') | default(false, true) | bool }}
|
||||
stored_due_signal_active: >-
|
||||
{{ state_attr(state_entity, 'once_due_signal_active') | default(false, true) | bool }}
|
||||
stored_due_signal_ts: >-
|
||||
{% set value = state_attr(state_entity, 'once_due_signal_ts') %}
|
||||
{{ 0 if value in [none, '', 'unknown', 'unavailable'] else value | float(0) }}
|
||||
|
||||
- variables:
|
||||
due_uses_state: >-
|
||||
{{ due_state_safe not in ['', none] and due_attribute_safe in ['', none] }}
|
||||
due_uses_attribute: "{{ due_attribute_safe not in ['', none] }}"
|
||||
due_signal_active: >-
|
||||
{{
|
||||
due_uses_state | bool
|
||||
and due_entity_safe != ''
|
||||
and is_state(due_entity_safe, due_state_safe)
|
||||
}}
|
||||
current_due_signal_ts: >-
|
||||
{% if due_entity_safe == '' %}
|
||||
0
|
||||
{% elif due_uses_attribute | bool %}
|
||||
{{ state_attr(due_entity_safe, due_attribute_safe) | float(0) }}
|
||||
{% elif due_entity_safe.startswith('event.') %}
|
||||
{{ as_timestamp(states(due_entity_safe), 0) }}
|
||||
{% else %}
|
||||
{% set source = states[due_entity_safe] %}
|
||||
{{ as_timestamp(source.last_changed, 0) if source is not none else 0 }}
|
||||
{% endif %}
|
||||
due_signal_valid: >-
|
||||
{{
|
||||
due_entity_safe != ''
|
||||
and states(due_entity_safe) not in ['unknown', 'unavailable']
|
||||
and (current_due_signal_ts | float(0)) > 0
|
||||
}}
|
||||
|
||||
- variables:
|
||||
new_due_signal: >-
|
||||
{% if due_uses_state | bool %}
|
||||
{{
|
||||
due_signal_valid | bool
|
||||
and due_signal_active | bool
|
||||
and not (stored_due_signal_active | bool)
|
||||
and (initial_due_behavior_safe != 'ignore' or stored_due_signal_ts > 0)
|
||||
}}
|
||||
{% else %}
|
||||
{{
|
||||
due_signal_valid | bool
|
||||
and (current_due_signal_ts | float(0)) > (stored_due_signal_ts | float(0))
|
||||
and (current_due_signal_ts | float(0)) > (stored_last_completed_ts | float(0))
|
||||
and (initial_due_behavior_safe != 'ignore' or stored_due_signal_ts > 0)
|
||||
}}
|
||||
{% endif %}
|
||||
once_due_ts: >-
|
||||
{% if new_due_signal | bool %}
|
||||
{{ current_due_signal_ts if not (due_uses_state | bool) else now_ts }}
|
||||
{% else %}
|
||||
{{ stored_once_due_ts }}
|
||||
{% endif %}
|
||||
due_signal_active_after: >-
|
||||
{{ due_signal_active if due_uses_state | bool else false }}
|
||||
due_signal_ts_after: >-
|
||||
{{
|
||||
[stored_due_signal_ts | float(0), current_due_signal_ts | float(0)]
|
||||
| max
|
||||
}}
|
||||
pending_before_completion: >-
|
||||
{{ stored_once_pending | bool or new_due_signal | bool }}
|
||||
|
||||
- variables:
|
||||
completion_entity_matched: >-
|
||||
{% set ns = namespace(matches=false) %}
|
||||
{% if pending_before_completion | bool %}
|
||||
{% for completion in completion_entities_safe %}
|
||||
{% if completion is mapping %}
|
||||
{% set entity_id = completion.entity_id | default('') %}
|
||||
{% set to_state = completion.to_state | default('') %}
|
||||
{% else %}
|
||||
{% set entity_id = completion %}
|
||||
{% set to_state = '' %}
|
||||
{% endif %}
|
||||
{% if entity_id != '' and states(entity_id) not in ['unknown', 'unavailable'] %}
|
||||
{% if entity_id.startswith('event.') %}
|
||||
{% set signal_ts = as_timestamp(states(entity_id), 0) %}
|
||||
{% else %}
|
||||
{% set source = states[entity_id] %}
|
||||
{% set signal_ts = as_timestamp(source.last_changed, 0) if source is not none else 0 %}
|
||||
{% endif %}
|
||||
{% if signal_ts > (once_due_ts | float(0))
|
||||
and signal_ts > (stored_last_completed_ts | float(0))
|
||||
and (to_state == '' or states(entity_id) == to_state) %}
|
||||
{% set ns.matches = true %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{{ ns.matches }}
|
||||
complete_now: >-
|
||||
{{
|
||||
pending_before_completion | bool
|
||||
and (mark_complete_bool | bool or completion_entity_matched | bool)
|
||||
}}
|
||||
|
||||
- variables:
|
||||
once_pending: >-
|
||||
{{ pending_before_completion | bool and not (complete_now | bool) }}
|
||||
last_completed_ts: >-
|
||||
{{ now_ts if complete_now | bool else stored_last_completed_ts }}
|
||||
completion_belongs_to_occurrence: >-
|
||||
{{
|
||||
(once_due_ts | float(0)) > 0
|
||||
and (last_completed_ts | float(0)) >= (once_due_ts | float(0))
|
||||
}}
|
||||
completion_age_seconds: >-
|
||||
{% if (last_completed_ts | float(0)) > 0 %}
|
||||
{{ [(now_ts | float(0)) - (last_completed_ts | float(0)), 0] | max | int(0) }}
|
||||
{% else %}
|
||||
0
|
||||
{% endif %}
|
||||
completion_hold_active: >-
|
||||
{{
|
||||
not (once_pending | bool)
|
||||
and completion_belongs_to_occurrence | bool
|
||||
and (completion_age_seconds | int(0)) < (completion_hold_seconds | int(10800))
|
||||
}}
|
||||
|
||||
- variables:
|
||||
job_status: >-
|
||||
{% if once_pending | bool %}
|
||||
Due
|
||||
{% elif completion_hold_active | bool %}
|
||||
Complete
|
||||
{% else %}
|
||||
Not Due
|
||||
{% endif %}
|
||||
minutes_since_required: >-
|
||||
{% if once_pending | bool and (once_due_ts | float(0)) > 0 %}
|
||||
{{ (((now_ts | float(0)) - (once_due_ts | float(0))) / 60) | round(0) | int }}
|
||||
{% else %}
|
||||
0
|
||||
{% endif %}
|
||||
hours_since_required: >-
|
||||
{% if once_pending | bool and (once_due_ts | float(0)) > 0 %}
|
||||
{{ (((now_ts | float(0)) - (once_due_ts | float(0))) / 3600) | round(0) | int }}
|
||||
{% else %}
|
||||
0
|
||||
{% endif %}
|
||||
hours_since_completed: >-
|
||||
{% if (last_completed_ts | float(0)) > 0 %}
|
||||
{{ ((completion_age_seconds | int(0)) / 3600) | int(0) }}
|
||||
{% else %}
|
||||
unknown
|
||||
{% endif %}
|
||||
minutes_since_completed: >-
|
||||
{% if (last_completed_ts | float(0)) > 0 %}
|
||||
{{ ((completion_age_seconds | int(0)) / 60) | int(0) }}
|
||||
{% else %}
|
||||
unknown
|
||||
{% endif %}
|
||||
completion_elapsed_human: >-
|
||||
{% if (last_completed_ts | float(0)) <= 0 %}
|
||||
Never
|
||||
{% elif (completion_age_seconds | int(0)) < 60 %}
|
||||
Just now
|
||||
{% elif (completion_age_seconds | int(0)) < 3600 %}
|
||||
{% set minutes = ((completion_age_seconds | int(0)) / 60) | int(0) %}
|
||||
{{ minutes }} {{ 'min' if minutes == 1 else 'mins' }}
|
||||
{% else %}
|
||||
{% set hours = ((completion_age_seconds | int(0)) / 3600) | int(0) %}
|
||||
{{ hours }} {{ 'hour' if hours == 1 else 'hours' }}
|
||||
{% endif %}
|
||||
last_completed_time: >-
|
||||
{% if (last_completed_ts | float(0)) > 0 %}
|
||||
{{ (last_completed_ts | float(0)) | timestamp_custom('%H:%M', true) }}
|
||||
{% else %}
|
||||
Never
|
||||
{% endif %}
|
||||
last_completed_human: >-
|
||||
{% set ts = last_completed_ts | float(0) %}
|
||||
{% if ts <= 0 %}
|
||||
Never
|
||||
{% else %}
|
||||
{% set d = ts | timestamp_custom('%Y-%m-%d', true) %}
|
||||
{% set t = ts | timestamp_custom('%H:%M', true) %}
|
||||
{% set today = now().strftime('%Y-%m-%d') %}
|
||||
{% set yesterday = (as_timestamp(today_at('00:00')) - 86400) | timestamp_custom('%Y-%m-%d', true) %}
|
||||
{% set completed_midnight = as_timestamp(strptime(d ~ ' 00:00:00', '%Y-%m-%d %H:%M:%S')) %}
|
||||
{% set days_ago = ((as_timestamp(today_at('00:00')) - completed_midnight) / 86400) | round(0) | int %}
|
||||
{% if d == today %}
|
||||
{{ t }}
|
||||
{% elif d == yesterday %}
|
||||
Yesterday @ {{ t }}
|
||||
{% else %}
|
||||
{{ days_ago }} days ago @ {{ t }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
next_due_human: >-
|
||||
{{ 'Due now' if once_pending | bool else 'Waiting for trigger' }}
|
||||
completed_for_due_ts: >-
|
||||
{% if (last_completed_ts | float(0)) >= (once_due_ts | float(0))
|
||||
and (once_due_ts | float(0)) > 0 %}
|
||||
{{ once_due_ts | float(0) }}
|
||||
{% else %}
|
||||
0
|
||||
{% endif %}
|
||||
|
||||
- action: mqtt.publish
|
||||
data:
|
||||
topic: "{{ topic_safe }}"
|
||||
retain: true
|
||||
qos: 1
|
||||
payload: >-
|
||||
{{
|
||||
{
|
||||
"job_id": job_id,
|
||||
"job_name": job_name,
|
||||
"repeat": "once",
|
||||
"repeat_type": "once",
|
||||
"due_time": "",
|
||||
"due_weekday": 0,
|
||||
"due_day": 0,
|
||||
"due_entity": due_entity_safe,
|
||||
"due_state": due_state_safe,
|
||||
"due_attribute": due_attribute_safe,
|
||||
"completion_hold_hours": completion_hold_hours_safe | float(3),
|
||||
"initial_due_behavior": initial_due_behavior_safe,
|
||||
"state": job_status | trim,
|
||||
"complete": completion_hold_active | bool,
|
||||
"last_completed_ts": last_completed_ts | float(0),
|
||||
"last_completed_time": last_completed_time,
|
||||
"last_completed_human": last_completed_human,
|
||||
"previous_period_start_ts": 0,
|
||||
"previous_due_ts": 0,
|
||||
"previous_completed": false,
|
||||
"previous_missed": false,
|
||||
"previous_missed_active": false,
|
||||
"missed": false,
|
||||
"completion_window_start": "",
|
||||
"completion_window_end": "",
|
||||
"has_completion_window": false,
|
||||
"completion_window_missed": false,
|
||||
"current_completion_window_start_ts": 0,
|
||||
"current_completion_window_end_ts": 0,
|
||||
"completed_for_period_ts": completed_for_due_ts | float(0),
|
||||
"completed_for_due_ts": completed_for_due_ts | float(0),
|
||||
"current_period_start_ts": once_due_ts | float(0),
|
||||
"next_period_start_ts": 0,
|
||||
"current_due_ts": once_due_ts | float(0),
|
||||
"next_due_ts": 0,
|
||||
"display_due_ts": (once_due_ts | float(0)) if once_pending | bool else 0,
|
||||
"next_due_human": next_due_human,
|
||||
"due_started": once_pending | bool,
|
||||
"overdue_started": false,
|
||||
"minutes_since_required": minutes_since_required | int(0),
|
||||
"hours_since_required": hours_since_required | int(0),
|
||||
"overdue_after_minutes": 0,
|
||||
"overdue_minutes": 0,
|
||||
"overdue_hours": 0,
|
||||
"hours_since_completed": hours_since_completed,
|
||||
"minutes_since_completed": minutes_since_completed,
|
||||
"completion_elapsed_human": completion_elapsed_human,
|
||||
"once_pending": once_pending | bool,
|
||||
"once_due_ts": once_due_ts | float(0),
|
||||
"once_due_signal_active": due_signal_active_after | bool,
|
||||
"once_due_signal_ts": due_signal_ts_after | float(0),
|
||||
"last_updated_local": (now_ts | float(0)) | timestamp_custom('%Y-%m-%d %H:%M:%S', true),
|
||||
"mqtt_topic": topic_safe
|
||||
} | tojson
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user