#Call service but prevent recursive automation

145 messages · Page 1 of 1 (latest)

static echo
#

I'm writing automation to have a 'master volume' slider for multi-zone audio. I have the master slider get its value set when a zone 'joins' the group. But I also have automation so that when an individual zone's volume changes, it recalculates the 'master' volume, which is an average of all zones. The idea being the master volume slider can change all zones relative to themselves at the same scale.

But if either of these events happen, it calls the other automation since one is triggered off of the master value changing and the other actually changes the value.

granite beacon
#

So to understand the process a bit better, if you have 3 zones (z1-3)
Z1: 10, z2: 6, z3: off, (master volume: (6+10)/2=8
Turn zone 3 on
Automation 1 sets zone 3 to master volume = 8
Automation 2 sees zone 3 has changed volume and recalculates master volume (6+10+8)/3 = 8
I'm not really seeing the problem here...

static echo
#

The issue is that both 1 and 2 call volume_set on one or the other, which triggers the other automation to run, resulting in an infinite loop

#

One thing I thought of was to create a 'script' entity that accepts a variable, but I don't know how to access those variables. Because then I could pass in which action to actually perform and then prevent the other action from happening unless that variable or key is set.

#

I think that would work if I could detect them. I'd have to play around with it.

granite beacon
#

Gotcha. You could try putting both triggers in the same automation in single mode - so it won't trigger again while it's running

#

You can also detect the context of a trigger (trigger.context.parent_id) which will be None for a manual change and refer to an automation if that changes it

crimson quartz
#

In your case, you'd make a template number. Where the state is a summation of the volumes with whatever algo you want and the service calls make the adjustment. No need for an automation or debouncing logic.

static echo
#

I ended up solving this by creating a separate entity that is set when one of the actions happens to determine what kind of action I am doing. Then when it triggers its recursive set on the other entity it checks to see if that variable is set and if it is it doesn't proceed

crimson quartz
#

If you make a template number properly, the service calls that set the states will naturally update the state of the number template without needing to denounce things like you’re doing

static echo
#

But I want it to. Otherwise how would I recalculate the other volumes when it's number changes

crimson quartz
#

The template does that

static echo
#

Sorry you are right. I didn't make a template number, I made it an input number

crimson quartz
#

Right, and that’s why you have to denounce things

#

Template number, you don’t

static echo
#

Interesting. I'll check out template number today. Thanks for the info!

static echo
#

@crimson quartz looking at number template, this is doing exactly what I'd like done. I normally do my automations / logic in node-red, so will have to adjust a bit for this template. But for the state, I need to get just the entity values where a media_player's source is set to this zone. Is there a way to get this?

crimson quartz
#

Yes, most likely, however I'd need to know what you mean "this zone"

#

screenshots of the media player and attributes in developer tools -> states page would help

static echo
#

Right. So here are the media_players. I would only want to use the ones that have a source of "Kitchen Music" for example to be included in the calculation. But this leads to another ineresting point. If a "zone" changes (i.e., if a player is on Kitchen source and changes to 'Porch' source), I would want to recalculate the template number in that case as well since the Kitchen zone has one less media player to contribute to its average calculation.

#

So is it possible to trigger / set a value of a number template? Or does it recalculate its state when the source of one of those players? Or would I issue a set_value action against the template and it'll recalculate its state then?

crimson quartz
static echo
#

Yeah

crimson quartz
#
{{ integration_entities('snapcast') 
    | select('search', '^media_player.')
    | select('is_state_attr', 'source', 'Kitchen Music')
    | map('state_attr', 'volume_level')
    | reject('none')
    | average }}
static echo
#

Oh this is awesome! Thanks

#

So in the set_value portion, I guess I'd need a similar way to pipe those results to a volume_set action right? Instead of average at the end, I'd have something else?

crimson quartz
#

set_value, you just need the list of entities

#
{{ integration_entities('snapcast') 
    | select('search', '^media_player.')
    | select('is_state_attr', 'source', 'Kitchen Music')
    | list }}
#

the volume level will always be volume_level: "{{ value }}"

static echo
crimson quartz
#

entity_id field needs to be in target:

static echo
#

So then how do I trigger the template number to recalculate when a zone volume changes?

crimson quartz
#

templates recalculate naturally

static echo
#

Ok I think I'm almost there, but getting this error when I change the master volume:

#

Failed to perform the action number/set_value. expected float for dictionary value @ data['value']

Here's what I've got:

- number:
  - name: Master Volume Kitchen
    set_value:
      - service: media_player.volume_set
        target:
          entity_id: >
            {{ integration_entities('snapcast')
            | select('search', '^media_player.')
            | select('is_state_attr', 'source', 'Kitchen Music')
            | list }}
        data:
          value: 0.25
    state: "{{ integration_entities('snapcast')
      | select('search', '^media_player.')
      | select('is_state_attr', 'source', 'Kitchen Music')
      | map('state_attr', 'volume_level')
      | reject('none')
      | average }}"
    step: .01
    min: 0
    max: 1
