New blueprint -- Message malformed: required key not provided

  1. What does “Message malformed: required key not provided @ data[‘action’][0][‘choose’][1][‘sequence’]” mean in English?
  2. Without a line number, how on earth do I find the error in a 731 line blueprint with lots of choose and sequence statements?
### ------------------------------------------------------------------------------------
###
###      HEATING X, Release 2
###      --------------------
###
### Release 1 (Feb 2023) had the following features
###   Controls one or more thermostats from a calendar
###   Allows temporary manual override
###   Optionally turns off thermostat if a door or window is opened,
###   Optionally turns off thermostat if the room is unoccupied for a while.
###
### Release 2 (Feb 2024) adds the following features
###   Away mode. Changes the heating of all rooms to the 'away temperature' setting when on, reverts to the usual schedules etc when off.
###   Background temperature can be specified to use when there is no active calendar event or room is unoccupied
###   TRV battery life saved by transmitting to TRV only when there is an actual change
###
### 06-Feb-23 | Andy Symons | Release 1. See feature list above
### 26-Jan-24 | Andy Symons | Release 2. See feature list above
###
### ------------------------------------------------------------------------------------

blueprint:
  name: "Heating X2" # temporary name for testing
  description: Controls one or more thermostats from a calendar, allows temporary manual override, and optionally turns off thermostat if a door or window is opened, or if the area is unoccupied for a while.
  domain: automation

  ### ----------------------------------------------------------------------------
  ### INPUTS
  ### ----------------------------------------------------------------------------

  input:
    thermostat_controls:
      name: DEVICE ENTITY - Thermostat control
      description: One or more thermostat entities that are to be controlled by this automation
      selector:
        entity:
          filter:
            domain: climate
          multiple: true

    thermostat_set_temperature_sensors:
      name: DEVICE ENTITY - Thermostat set temperature
      description: The (template) sensors that read the set temperature from the specified thermostats
      selector:
        entity:
          filter:
            domain: sensor
          multiple: true

    door_or_window_open_sensors:
      name: DEVICE ENTITY - Door or window open sensors
      description: Zero or more sensors that detect whether a door or window is open
      selector:
        entity:
          filter:
            domain: binary_sensor
            device_class: opening
          multiple: true
      default: []

    area_occupancy_sensors:
      name: DEVICE ENTITY - area occupancy sensors
      description: Zero or more sensors that detect whether there is anyone in the area
      selector:
        entity:
          filter:
            domain: binary_sensor
            device_class: occupancy
          multiple: true
      default: []

    away_switch:
      name: DEVİCE ENTITY - Away switch
      description: A switch, input boolean, or binary sensor that changes all rooms to 'away' mode
      selector:
        entity:

    area_calendar:
      name: CALENDAR ENTITY - area calendar
      description: The calendar dedicated to scheduling events for this area
      selector:
        entity:
          filter:
            domain: calendar

    manual_temperature:
      name: HELPER -  Temperature set manually
      description: The global variable (helper) to hold the required temperature
      selector:
        entity:
          filter:
            domain: input_number

    set_temperature:
      name: HELPER - Current set temperature
      description: The global variable (helper) to hold the required temperature
      selector:
        entity:
          filter:
            domain: input_number

    required_temperature:
      name: HELPER - Required temperature (next set temperature)
      description: The global variable (helper) to hold the required temperature
      selector:
        entity:
          filter:
            domain: input_number

    setting_reason:
      name: HELPER - Setting reason
      description: The global variable (helper) into which the automation writes the reason for the current setting (for use on a dashboard)
      selector:
        entity:
          filter:
            domain: input_text

    door_or_window_open_timer:
      name: HELPER - Door or window open timer
      description: The global variable (helper) to hold the timer for the period since a door or window was opened
      selector:
        entity:
          filter:
            domain: timer

    unoccupancy_timer:
      name: HELPER - Unoccupancy timer
      description: The global variable (helper) to hold the timer for the period since the area was last unoccupied
      selector:
        entity:
          filter:
            domain: timer

    warmup_timer:
      name: HELPER - Warmup timer
      description: The global variable (helper) to hold the timer for the event warmup period
      selector:
        entity:
          filter:
            domain: timer

    manual_override_timer:
      name: HELPER - Manual override timer
      description: The global variable (helper) to hold the timer for a manual intervention
      selector:
        entity:
          filter:
            domain: timer

    echoblock_timer:
      name: HELPER - Echoblock timer
      description: The timer for use inside the automation to disinguish genuine manual changes of the set temperature from those set by the automation
      selector:
        entity:
          filter:
            domain: timer

    minimum_thermostat_temperature:
      name: PARAMETER - Minimum thermostat temperature
      description: The minimum temperature that can be set on the thermostat
      selector:
        number:
          min: 0
          max: 100
      default: 5

    maximum_thermostat_temperature:
      name: PARAMETER - Maximum thermostat temperature
      description: The maximum temperature that can be set on the thermostat
      selector:
        number:
          min: 0
          max: 100
      default: 95

    frost_setting:
      name: PARAMETER - Frost setting
      description: The temperature to be used when the heating is turned off
      selector:
        number:
          min: 0
          max: 100
      default: 5

    away_temperature:
      name: PARAMETER - Away temperature
      description: The temperature to be used when away mode is in operation  
      selector:
        number:
          min: 0
          max: 100
      default: 6

    background_temperature:
      name: PARAMETER - Frost setting
      description: The temperature to be used when there is no calendar event or the room is unoccupied
      selector:
        number:
          min: 0
          max: 100
      default: 7

    warmup_period:
      name: PARAMETER - Warmup period
      description: The period of time from the start of a new event for which area unoccupancy will be ignored
      selector:
        time:
      default: "02:00:00"

    manual_override_period:
      name: PARAMETER - Manual override period
      description: The time period for which a manual intervention will override the schedule
      selector:
        time:
      default: "02:00:00"

    door_or_window_open_period:
      name: PARAMETER - Door or window open period
      description: The time period for which a door or window may be open before the heating is turned off
      selector:
        time:
      default: "0:03:00"

    unoccupancy_period:
      name: PARAMETER - Unoccupancy period
      description: The time period for which the area may be unoccupied before the heating is turned off
      selector:
        time:
      default: "01:00:00"

