Debugging my radiator control blueprint - Traces OK but wrong result

With many thanks to @CentralCommand, @DarkWarden, @Didgeridrew, and @EdwardTFN I now have blueprint code with correct syntax – no error messages or silent rejections. Most of it is working too but there are a couple of puzzling cases where the trace says it is going down the right path but the output is nevertheless wrong! That puzzles me. Can anyone see what is happening?

I am testing by setting up an instance of the blueprint connected to a sample radiator controller, presence detector and closure detector on my desk. I am exposing all the helpers (used as persistent global variables) in a dashboard alongside a thermostat card. Note in particular the ‘radiator_setting_reason’ helper, with which the automation tells the user why the thermostat has the setting it has. It also tells me what path the automation followed in action step 2. The problem is that the trace tells me a different story. Puzzling.

Working:
The automation correctly reports that the heating is turned off when the room is unoccupied or a door or window is opened. It reverts back to ‘turned off because nothing is scheduled’ when the room is occupied again and the doors and windows are closed. I tested with all combinations of opening and occupancy. So far so good.

Two cases do not work
a) when the temperature is set manually
b) when the temperature is set by calendar event

In both cases ‘Step 1’ executes correctly. For the manual case, the temperature is captured and the timer is started. At a calendar event start, the event name (aka summary), start time, end time, description and the temperature set in the description are all captured correctly.
The problem is in step 2: According to the trace, the automation is following the correct path to set the manual/event temperature on the radiator thermostat, and report ‘Set manually’ or ‘Set according to a calendar event’. However, the thermostat is being set to 5 (regarded as ‘off’) and the dashboard is showing ‘Turned off because nothing is scheduled’.
The six branches are all the same in structure and essentially the same as my model automation created using the UI, on which I based the blueprint.

Is there a blindingly obvious error that I am overlooking, or something very subtle?? How can I debug if tracing and the actual output do not tally?

Screenshots

  1. Scheduled event - trace
    (Edit: sorry, wrong screenshot included)

  2. Schedule event - result

  3. Manual override - trace

  4. Manual override - result

Code

### ----------------------------------------------------------------------------
### ANDY'S RADIATOR CONTROLLER
###
### Andy Symons 2-Feb-23
###
### Controls radiator temperature from a calendar,
###   allows temporary manual override,
###   and turns off radiator if a door or window is opened,
###   or if the room is unoccupied for a while
### ----------------------------------------------------------------------------

blueprint:
  name: Andys Radiator Controller
  description: Controls radiator temperature from a calendar, allows temporary manual override, and turns off radiator if a door or window is opened or if the room is unoccupied for a while
  domain: automation

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

  input:
    radiator_thermostat:
      name: Radiator thermostat
      description: The entity that controls the radiator TRV
      selector:
        entity:
          domain: climate

    radiator_set_temperature_sensor:
      name: Radiator thermostat set temperature
      description: A (template) sensor that reads the set temperature from the radiator thermostat
      selector:
        entity:
          domain: sensor

    radiator_calendar:
      name: Radiator calendar
      description: The calendar dedicated to scheduling events for this radiator
      selector:
        entity:
          domain: calendar

    door_or_window_opening_sensor:
      name: Door or window opening sensor
      description: The sensor [group] that detects whether any external door or window is open
      selector:
          entity:
            domain: binary_sensor
            device_class: opening

    door_or_window_open_recognition_time:
      name: Door or window open recognition time
      description: The time for which a door or window can be open before the heating switches off
      selector:
        time:
      default: "00:03:00"

    door_or_window_closed_recognition_time:
      name: Door or window closed recognition time
      description: The time for which a door or window has to be closed before the fact is used
      selector:
        time:
      default: "00:01:00"

    room_occupancy_sensor:
      name: Room occupancy sensor
      description: The sensor [group] that detects whether there is anyone in the room
      selector:
        entity:
          domain: binary_sensor
          device_class: occupancy

    room_occupancy_recognition_time:
      name: Room occupancy recognition time
      description: The time for which the room has to be unoccupied before the fact is used
      selector:
        time:
      default: "00:02:00"

    room_unoccupancy_recognition_time:
      name: Room unoccupancy recognition time
      description: The time for which the room can be unoccupied before the heating switches off (except suring the warm-up period)
      selector:
        duration:
      default: "01:00:00" 

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

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

    event_name:
      name: Event name
      description: The global variable (helper) to hold the name (aka Summary) of the current event (if any)
      selector:
        entity:
          domain: input_text

    event_description:
      name: Event description
      description: The global variable (helper) to hold the description of the current event (if any)
      selector:
        entity:
          domain: input_text

    event_start:
      name: Event start
      description: The global variable (helper) to hold the start date and time of the current event (if any)
      selector:
        entity:
          domain: input_datetime

    event_end:
      name: Event end
      description: The global variable (helper) to hold the end date and time of the current event (if any)
      selector:
        entity:
          domain: input_datetime

    event_end_offset_time:
      name: Event end offset time
      description: The time for which acting on an event end is deferred in order to detect a contiguous event
      selector:
        time:
      default: "00:05:00"

    event_temperature:
      name: Event temperature
      description: The global variable (helper) to hold the temperature specified in the current event (if any)
      selector:
        entity:
          domain: input_number

    manual_temperature:
      name: Manual temperature
      description: The global variable (helper) to hold the temperature specified by manual control of the device or the dashboard
      selector:
        entity:
          domain: input_number

    manual_mode_recognition_period:
      name: Manual mode recognition time
      description: The time period for which a manual setting has to be stable before it is taken as a new manual setting
      selector:
        time:
      default: "00:00:10"

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

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

    setting_reason:
      name: 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:
          domain: input_text

    echoblock_timer:
      name: 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:
          domain: timer

    echoblock_period:
      name: Echoblock period
      description: The time period for which am echo may be received from the automation setting the thermostat
      selector:
        time:
      default: "00:00:05"

