#:########################################################################################:#
# Jobs Tracker Job List Package                                                           #
#:########################################################################################:#
#
# TITLE:
#   Jobs Tracker Job List
#
# FILE:
#   jobs_tracker_jobs.yaml
#
# VERSION:
#   V2.4 2026-07-21
#
# PURPOSE:
#   Defines the job registry, MQTT Discovery entities, completion triggers and
#   summary output for the Jobs Tracker system.
#
# DEPENDS ON:
#   jobs_tracker_processor.yaml V1.6 or newer
#
# 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.
#
#   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
#   uses a brand-new physical button, event entity, sensor or other completion
#   entity, add that entity once to jobs_tracker_completion_entities.
#
#   MQTT Discovery is used for job sensors, complete binary sensors, manual
#   completion buttons, summary sensors and count sensors. This means adding a new
#   job does not require hand-writing new MQTT sensor YAML blocks.
#
# JOB STATES:
#   Not Due
#   Missed
#   Due
#   Overdue
#   Complete
#
# VERSION HISTORY:
#   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.
#     - Restored stale event timestamps are no longer accepted as completions.
#     - Discovery device firmware version updated to 2.4.
#
#   V2.3 2026-07-20
#     - All completion methods now obey the configured completion window.
#     - Physical completion entities, Home Assistant discovery buttons and direct
#       MQTT completion commands are ignored outside the job's window.
#     - Jobs without an explicit window continue to use the full-day default of
#       00:00 through 23:59.
#     - Discovery device firmware version updated to 2.3.
#
#   V2.2 2026-07-10
#     - Renamed Previous Missed to Missed.
#     - Windowed jobs now show Missed when the completion window ends.
#     - Dog feeding can show Missed after the morning/evening window, while still
#       resetting to Not Due / Due in the next daily period.
#     - Discovery device firmware version updated to 2.2.
#
#   V2.1 2026-07-08
#     - Fixed MQTT Discovery value_template strings by escaping inner templates.
#     - This prevents Home Assistant rendering value_json too early when publishing
#       discovery config.
#     - Discovery device firmware version updated to 2.1.
#     - Discovery automation now bootstraps job states after publishing discovery.
#
#   V2.0 2026-07-07
#     - Refactored around a single job registry block.
#     - Added per-job completion_entities.
#     - Added optional per-job completion windows.
#     - Dog feeding remains two separate daily jobs with one shared physical
#       button and separate completion windows.
#     - Replaced static MQTT sensor blocks with MQTT Discovery publishing.
#     - Added MQTT manual completion buttons for each job.
#     - Summary and counts now loop over the job registry.
#
# NOTES:
#   - due_weekday uses Python numbering:
#       Monday=0, Tuesday=1, Wednesday=2, Thursday=3,
#       Friday=4, Saturday=5, Sunday=6
#
#   - completion_window_start and completion_window_end are optional.
#     If omitted/commented, the completion entity is accepted at any time in
#     the current reset period.
#
#   - Dog feeding uses completion windows because both feeding jobs share the
#     same physical button.
#
#:########################################################################################:#

