Listen_state() duration parameter not working?

Hi all,

I’m trying to automate some sliders that control the volume of some Chromecasts and are also updated when the volume is changed by another method. However, when you slide the volume control (in the Home app or Spotify app, for example) it triggers a state change for every level of volume in between the start and final value, which causes some problems with the feedback loop to update the slider.

I have tried to add the duration = t parameter so that the callback on triggers once the volume has settled on the final value, but it does not seem to make any difference. It could also be something to do with how I’ve built the app, but I was expecting this to solve the problem based on the documentation.

Am I using this right?

Code below:

# Chromecast volume card
#
# Args:
#   name: name of chromecast
#

class ChromecastVolume(appapi.AppDaemon):

  def initialize(self):


    self.name = self.args['name']
    self.slider = "input_slider.chromecast_volume_{}".format(self.name)
    self.sensor = "sensor.chromecast_volume_{}".format(self.name)
    self.media_player = "media_player.{}".format(self.name)

    # !!!!
    # check duration parameter - slide volume change still fires multiple service calls
    # !!!!
    self.listen_state(self.update_slider, entity = self.sensor, duration = 1)
    self.listen_state(self.update_volume, entity = self.slider)

    self.listen_state(self.mute_on, entity = "input_boolean.chromecast_mute", new = "on")
    self.listen_state(self.mute_off, entity = "input_boolean.chromecast_mute", new = "off")

  def update_volume(self, entity, attribute, old_state, new_state, kwargs):

    sensor_value = self.get_state(self.sensor)
    set_cc_vol = float(new_state) / 10

    if sensor_value != set_cc_vol:
        self.log("update volume from {} to {}".format(sensor_value, new_state))
        self.call_service("media_player/volume_set", entity_id = self.media_player, volume_level = set_cc_vol)

  def update_slider(self, entity, attribute, old_state, new_state, kwargs):

    slider_value = self.get_state(self.slider)
    set_slider_vol = float(new_state) * 10

    if slider_value != set_slider_vol:
      self.log("update slider from {} to {}".format(slider_value, new_state))
      self.call_service("input_slider/select_value", entity_id = self.slider, value = set_slider_vol)

  def mute_on(self, entity, attribute, old_state, new_state, kwargs):

    self.call_service("media_player/volume_mute", entity_id = self.media_player, is_volume_muted = "true")

  def mute_off(self, entity, attribute, old_state, new_state, kwargs):

    self.call_service("media_player/volume_mute", entity_id = self.media_player, is_volume_muted = "false")

Strange, it doesn’t for me. Its actually one of the reasons I switched from OpenHab to HA.

An extract from my eTRV code is

        self.listen_state(self.slider_changed, self.input_slider)
    def slider_changed(self, entity, attribute, old, new, kwargs):
        if float(old) == float(new):
            self.log("Not sending repeat message", level = "DEBUG")
        else:
            reason = self.get_state(entity, "reason")
...

And I definitely only get an output when the slider stops moving.

Do you mean the HA slider? If so, yes that is how mine behaves as well - only triggering once you have released it.

Unless I’m missing something obvious this is my approach:

Two callbacks:

  1. one to change the volume of the Chromecast if you use the input_slider the set the volume in HA.
  2. one to update the slider in HA if the volume on the Chromecast is changed through another app.

‘1’ works fine and behaves as you described.

‘2’ however, has some problems depending on how you change the volume of the Chromecast. If you use either Spotify or the Google Home app to change the volume and drag the slider (rather than use volume rockers or click on the line to select) then you get a huge series of requests which causes a problems.

I thought the duration paramater added to the sensor that looks at the Chromecast volume would solve this though, but it does not seem to. Could be another problem I’m missing.

Sorry, I understand now.

I actually have a similar problem, as my eTrv is controlled by MQTT, and messages can come from other clients. I listen for the MQTT message and use the set_state

    def temp_command_event(self, entity, attribute, old, new, kwargs):
        self.log("temp_command_event: new = {}".format(new), level = "DEBUG")

        # Stores mqtt_message in state attributes so that another message
        # is not sent in the callback handler
        adjusted_target = float(new) - self.target_offset
        if float(self.get_state(self.input_slider)) != adjusted_target:
            self.set_state(self.input_slider, state = adjusted_target,
                    attributes = { "reason" : "mqtt_message"})

The slider_changed callback then checks for the reason attribute to decide whether to do anything or not.

Not sure I quite follow, are all of the messages sent via MQTT not the same?

Or do you have different MQTT messages depending on where it was updated? Good solution, but I don’t think I can use it here as all control goes through Chromecast component and you can’t differentiate, if I’ve understood you correctly, anyway.

i dont think duration works that way.

from the docs:

If duration is supplied as a parameter, the callback will not fire unless the state listened for is maintained for that number of seconds. This makes the most sense if a specific attribute is specified (or the default of state is used), and in conjunction with the old or new parameters, or both.