crimson quartz
#

use multi-line notation for state

#

just like you did with entity_id

#

no quotes, on following lines with >

static echo
#

That returns a string, but it's expecting a float

#

Is there a way to cast it with multiline?

crimson quartz
#

post the full error

static echo
#

2024-10-15 13:05:09.628 ERROR (MainThread) [homeassistant.components.template.template_entity] Error validating template result '0.47"' from template 'Template<template=({{ integration_entities('snapcast') | select('search', '^media_player.') | select('is_state_attr', 'source', 'Kitchen Music') | map('state_attr', 'volume_level') | reject('none') | average }}") renders=4>' for attribute '_attr_native_value' in entity number.master_volume_kitchen validation message 'expected float'

crimson quartz
static echo
#

Ok that errors gone. So now, with that in place, adjusting the zone volume isn't changing the template's value. And changing the template's value is giving this error:
Failed to perform the action number/set_value. expected float for dictionary value @ data['value']

crimson quartz
#

you didn't use set_volume properly

static echo
#

Current template:

- number:
  - name: Master Volume Office
    set_value:
      - service: media_player.volume_set
        target:
          entity_id: >
            {{ integration_entities('snapcast')
            | select('search', '^media_player.')
            | select('is_state_attr', 'source', 'Office Music')
            | list }}
        data:
          volume_level: 0.25
    state: >
      {{ integration_entities('snapcast')
      | select('search', '^media_player.')
      | select('is_state_attr', 'source', 'Office Music')
      | map('state_attr', 'volume_level')
      | reject('none')
      | average }}
    step: .01
    min: 0
    max: 1
crimson quartz
#

you used value: 0.25 for some reason when that should be volume_level: "{{ value }}"

static echo
#

Well I need to add in logic to calculate the zone based on a scale.

crimson quartz
#

no?

#

the slider value should go straight into the media player

#

you have the number set from 0 to 1, which is the same range for media player volume

static echo
#

Well if zone 1 is at 25% (0.25) and zone 2 is at let's say .5, then adjusting the master volume should increase them relative to their current value.

crimson quartz
#

no

#

if you want that, you need to change the state calculation

#

if you want each one to move individually, you need to change your set_value function to adjust them all individually

static echo
#

Is it possible to do that dynamically where it only does a set on the zones that have the correct source?

crimson quartz
#

You'd have to build your data and do a repeat

#

in the set_value area

static echo
#

Ok

crimson quartz
#

and you'd need to get the previous volume level

static echo
#

Right.

#

This is very interesting and the furthest I've gone down this dynamic YAML path. I may just keep this in a node-red automation with a debounce since I was able to just script it all with javascript.

#

With the debounce.

crimson quartz
#

it's really not too hard

static echo
#

is it possible to do some sort of debug statements in this to see what kind of values I'm passing around?

crimson quartz
#
- number:
  - name: Master Volume Office
    set_value:
      - variables:
          delta: >
            {{ value - this.state | default(0) }}
          entities: >
            {{ integration_entities('snapcast')
              | select('search', '^media_player.')
              | select('is_state_attr', 'source', 'Office Music')
              | list }}
          targets: >
            {% set ns = namespace(items=[]) %}
            {% for entity in entities if state_attr(entity, 'volume_level') | is_number %}
              {% set ns.items = ns.items + [{'entity': entity, 'level': state_attr(entity, 'volume_level') + delta}] %}
            {% endfor %}
            {{ ns.items }}
      - condition: template
        value_template: "{{ targets | length > 0 }}"
      - repeat:
          for_each: "{{ targets }}"
          sequence:
          - service: media_player.volume_set
            target:
              entity_id: "{{ repeat.item.entity }}"
            data:
              volume_level: "{{ repeat.item.level }}"
    state: >
      {{ integration_entities('snapcast')
      | select('search', '^media_player.')
      | select('is_state_attr', 'source', 'Office Music')
      | map('state_attr', 'volume_level')
      | reject('none')
      | average }}
    step: .01
    min: 0
    max: 1