script:
  jobs_tracker_dispatch:
    alias: "Jobs Tracker - Dispatch"
    description: "Runs the generic processor against either all jobs or one completed job."
    mode: queued
    max: 20

    fields:
      update_all:
        description: "True to refresh all jobs."
        example: true

      job_id:
        description: "Job ID to mark complete."
        example: "dog_feeding_morning"

    sequence:
      #:####################################################################################:#
      # JOB REGISTRY                                                                         #
      #:####################################################################################:#
      #
      # Add or edit jobs here.
      #
      # To add a new job:
      #   1. Copy one full job block.
      #   2. Change job_id, job_name, schedule settings and completion_entities.
      #   3. If the completion entity is brand new, add it once to
      #      jobs_tracker_completion_entities below.
      #
      # Completion windows:
      #   - Leave commented out for normal jobs.
      #   - Uncomment and set for jobs sharing the same physical button.
      #
      # MQTT Discovery is generated from this list.

      - variables:
          jobs_tracker_jobs: &jobs_tracker_jobs
            - job_id: dog_feeding_morning
              job_name: "Dog Feeding Morning"
              repeat_type: daily
              due_time: "06:00"
              due_weekday: 0
              due_day: 1
              completion_entities:
                - event.jobs_dog_fed_button_x14bt_action
              completion_window_start: "00:00"
              completion_window_end: "11:59"
              icon: mdi:dog-side
              mqtt_topic: "viewroad-status/jobs_tracker/dog_feeding_morning"
              state_entity: "sensor.jobs_tracker_dog_feeding_morning"

            - job_id: dog_feeding_evening
              job_name: "Dog Feeding Evening"
              repeat_type: daily
              due_time: "17:00"
              due_weekday: 0
              due_day: 1
              completion_entities:
                - event.jobs_dog_fed_button_x14bt_action
              completion_window_start: "12:00"
              completion_window_end: "23:59"
              icon: mdi:dog-side
              mqtt_topic: "viewroad-status/jobs_tracker/dog_feeding_evening"
              state_entity: "sensor.jobs_tracker_dog_feeding_evening"

            - job_id: dog_depooping
              job_name: "Dog De-pooping"
              repeat_type: weekly
              due_time: "08:00"
              due_weekday: 4
              due_day: 1
              completion_entities:
                - event.jobs_dog_depooped_button_x15bt_action
              # completion_window_start: "00:00"
              # completion_window_end: "23:59"
              icon: mdi:emoticon-poop
              mqtt_topic: "viewroad-status/jobs_tracker/dog_depooping"
              state_entity: "sensor.jobs_tracker_dog_depooping"

            - job_id: dishwasher_unpacked
              job_name: "Dishwasher Unpacked"
              repeat_type: daily
              due_time: "15:30"
              due_weekday: 0
              due_day: 1
              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"
              repeat_type: daily
              due_time: "15:30"
              due_weekday: 0
              due_day: 1
              completion_entities:
                - event.kitchen_jobs_kitchen_completed_buttons_button_2
              # completion_window_start: "00:00"
              # completion_window_end: "23:59"
              icon: mdi:trash-can
              mqtt_topic: "viewroad-status/jobs_tracker/rubbish_removed"
              state_entity: "sensor.jobs_tracker_rubbish_removed"

          #:################################################################################:#
          # COMPLETION ENTITY TRIGGER LIST                                                   #
          #:################################################################################:#
          #
          # Add brand-new physical/action/sensor completion entities here.
          # If a new job reuses an entity already listed here, no change is needed.

          jobs_tracker_completion_entities: &jobs_tracker_completion_entities
            - event.jobs_dog_fed_button_x14bt_action
            - event.jobs_dog_depooped_button_x15bt_action
            - event.kitchen_jobs_kitchen_completed_buttons_button_1
            - event.kitchen_jobs_kitchen_completed_buttons_button_2

      - choose:
          #:################################################################################:#
          # Refresh all jobs                                                                 #
          #:################################################################################:#

          - conditions:
              - condition: template
                value_template: "{{ update_all | default(false) | bool }}"
            sequence:
              - repeat:
                  for_each: "{{ jobs_tracker_jobs }}"
                  sequence:
                    - action: script.jobs_tracker_update_job
                      data:
                        job_id: "{{ repeat.item.job_id }}"
                        job_name: "{{ repeat.item.job_name }}"
                        repeat_type: "{{ repeat.item.repeat_type }}"
                        due_time: "{{ repeat.item.due_time }}"
                        due_weekday: "{{ repeat.item.due_weekday }}"
                        due_day: "{{ repeat.item.due_day }}"
                        mqtt_topic: "{{ repeat.item.mqtt_topic }}"
                        state_entity: "{{ repeat.item.state_entity }}"
                        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                                                              #
        #:##################################################################################:#

        default:
          - repeat:
              for_each: "{{ jobs_tracker_jobs }}"
              sequence:
                - if:
                    - condition: template
                      value_template: "{{ repeat.item.job_id == job_id }}"
                  then:
                    - action: script.jobs_tracker_update_job
                      data:
                        job_id: "{{ repeat.item.job_id }}"
                        job_name: "{{ repeat.item.job_name }}"
                        repeat_type: "{{ repeat.item.repeat_type }}"
                        due_time: "{{ repeat.item.due_time }}"
                        due_weekday: "{{ repeat.item.due_weekday }}"
                        due_day: "{{ repeat.item.due_day }}"
                        mqtt_topic: "{{ repeat.item.mqtt_topic }}"
                        state_entity: "{{ repeat.item.state_entity }}"
                        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"

      - action: script.jobs_tracker_publish_summary

  jobs_tracker_route_completion:
    alias: "Jobs Tracker - Route Completion"
    description: "Routes a physical/sensor/MQTT completion action to matching jobs."
    mode: queued
    max: 20

    fields:
      completion_entity:
        description: "Entity ID that triggered completion."
        example: "event.jobs_dog_fed_button_x14bt_action"

      manual_job_id:
        description: "Job ID completed by MQTT/manual button."
        example: "dog_feeding_morning"

    sequence:
      - variables:
          jobs_tracker_jobs: *jobs_tracker_jobs
          completion_entity_safe: "{{ completion_entity | default('') }}"
          manual_job_id_safe: "{{ manual_job_id | default('') }}"
          now_minutes: "{{ (now().hour * 60) + now().minute }}"

      - repeat:
          for_each: "{{ jobs_tracker_jobs }}"
          sequence:
            - variables:
                completion_entities_safe: "{{ repeat.item.completion_entities | default([]) }}"
                window_start_safe: "{{ repeat.item.completion_window_start | default('00:00') }}"
                window_end_safe: "{{ repeat.item.completion_window_end | default('23:59') }}"

                window_start_minutes: >-
                  {% set t = window_start_safe.split(':') %}
                  {{ (t[0] | int(0) * 60) + (t[1] | int(0)) }}

                window_end_minutes: >-
                  {% set t = window_end_safe.split(':') %}
                  {{ (t[0] | int(23) * 60) + (t[1] | int(59)) }}

                within_completion_window: >-
                  {% set n = now_minutes | int(0) %}
                  {% set s = window_start_minutes | int(0) %}
                  {% set e = window_end_minutes | int(1439) %}
                  {% if s <= e %}
                    {{ n >= s and n <= e }}
                  {% else %}
                    {{ n >= s or n <= e }}
                  {% endif %}

                physical_entity_matches: >-
                  {{
                    completion_entity_safe != ''
                    and completion_entity_safe in completion_entities_safe
                  }}

                manual_job_matches: >-
                  {{
                    manual_job_id_safe != ''
                    and manual_job_id_safe == repeat.item.job_id
                  }}

                completion_source_matches: >-
                  {{
                    physical_entity_matches | bool
                    or manual_job_matches | bool
                  }}

                should_complete_job: >-
                  {{
                    completion_source_matches | bool
                    and within_completion_window | bool
                  }}

            - if:
                - condition: template
                  value_template: "{{ should_complete_job | bool }}"
              then:
                - action: script.jobs_tracker_dispatch
                  data:
                    job_id: "{{ repeat.item.job_id }}"

  jobs_tracker_publish_discovery:
    alias: "Jobs Tracker - Publish MQTT Discovery"
    description: "Publishes retained MQTT Discovery config for Jobs Tracker entities."
    mode: queued
    max: 5

    sequence:
      - variables:
          jobs_tracker_jobs: *jobs_tracker_jobs
          discovery_prefix: "homeassistant"
          device_identifiers:
            - viewroad_jobs_tracker
          device_name: "Jobs Tracker"
          device_manufacturer: "View Road Home Assistant"
          device_model: "Shared household jobs tracker"
          device_sw_version: "2.4"

          value_template_state: >-
            {% raw %}{{ value_json.state | default('unknown') }}{% endraw %}

          value_template_complete_count: >-
            {% raw %}{{ value_json.complete_count | default(0) }}{% endraw %}

          value_template_not_due_count: >-
            {% raw %}{{ value_json.not_due_count | default(0) }}{% endraw %}

          value_template_missed_count: >-
            {% raw %}{{ value_json.missed_count | default(value_json.previous_missed_count | default(0)) }}{% endraw %}

          value_template_due_count: >-
            {% raw %}{{ value_json.due_count | default(0) }}{% endraw %}

          value_template_overdue_count: >-
            {% raw %}{{ value_json.overdue_count | default(0) }}{% endraw %}

          value_template_total_count: >-
            {% raw %}{{ value_json.total_count | default(0) }}{% endraw %}

          value_template_job_complete: >-
            {% raw %}{{ 'ON' if value_json.state == 'Complete' else 'OFF' }}{% endraw %}

      #:####################################################################################:#
      # Summary sensor discovery                                                             #
      #:####################################################################################:#

      - action: mqtt.publish
        data:
          topic: "{{ discovery_prefix }}/sensor/jobs_tracker_summary/config"
          retain: true
          qos: 1
          payload: >-
            {{
              {
                "name": "Jobs Tracker - Summary",
                "unique_id": "jobs_tracker_summary",
                "object_id": "jobs_tracker_summary",
                "state_topic": "viewroad-status/jobs_tracker/summary",
                "value_template": value_template_state,
                "json_attributes_topic": "viewroad-status/jobs_tracker/summary",
                "icon": "mdi:clipboard-list",
                "device": {
                  "identifiers": device_identifiers,
                  "name": device_name,
                  "manufacturer": device_manufacturer,
                  "model": device_model,
                  "sw_version": device_sw_version
                }
              } | tojson
            }}

      # Remove old V2.1 discovery topic for the renamed count sensor.
      - action: mqtt.publish
        data:
          topic: "{{ discovery_prefix }}/sensor/jobs_tracker_previous_missed_count/config"
          retain: true
          qos: 1
          payload: ""

      #:####################################################################################:#
      # Count sensor discovery                                                               #
      #:####################################################################################:#

      - repeat:
          for_each:
            - count_id: complete_count
              name: "Jobs Tracker - Complete Count"
              icon: mdi:clipboard-check
              value_template: "{{ value_template_complete_count }}"

            - count_id: not_due_count
              name: "Jobs Tracker - Not Due Count"
              icon: mdi:clipboard-clock-outline
              value_template: "{{ value_template_not_due_count }}"

            - count_id: missed_count
              name: "Jobs Tracker - Missed Count"
              icon: mdi:clipboard-remove-outline
              value_template: "{{ value_template_missed_count }}"

            - count_id: due_count
              name: "Jobs Tracker - Due Count"
              icon: mdi:clipboard-alert
              value_template: "{{ value_template_due_count }}"

            - count_id: overdue_count
              name: "Jobs Tracker - Overdue Count"
              icon: mdi:clipboard-alert-outline
              value_template: "{{ value_template_overdue_count }}"

            - count_id: total_count
              name: "Jobs Tracker - Total Count"
              icon: mdi:clipboard-list-outline
              value_template: "{{ value_template_total_count }}"

          sequence:
            - action: mqtt.publish
              data:
                topic: "{{ discovery_prefix }}/sensor/jobs_tracker_{{ repeat.item.count_id }}/config"
                retain: true
                qos: 1
                payload: >-
                  {{
                    {
                      "name": repeat.item.name,
                      "unique_id": "jobs_tracker_" ~ repeat.item.count_id,
                      "object_id": "jobs_tracker_" ~ repeat.item.count_id,
                      "state_topic": "viewroad-status/jobs_tracker/summary",
                      "value_template": repeat.item.value_template,
                      "icon": repeat.item.icon,
                      "device": {
                        "identifiers": device_identifiers,
                        "name": device_name,
                        "manufacturer": device_manufacturer,
                        "model": device_model,
                        "sw_version": device_sw_version
                      }
                    } | tojson
                  }}

      #:####################################################################################:#
      # Per-job sensor, binary sensor and manual button discovery                            #
      #:####################################################################################:#

      - repeat:
          for_each: "{{ jobs_tracker_jobs }}"
          sequence:
            - action: mqtt.publish
              data:
                topic: "{{ discovery_prefix }}/sensor/jobs_tracker_{{ repeat.item.job_id }}/config"
                retain: true
                qos: 1
                payload: >-
                  {{
                    {
                      "name": "Jobs Tracker - " ~ repeat.item.job_name,
                      "unique_id": "jobs_tracker_" ~ repeat.item.job_id,
                      "object_id": "jobs_tracker_" ~ repeat.item.job_id,
                      "state_topic": repeat.item.mqtt_topic,
                      "value_template": value_template_state,
                      "json_attributes_topic": repeat.item.mqtt_topic,
                      "icon": repeat.item.icon | default("mdi:clipboard-check-outline"),
                      "device": {
                        "identifiers": device_identifiers,
                        "name": device_name,
                        "manufacturer": device_manufacturer,
                        "model": device_model,
                        "sw_version": device_sw_version
                      }
                    } | tojson
                  }}

            - action: mqtt.publish
              data:
                topic: "{{ discovery_prefix }}/binary_sensor/job_complete_{{ repeat.item.job_id }}/config"
                retain: true
                qos: 1
                payload: >-
                  {{
                    {
                      "name": "Job Complete - " ~ repeat.item.job_name,
                      "unique_id": "job_complete_" ~ repeat.item.job_id,
                      "object_id": "job_complete_" ~ repeat.item.job_id,
                      "state_topic": repeat.item.mqtt_topic,
                      "value_template": value_template_job_complete,
                      "payload_on": "ON",
                      "payload_off": "OFF",
                      "icon": repeat.item.icon | default("mdi:clipboard-check-outline"),
                      "device": {
                        "identifiers": device_identifiers,
                        "name": device_name,
                        "manufacturer": device_manufacturer,
                        "model": device_model,
                        "sw_version": device_sw_version
                      }
                    } | tojson
                  }}

            - action: mqtt.publish
              data:
                topic: "{{ discovery_prefix }}/button/jobs_tracker_complete_{{ repeat.item.job_id }}/config"
                retain: true
                qos: 1
                payload: >-
                  {{
                    {
                      "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,
                      "command_topic": "viewroad-commands/jobs_tracker/" ~ repeat.item.job_id ~ "/complete",
                      "payload_press": "PRESS",
                      "icon": repeat.item.icon | default("mdi:clipboard-check-outline"),
                      "device": {
                        "identifiers": device_identifiers,
                        "name": device_name,
                        "manufacturer": device_manufacturer,
                        "model": device_model,
                        "sw_version": device_sw_version
                      }
                    } | tojson
                  }}

  jobs_tracker_publish_summary:
    alias: "Jobs Tracker - Publish Summary"
    description: "Publishes retained MQTT JSON summary for all tracked jobs."
    mode: queued
    max: 5

    sequence:
      - variables:
          jobs_tracker_jobs: *jobs_tracker_jobs

      - action: mqtt.publish
        data:
          topic: "viewroad-status/jobs_tracker/summary"
          retain: true
          qos: 1
          payload: >-
            {% set ns = namespace(
              jobs=[],
              complete=0,
              not_due=0,
              missed=0,
              due=0,
              overdue=0,
              unknown=0
            ) %}

            {% for job in jobs_tracker_jobs %}
              {% set s = states(job.state_entity) %}
              {% if s == 'Complete' %}
                {% set ns.complete = ns.complete + 1 %}
              {% elif s == 'Not Due' %}
                {% set ns.not_due = ns.not_due + 1 %}
              {% elif s == 'Missed' %}
                {% set ns.missed = ns.missed + 1 %}
              {% elif s == 'Due' %}
                {% set ns.due = ns.due + 1 %}
              {% elif s == 'Overdue' %}
                {% set ns.overdue = ns.overdue + 1 %}
              {% else %}
                {% set ns.unknown = ns.unknown + 1 %}
              {% endif %}

              {% set ns.jobs = ns.jobs + [
                {
                  "job_id": job.job_id,
                  "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,
                  "completion_entities": job.completion_entities | default([]),
                  "completion_window_start": job.completion_window_start | default(""),
                  "completion_window_end": job.completion_window_end | default(""),
                  "mqtt_topic": job.mqtt_topic,
                  "state_entity": job.state_entity,
                  "next_due_human": state_attr(job.state_entity, 'next_due_human'),
                  "last_completed_human": state_attr(job.state_entity, 'last_completed_human'),
                  "missed": state_attr(job.state_entity, 'missed'),
                  "completion_window_missed": state_attr(job.state_entity, 'completion_window_missed'),
                  "previous_missed": state_attr(job.state_entity, 'previous_missed'),
                  "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')
                }
              ] %}
            {% endfor %}

            {% set parts = [] %}
            {% if ns.overdue > 0 %}
              {% set parts = parts + [ns.overdue ~ ' Overdue'] %}
            {% endif %}
            {% if ns.due > 0 %}
              {% set parts = parts + [ns.due ~ ' Due'] %}
            {% endif %}
            {% if ns.missed > 0 %}
              {% set parts = parts + [ns.missed ~ ' Missed'] %}
            {% endif %}
            {% if ns.not_due > 0 %}
              {% set parts = parts + [ns.not_due ~ ' Not Due'] %}
            {% endif %}

            {% if parts | count > 0 %}
              {% set summary_state = parts | join(', ') %}
            {% elif ns.complete == (jobs_tracker_jobs | count) %}
              {% set summary_state = 'All Complete' %}
            {% elif ns.unknown > 0 %}
              {% set summary_state = ns.unknown ~ ' Unknown' %}
            {% else %}
              {% set summary_state = 'Unknown' %}
            {% endif %}

            {{
              {
                "state": summary_state,
                "complete_count": ns.complete,
                "not_due_count": ns.not_due,
                "missed_count": ns.missed,
                "previous_missed_count": ns.missed,
                "due_count": ns.due,
                "overdue_count": ns.overdue,
                "unknown_count": ns.unknown,
                "total_count": jobs_tracker_jobs | count,
                "jobs": ns.jobs,
                "last_updated_local": now().strftime('%Y-%m-%d %H:%M:%S')
              } | tojson
            }}