so like you do it, it has no clue what state it should have for the duration.

i think you better use a form of run_in in your callback.

like:

  def initialize(self):
     self.last_called = self.datetime()

  def update_slider(self, entity, attribute, old_state, new_state, kwargs):

    if (self.datetime() - self.last_called) < datetime.timedelta(seconds=1):
      self.cancel_timer(self.handle)
    self.handle = self.run_in(self.update_slider_now,2,new_state = new_state)
    self.last_called = self.datetime()

  def update_slider_now(self,kwargs):
    slider_value = self.get_state(self.slider)
    set_slider_vol = float(kwargs["new_state"]) * 10
    self.call_service("input_slider/select_value", entity_id = self.slider, value = set_slider_vol)

that way you create an update delay from 2 seconds that gets cancelled and renewed if within a second another update comes.

Don’t get distracted by the MQTT. I think the problem is the same (unless I am mistaken again) - you want to update the slider without activating what happens when the slider is moved manually.

In your case, you would update the update_slider method to use set_state instead of call_service(), and change update_volume to check if the reason for the change is the HA gui, or your sensor changing

something like

  def update_volume(self, entity, attribute, old_state, new_state, kwargs):
    if self.get_state(entity, "reason") == "chromecast":
        self.set_state(entity, attributes = {"reason" : ""})
        return

    sensor_value = self.get_state(self.sensor)
    set_cc_vol = float(new_state) / 10

    if sensor_value != set_cc_vol:
        self.log("update volume from {} to {}".format(sensor_value, new_state))
        self.call_service("media_player/volume_set", entity_id = self.media_player, volume_level = set_cc_vol)

  def update_slider(self, entity, attribute, old_state, new_state, kwargs):

    slider_value = self.get_state(self.slider)
    set_slider_vol = float(new_state) * 10

    if slider_value != set_slider_vol:
      self.log("update slider from {} to {}".format(slider_value, new_state))
      self.set_state(self.slider, state=set_slider_vol, attributes = {"reason":"chromecast"})

Thanks for clarifying, I had read the description in the docs as meaning that it would only trigger if the new state held that value for a given period of time, makes sense as to why it doesn’t work now.

Thanks for the suggestion, I’ll take a look tonight and report back.

1 Like

Ah, I get what you mean now, thanks for explaining.

I’ll have a go at putting something like that in, will report back with the final version.

Thanks again.

I’ve had a go at implementing the additional attribute, but it’s still not behaving as I would expect.

Unless I’m mistaken, changing the ‘reason’ attribute triggers the state change callback again, so the update_volume method gets called twice in a row, making the setting and checking of reason pointless? I might have done it wrong though but I’ll try separating it into a separate variable as changing that won’t trigger the callback.

class ChromecastVolume(appapi.AppDaemon):

  def initialize(self):

    self.cc_name = self.args["cc_name"]
    self.slider = "input_slider.chromecast_volume_{}".format(self.cc_name)
    self.sensor = "sensor.chromecast_volume_{}".format(self.cc_name)    
    self.media_player = "media_player.{}".format(self.cc_name)

    self.listen_state(self.update_slider, entity = self.sensor)
    self.listen_state(self.update_volume, entity = self.slider, attribute="all")

    self.listen_state(self.mute_on, entity = "input_boolean.chromecast_mute", new = "on")
    self.listen_state(self.mute_off, entity = "input_boolean.chromecast_mute", new = "off")

  def update_volume(self, entity, attribute, old_state, new_state, kwargs):

    if self.get_state(entity, "reason") == "chromecast":
        self.set_state(entity, attributes = {"reason" : ""})
        return

    sensor_value = round(float(self.get_state(self.sensor)), 2)
    set_cc_vol = round(float(new_state), 2)

    if sensor_value != set_cc_vol:
        self.log("update volume from {} to {}".format(sensor_value, set_cc_vol))
        self.call_service("media_player/volume_set", entity_id = self.media_player, volume_level = set_cc_vol)

  def update_slider(self, entity, attribute, old_state, new_state, kwargs):

    slider_value = round(float(self.get_state(self.slider)), 2)
    set_slider_vol = round(float(new_state), 2)

    if slider_value != set_slider_vol:
      self.log("update slider from {} to {}".format(slider_value, set_slider_vol))
      self.set_state(self.slider, state = set_slider_vol, attributes = {"reason" : "chromecast"})

  def mute_on(self, entity, attribute, old_state, new_state, kwargs):

    self.call_service("media_player/volume_mute", entity_id = self.media_player, is_volume_muted = "true")

  def mute_off(self, entity, attribute, old_state, new_state, kwargs):

    self.call_service("media_player/volume_mute", entity_id = self.media_player, is_volume_muted = "false")

I expect I have another check in the callback to see if the new state is the same as the old, which is why I haven’t noticed this. But an object variable would seem to do the job as well. I’m rather annoyed I didn’t think of that.