#

or you could do it in the repeat loop

#

which might be easier for you to follow

static echo
#

I'm still getting this when changing the master volume slider: Failed to perform the action number/set_value. expected float for dictionary value @ data['value']

crimson quartz
#
- number:
  - name: Master Volume Office
    set_value:
      - variables:
          entities: >
            {{ integration_entities('snapcast')
              | select('search', '^media_player.')
              | select('is_state_attr', 'source', 'Office Music')
              | list }}
      - repeat:
          for_each: "{{ entities }}"
          sequence:
          - service: media_player.volume_set
            target:
              entity_id: "{{ repeat.item }}"
            data:
              volume_level: >
                {% set current = state_attr(repeat.item, 'volume_level') %}
                {{ current + (value - current) }}
    state: >
      {{ integration_entities('snapcast')
      | select('search', '^media_player.')
      | select('is_state_attr', 'source', 'Office Music')
      | map('state_attr', 'volume_level')
      | reject('none')
      | average }}
    step: .01
    min: 0
    max: 1
crimson quartz
#

so I have no idea how you're getting that error.

static echo
#

Yeah I'm not sure...

crimson quartz
#

literally copy/paste what I wrote

#

don't alter your code

static echo
#

I did

crimson quartz
#

so where are you calling set_value?

#

how are you "testing" this

static echo
#

I've got the slider in the UI. Bubble-card. Here's the YAML.'

crimson quartz
#

don't use bubble card, go to the entity itself

static echo
#

ok wait

#

That was a typo on my end in the UI.

#

Ok, so now it works when I change a zone volume, but changing the master doesn't update the zone volume.

#

Master Volume Office: Repeat 'for_each' must be a list of items in Master Volume Office, got:

crimson quartz
#

fixed, try again

#

or just use this

static echo
#

That worked!

crimson quartz
#
- number:
  - name: Master Volume Office
    set_value:
      - repeat:
          for_each: >
            {{ integration_entities('snapcast')
              | select('search', '^media_player.')
              | select('is_state_attr', 'source', 'Office Music')
              | list }}
          sequence:
          - service: media_player.volume_set
            target:
              entity_id: "{{ repeat.item }}"
            data:
              volume_level: >
                {% set current = state_attr(repeat.item, 'volume_level') %}
                {{ current + (value - current) }}
    state: >
      {{ integration_entities('snapcast')
      | select('search', '^media_player.')
      | select('is_state_attr', 'source', 'Office Music')
      | map('state_attr', 'volume_level')
      | reject('none')
      | average }}
    step: .01
    min: 0
    max: 1
#

both are the same essentially

static echo
#

Man, thanks for all the help!

#

I need to get into the scripting with YAML in HA

crimson quartz
static echo
#

haha, it did

static echo
#

@crimson quartz Ok last question I think and I'll quit bugging you. It doesn't seem like the same scale is being applied to all volumes. I also tried setting the scale to new_value / old_value of the master volume and multiplying the individual volume by that. Either way I think it works, but the scale is wrong, like maybe the way I'm referencing the old volume is getting updated during the run of the set_value? Not sure if this is a math issue or a state issue with the values.

crimson quartz
#

it's the same delta

#

value - current

#

they all move at the same rate, if you want the rate to be dynamic for each one, I'd need to know how you had it before

static echo
#

I would just imagine that if Zone A is 20% and zone B is 50%, then as I increased the master volume, zone A would always be less than zone B right? But zone A eventually gets higher than B

#

Maybe I'm wrong

crimson quartz
#

A will never get higher than b

#

your master would be at 35. You'd move master to 45, so A would move to 30 and B would move to 60

#

You could try this instead