mode: queued # use all triggers but avoid conflicting states

## ----------------------------------------------------------------------------
## LOCAL VARİABLES
## needed to capture global variable values for use in templates
## ----------------------------------------------------------------------------

variables:
  local_thermostat_set_temperature_sensors: !input thermostat_set_temperature_sensors
  local_door_or_window_open_sensors: !input door_or_window_open_sensors
  local_required_temperature: !input required_temperature
  local_set_temperature: !input set_temperature
  local_manual_override_timer: !input manual_override_timer
  local_maximum_thermostat_temperature: !input maximum_thermostat_temperature
  local_minimum_thermostat_temperature: !input minimum_thermostat_temperature
  local_area_calendar: !input area_calendar
  local_area_occupancy_sensors: !input area_occupancy_sensors

## ----------------------------------------------------------------------------
## TRIGGERS
## ----------------------------------------------------------------------------

trigger:
  # 1. Calendar state to on (active event)
  - platform: state
    entity_id: !input area_calendar
    to: "on"
    id: calendar_state_to_on

  # 2. Calendar state to off, unknown, or unavailable (no active event)
  - platform: state
    entity_id: !input area_calendar
    from: "on"
    id: calendar_state_to_off

  # 3. Calendar event start
  - platform: calendar
    event: start
    offset: "0:0:0"
    entity_id: !input area_calendar
    id: calendar_event_start

  # 4. Calendar event end
  - platform: calendar
    event: end
    offset: "0:0:0"
    entity_id: !input area_calendar
    id: calendar_event_end

  # 5. Change in any one of the thermostat set temperatures
  - platform: state
    entity_id: !input thermostat_set_temperature_sensors
    for:
      seconds: 5
    id: set_temperature_change

  # 6. End of manual override (timer idle, paused, unknown, or unavailable)
  - platform: state
    entity_id: !input manual_override_timer
    from: active
    id: manual_override_end

  # 7. area becomes unoccupied: sensor available and off (clear)
  - platform: state
    entity_id: !input area_occupancy_sensors
    to: "off"
    for:
      seconds: 10
    id: area_unoccupied

  # 8. area becomes occupied: sensor 'detected' (on), 'unknown', or 'unavailable'
  - platform: state
    entity_id: !input area_occupancy_sensors
    from: "off"
    for:
      seconds: 10
    id: area_occupied

  # 9. A door or window is opened
  - platform: state
    entity_id: !input door_or_window_open_sensors
    to: "on"
    for:
      seconds: 10
    id: door_or_window_opened

  # 10. A doors or windows is closed, or becomes 'unknown' or 'unavailable'
  - platform: state
    entity_id: !input door_or_window_open_sensors
    from: "on"
    for:
      seconds: 10
    id: doors_and_windows_closed

  # 11. Door or window open timer finished (time to turn off the heating)
  - platform: state
    entity_id: !input door_or_window_open_timer
    from: active
    id: door_or_window_open_timer_end

  # 12. Unoccupancy timer finished (time to turn off the heating)
  - platform: state
    entity_id: !input unoccupancy_timer
    from: active
    id: unoccupancy_timer_end

  # 13. Away switch change
  - platform: state
    entity_id: !input away_switch

  # 14. end of warmup period
  - platform: state
    entity_id: !input warmup_timer
    from: active
    id: warmup_timer_end


      ## ----------------------------------------------------------------------------
      ## ACTIONS STEP 1 -- RESPOND TO TRIGGERS 
      ## ----------------------------------------------------------------------------