mode: single

## ----------------------------------------------------------------------------
## LOCAL VARİABLES
## needed to capture global varable values for use in templates
## ----------------------------------------------------------------------------
variables:
  local_event_name: !input event_name
  local_event_description: !input event_description
  local_radiator_set_temperature: !input radiator_set_temperature_sensor

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

trigger:
  # 1. Calendar event start
  - platform: calendar
    event: start
    entity_id: !input radiator_calendar
    id: event_start

  # 2. Calendar event end
  - platform: calendar
    event: end
    offset: !input event_end_offset_time
    entity_id: !input radiator_calendar
    id: event_end

  # 3. Calendar state to on
  # (Similar to event start but can be around 30 seconds later)
  - platform: state
    entity_id: !input radiator_calendar
    from: "off"
    to: "on"
    id: calendar_state_to_on

  # 4. Calendar state from on to off
  # (Similar to event end but can be around 30 seconds later)
  - platform: state
    entity_id: !input radiator_calendar
    from: "on"
    to: "off"
    id: calendar_state_to_off

  # 5. Change in the thermostat set temperature
  # (could be manual override but could also be an echo at this stage)
  - platform: state
    entity_id: !input radiator_set_temperature_sensor
    for: !input manual_mode_recognition_period
    id: set_temperature_change

  # 6. End of manual override
  - platform: state
    entity_id: !input manual_override_timer
    from: active
    to: idle
    id: manual_override_end

  # 7. Room becomes unoccupied
  - platform: state
    entity_id: !input room_occupancy_sensor
    from: "on"
    to: "off"
    for: !input room_unoccupancy_recognition_time
    id: room_unoccupied

  # 8. Room becomes occupied
  - platform: state 
    entity_id: !input room_occupancy_sensor
    from: "off"
    to: "on"
    for: !input room_occupancy_recognition_time
    id: room_occupied

  # 9. A door or window is opened
  - platform: state
    entity_id: !input door_or_window_opening_sensor
    from: "off"
    to: "on"
    for: !input door_or_window_open_recognition_time
    id: door_or_window_opened

  # 10. All doors and windows closed
  - platform: state
    entity_id: !input door_or_window_opening_sensor
    from: "on"
    to: "off"
    for: !input door_or_window_closed_recognition_time
    id: doors_and_windows_closed