#
- number:
  - name: Master Volume Office
    set_value:
      - variables:
          last_state: "{{ this.state | default(0) }}"
      - repeat:
          for_each: >
            {{ integration_entities('snapcast')
              | select('search', '^media_player.')
              | select('is_state_attr', 'source', 'Office Music')
              | list }}
          sequence:
          - service: media_player.volume_set
            target:
              entity_id: "{{ repeat.item }}"
            data:
              volume_level: >
                {% set current = state_attr(repeat.item, 'volume_level') %}
                {{ current + (value - last_state) }}
    state: >
      {{ integration_entities('snapcast')
      | select('search', '^media_player.')
      | select('is_state_attr', 'source', 'Office Music')
      | map('state_attr', 'volume_level')
      | reject('none')
      | average }}
    step: .01
    min: 0
    max: 1
#

slight modification

static echo
#

It's just jumping a LOT. Here are some numbers for an action

#

Zone A: 0.26
master: 0.26

I tap in the center of the slider, should be around .5, the value is now 0.74

#

Almost like it's off by an order of magnitude

#

Ah, I wonder if last_state is always 0, that would be .26 + (0.5 - 0) and that would make sense

static echo
#

yeah, I think the issue is there is no previous state, at least not via that variable.

crimson quartz
#

You have to move it once to get a previous state

static echo
#

I still think something's off. Prob. with the math. If I hit around 50% on the master volume, it doesn't actually go that high. The highest volume does and the master is averaged to lower than that

#

Yeah, it's not getting that state or something. If press in the middle over and over, it jumps between 70ish and 30ish

crimson quartz
#

that's how it's going to work

static echo
#

Ok. I think the logic I want is the master volume will go to that value and the other volumes will be set based on the scaled difference

crimson quartz
#

what do you mean "scaled difference"

#

FYI scaling something in math is when you multiply it by a value to increase or decrease it's value

#

which im 99% sure is not what we want here

#

what we are doing is offsetting the value

static echo
#

What I'm thinking and what I thought was how things like Sonos or grouped media players worked was let's say master is at 25% and other volumes are whatever they are. If I move the master volume to 50%, I would expect that 50% is the new average.

#

And the other volumes are adjusted accordingly

crimson quartz
#

that's not necessarily how averaging works

#

take our previous example

#

you also have to realize that HA will chop values, so if you move from 50% to 60% and one of the values was at 100%, it won't go past 100% which will adjust your average lower than where you moved the slider

#

I suppose you could change the equation to {{ value - (value - current) }}

static echo
#

I guess this is what I was expecting to see (this is what my old automation did)

Master: 49%
Zone A: 40%
Zone B: 62%

I change master to 62%
Zone A: 49%
Zone B: 75%

So when I change the master slider to a value, it's value becomes that new position.

#

If I remove Zone A, Master volume calculates to match 75%. If I add back Zone A, it calculates and puts the master volume back to 62%

#

Since that's the average between the 2 active zones.

crimson quartz
#

zone A: last_value = 49, current=40, value = 62.

Equation 1: {{ current + (value - last_state) }} -> 40 + (62 - 49) = 53
Equation 2: {{ value - (value - current) }} -> 62 - (62 - 40) = 40
Equation 3: {{ value - (value - last_state) }} -> 62 - (62 - 49) = 49

#

so it looks like you want {{ value - (value - last_state) }}

static echo
#

Got it! Last piece: this is giving me an error if no media players are in that zone, I imagine I need a default?

    state: >
      {{ integration_entities('snapcast')
      | select('search', '^media_player.+_snapcast_client')
      | select('is_state_attr', 'source', 'Office Music')
      | map('state_attr', 'volume_level')
      | reject('none')
      | average }}
#

Template error: average got invalid input '(<generator object select_or_reject at 0x7a2f003bad40>,)' when rendering template '{{ integration_entities('snapcast') | select('search', '^media_player.+_snapcast_client') | select('is_state_attr', 'source', 'Porch Music') | map('state_attr', 'volume_level') | reject('none') | average }}' but no default was specified****

#

What's the proper way to pass in a default here? I'm not sure of the order of operations givin these pipes in the templating system

granite beacon
#

map('state_attr', 'volume_level', default=none) iirc

static echo
#

Are there any good resources on getting better with this templating language?

granite beacon
#

Helping people with stuff here is how i'm learning myself 😛

static echo
#

Thank you both for all the help! This has been quite the learning experience.