action:
  # Each choice responds to a specific trigger (where a response is needed))
  - choose:
      #
      # a. Manual override start
      #
      - conditions:
          - condition: trigger
            id: set_temperature_change
          #ignore if it is just an echo from a setting from this automation
          - condition: state
            entity_id: !input echoblock_timer
            state: idle
          #ignore if same value as before (e.g. triggered by template reload)
          - condition: template
            value_template: "{{ not states(local_set_temperature) == states(local_required_temperature) }}"
        sequence:
          - service: timer.start
            data:
              duration: !input manual_override_period
            target:
              entity_id: !input manual_override_timer
      #
      # b. Manual override end
      #
      - conditions:
          - condition: trigger
            id: manual_override_end
      #
      # c. Door or window opened - start timer
      #
      - conditions:
          - condition: trigger
            id: door_or_window_opened
            # Do not restart timer if already running
          - condition: not
            conditions:
              - condition: state
                entity_id: !input door_or_window_open_timer
                state: active
        sequence:
          - service: timer.start
            data:
              duration: !input door_or_window_open_period
            target:
              entity_id: !input door_or_window_open_timer
      #
      # d. Doors and windows closed - restart and pause timer
      #
      - conditions:
          - condition: trigger
            id: doors_and_windows_closed
        sequence:
          - service: timer.start
            data:
              duration: !input door_or_window_open_period
            target:
              entity_id: !input door_or_window_open_timer
          - service: timer.pause # Keeps it in the active state
            target:
              entity_id: !input door_or_window_open_timer
      #
      # e. Unoccupancy - start timer
      #
      - conditions:
          - condition: trigger
            id: area_unoccupied
            # Do not restart timer if already running
          - condition: not
            conditions:
              - condition: state
                entity_id: !input unoccupancy_timer
                state: active
        sequence:
          - service: timer.start
            data:
              duration: !input unoccupancy_period
            target:
              entity_id: !input unoccupancy_timer
      #
      # f. area occupied - restart and pause timer
      #
      - conditions:
          - condition: trigger
            id: area_occupied
        sequence:
          - service: timer.start
            data:
              duration: !input unoccupancy_period
            target:
              entity_id: !input unoccupancy_timer
          - service: timer.pause # Keeps it in the active state
            target:
              entity_id: !input unoccupancy_timer

  ## ----------------------------------------------------------------------------
  ## ACTIONS STEP 2 -- DETERMINE THE REQUIRED TEMPERATURE ACCORDNG TO THE STATE
  ## ----------------------------------------------------------------------------

  ## Each choice sets the required temperature and the reason text, for later use
  ## The states are tested in order of their priority over other states  
  - choose:

      # 1. If 'away' switch is on
      - conditions:
          - condition: state
            entity_id: !input away_switch
            state: "on"
        sequence:
          - service: input_number.set_value
            data:
              value: !input away_temperature
            target:
              entity_id: !input required_temperature
          - service: input_text.set_value
            data:
              value: "Set to away temperature because away mode is turned on"
            target:
              entity_id: !input setting_reason

      # 2. If a door or window is open
      - conditions:
          - condition: state
            entity_id: !input door_or_window_open_timer
            state: idle
          - condition: template
            value_template: "{{ local_door_or_window_open_sensors | select ('is_state', 'on') | list | count > 0 }}"
        sequence:
          - service: input_number.set_value
            data:
              value: !input frost_setting
            target:
              entity_id: !input required_temperature
          - service: input_text.set_value
            data:
              value: "Turned off because a door or window is open"
            target:
              entity_id: !input setting_reason

      # 2. If the area has been unoccupied for the set period, and we are not in a warmup period ***
      - conditions:
          - condition: state
            entity_id: !input unoccupancy_timer
            state: idle
          - condition: state
            entity_id: !input warmup_timer
            state: idle
          - condition: template
            value_template: "{{ local_area_occupancy_sensors | reject ('is_state', [ 'unknown',  'unavailable' ] ) | select ('is_state', 'on') | list | count > 0 }}"
        sequence:
          - service: input_number.set_value
            data:
              value: !input background_temperature
            target:
              entity_id: !input required_temperature
          - service: input_text.set_value
            data:
              value: "Set to background temperature because the area is unoccupied"
            target:
              entity_id: !input setting_reason

      # 3. If there is a manual override in operation
      - conditions:
          - condition: state
            entity_id: !input manual_override_timer
            state: active
        sequence:
          - service: input_number.set_value
            data:
              value: "{{ states(local_thermostat_set_temperature) }}"
            target:
              entity_id: !input required_temperature
          - service: input_text.set_value
            data:
              value: "Set manually to {{ states(local_required_temperature) }}. Time left {{ ( as_timestamp(state_attr(local_manual_override_timer,'finishes_at'))-as_timestamp(now()) ) |  timestamp_custom('%H:%M', False, 0) }}"
            target:
              entity_id: !input setting_reason

      # 4. If there is an active calendar event and a valid temperature 

          - choose:
              # 4.1 If there is no temperature field
              - conditions:
                  - condition: template
                    value_template: >-
                      {{ not state_attr(local_area_calendar, 'description').split('#') | count >= 3 }}
                sequence:
                  - service: input_number.set_value
                    data:
                      value: !input background_temperature
                    target:
                      entity_id: !input required_temperature
                  - service: input_text.set_value
                    data:
                      value: >-
                        {{ "Set to background temperature because the calendar event '" + state_attr(local_area_calendar, 'message') + "' does not specify a temperature." }}
                    target:
                      entity_id: !input setting_reason

              # 4.2 If there is a temperature field that is not a number
              - conditions:
                  - condition: template
                    value_template: >-
                      {{ not state_attr(local_area_calendar, 'description').split('#')[1] | is_number }}
                sequence:
                  - service: input_number.set_value
                    data:
                      value: !input background_temperature
                    target:
                      entity_id: !input required_temperature
                  - service: input_text.set_value
                    data:
                      value: >-
                        {{ "Set to background temperature because the calendar event '" + state_attr(local_area_calendar, 'message') + "' does not specify a valid number for the temperature." }}
                    target:
                      entity_id: !input setting_reason

              # 4.3 If there is a temperature that is too low
              - conditions:
                  - condition: template
                    value_template: >-
                      {{ not state_attr(local_area_calendar, 'description').split('#')[1] | float(0)) >= local_minimum_thermostat_temperature }}
                sequence:
                  - service: input_number.set_value
                    data:
                      value: !input background_temperature
                    target:
                      entity_id: !input required_temperature
                  - service: input_text.set_value
                    data:
                      value: >-
                        {{ "Set to background temperature because the calendar event '" + state_attr(local_area_calendar, 'message') + "' specifies a temperature below the minimum." }}
                    target:
                      entity_id: !input setting_reason

              # 4.4 If there is a temperature that is too high
              - conditions:
                  - condition: template
                    value_template: >-
                      {{ not ( state_attr(local_area_calendar, 'description').split('#')[1] | float(0)) <= local_maximum_thermostat_temperature }}
                sequence:
                  - service: input_number.set_value
                    data:
                      value: !input background_temperature
                    target:
                      entity_id: !input required_temperature
                  - service: input_text.set_value
                    data:
                      value: >-
                        {{ "Set to background temperature because the calendar event '" + state_attr(local_area_calendar, 'message') + "' specifies a temperature above the maximum." }}
                    target:
                      entity_id: !input setting_reason

            # 4.5 Else, the normal case -- there is an active calendar event with a valid temperature
            default:
              sequence:
                - service: input_number.set_value
                  data:
                    value: "{{ state_attr(local_area_calendar, 'description').split('#')[1] | float(0) }}"
                  target:
                    entity_id: !input required_temperature
                - service: input_text.set_value
                  data:
                    value: >-
                      {{ "Set to " +  states(local_required_temperature) + " by calendar event '" + state_attr(local_area_calendar, 'message') + "' until " + ( as_timestamp(state_attr(local_area_calendar, 'end_time'))  ) |  timestamp_custom('%a %d %b %Y at %H:%M') + "." }}
                  target:
                    entity_id: !input setting_reason

      # 5. If there is no active calendar event
      - conditions:
          - condition: state
            entity_id: !input area_calendar
            state: "off"
        sequence:
          - service: input_number.set_value
            data:
              value: !input background_temperature
            target:
              entity_id: !input required_temperature
          - service: input_text.set_value
            data:
              value: >-
                {{ "Set to background temperature because nothing is scheduled."}} 
                {% if state_attr(local_area_calendar, 'message') %}
                {{ "The next event is '" + state_attr(local_area_calendar, 'message') + "' " + ( as_timestamp(state_attr(local_area_calendar, 'start_time')) )  |  timestamp_custom('on %a %d %b %Y at %H:%M', false)}}
                {% else %}
                {{ "There are no future events." }}
                {% endif %}
            target:
              entity_id: !input setting_reason

      #6. If the calendar state becomes unknown
      - conditions:
          - condition: state
            entity_id: !input area_calendar
            state: unknown
        sequence:
          - service: input_number.set_value
            data:
              value: !input background_temperature
            target:
              entity_id: !input required_temperature
          - service: input_text.set_value
            data:
              value: Set to background temperature because the calendar state is unknown
            target:
              entity_id: !input setting_reason

      #7. If the calendar becomes unavailable
      - conditions:
          - condition: state
            entity_id: !input area_calendar
            state: unavailable
        sequence:
          - service: input_number.set_value
            data:
              value: !input background_temperature
            target:
              entity_id: !input required_temperature
          - service: input_text.set_value
            data:
              value: Set to background temperature because the calendar is unavailable
            target:
              entity_id: !input setting_reason

    # Default - should never happen!
    default:
      - service: input_number.set_value
        data:
          value: !input frost_setting
        target:
          entity_id: !input required_temperature
      - service: input_text.set_value
        data:
          value: Turned off by because room state is unknown 
        target:
          entity_id: !input setting_reason

  ## ----------------------------------------------------------------------------
  ## ACTIONS STEP 4 -- COMMUNICATE WITH THE TRV(s)
  ## ----------------------------------------------------------------------------

  # Check not already set
  - if:
      - condition: template
        value_template: >-
          {{ states(local_required_temperature) <> states(local_set_temperature) }}
    then:
      # Send the command
      - service: climate.set_temperature
        data:
          temperature: local_required_temperature
        target:
          entity_id: !input thermostat_controls
      # Start the echoblock timer
      - service: timer.start
        data:
          duration:
            seconds: 10
        target:
          entity_id: !input echoblock_timer
      # Wait 10 seconds for command to be received and reflected back
      - delay: "00:00:10"
      # Check TRV(s) responded
      # Need a loop to test each individually?
      - if:
          - condition: template
            value_template: >-
              {{ states(local_required_temperature) | float(0) <> states(local_set_temperature) | float(0) }}
        then:
          - service: input_text.set_value
            data:
              value: "Turned off because a door or window is open"
            target:
              entity_id: !input setting_reason
          - service: notify.notify
            data:
              title: "Heating X error"
              message: "{{'TRV' states(local_thermostat_set_temperature_sensors) 'is not responding'}}"