## ----------------------------------------------------------------------------
## ACTIONS STEP 1 -- SET THE STATE VARİABLES
## ----------------------------------------------------------------------------
action:
  - choose:
      #
      # a. Calendar event start
      #
      - conditions:
          - condition: trigger
            id: event_start
        sequence:
          # i. capture the event name, start, end and description
          - service: input_text.set_value
            data:
              value: "{{ trigger.calendar_event.summary }}"
            target:
              entity_id: !input event_name
          - service: input_datetime.set_datetime
            data:
              datetime: "{{ trigger.calendar_event.start }}"
            target:
              entity_id: !input event_start
          - service: input_datetime.set_datetime
            data:
              datetime: "{{ trigger.calendar_event.end }}"
            target:
              entity_id: !input event_end
          - service: input_text.set_value
            data:
              value: "{{ trigger.calendar_event.description }}"
            target:
              entity_id: !input event_description

          # ii. Extract the temperature from the description (enclosed in hashes) and round to one decimal place
          - service: input_number.set_value
            data:
              value: >-
                {% set temperature_text = states(local_event_description).split('#')[1] %}
                {{ '%0.1f' | format(float(temperature_text)) }}
            target:
              entity_id: !input event_temperature

          # iii. start the warmup period timer
          - service: timer.start
            data:
              duration: !input warmup_period
            target:
              entity_id: !input warmup_timer
      #
      # b. Calendar event end
      #
      - conditions:
          - condition: trigger
            id: event_end
          # Only act if the ending event is the current one; otherwise a consecutive event has already started
          - condition: template
            value_template: >-
              {{ trigger.calendar_event.summary == states(local_event_name) }}
        sequence:
          # i. Clear the event name
          - service: input_text.set_value
            data:
              value: (none)
            target:
              entity_id: !input event_name
          # ii. Clear the event description
          - service: input_text.set_value
            data:
              value: (none)
            target:
              entity_id: !input event_description
          # iii. Clear the event temperature
          - service: input_number.set_value
            data:
              value: 5
            target:
              entity_id: !input event_temperature
          # iv. Clear the warmup timer
          - service: timer.cancel
            data: {}
            target:
              entity_id: !input warmup_timer
      #
      # c. 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
        sequence:
          - service: timer.start
            data:
              duration: !input manual_override_period
            target:
              entity_id: !input manual_override_timer
          - service: input_number.set_value
            data:
              value: "{{ states(local_radiator_set_temperature) }}"
            target:
              entity_id: !input manual_temperature
      #
      # d. Manual override end
      #
      - conditions:
          - condition: trigger
            id: manual_override_end
        sequence:
          - service: input_number.set_value
            data:
              value: 5
            target:
              entity_id: !input manual_temperature

  ## ----------------------------------------------------------------------------
  ## ACTIONS STEP 2 -- SET THE TEMPERATURE ON THE THERMOSTAT AND RECORD WHY
  ## ----------------------------------------------------------------------------
  - choose:
      # 1. If a door or window has been open for the set time, turn the heating off
      - conditions:
          - condition: state
            state: "on"
            for: !input door_or_window_open_recognition_time
            entity_id: !input door_or_window_opening_sensor
        sequence:
          - service: climate.set_temperature
            data:
              temperature: 5
            target:
              entity_id: !input radiator_thermostat
          - 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 room has been unoccupied for the set time, turn the heating off
      - conditions:
          - condition: state
            entity_id: !input room_occupancy_sensor
            state: "off"
            for: !input room_unoccupancy_recognition_time
            # Do not act on unoccupancy during the warmup period
          - condition: state
            entity_id: !input warmup_timer
            state: idle
        sequence:
          - service: climate.set_temperature
            data:
              temperature: 5
            target:
              entity_id: !input radiator_thermostat
          - service: input_text.set_value
            data:
              value: Turned off because the room 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: climate.set_temperature
            data:
              temperature: !input manual_temperature
            target:
              entity_id: !input radiator_thermostat
          - service: input_text.set_value
            data:
              value: Set manually
            target:
              entity_id: !input setting_reason

      # 4. If there is an active calendar event
      - conditions:
          - condition: state
            entity_id: !input radiator_calendar
            state: "on"
        sequence:
          - service: climate.set_temperature
            data:
              temperature: !input event_temperature
            target:
              entity_id: !input radiator_thermostat
          - service: input_text.set_value
            data:
              value: Set according to a calendar event
            target:
              entity_id: !input setting_reason

      # 5. If there is no active calendar event
      - conditions:
          - condition: state
            entity_id: !input radiator_calendar
            state: "off"
        sequence:
          - service: climate.set_temperature
            data:
              temperature: 5
            target:
              entity_id: !input radiator_thermostat
          - service: input_text.set_value
            data:
              value: Turned off because nothing is scheduled
            target:
              entity_id: !input setting_reason

    # Default should never happen!
    default:
      - service: climate.set_temperature
        data:
          temperature: 5
        target:
          entity_id: !input radiator_thermostat
      - service: input_text.set_value
        data:
          value: Turned off by default
        target:
          entity_id: !input setting_reason

  # Start the echo-block timer
  - service: timer.start
    data:
      duration: !input echoblock_period
    target:
      entity_id: !input echoblock_timer

Lot’s to read… :smiley:
I will take a look later…

Do you have anything in your log related to this blueprint?

Well I mean here’s all your triggers:

So the automation will run when any of these fire. And here’s all your conditions in step 2:

If all of these conditions are false then its going to head to default. It seems like there’s a good number of ways that could happen honestly:

  1. Door/window closed when there’s no active event - doesn’t seem to match any of these, goes to default
  2. Room becomes occupied when there’s no active event - doesn’t seem to match any of these, goes to default
  3. Room becomes unoccupied for room_unoccupancy_recognition_time but warmup_timer is running - doesn’t seem to match any of these, goes to default

Probably more, you have a lot of triggers and very few conditions. Seems like many scenarios will go to default and considering you have a comment that says “this should never happen!” that seems bad.

In the particular example you have above I think the problem can be pretty easily identified by your own comment on trigger #3:

So when trigger #1 fires (Calendar event start) it is not going to match any of your 4 conditions. Because the calendar entity isn’t going to turn on until ~30 seconds later so condition #4 (If there is an active calendar event) will be false for that trigger. Therefore unless one of the other 3 conditions happens to evaluate to true that trigger is going to pass right through to default.

Duh! Good question and silly of me not to look. Yes there was and it led me to the error, though not to an explanation of why the trace was so misleading.

The errors are in the statements

temperature: !input manual_temperature

and

temperature: !input event_temperature

I wanted to load the temperature with the contents of the helper identified in the input. I am guessing it is actually giving the name?
I solved it by introducing local variables to capture the input and access them from a template:

        sequence:
          - variables:
              local_manual_temperature: !input manual_temperature
          - service: climate.set_temperature
            data:
              temperature: "{{ states(local_manual_temperature) }}"
            target:
              entity_id: !input radiator_thermostat

       sequence:
          - variables:
              local_event_temperature:  !input event_temperature
          - service: climate.set_temperature
            data:
              temperature: "{{ states(local_event_temperature) }}"
            target:
              entity_id: !input radiator_thermostat

The whole blueprint seems to be working now yay! :slight_smile: :slight_smile:

Q: Is there some documentation somewhere that explains when the contents of a variable are used and when its name is used?

Apparently

temperature: !input manual_temperature

is referring to the name, whereas

variables:
  local_manual_temperature: !input manual_temperature

is referring to the content. Why?

thanks for your interest, but no, that is not the case. Step 1 sets the state variables (helpers) depending on the triggers (using a trigger_id); Step 2 sets the temperature on the thermostat according to the state variables (devices and helpers). I had this logic working in an automation before transferring it to a blueprint. The errors came from that transfer, mostly through the way blueprint inputs are used.

FYI, Step 2 is a choice in which the precedence is important – only the first is used;

  1. If a door or window is open the heating is turned off
  2. (Else) if the room is unoccupied the heating is turned off
  3. (Else) if a manual override is in effect, the temperature is set to the manual value
  4. (Else) if a calendar event is active, the temperature is set to the event value
  5. (Else) if no calendar event is active, the heating is turned off
  6. (By default) the heating is turned off – but this is just a debugging aid in case the state variables are not correctly set for some reason. If they are set correctly one of the above will apply.

I have to use the calendar event trigger because that is the only time the event details can be accessed, and I also use the apparently redundant calendar state because that is the only one that can be accessed at any time.

This logic and the use of helpers as persistent global variables ensures that if for example manual override is invoked during a calendar event, and ends before the event ends, the temperature reverts to that in the event. If a window is opened while in manual override, then closed again before the end of the manual override period, the manual temperature is resumed … and so on in any combination. It will also continue to work if the system is restarted.

I missed this one. Sorry its a small box with a lot of text hidden in the scroll. That definitely changes things, given that one I’m not sure how it ever gets to the default. Calendar entities are always either on or off so it should never really get past that point. Which covers my concerns.

Good that you found the issue, but don’t you have any warning related to the automation being triggered when it is already running?
Every time I have an automation with too many triggers, like this one, I question myself if the single mode is the best and in many cases I choose something else.

Also, you put everything in inputs, making the Blueprint quite customizable, but you have the “off” temperature hard coded:

Any reason for that?
That will make harder for other people to use (specially the one using temperatures in Fahrenheit), if you ever decide to share this Blueprint.

By the way, great job you have done!

Exactly. I really only put the default there as as debug aid and to ensure the heating would not get stuck ‘on’. In theory, the only difference with calendar off is the ‘reason’ message. I guess it might get there if the calendar were to be deleted or a device becomes unavailable – such robustness checks are ongoing. Testing is going well now on the bench. The next step is to try it for real next week in a house with 11 radiators. I will publish it after that trial and a bit of tidying up.