Return a Variable which is a dictionary in Script to Automation

Hi All,

Need some help returning a variable defined in the script as a dictionary, but when returned it generates an error as returning a string.
Here is the code:

alias: HVAC Status
variables:
  result: >

    {# Seed Results #}   {% set tp = namespace() %} {% set tp.name = 'Off' %} {%
    set tp.start = '01:01:01' %} {% set tp.stop = '23:23:23' %} {% set tp.eco =
    away_temperature %} {% set tp.comfort = away_temperature %} {% set tp.fan =
    '' %} {% set tp.vertical = '' %} {% set tp.horizontal = '' %} {% set
    tp.days_of_week = [] %} {% set tp.new_hvac_mode = 'off' %}

 


    {{ tp }}
sequence:
  - stop: All done.
    response_variable: result
mode: single

and the error is:

ERROR (MainThread) [homeassistant.components.automation.ground_hvac_control] Ground HVAC Control: Error executing script. Error for call_service at pos 2: Failed to process the returned action response data, expected a dictionary, but got <class ‘str’>

Hi Craig Rayner,

Something like this?

Sir Good Enough,
Thanks for the reply. I have no issue creating dictions variables. I will to pass the variable back to the calling script, with having to break it all open manually, which is the only way I have found to pass the variable back to the calling script.

So:

  {{ { 'name': tp.name, 'fan': tp.fan} }}  

works to pass back a dictionary,
but

{{ tp }}

does not work, where ‘tp’ is a variable of type dictionary.

p.s. I think it is a namespace issue, so a small script that iterates over the dictionary and sends it back is probably what I will have to settle for, but that eludes me as well.

My post pointed to another post that set the dict up correctly. What you have is sort of a list not a dictionary.

I’m not any kind of an expert in these things, but that template didn’t look right to me.

Thanks,
The dictionary is in the correct format, as

alias: HVAC Control Status
variables:
  result: >-
    {# Seed Results #}     

    {% set tp = namespace() %}
    {% set tp.name = 'Off' %}
    {% set tp.start = '01:01:01' %}
    {% set tp.stop = '23:23:23' %}

    {{ {'name':tp.name, 'start':tp.start, 'stop':tp.stop} }}
sequence:
  - stop: All done.
    response_variable: result
mode: single

works correctly, no errors.

I am looking for a solution that allows the dictionary to be passed back to the calling script/automation as a dictionary. I believe that what is happening is the dictionary fails to pass as it moves out of the defined namespace, and is turned into a string by the process of return.

You can just create the dictionary in YAML, no templates needed. But if you want to do this in templates there are options also. Here’s 3 ways to get to the same result:

alias: HVAC Control Status
sequence:
  - variables:
      result1:
        name: "Off"
        start: "01:01:01"
        stop: "23:23:23"
        eco: away_temperature
      result2: |
        {{ {
          "name": "Off",
          "start": "01:01:01",
          "stop": "23:23:23",
          "eco": "away_temperature"
        } }}
      result3: |
        {{ dict.from_keys(
            [
              ("name", "Off"),
              ("start", "01:01:01"),
              ("stop", "23:23:23"),
              ("eco", "away_temperature")
            ]
          )
        }}
  - stop: All done.
    response_variable: result3
mode: single
description: ""

Edit: I should clarify that the namespace() function will create a namespace object, which is not a dictionary. It does behave similar to a dictionary, with the exception that you can modify it, and it has special scope. But it isn’t a dictionary, and you need a dictionary.

2 Likes

tp in your code is namespace, not a dict.

1 Like

So I used the example above form:

alias: HVAC Control Status
variables:
  result: |-
    {# Seed Results #}   {% set tp = { 
      name: 'Off', 
      start: '01:01:01',
      stop: '23:23:23',
      comfort: 21,
      eco: 21,
      fan: 'auto',
      vertical: 'stopped',
      horizontal: 'stopped',
      days_of_week: [],
      hvac_mode: "off",
      target: 21,
      current_temperature: 21,
      current_time: now().strftime('%H:%M:%S'),
      time_found: false,
      current_day_of_week: now().strftime('%A'),
      status_change: false,
      config_status: true,
      error_message: [] 
      }
    %}
    {{ tp }}
sequence:
  - stop: All done.
    response_variable: result
mode: single

returns:

2024-11-21 11:17:36.132 ERROR (MainThread) [homeassistant.components.automation.ground_heating] Ground Heating: Error executing script. Error for call_service at pos 1: Failed to process the returned action response data, expected a dictionary, but got <class 'str'>
2024-11-21 11:17:36.132 ERROR (MainThread) [homeassistant.components.automation.ground_heating] Error while executing automation automation.ground_heating: Failed to process the returned action response data, expected a dictionary, but got <class 'str'>

So it appears that a dictionary is not a dictionary. I am beginning to hate Home Assistant / Jinja2. The documentation talks of namespace, all the examples I have found talk of namespace, but does any of it work, no.

I wish HA used Twig for templating. So much better documented and implemented.

I will just list them all out, as that works, but is a pain in the neck to program even the smallest change.

changed the code to:

alias: HVAC Control Status
variables:
  result: >-
    {# Seed Results #}   
    
    {% set tp = { 
      name: 'Off', 
      start: '01:01:01',
      stop: '23:23:23',
      comfort: 21,
      eco: 21,
      fan: 'auto',
      vertical: 'stopped',
      horizontal: 'stopped',
      days_of_week: [],
      hvac_mode: "off",
      target: 21,
      current_temperature: 21,
      current_time: now().strftime('%H:%M:%S'),
      time_found: false,
      current_day_of_week: now().strftime('%A'),
      status_change: false,
      config_status: true,
      error_message: [] 
      }
    %}

    {% set tp.current_temperature = states(climate_measurement) %}
    
    {{ {
      'name': tp.name, 
      'fan': tp.fan, 
      'vertical': tp.vertical, 
      'horizontal': tp.horizontal, 
      'mode': tp.new_hvac_mode, 
      'status_change': tp.status_change, 
      'config_status': tp.config_status, 
      'error_message': tp.error_message
      } }}
sequence:
  - stop: All done.
    response_variable: result
mode: single

and now the error is:

2024-11-21 11:27:24.968 ERROR (MainThread) [homeassistant.components.script.hvac_control_status] HVAC Control Status: Error rendering variables: TemplateRuntimeError: cannot assign attribute on non-namespace object
2024-11-21 11:27:24.968 ERROR (MainThread) [homeassistant.components.automation.ground_heating] Ground Heating: Error executing script. Error rendering template for call_service at pos 1: TemplateRuntimeError: cannot assign attribute on non-namespace object
2024-11-21 11:27:24.969 ERROR (MainThread) [homeassistant.components.automation.ground_heating] Error while executing automation automation.ground_heating: TemplateRuntimeError: cannot assign attribute on non-namespace object
2024-11-21 11:27:24.972 ERROR (MainThread) [homeassistant.components.script.hvac_control_status] HVAC Control Status: Error rendering variables: TemplateRuntimeError: cannot assign attribute on non-namespace object
2024-11-21 11:27:24.972 ERROR (MainThread) [homeassistant.components.automation.ground_heating] Ground Heating: Error executing script. Error rendering template for call_service at pos 1: TemplateRuntimeError: cannot assign attribute on non-namespace object
2024-11-21 11:27:24.973 ERROR (MainThread) [homeassistant.components.automation.ground_heating] Error while executing automation automation.ground_heating: TemplateRuntimeError: cannot assign attribute on non-namespace object

Yes, namespace appears the only way to create a workable dict.

You’ve got to pay a little better attention to my examples. All 3 work.

The first is YAML and should be fairly simple to follow. The next is how you’d write a dictionary in JSON, inside a template. The last uses python functions to create a dictionary from a list of tuples.

Pay attention to which ones need quotes for the keys and/or values, and also note that each method is going to have different rules for how (or if) you can add/remove keys or modify values.

If you let me know what you objective is I can probably help.

There are many methods to create dictionaries, but namespace() is not one of them.

3 Likes

I need to modify these variables, hence a namespace.

You don’t need a namespace to specify computed values in a dict that’s defined in YAML. Use a template for each key’s value.

In the following example, the variable result contains a dictionary where several of its keys have statically assigned values and others are dynamically assigned using templates.

alias: HVAC Control Status
sequence:
  - variables:
      result:
        name: 'Off'
        start: '01:01:01'
        stop: '23:23:23'
        comfort: 21
        eco: 21
        fan: 'auto'
        vertical: 'stopped'
        horizontal: 'stopped'
        days_of_week: []
        hvac_mode: "off"
        target: 21
        current_temperature: "{{ states(climate_measurement) }}"
        current_time: "{{ now().strftime('%H:%M:%S') }}"
        time_found: false
        current_day_of_week: "{{ now().strftime('%A') }}"
        status_change: false
        config_status: true
        error_message: []
  - stop: All done.
    response_variable: result
mode: single

No.

Post links to the documentation and examples you found.

2 Likes

and none of these work when you try and alter them within for loops in the script.

Why?

Because for loops MUST USE A NAMESPACE VARIABLE to return a value from inside the for loop, as any variable defined in the for loop can only be used in the scope of the for loop. The fact that a for loop holds a scope separate to the script in which it resides is a mystery.

p.s. This works for me to return the dictionary:

      {{ {
        'name': tp.name, 
        'fan': tp.fan, 
        'vertical': tp.vertical, 
        'horizontal': tp.horizontal, 
        'mode': tp.new_hvac_mode, 
        'status_change': tp.status_change, 
        'config_status': tp.config_status, 
        'error_message': tp.error_message
        } }}

within a script like:

alias: HVAC Control Status
variables:
  result: >-
    {# Seed Results #}  
    
    {% set tp = namespace() %}
    {% set tp.name = 'Off' %}
    {% set tp.start = '01:01:01' %}
    {% set tp.stop = '23:23:23' %}
    {% set tp.eco = away_temperature %}
    {% set tp.comfort = away_temperature %}
    {% set tp.fan = 'auto' %}
    {% set tp.vertical = 'stopped' %}
    {% set tp.horizontal = 'stopped' %}
    {% set tp.days_of_week = [] %}
    {% set tp.hvac_mode = "off" %}
    {% set tp.target = 21 %}
    {% set tp.current_temperature = states(current_temperature) %}
    {% set tp.current_time = now().strftime('%H:%M:%S') %}  
    {% set tp.time_found = false %}  
    {% set tp.current_day_of_week = now().strftime('%A') %}
    {% set tp.status_change = false %}
    {% set tp.config_status = true %}
    {% set tp.error_message = [] %}
    {% set tp.climate_mode = states(climate_mode) %}
    
    {% set variable_test = true %} {% if states(room_presence) in
    ['unknown','unavailble'] %}
      {% set tp.error_message = tp.error_message + ['The Room Presence Sensor is showing a state of "unknown" or "unavailable".'] %}
      {% set variable_test = false %}
    {% endif %} {#

          room_presence: !input room_presence
      manual_control: "{{ manual_temperature_control }}"
      manual_temperature: "{{ manual_temperature }}"
      climate_mode: "{{ climate_mode }}"
      current_temperature: "{{ climate_measurement }}"
      climate_entity: "{{ climate_entity }}"
      fan_modes: "{{ fan_mode }}"
      vertical_swing_modes: "{{ vertical_swing_mode }}"
      horizontal_swing_modes: "{{ horizontal_swing_mode }}"
      tp_name: "{{ tp_name }}"
      tp_names: "{{ tp_names }}"
      tp_starts: "{{ tp_starts }}"
      tp_stops: "{{ tp_stops }}"
      tp_fan_modes: "{{ tp_fan_modes }}"
      tp_vertical_swing_modes: "{{ tp_vertical_swing_modes }}"
      tp_horizontal_swing_modes: "{{ tp_horizontal_swing_modes }}"
      tp_eco_temperatures: "{{ tp_eco_temperatures }}"
      tp_comfort_temperatures: "{{ tp_comfort_temperatures }}"
      tp_climate_modes: "{{ tp_climate_modes }}"
      away_temperature: "{{ away_temperature }}"
      no_room_presence_wait: "{{ no_room_presence_wait }}"
      tp_days_of_week: "{{ tp_days_of_week }}"    
    #}


    {% if variable_test %}
      
      {% for item in tp_names %}
        {% if item != "Not Used" %}
          {% if tp_stops[loop.index0] != '00:00:00' and tp_starts[loop.index0] > tp_stops[loop.index0] %}
            {% set tp.error_message = tp.error_message + ['The start time (' ~ tp_starts[loop.index0] ~ ') must be before the stop (' ~ tp_stops[loop.index0] ~ ') time in the time period. Time Period ' ~ loop.index] %}
            {% set tp.config_status = false %}
          {% endif %}
          {% set eco_temp = tp_eco_temperatures[loop.index0] %}
          {% if not is_number(eco_temp) %}{% set eco_temp = 21 %}{% endif %}
          {% set comfort_temp = tp_comfort_temperatures[loop.index0] %}
          {% if not is_number(comfort_temp) %}{% set comfort_temp = 21 | float %}{% endif %}
          {% if tp_climate_modes[loop.index0] == 'Cooling' and eco_temp < comfort_temp %}
            {% set tp.error_message = tp.error_message + ['In Cooling mode, the eco temperature (' ~ eco_temp ~ ') must be greater than or equal to the comfort temperature (' ~ comfort_temp ~ ') in the time period. Time Period ' ~ loop.index] %}
            {% set tp.config_status = false %}
          {% endif %}
          {% if tp_climate_modes[loop.index0] == 'Heating' and eco_temp > comfort_temp %}
            {% set tp.error_message = tp.error_message + ['In Heating mode, the eco temperature (' ~ eco_temp ~ ') must be less than or equal to the comfort temperature (' ~ comfort_temp ~ ') in the time period. Time Period ' ~ loop.index] %}
            {% set tp.config_status = false %}
          {% endif %}
        {% endif %}
        {% if item != "Not Used" and tp.time_found == false %}
          {% if tp_climate_modes[loop.index0] == states(climate_mode) %}
            {% set tp.name = item %}
            {% set tp.start = tp_starts[loop.index0] %}
            {% set tp.stop = tp_stops[loop.index0] %}
            {% set tp.eco = tp_eco_temperatures[loop.index0] %}
            {% if not is_number(tp.eco) %}{% set tp.eco = 21 %}{% endif %}
            {% set tp.comfort = tp_comfort_temperatures[loop.index0] %}
            {% if not is_number(tp.comfort) %}{% set tp.comfort = 21 | float %}{% endif %}
            {% set tp.fan = tp_fan_modes[loop.index0] %}
            {% set tp.vertical = tp_vertical_swing_modes[loop.index0] %}
            {% set tp.horizontal = tp_horizontal_swing_modes[loop.index0] %}
            {% set tp.days_of_week = tp_days_of_week[loop.index0] %}
            
            {% if tp.current_day_of_week in tp.days_of_week %}
              {% if tp.current_time >= tp_starts[loop.index0] and (tp_stops[loop.index0] == '00:00:00' or tp.current_time < tp_stops[loop.index0]) %}
                {% set tp.start = tp_starts[loop.index0] %}
                {% set tp.stop = tp_stops[loop.index0] %}
                {% set tp.time_found = true %}
              {% endif %}
            {% endif %}
          {% endif %}
        {% endif %}
      {% endfor %}
      
      {% if is_number(tp.comfort) %}
        {% set tp.target = tp.comfort | float %}
      {% else %}
        {% set tp.target = 21 | float %}
      {% endif %}  

      {% if room_presence != "" %}
        {% set presence_last_change = state_attr(room_presence, 'last_change') %} 

        {% if states(room_presence) == 'off' and as_timestamp(now()) - presence_last_change > no_room_presence_wait %}
          {% if is_number(tp.eco) %}
            {% set tp.target = tp.eco | float %} 
          {% else %}
            {% set tp.target = 21 | float %}
          {% endif %}
        {% endif %}
      {% endif %}

      {% if states(manual_control) == 'on' and tp.climate_mode in ['Heating','Cooling'] %}
          {% if is_number(states(manual_temperature)) %}
            {% set tp.target = states(manual_temperature) | float %} 
          {% else %}
            {% set tp.target = 21 | float %}
          {% endif %}
          {% set tp.name = 'Manual' %}
      {% endif %}

      {% set tp.air_conditioning_needed = false %}  

      {% set tp.new_hvac_mode = 'off' %}

      {% set tp.climate_mode = states(climate_mode) %}

      {% set p_name = "" %}

      {% set p_time = "" %}

      {% if tp.climate_mode == 'Heating' %}
        {% set tp.new_hvac_mode = 'heat' %}
        {% if is_number(tp.current_temperature) %}
          {% set tp.current_temperature = tp.current_temperature | float %}
        {% else %}
          {% set tp.current_temperature = 21 | float %}
        {% endif %}
        {% if tp.current_temperature < (tp.target - 0.5) or states(climate_entity) == 'heat' %}
          {% set tp.air_conditioning_needed = true %}
        {% endif %}
        {% if tp.current_temperature > tp.target %}
          {% set tp.air_conditioning_needed = false %}
        {% endif %}
      {% endif %}

      {% if tp.climate_mode == 'Cooling' %}
        {% set tp.new_hvac_mode = 'cool' %}
        {% if is_number(tp.current_temperature) %}
          {% set tp.current_temperature = tp.current_temperature | float %}
        {% else %}
          {% set tp.current_temperature = 27 | float %}
        {% endif %}
        {% if tp.current_temperature > (tp.target + 0.5) or states(climate_entity) == 'cool' %}
          {% set tp.air_conditioning_needed = true %}
        {% endif %}
        {% if tp.current_temperature < tp.target %}
          {% set tp.air_conditioning_needed = false %}
        {% endif %}
      {% endif %}

      {% if tp.name == 'Off' or states(manual_control) == 'off' %}
        {% set tp.new_hvac_mode = 'off' %}
      {% endif %}

      {% if not tp.air_conditioning_needed %}
        {% set tp.new_hvac_mode = 'off' %}
      {% endif %}

      {% set current_hvac_mode = states(climate_entity) %}

      {% set current_fan_mode = state_attr(climate_entity, 'fan_mode') %}

      {% set current_vertical_swing_mode = state_attr(climate_entity, 'swing_mode') %}

      {% if states(fan_modes) != current_fan_mode %}
        {% set tp.status_change = true %}
      {% endif %}

      {% if states(vertical_swing_modes) != current_vertical_swing_mode %}
        {% set tp.status_change = true %}
      {% endif %}

      {% if tp.name != states(tp_name) %}
        {% set tp.status_change = true %}
      {% endif %}

      {% if current_hvac_mode != tp.new_hvac_mode %}
        {% set tp.status_change = true %}
      {% endif %}

      {{ {
        'name': tp.name, 
        'fan': tp.fan, 
        'vertical': tp.vertical, 
        'horizontal': tp.horizontal, 
        'mode': tp.new_hvac_mode, 
        'status_change': tp.status_change, 
        'config_status': tp.config_status, 
        'error_message': tp.error_message
        } }}
        
    {% else %}
      {% set tp.config_status = false %}
      {% set tp.error_message = tp.error_message + ['Something when wrong at variable recognition.'] %}
      {{ {
        'name': tp.name, 
        'fan': tp.fan, 
        'vertical': tp.vertical, 
        'horizontal': tp.horizontal, 
        'mode': tp.new_hvac_mode, 
        'status_change': tp.status_change, 
        'config_status': tp.config_status, 
        'error_message': tp.error_message
        } }}

    {% endif %}
sequence:
  - stop: All done.
    response_variable: result
mode: restart

I think you’re conflating separate issues, but also I think you’ve pounded the square peg hard enough into the round hole that you achieved the result you wanted anyway.

Yes you do have to use a namespace object inside a for loop if you want to access the changed variable outside of that for loop. That does not require a dictionary.

And yes creating a new dictionary (using the 2nd form I showed in my example) will of course work to output a dictionary. That does not require a namespace.

If you’re happy with what you’ve created, then let it be. I was typing this out before I saw your latest post, and now I don’t think it will be helpful for your current situation, but you may find it useful if you decide to actually use dictionaries in the future on another project:

Working with Dictionaries

You can’t directly change a key or value in a dictionary. You have to break it apart into something else that you can change, like a list, and then modify it, and then save it back as a new (or copy it over the old) dictionary.

Assume we start with a simple dictionary:

{% set my_dict = {
  "a": 25,
  "b": "some text",
  "charlie": 8.5
} %}

To change or add new keys in an existing dictionary:

{% set my_dict = dict(my_dict.items(), a="one", d=3.14) %}

To combine two dictionaries together (matching keys between otherdict and my_dict will take their values from otherdict):

{% set my_dict = dict(my_dict.items(), **otherdict) %}

To remove one or more keys from a dictionary:

{% set my_dict = dict(my_dict.items() | rejectattr(0,'==','b')) %}

True, but as mentioned before, namespace != dict. That first example you posted claimed to define a dict but it wasn’t (it was a namespace).

Plus we’re now 14 posts deep into this topic and you finally reveal that your application employs a for-loop (perhaps you knew all along you would use a for-loop but overlooked to mention it) . Every example you posted before that was an attempt to define a dictionary (with no mention of using a for-loop).

Your application doesn’t need a for-loop or a namespace … unless this is part of some sort of coding game that obliges you to employ them. :man_shrugging:

The reason you’re having trouble is because you aren’t embracing yaml & jinja. I can tell that you provide those scripts with separate lists of items, which forces you to use indexed values. Which ultimately makes this harder in general. You spend so much time in your if statements and loops setting variables equal to things when it can be completely avoided if you provide your script with a single list of dictionaries. If you post how you plan to use the script, I can help you reduce that code to 25 or 30 lines of code that will be much easier to use.

As for your complaints about documentation, you should read up on Jinja. HA documentation does not cover namespace because it’s a built-in for jinja. At the top of the templating page is a link to Jinja docs, which covers namespace.