You know it’s going to be in the actions section of the blueprint ( [‘action’] ) and it’s referencing “choose” (which is an action)

under the “action:” section it will be the first action performed. lists in HA are 0 indexed so index 0 ( [0] ) will be the first action in the list.

so that narrows it down to this (the first “choose:”):

- choose:
      #
      # a. Manual override start
      #
      - conditions:
          - condition: trigger
            id: set_temperature_change
          #ignore if it is just an echo from a setting from this automation
          - condition: state
            entity_id: !input echoblock_timer
            state: idle
          #ignore if same value as before (e.g. triggered by template reload)
          - condition: template
            value_template: "{{ not states(local_set_temperature) == states(local_required_temperature) }}"
        sequence:
          - service: timer.start
            data:
              duration: !input manual_override_period
            target:
              entity_id: !input manual_override_timer
      #
      # b. Manual override end
      #
      - conditions:
          - condition: trigger
            id: manual_override_end
      #
      # c. Door or window opened - start timer
      #
      - conditions:
          - condition: trigger
            id: door_or_window_opened
            # Do not restart timer if already running
          - condition: not
            conditions:
              - condition: state
                entity_id: !input door_or_window_open_timer
                state: active
        sequence:
          - service: timer.start
            data:
              duration: !input door_or_window_open_period
            target:
              entity_id: !input door_or_window_open_timer
      #
      # d. Doors and windows closed - restart and pause timer
      #
      - conditions:
          - condition: trigger
            id: doors_and_windows_closed
        sequence:
          - service: timer.start
            data:
              duration: !input door_or_window_open_period
            target:
              entity_id: !input door_or_window_open_timer
          - service: timer.pause # Keeps it in the active state
            target:
              entity_id: !input door_or_window_open_timer
      #
      # e. Unoccupancy - start timer
      #
      - conditions:
          - condition: trigger
            id: area_unoccupied
            # Do not restart timer if already running
          - condition: not
            conditions:
              - condition: state
                entity_id: !input unoccupancy_timer
                state: active
        sequence:
          - service: timer.start
            data:
              duration: !input unoccupancy_period
            target:
              entity_id: !input unoccupancy_timer
      #
      # f. area occupied - restart and pause timer
      #
      - conditions:
          - condition: trigger
            id: area_occupied
        sequence:
          - service: timer.start
            data:
              duration: !input unoccupancy_period
            target:
              entity_id: !input unoccupancy_timer
          - service: timer.pause # Keeps it in the active state
            target:
              entity_id: !input unoccupancy_timer

