################################################################################
# PACKAGE: Appliance Monitor Processor
################################################################################
#
# Version: 1.3
# Date: 2026-07-11
#
# Purpose:
#   Shared appliance power monitoring processor.
#
#   This script is called by appliances_status_monitoring.yaml. It reads the
#   previous appliance monitor state from the appliance monitor MQTT sensor,
#   calculates the next state and cycle totals, publishes retained JSON state
#   back to MQTT, and calls the View Road announcement router when a cycle
#   finishes.
#
# Design:
#   - One shared processing script.
#   - MQTT retained JSON state per appliance.
#   - One MQTT sensor per appliance.
#   - Timestamp-based energy and cost calculation.
#   - Rolling average state detection.
#   - Completion announcements through script.view_road_announcement.
#
# Default values:
#   debug_flow: false
#   resolution: 7
#   standby_power: 2
#   operating_power_min: 12
#   paid_tariff_start: "00:00"
#   paid_tariff_end: "21:00"
#   paid_tariff_cost: 0.2023
#   offpeak_tariff_cost: 0.0
#   max_sample_gap_seconds: 900
#   min_cycle_notify_seconds: 300
#
# States:
#   Off
#   Standby
#   Operating
#   Finished
#
# Notes:
#   Version 1.3:
#     - Updated completion announcements to support all current View Road
#       announcement router channels.
#     - Added pushover_kim to the default appliance completion channels.
#     - Added mqtt_view_rd_feed support by passing mqtt_feed to the announcement
#       router.
#     - Added led_matrix_2 support.
#     - Added optional announcement_enabled and announcement_payload fields.
#     - Kept existing field names for backwards compatibility.
#
#   Version 1.2:
#     - Added min_cycle_notify_seconds.
#     - Finished cycles shorter than this are still recorded, but do not send
#       notifications.
#     - Added MQTT attributes showing whether notification was allowed or
#       suppressed.
#
#   Version 1.1:
#     - Accepts lower-case retained states from previous package versions, such
#       as standby, operating, and finished.
#
################################################################################
# FINISHED ANNOUNCEMENT
################################################################################
#
# The finishing script calls:
#
#   script.view_road_announcement
#
# Current View Road announcement router channels:
#   pushover_zorruno
#   pushover_kim
#   sony_tv_lounge
#   lounge_google_home_voice
#   google_home_voice
#   lounge_touchscreen_voice
#   mqtt_view_rd_feed
#   led_matrix_1
#   led_matrix_2
#
# Notes:
#   - google_home_voice is an alias for lounge_google_home_voice.
#   - lounge_touchscreen_voice is currently a placeholder route in the router.
#   - mqtt_view_rd_feed requires mqtt_feed to be populated.
#   - led_matrix_1 and led_matrix_2 use short_announcement and indicator.
#
# Announcements can use:
#   announcement_enabled
#   announcement_channels
#   title
#   short_announcement
#   long_announcement
#   announcement_payload
#   payload
#   matrix_indicator
#   mqtt_feed
#
################################################################################