your right.
changing an atrribute sets of the listen_state again.
it could work if you just use an variable in the app that gets that value and not an atrribute.
but you would still get a race. you would get half the callbacks from your original approach.

if i am correct its like this:

changing the volumeslider from chromecast from 10 to 20 causes 10 times a listen_state call
from 10 to 11, from 11 to 12, etc.

you only want the callback to have effect after the last change (to 20)
the only way to do that is to ignore everything that changes shortly after another.
so create a delay for the reaction.

Yes, I think it is necessary to remove the additional callbacks anyway so I’m just doing that at the moment.

I tried to implement your delay suggestion but ended up in a situation where when you changed the slider twice within the delay time limit, it would get stuck in a loop changing between the two values. I think it was probably just me rather than an incorrect approach though. I’ll put some more code up when I’ve finished the first part and had a proper stab at the delay again.

hmm, with the way i did put up the code i cant see no problem with 2 series of changes inside the timespan from 2 seconds.

but you can easy debug and see where it goes wrong:

def initialize(self):
     self.last_called = self.datetime()
     self.laststate = 0

  def update_slider(self, entity, attribute, old_state, new_state, kwargs):

    if (self.datetime() - self.last_called) < datetime.timedelta(seconds=1):
      self.cancel_timer(self.handle)
      self.log("canceled change to: " + str(self.laststate))
    self.handle = self.run_in(self.update_slider_now,2,new_state = new_state)
    self.last_called = self.datetime()
    self.laststate = new_state

  def update_slider_now(self,kwargs):
    self.log("now changing to volume: " + str(kwargs["new_state"]))
    slider_value = self.get_state(self.slider)
    set_slider_vol = float(kwargs["new_state"]) * 10
    self.call_service("input_slider/select_value", entity_id = self.slider, value = set_slider_vol)

that should show all ignored changes, and finally the value that its set to.

Right, first of all I got the suggestion from @gpbenton implemented, which nicely stopped the feedback loop. I then implemented your delay suggestion, thanks again for the code, much appreciated!

For anyone that ends up here, the code is below and the rest of my config can be found here and here.

import appdaemon.appapi as appapi
from datetime import timedelta

# Chromecast volume card
# 
# Updates the volume of a chromecast following a change of value from an input_slider. 
# Conversely, it will update the slider value should the volume of the chromecast be 
# updated from another input source (Google Home app, for example)
# Requires the following components to be available in Homeassistant:
# input_slider.chromecast_volume_<name> : values 0-1, 0.01 increments
# sensor.chromecast_volume_<name> : see template sensor for chromecast volume
# media_player.<name> : chromecast device
# 
# Args:
#   name: name of Chromecast as it appears in Homeassitant
#

class ChromecastVolume(appapi.AppDaemon):

  def initialize(self):

    self.cc_name = self.args["cc_name"]
    self.slider = "input_slider.chromecast_volume_{}".format(self.cc_name)
    self.sensor = "sensor.chromecast_volume_{}".format(self.cc_name)    
    self.media_player = "media_player.{}".format(self.cc_name)

    self.last_called = self.datetime()
    self.laststate = 0

    self.prevent_slider_loop = False
    self.prevent_vol_loop = False
    
    self.listen_state(self.update_slider, entity = self.sensor)
    self.listen_state(self.update_volume, entity = self.slider)
    self.listen_state(self.mute_on, entity = "input_boolean.chromecast_mute", new = "on")
    self.listen_state(self.mute_off, entity = "input_boolean.chromecast_mute", new = "off")

  def update_volume(self, entity, attribute, old_state, new_state, kwargs):
  
    if self.prevent_vol_loop == True:
      self.prevent_vol_loop = False
      return

    self.prevent_slider_loop = True

    set_cc_vol = round(float(new_state), 2)
    self.call_service("media_player/volume_set", entity_id = self.media_player, volume_level = set_cc_vol)

  def update_slider(self, entity, attribute, old_state, new_state, kwargs):
  
    if (self.datetime() - self.last_called) < timedelta(seconds=1):
      self.cancel_timer(self.handle)
    self.handle = self.run_in(self.update_slider_now,2,new_state = new_state)
    self.last_called = self.datetime()
    self.laststate = new_state

  def update_slider_now(self,kwargs):
    
    if self.prevent_slider_loop == True:
      self.prevent_slider_loop = False
      return

    self.prevent_vol_loop = True
    
    set_slider_vol = round(float(kwargs["new_state"]), 2)
    self.call_service("input_slider/select_value", entity_id = self.slider, value = set_slider_vol)

  def mute_on(self, entity, attribute, old_state, new_state, kwargs):

    self.call_service("media_player/volume_mute", entity_id = self.media_player, is_volume_muted = "true")

  def mute_off(self, entity, attribute, old_state, new_state, kwargs):

    self.call_service("media_player/volume_mute", entity_id = self.media_player, is_volume_muted = "false")
2 Likes