then it’s saying that the choose action in that action step has an error that is missing the key called “sequence:” from the second condition in the list since it is referencing index 1 ( [1] ).

so index 1 of the first “choose:” is this:

      - conditions:
          - condition: trigger
            id: manual_override_end
      #
      # c. Door or window opened - start timer
      #
      - conditions:

notice that there is no “sequence:” key between the first “- conditions:” section there (which is the second condition in the first choose action) and the next “- conditions:” section.

every choose conditions section section has to have an associated sequence section. yours is missing that one.

1 Like

There’s no sequence for the second option (index: 1) of the first action (index: 0, type: choose) .

1 Like

Thank you both very much for deciphering that!
Is this documented somewhere?

nothing specific in the HA documentation except for one reference in passing to it:

Templating Docs

but since jinja2 (which is the templating rendering engine used by HA) is loosely based on Python you can look at the Python docs for explanation.

here is a decent explanation that I found:

I can’t tell you what info there can be used in jinja2 but the docs specifically for jinja2 templates can be found here:

https://jinja.palletsprojects.com/en/3.0.x/templates/

But even then the HA templates are only a subset of the full jinja2 templates so not all of the commands are available in HA templates (but most are). And HA has added a few more that aren’t in the standard jinja2 library as well.

So you should always start with the standard HA templating docs first as that will get you the closest to what is used. Then if you need additional info then go to the Jinja2 templating docs. Then use the Python docs.

Finally if all else fails you can come back here and beg ask for help. :laughing:

1 Like

Thanks.
And I am already begging for more help in my new post
New blueprint – `Message malformed: Unexpected value for condition: ‘None’