script:
  appliance_monitor_process_sample:
    alias: Appliance Monitor - Process Sample
    icon: mdi:meter-electric
    mode: queued
    max: 50

    fields:
      appliance_id:
        name: Appliance ID
        description: Short unique ID, for example washing_machine.
        selector:
          text:

      appliance_name:
        name: Appliance Name
        description: Human readable appliance name.
        selector:
          text:

      appliance_action:
        name: Appliance Action
        description: Human readable action, for example washing.
        selector:
          text:

      power_w:
        name: Power W
        description: Current instantaneous appliance power in watts.
        selector:
          number:
            min: 0
            max: 10000
            step: 0.1
            unit_of_measurement: W

      monitor_sensor:
        name: Monitor Sensor
        description: MQTT monitor sensor used to read previous state.
        selector:
          entity:
            domain: sensor

      state_topic:
        name: MQTT State Topic
        description: Retained MQTT topic used to publish monitor state JSON.
        selector:
          text:

      min_cycle_notify_seconds:
        name: Minimum Cycle Notify Seconds
        description: Finished cycles shorter than this are recorded but do not send notifications.
        default: 300
        selector:
          number:
            min: 0
            max: 86400
            step: 1
            unit_of_measurement: s

      announcement_enabled:
        name: Announcement Enabled
        description: Whether a valid finished cycle should send an announcement.
        default: true
        selector:
          boolean:

      announcement_channels:
        name: Announcement Channels
        description: Comma-separated View Road announcement router channels.
        default: "pushover_zorruno, pushover_kim, sony_tv_lounge, lounge_google_home_voice, mqtt_view_rd_feed, led_matrix_1"
        selector:
          text:

      title:
        name: Announcement Title
        description: Announcement title passed to the View Road announcement router.
        selector:
          text:

      short_announcement:
        name: Short Announcement
        description: Short announcement text for TV, voice and LED matrix.
        selector:
          text:

      long_announcement:
        name: Long Announcement
        description: Long announcement text for Pushover and detailed channels.
        selector:
          text:
            multiline: true

      announcement_payload:
        name: Announcement Payload
        description: Optional payload passed to the announcement router.
        selector:
          text:

      matrix_indicator:
        name: Matrix Indicator
        description: LED matrix indicator data.
        example: "wash,1,15,10,6,5,1,0"
        selector:
          text:

      mqtt_feed:
        name: MQTT Activity Feed
        description: Activity feed topic suffix used by mqtt_view_rd_feed.
        example: "washing_machine"
        selector:
          text:

    sequence:
      ##########################################################################
      # CORE DEFAULTS AND PASSED VALUES
      ##########################################################################

      - variables:
          am_appliance_id: "{{ appliance_id | default('appliance', true) | string }}"
          am_appliance_name: "{{ appliance_name | default('Appliance', true) | string }}"
          am_appliance_action: "{{ appliance_action | default('running', true) | string }}"
          am_power_w: "{{ power_w | default(0, true) | float(0) }}"
          am_monitor_sensor: "{{ monitor_sensor | default('', true) | string }}"
          am_state_topic: "{{ state_topic | default('', true) | string }}"

          am_debug_flow: "{{ debug_flow | default(false, true) | bool(false) }}"
          am_resolution: "{{ resolution | default(7, true) | int(7) }}"
          am_standby_power: "{{ standby_power | default(2, true) | float(2) }}"
          am_operating_power_min: "{{ operating_power_min | default(12, true) | float(12) }}"

          am_paid_tariff_start: "{{ paid_tariff_start | default('00:00', true) | string }}"
          am_paid_tariff_end: "{{ paid_tariff_end | default('21:00', true) | string }}"
          am_paid_tariff_cost: "{{ paid_tariff_cost | default(0.2023, true) | float(0.2023) }}"
          am_offpeak_tariff_cost: "{{ offpeak_tariff_cost | default(0.0, true) | float(0.0) }}"

          am_max_sample_gap_seconds: "{{ max_sample_gap_seconds | default(900, true) | int(900) }}"
          am_min_cycle_notify_seconds: "{{ min_cycle_notify_seconds | default(300, true) | int(300) }}"

      ##########################################################################
      # ANNOUNCEMENT DEFAULTS AND PASSED VALUES
      ##########################################################################
      #
      # These values are passed to script.view_road_announcement when an
      # appliance cycle successfully finishes.
      #
      # Current supported router channels:
      #   pushover_zorruno
      #   pushover_kim
      #   sony_tv_lounge
      #   lounge_google_home_voice
      #   google_home_voice
      #   lounge_touchscreen_voice
      #   mqtt_view_rd_feed
      #   led_matrix_1
      #   led_matrix_2
      #
      # Notes:
      #   - google_home_voice is an alias for lounge_google_home_voice.
      #   - lounge_touchscreen_voice is currently a placeholder route.
      #   - mqtt_view_rd_feed requires mqtt_feed to be populated.
      #
      ##########################################################################

      - variables:
          am_announcement_enabled: "{{ announcement_enabled | default(true, true) | bool(true) }}"

          am_default_announcement_channels: >-
            pushover_zorruno, pushover_kim, sony_tv_lounge, lounge_google_home_voice, mqtt_view_rd_feed, led_matrix_1

          am_announcement_channels: >-
            {{ announcement_channels | default(am_default_announcement_channels, true) | string }}

          am_short_announcement: >-
            {{ short_announcement | default((am_appliance_action | title) ~ ' complete', true) | string }}

          am_long_announcement_override: >-
            {{ long_announcement | default('', true) | string }}

          am_announcement_title: >-
            {{ title | default(am_appliance_name ~ ' Notification', true) | string }}

          am_announcement_payload: >-
            {% if announcement_payload is defined %}
              {{ announcement_payload }}
            {% elif payload is defined %}
              {{ payload }}
            {% else %}
              {{ '' }}
            {% endif %}

          am_matrix_indicator: >-
            {{ matrix_indicator | default('', true) | string }}

          am_mqtt_feed: >-
            {{ mqtt_feed | default(am_appliance_id, true) | string }}

      ##########################################################################
      # PREVIOUS STATE FROM MQTT MONITOR SENSOR ATTRIBUTES
      ##########################################################################

      - variables:
          am_now_ts: "{{ as_timestamp(now()) }}"

          am_previous_state: >-
            {%- set s = states(am_monitor_sensor) | string | lower | trim -%}
            {{ {'operating': 'Operating', 'finished': 'Finished', 'standby': 'Standby', 'off': 'Off'}.get(s, 'Off') }}

          am_last_power_w: "{{ state_attr(am_monitor_sensor, 'last_power_w') | float(am_power_w) }}"
          am_last_sample_ts: "{{ state_attr(am_monitor_sensor, 'last_sample_ts') | float(0) }}"

          am_existing_cycle_start_ts: "{{ state_attr(am_monitor_sensor, 'cycle_start_ts') | float(0) }}"
          am_existing_cycle_stop_ts: "{{ state_attr(am_monitor_sensor, 'cycle_stop_ts') | float(0) }}"

          am_existing_cycle_energy_kwh: "{{ state_attr(am_monitor_sensor, 'cycle_energy_kwh') | float(0) }}"
          am_existing_cycle_paid_energy_kwh: "{{ state_attr(am_monitor_sensor, 'cycle_paid_energy_kwh') | float(0) }}"
          am_existing_cycle_free_energy_kwh: "{{ state_attr(am_monitor_sensor, 'cycle_free_energy_kwh') | float(0) }}"
          am_existing_cycle_cost: "{{ state_attr(am_monitor_sensor, 'cycle_cost') | float(0) }}"

          am_previous_recent_samples_json: >-
            {% set raw = state_attr(am_monitor_sensor, 'recent_samples_json') | string | trim %}
            {{ raw if raw.startswith('[') else '[]' }}

      ##########################################################################
      # ELAPSED TIME
      ##########################################################################

      - variables:
          am_elapsed_sec_raw: >-
            {% if am_last_sample_ts | float(0) > 0 %}
              {{ (am_now_ts | float(0)) - (am_last_sample_ts | float(0)) }}
            {% else %}
              0
            {% endif %}

          am_effective_elapsed_sec: >-
            {% set elapsed = am_elapsed_sec_raw | float(0) %}
            {% if elapsed <= 0 %}
              0
            {% elif elapsed > (am_max_sample_gap_seconds | int(900)) %}
              {{ am_max_sample_gap_seconds | int(900) }}
            {% else %}
              {{ elapsed }}
            {% endif %}

      ##########################################################################
      # ROLLING AVERAGE
      ##########################################################################

      - variables:
          am_previous_recent_samples_list: >-
            {% set raw_json = state_attr(am_monitor_sensor, 'recent_samples_json') %}
            {% set raw_list = state_attr(am_monitor_sensor, 'recent_samples') %}
            {% if raw_json is string %}
              {{ raw_json | from_json(default=[]) }}
            {% elif raw_json is iterable and raw_json is not string %}
              {{ raw_json | list }}
            {% elif raw_list is iterable and raw_list is not string %}
              {{ raw_list | list }}
            {% else %}
              []
            {% endif %}

          am_recent_samples_list: >-
            {% set old = am_previous_recent_samples_list %}
            {% set ns = namespace(items=[am_power_w | float(0)]) %}
            {% for item in old %}
              {% if loop.index < (am_resolution | int(7)) %}
                {% set ns.items = ns.items + [item | float(0)] %}
              {% endif %}
            {% endfor %}
            {{ ns.items }}

          am_recent_samples_json: >-
            {{ am_recent_samples_list | to_json }}

          am_average_power_w: >-
            {% set samples = am_recent_samples_list %}
            {% if samples | count > 0 %}
              {{ ((samples | sum) / (samples | count)) | round(3) }}
            {% else %}
              0
            {% endif %}

      ##########################################################################
      # STATE MACHINE
      ##########################################################################

      - variables:
          am_new_state: >-
            {% set avg = am_average_power_w | float(0) %}
            {% set standby = am_standby_power | float(2) %}
            {% set operate = am_operating_power_min | float(12) %}
            {% set prev = am_previous_state | string %}
            {% if prev == 'Operating' and avg < operate %}
              Finished
            {% elif avg < standby %}
              Off
            {% elif avg >= operate %}
              Operating
            {% elif avg >= standby and prev in ['Off', 'Finished'] %}
              Standby
            {% else %}
              {{ prev }}
            {% endif %}

          am_state_changed: "{{ (am_new_state | string) != (am_previous_state | string) }}"
          am_cycle_started: "{{ am_new_state == 'Operating' and am_previous_state != 'Operating' }}"
          am_cycle_finished: "{{ am_new_state == 'Finished' and am_previous_state == 'Operating' }}"

      ##########################################################################
      # ACCUMULATION WINDOW
      ##########################################################################

      - variables:
          am_accumulation_seconds: >-
            {% set elapsed = am_effective_elapsed_sec | float(0) %}
            {% if elapsed <= 0 %}
              0
            {% elif am_previous_state == 'Operating' and am_new_state == 'Operating' %}
              {{ elapsed }}
            {% elif am_previous_state != 'Operating' and am_new_state == 'Operating' %}
              {{ elapsed / 2 }}
            {% elif am_previous_state == 'Operating' and am_new_state == 'Finished' %}
              {{ elapsed / 2 }}
            {% else %}
              0
            {% endif %}

          am_accumulation_average_power_w: >-
            {% set last = am_last_power_w | float(0) %}
            {% set current = am_power_w | float(0) %}
            {% set operate = am_operating_power_min | float(12) %}
            {% if am_previous_state == 'Operating' and am_new_state == 'Operating' %}
              {{ ((last + current) / 2) | round(3) }}
            {% elif am_previous_state != 'Operating' and am_new_state == 'Operating' %}
              {{ ((operate + current) / 2) | round(3) }}
            {% elif am_previous_state == 'Operating' and am_new_state == 'Finished' %}
              {{ ((last + operate) / 2) | round(3) }}
            {% else %}
              0
            {% endif %}

          am_accumulation_start_ts: >-
            {% set elapsed = am_effective_elapsed_sec | float(0) %}
            {% set accum = am_accumulation_seconds | float(0) %}
            {% if accum <= 0 %}
              {{ am_now_ts | float(0) }}
            {% elif am_previous_state == 'Operating' and am_new_state == 'Operating' %}
              {{ (am_now_ts | float(0)) - elapsed }}
            {% elif am_previous_state != 'Operating' and am_new_state == 'Operating' %}
              {{ (am_now_ts | float(0)) - accum }}
            {% elif am_previous_state == 'Operating' and am_new_state == 'Finished' %}
              {{ (am_now_ts | float(0)) - elapsed }}
            {% else %}
              {{ am_now_ts | float(0) }}
            {% endif %}

          am_accumulation_stop_ts: >-
            {% set elapsed = am_effective_elapsed_sec | float(0) %}
            {% set accum = am_accumulation_seconds | float(0) %}
            {% if accum <= 0 %}
              {{ am_now_ts | float(0) }}
            {% elif am_previous_state == 'Operating' and am_new_state == 'Operating' %}
              {{ am_now_ts | float(0) }}
            {% elif am_previous_state != 'Operating' and am_new_state == 'Operating' %}
              {{ am_now_ts | float(0) }}
            {% elif am_previous_state == 'Operating' and am_new_state == 'Finished' %}
              {{ (am_now_ts | float(0)) - elapsed + accum }}
            {% else %}
              {{ am_now_ts | float(0) }}
            {% endif %}

          am_interval_mid_ts: >-
            {% set start = am_accumulation_start_ts | float(0) %}
            {% set accum = am_accumulation_seconds | float(0) %}
            {% if accum > 0 %}
              {{ start + (accum / 2) }}
            {% else %}
              {{ am_now_ts | float(0) }}
            {% endif %}

      ##########################################################################
      # TARIFF CALCULATION
      ##########################################################################

      - variables:
          am_paid_start_minutes: >-
            {% set p = (am_paid_tariff_start | string).split(':') %}
            {% set hh = p[0] if p | count > 0 else 0 %}
            {% set mm = p[1] if p | count > 1 else 0 %}
            {{ (hh | int(0)) * 60 + (mm | int(0)) }}

          am_paid_end_minutes: >-
            {% set p = (am_paid_tariff_end | string).split(':') %}
            {% set hh = p[0] if p | count > 0 else 0 %}
            {% set mm = p[1] if p | count > 1 else 0 %}
            {{ (hh | int(0)) * 60 + (mm | int(0)) }}

          am_interval_mid_minutes: >-
            {% set h = am_interval_mid_ts | float(0) | timestamp_custom('%H', true) | int(0) %}
            {% set m = am_interval_mid_ts | float(0) | timestamp_custom('%M', true) | int(0) %}
            {{ h * 60 + m }}

          am_is_paid_interval: >-
            {% set start = am_paid_start_minutes | int(0) %}
            {% set end = am_paid_end_minutes | int(0) %}
            {% set mid = am_interval_mid_minutes | int(0) %}
            {% if start == end %}
              true
            {% elif start < end %}
              {{ mid >= start and mid < end }}
            {% else %}
              {{ mid >= start or mid < end }}
            {% endif %}

          am_tariff_rate: >-
            {% if am_is_paid_interval | bool(false) %}
              {{ am_paid_tariff_cost | float(0.2023) }}
            {% else %}
              {{ am_offpeak_tariff_cost | float(0.0) }}
            {% endif %}

      ##########################################################################
      # INTERVAL ENERGY AND COST
      ##########################################################################

      - variables:
          am_interval_energy_kwh: >-
            {{
              (
                (am_accumulation_average_power_w | float(0))
                * (am_accumulation_seconds | float(0))
                / 3600000
              ) | round(6)
            }}

          am_interval_cost: >-
            {{ ((am_interval_energy_kwh | float(0)) * (am_tariff_rate | float(0))) | round(6) }}

          am_should_accumulate: "{{ (am_accumulation_seconds | float(0)) > 0 }}"

      ##########################################################################
      # CYCLE TOTAL BASES
      ##########################################################################

      - variables:
          am_cycle_energy_base_kwh: >-
            {% if am_cycle_started | bool(false) %}
              0
            {% else %}
              {{ am_existing_cycle_energy_kwh | float(0) }}
            {% endif %}

          am_cycle_paid_energy_base_kwh: >-
            {% if am_cycle_started | bool(false) %}
              0
            {% else %}
              {{ am_existing_cycle_paid_energy_kwh | float(0) }}
            {% endif %}

          am_cycle_free_energy_base_kwh: >-
            {% if am_cycle_started | bool(false) %}
              0
            {% else %}
              {{ am_existing_cycle_free_energy_kwh | float(0) }}
            {% endif %}

          am_cycle_cost_base: >-
            {% if am_cycle_started | bool(false) %}
              0
            {% else %}
              {{ am_existing_cycle_cost | float(0) }}
            {% endif %}

      ##########################################################################
      # CYCLE TOTALS
      ##########################################################################

      - variables:
          am_cycle_energy_new_kwh: >-
            {% set add_kwh = am_interval_energy_kwh | float(0) if am_should_accumulate | bool(false) else 0 %}
            {{ ((am_cycle_energy_base_kwh | float(0)) + add_kwh) | round(6) }}

          am_cycle_cost_new: >-
            {% set add_cost = am_interval_cost | float(0) if am_should_accumulate | bool(false) else 0 %}
            {{ ((am_cycle_cost_base | float(0)) + add_cost) | round(6) }}

          am_cycle_paid_energy_new_kwh: >-
            {% set add_kwh = am_interval_energy_kwh | float(0) if (am_should_accumulate | bool(false) and am_is_paid_interval | bool(false)) else 0 %}
            {{ ((am_cycle_paid_energy_base_kwh | float(0)) + add_kwh) | round(6) }}

          am_cycle_free_energy_new_kwh: >-
            {% set add_kwh = am_interval_energy_kwh | float(0) if (am_should_accumulate | bool(false) and not (am_is_paid_interval | bool(false))) else 0 %}
            {{ ((am_cycle_free_energy_base_kwh | float(0)) + add_kwh) | round(6) }}

      ##########################################################################
      # CYCLE TIMES
      ##########################################################################

      - variables:
          am_cycle_start_ts_new: >-
            {% if am_cycle_started | bool(false) %}
              {{ am_accumulation_start_ts | float(0) }}
            {% elif am_existing_cycle_start_ts | float(0) > 0 %}
              {{ am_existing_cycle_start_ts | float(0) }}
            {% else %}
              {{ am_now_ts | float(0) }}
            {% endif %}

          am_cycle_stop_ts_new: >-
            {% if am_cycle_finished | bool(false) %}
              {{ am_accumulation_stop_ts | float(0) }}
            {% elif am_cycle_started | bool(false) %}
              {{ am_accumulation_start_ts | float(0) }}
            {% elif am_existing_cycle_stop_ts | float(0) > 0 %}
              {{ am_existing_cycle_stop_ts | float(0) }}
            {% else %}
              {{ am_now_ts | float(0) }}
            {% endif %}

          am_cycle_duration_seconds_new: >-
            {% set start = am_cycle_start_ts_new | float(0) %}
            {% if am_new_state == 'Operating' %}
              {% set stop = am_now_ts | float(0) %}
            {% else %}
              {% set stop = am_cycle_stop_ts_new | float(0) %}
            {% endif %}
            {% if start > 0 and stop > start %}
              {{ (stop - start) | int(0) }}
            {% else %}
              0
            {% endif %}

      ##########################################################################
      # FORMATTED VALUES
      ##########################################################################

      - variables:
          am_cycle_energy_formatted: >-
            {% set kwh = am_cycle_energy_new_kwh | float(0) %}
            {% if kwh >= 1 %}
              {{ '%.1f' | format(kwh) }}kWh
            {% elif kwh < 0.001 %}
              <1Wh
            {% else %}
              {{ '%.1f' | format(kwh * 1000) }}Wh
            {% endif %}

          am_cycle_cost_formatted: >-
            {% set val = am_cycle_cost_new | float(0) %}
            {% if val < 0.01 %}
              <1c
            {% elif val < 1 %}
              {{ (val * 100) | round(0) | int(0) }}c
            {% else %}
              ${{ '%.2f' | format(val) }}
            {% endif %}

          am_cycle_offpeak_savings: >-
            {% set val = ((am_cycle_energy_new_kwh | float(0)) * (am_paid_tariff_cost | float(0.2023))) - (am_cycle_cost_new | float(0)) %}
            {{ ([0, val] | max) | round(4) }}

          am_cycle_offpeak_savings_formatted: >-
            {% set val = am_cycle_offpeak_savings | float(0) %}
            {% if val < 0.01 %}
              <1c
            {% elif val < 1 %}
              {{ (val * 100) | round(0) | int(0) }}c
            {% else %}
              ${{ '%.2f' | format(val) }}
            {% endif %}

          am_cycle_duration_formatted: >-
            {% set d = am_cycle_duration_seconds_new | int(0) %}
            {% set h = (d // 3600) | int(0) %}
            {% set m = ((d % 3600) // 60) | int(0) %}
            {% if h < 1 %}
              {{ '%02dmin' | format(m) }}
            {% else %}
              {{ '%02dh %02dmin' | format(h, m) }}
            {% endif %}

          am_cycle_start_time_formatted: >-
            {{ am_cycle_start_ts_new | float(0) | timestamp_custom('%H:%M', true) }}

          am_cycle_stop_time_formatted: >-
            {{ am_cycle_stop_ts_new | float(0) | timestamp_custom('%H:%M', true) }}

          am_cycle_start_local: >-
            {{ am_cycle_start_ts_new | float(0) | timestamp_custom('%Y-%m-%d %H:%M:%S', true) }}

          am_cycle_stop_local: >-
            {{ am_cycle_stop_ts_new | float(0) | timestamp_custom('%Y-%m-%d %H:%M:%S', true) }}

          am_last_sample_local: >-
            {{ am_now_ts | float(0) | timestamp_custom('%Y-%m-%d %H:%M:%S', true) }}

      ##########################################################################
      # SUMMARY STRINGS
      ##########################################################################

      - variables:
          am_summary_short: >-
            Your {{ am_appliance_action }} is complete.

          am_summary: >-
            Your {{ am_appliance_action }} is complete.
            It started at {{ am_cycle_start_time_formatted | trim }},
            finished at {{ am_cycle_stop_time_formatted | trim }},
            and used {{ am_cycle_energy_formatted | trim }},
            taking {{ am_cycle_duration_formatted | trim }}
            at a cost of {{ am_cycle_cost_formatted | trim }}
            (Off-peak savings of {{ am_cycle_offpeak_savings_formatted | trim }}).

          am_last_summary_new: >-
            {% if am_cycle_started | bool(false) %}
              Cycle running
            {% elif am_cycle_finished | bool(false) %}
              {{ am_summary }}
            {% else %}
              {{ state_attr(am_monitor_sensor, 'last_summary') | default('No cycle recorded', true) }}
            {% endif %}

          am_long_announcement: >-
            {% if am_long_announcement_override | trim | length > 0 %}
              {{ am_long_announcement_override }}
            {% else %}
              {{ am_summary }}
            {% endif %}

      ##########################################################################
      # NOTIFICATION DECISION
      ##########################################################################
      #
      # Very short cycles are still recorded, but no announcement is sent.
      # Default minimum is 300 seconds / 5 minutes.
      #
      ##########################################################################

      - variables:
          am_cycle_notify_allowed: >-
            {{
              am_cycle_finished | bool(false)
              and (am_cycle_duration_seconds_new | int(0)) >= (am_min_cycle_notify_seconds | int(300))
            }}

          am_cycle_announcement_allowed: >-
            {{
              am_cycle_notify_allowed | bool(false)
              and am_announcement_enabled | bool(true)
            }}

          am_cycle_notification_suppressed: >-
            {{
              am_cycle_finished | bool(false)
              and not (am_cycle_notify_allowed | bool(false))
            }}

          am_cycle_notification_suppressed_reason: >-
            {% if am_cycle_notification_suppressed | bool(false) %}
              Cycle was shorter than {{ am_min_cycle_notify_seconds | int(300) }} seconds
            {% elif am_cycle_notify_allowed | bool(false) and not (am_announcement_enabled | bool(true)) %}
              Announcement disabled for this appliance
            {% endif %}

      ##########################################################################
      # PUBLISH RETAINED MQTT STATE
      ##########################################################################

      - action: mqtt.publish
        data:
          topic: "{{ am_state_topic }}"
          retain: true
          qos: 0
          payload: >-
            {{
              dict(
                appliance_id=am_appliance_id,
                appliance_name=am_appliance_name,
                appliance_action=am_appliance_action,

                state=am_new_state,
                previous_state=am_previous_state,
                state_changed=am_state_changed | bool(false),

                power_w=am_power_w | float(0),
                last_power_w=am_power_w | float(0),
                previous_power_w=am_last_power_w | float(0),
                average_power_w=am_average_power_w | float(0),
                recent_samples=am_recent_samples_list,
                recent_samples_json=am_recent_samples_json,

                last_sample_ts=am_now_ts | float(0),
                last_sample_local=am_last_sample_local,

                cycle_start_ts=am_cycle_start_ts_new | float(0),
                cycle_stop_ts=am_cycle_stop_ts_new | float(0),
                cycle_start_local=am_cycle_start_local,
                cycle_stop_local=am_cycle_stop_local,

                cycle_duration_seconds=am_cycle_duration_seconds_new | int(0),
                cycle_duration_formatted=am_cycle_duration_formatted | trim,

                min_cycle_notify_seconds=am_min_cycle_notify_seconds | int(300),
                cycle_notify_allowed=am_cycle_notify_allowed | bool(false),
                cycle_announcement_allowed=am_cycle_announcement_allowed | bool(false),
                announcement_enabled=am_announcement_enabled | bool(true),
                announcement_channels=am_announcement_channels,
                mqtt_feed=am_mqtt_feed,
                cycle_notification_suppressed=am_cycle_notification_suppressed | bool(false),
                cycle_notification_suppressed_reason=am_cycle_notification_suppressed_reason | trim,

                cycle_energy_kwh=am_cycle_energy_new_kwh | float(0),
                cycle_paid_energy_kwh=am_cycle_paid_energy_new_kwh | float(0),
                cycle_free_energy_kwh=am_cycle_free_energy_new_kwh | float(0),
                cycle_energy_formatted=am_cycle_energy_formatted | trim,

                cycle_cost=am_cycle_cost_new | float(0),
                cycle_cost_formatted=am_cycle_cost_formatted | trim,

                cycle_offpeak_savings=am_cycle_offpeak_savings | float(0),
                cycle_offpeak_savings_formatted=am_cycle_offpeak_savings_formatted | trim,

                paid_tariff_start=am_paid_tariff_start,
                paid_tariff_end=am_paid_tariff_end,
                paid_tariff_cost=am_paid_tariff_cost | float(0),
                offpeak_tariff_cost=am_offpeak_tariff_cost | float(0),

                accumulation_seconds=am_accumulation_seconds | float(0),
                accumulation_average_power_w=am_accumulation_average_power_w | float(0),
                interval_energy_kwh=am_interval_energy_kwh | float(0),
                interval_cost=am_interval_cost | float(0),
                is_paid_interval=am_is_paid_interval | bool(false),

                summary_short=am_summary_short,
                summary=am_summary,
                last_summary=am_last_summary_new
              ) | to_json
            }}

      ##########################################################################
      # DEBUG LOGBOOK ENTRY
      ##########################################################################

      - if:
          - condition: template
            value_template: "{{ am_debug_flow | bool(false) }}"
        then:
          - action: logbook.log
            data:
              name: "Appliance Monitor"
              message: >-
                {{ am_appliance_name }}: {{ am_previous_state }} -> {{ am_new_state }},
                power {{ am_power_w | round(1) }}W,
                average {{ am_average_power_w | round(1) }}W,
                cycle {{ am_cycle_energy_new_kwh | round(3) }}kWh,
                cost {{ am_cycle_cost_new | round(4) }}

      ##########################################################################
      # SHORT CYCLE SUPPRESSION LOG
      ##########################################################################

      - if:
          - condition: template
            value_template: "{{ am_cycle_notification_suppressed | bool(false) }}"
        then:
          - action: logbook.log
            data:
              name: "Appliance Monitor"
              message: >-
                {{ am_appliance_name }} finished after
                {{ am_cycle_duration_formatted | trim }}, below the
                {{ am_min_cycle_notify_seconds | int(300) }} second notification
                threshold. No announcement was sent.

      ##########################################################################
      # ANNOUNCEMENT DISABLED LOG
      ##########################################################################

      - if:
          - condition: template
            value_template: >-
              {{
                am_cycle_notify_allowed | bool(false)
                and not (am_announcement_enabled | bool(true))
              }}
        then:
          - action: logbook.log
            data:
              name: "Appliance Monitor"
              message: >-
                {{ am_appliance_name }} finished after
                {{ am_cycle_duration_formatted | trim }}, but appliance
                completion announcements are disabled for this appliance.

      ##########################################################################
      # FINISHED ANNOUNCEMENT
      ##########################################################################
      # This calls script.view_road_announcement from the announcement router
      # package when a valid appliance cycle finishes.
      #
      # All supported router channels can be used through am_announcement_channels.
      #
      # mqtt_view_rd_feed requires:
      #   mqtt_feed
      #
      # led_matrix_1 and led_matrix_2 require:
      #   short_announcement
      #   indicator
      #
      ##########################################################################

      - if:
          - condition: template
            value_template: "{{ am_cycle_announcement_allowed | bool(false) }}"
        then:
          - action: script.view_road_announcement
            data:
              channels: "{{ am_announcement_channels }}"
              title: "{{ am_announcement_title }}"
              short_announcement: "{{ am_short_announcement }}"
              long_announcement: "{{ am_long_announcement }}"
              payload: "{{ am_announcement_payload }}"
              indicator: "{{ am_matrix_indicator }}"
              mqtt_feed: "{{ am_mqtt_feed }}"
