#How to sync light state _properly_?

1 messages · Page 1 of 1 (latest)

burnt wing
#

I've recently got a handful of Nanoleaf's Sense+ remotes, which work amazingly well... With the limited number of products they support. In my office, I have both a Shapes and an older Light Panels setup. Sadly, Sense+ at this moment does not support the latter.

However HA does expose state changes for both, and I was hoping to utilise an automation to be triggered by a state change on the Shapes device, then use that state change to create a differential list of state and attribute fields that changed (e.g. if I change the effect, or the brightness, etc.), then apply those changes to the Light Panels (potentially with a 200-400ms debounce to avoid duplicate events when I control the two lights as a group).

But I can't seem to get the logic right. My idea was to create a variable and template the hell out of it to give me a dict of changed attributes, something like this:

{ 'state': { 'from': 'off', 'to': 'on'}, 'brightness': { 'from': 0, 'to': '80'}, 'effect': { 'from': 'off', 'to': 'Forest'}

I'm already at odds with Python, Jinja templating makes it even harder, and HA's limitations (e.g. no access to the various dict methods like update) just make me want to hang my head in the wall.

I really want to avoid hardcoding the attributes that I monitor, so a solution that could dynamically filter out changed attributes and map them to the above format would be the best approach for me I think.

Any ideas how this could be nicely implemented?

marsh trout
#

State changes already have all that information in triggers

#

and jinja has the ability to combine dictionaries, it has many ways to do this actually. Just not the same way as python

#

so at this point, you need to just share your current automation and what you want it to do

burnt wing
#

@marsh trout apologies for the late reply, got a work call.

While you're right that the state base trigger contains the from and to snapshots, they're not, in my experience, a diff list. At least on my Nanoleaf lights, I got the whole light state snapshot from both before and after the change event.

I did try making a Jinja based approach to create this diff list, but the below code will not work for obvious reasons:

variables:
  diff: >
    {% set diff = dict() %}
    {% if trigger.from_state.state != trigger.to_state.state %}
      {% set _ = diff.update({
        'state': {
          'from': trigger.from_state.state,
          'to': trigger.to_state.state
        }
      }) %}
    {% endif %}
    {% set keys = (trigger.from_state.attributes.keys() | list) + (trigger.to_state.attributes.keys() | list) %}
    {% for k in keys | unique %}
      {% if trigger.from_state.attributes[k] != trigger.to_state.attributes[k] %}
        {% set _ = diff.setdefault('attributes', {}).update({
          k: {
            'from': trigger.from_state.attributes[k],
            'to': trigger.to_state.attributes[k]
          }
        }) %}
      {% endif %}
    {% endfor %}
    {{ diff }}

This would've resulted in a Python object, that, when rendered to JSON, would have looked something like this:


{
    'state': {'from': 'off', 'to': 'on'},
    'attributes': {
        'brightness': {'from': 100, 'to': 120},
        'rgb_color': {'from': [255, 200, 150], 'to': [255, 180, 150]},,
        'effect': {'from': 'none', 'to': 'Forest'}
    }
}

This could've been then used to determine what to do with the other light: turn it on or off, set the effect, RGB colour, brightness, etc.

Since the trigger.to_state variable would've had a full snapshot, I would be applying unneeded parameters (plus I'd want to exclude a handful of read-only fields too).

#

The end result automation would look something like this:

action:
  - choose:
      # If target should be off, we just turn it off (attributes don't matter when off)
      - conditions: "{{ diff.state is defined and diff.state.to == 'off' }}"
        sequence:
          - service: light.turn_off
            target:
              entity_id: light.target_light

    default:
      # In all other cases (on with or without attribute changes), turn on with attributes
      - service: light.turn_on
        target:
          entity_id: light.target_light
        data: >
          {% set out = dict() %}
          {% if diff.attributes is defined %}
            {% for k,v in diff.attributes.items() %}
              {% set _ = out.update({ k: v.to }) %}
            {% endfor %}
          {% endif %}
          {{ out }}