#:########################################################################################:#
# AUTOMATIONS                                                                              #
#:########################################################################################:#

automation:
  - id: jobs_tracker_publish_discovery
    alias: "Jobs Tracker - Publish Discovery"
    description: "Publishes retained MQTT Discovery config and bootstraps Jobs Tracker state."
    mode: single

    triggers:
      - trigger: homeassistant
        event: start
        id: ha_start

      - trigger: time
        at: "03:05:00"
        id: daily_republish

    actions:
      - action: script.jobs_tracker_publish_discovery

      - variables:
          jobs_tracker_jobs: *jobs_tracker_jobs

      # Do not refresh until every retained timestamp has returned from MQTT.
      # Aborting on timeout protects valid retained data from being replaced by 0.
      - wait_template: >-
          {% set ns = namespace(ready=true) %}
          {% for job in jobs_tracker_jobs %}
            {% if not is_number(state_attr(job.state_entity, 'last_completed_ts')) %}
              {% set ns.ready = false %}
            {% endif %}
          {% endfor %}
          {{ ns.ready }}
        timeout: "00:01:30"
        continue_on_timeout: false

      # Recalculate each job only after retained state is confirmed ready.
      - action: script.jobs_tracker_dispatch
        data:
          update_all: true

  - id: jobs_tracker_refresh_all_jobs
    alias: "Jobs Tracker - Refresh All Jobs"
    description: "Refreshes derived due/overdue values for every job."
    mode: single

    triggers:
      - trigger: time_pattern
        minutes: "/1"
        id: tick

    variables:
      jobs_tracker_jobs: *jobs_tracker_jobs

    conditions:
      # Skip the minute tick until every retained timestamp is available.
      - condition: template
        value_template: >-
          {% set ns = namespace(ready=true) %}
          {% for job in jobs_tracker_jobs %}
            {% if not is_number(state_attr(job.state_entity, 'last_completed_ts')) %}
              {% set ns.ready = false %}
            {% endif %}
          {% endfor %}
          {{ ns.ready }}

    actions:
      - action: script.jobs_tracker_dispatch
        data:
          update_all: true

  - id: jobs_tracker_completion_actions
    alias: "Jobs Tracker - Completion Actions"
    description: "Marks jobs complete when physical/action/sensor entities change."
    mode: queued
    max: 20

    triggers:
      - trigger: state
        entity_id: *jobs_tracker_completion_entities

    conditions:
      - condition: template
        value_template: >-
          {% set valid_state = trigger.to_state is not none
            and trigger.to_state.state not in ['unknown', 'unavailable'] %}
          {% set event_entity = trigger.entity_id.startswith('event.') %}
          {% set event_ts = as_timestamp(trigger.to_state.state, 0)
            if valid_state and event_entity else 0 %}
          {% set event_age = as_timestamp(now()) - event_ts %}
          {{
            valid_state
            and (
              not event_entity
              or (event_ts > 0 and event_age >= 0 and event_age <= 30)
            )
          }}

    actions:
      - action: script.jobs_tracker_route_completion
        data:
          completion_entity: "{{ trigger.entity_id }}"

  - id: jobs_tracker_mqtt_manual_completion
    alias: "Jobs Tracker - MQTT Manual Completion"
    description: "Marks one job complete from its MQTT Discovery manual button."
    mode: queued
    max: 20

    triggers:
      - trigger: mqtt
        topic: "viewroad-commands/jobs_tracker/+/complete"

    actions:
      - variables:
          manual_job_id: "{{ trigger.topic.split('/')[2] }}"

      - action: script.jobs_tracker_route_completion
        data:
          manual_job_id: "{{ manual_job_id }}"