hexed marlin
#

You need to use a namespace. The for loop uses a different scope, so changes in the loop are not visible outside it

hexed marlin
#

Besides that, there is no update method in jinja, but you can use dict(dict1, **dict2) to update a dictionary

variables:
  diff: >
    {% set ns = namespace(state={}, attributes={}) %}
    {% if trigger.from_state.state != trigger.to_state.state %}
      {% set ns.state = {
        'state': {
          'from': trigger.from_state.state,
          'to': trigger.to_state.state
        }
      } %}
    {% endif %}
    {% set keys = (trigger.from_state.attributes.keys() | list) + (trigger.to_state.attributes.keys() | list) %}
    {% for k in keys | unique %}
      {% if trigger.from_state.attributes.get(k) != trigger.to_state.attributes.get(k) %}
        {% set ns.attributes = ns.attributes | combine({
          k: {
            'from': trigger.from_state.attributes.get(k),
            'to': trigger.to_state.attributes.get(k)
          }
        }) %}
      {% endif %}
    {% endfor %}
    {{ ns.state | combine(dict(attributes=ns.attributes) if ns.attributes else {}) }}
#

tested with

{% set trigger = dict(from_state=dict(state='on', attributes=dict(test=0)), to_state=dict(state='off', attributes=dict(test=1))) %}

which resulted in


{
  "state": {
    "from": "on",
    "to": "off"
  },
  "attributes": {
    "test": {
      "from": 0,
      "to": 1
    }
  }
}
#

and your action then should look like this

      - action: light.turn_on
        target:
          entity_id: light.target_light
        data: >
          {% set ns = namespace(out=dict()) %}
          {% if diff.attributes is defined %}
            {% for k,v in diff.attributes.items() %}
              {% set ns.out = ns.out | combine({k: v}) %}
            {% endfor %}
          {% endif %}
          {{ ns.out }}
#

some notes:

  • if you change the name of a light, this will be an attribute change, and will probably trigger your automation, resulting in a faulty action, you might want to filter that out
  • if you're only interested in applying the to state, you could decide to only store the attribute keys which have change, with the to value. Then you don't need to create a new dict, you can just use diff.attributes in the action
marsh trout
#
{% set ns.out = ns.out | combine({k: v}) %}
#

nice thing about this is that it allows you to use anything as the key

#

you're no longer restricted to strings that don't start with a number.

marsh trout
hexed marlin
#

oh crap, I forgot about that combine method

hexed marlin
hexed marlin
#

everything updated in the posts above

burnt wing
burnt wing
#

Much appreciated, gentlemen!

burnt wing
#

Thanks again @hexed marlin - the variable part works beautifully. However the created dict doesn't seem to be directly usable, as while {{ diff }} prints the diff object properly, {{ diff.state }} and {{ diff.attributes }} is not defined, nor can I access those as a regular dict key-value pair:

# setup
variables:
  test1: '{{ diff }}'
  test2: '{{ diff.state }}'
  test3: '{{ diff.attributes }}'
  test4: '{{ diff[state] }}'
  test5: '{{ diff['state'] }}'

# result
test1: >-
  {'state': {'from': 'on', 'to': 'off'}, 'attributes': {'effect': {'from':
  'Forest', 'to': None}, 'color_mode': {'from': <ColorMode.HS: 'hs'>, 'to':
  None}, 'brightness': {'from': 175, 'to': None}, 'hs_color': {'from': (0, 0),
  'to': None}, 'rgb_color': {'from': (255, 255, 255), 'to': None}, 'xy_color':
  {'from': (0.323, 0.329), 'to': None}}}
test2: ''
test3: ''
test4: ''
test5: ''
hexed marlin
#

Looks like the enum value for color_mode is causing it to be stored as a string instead of a mapping