[AppDaemon] Control Brightness Level via Slider!

Hi all!

I’m relatively new here, but have a very good grasp on how to code in python 2.7, 3.3+ and so HomeAssistant jumped right off the shelf at me. Getting used to the platform was fairly easy, but quickly became limiting. I have many lights in might apartment that are controlled via a single panel, and I found that tapping/clicking on a light just to control its brightness was incredibly tedious, so I tried to make a simple automation to control it!

- alias: "Set Brightness Level from Slider"
  hide_entity: False
  trigger:
    platform: state
    entity_id: input_slider.lights_brightness_level
  action: >-
      service: light.turn_on
      {%- for state in states.light if state.state == "on" -%}
        entity_id: {{ state.entity_id }}
        data_template:
          brightness: {{ (trigger.to_state | int * 255 / 100) | round }}
      {%- endfor -%}

However there was one problem with that … you can’t dynamically determine entity_ids! Templating only allows you to go so far as to format or manipulate the data you want to send TO the entity which bummed me out. This is a nascent project though, so I quickly decided to port over my automations to AppDaemon apps! This thing is great, I love it. The above automation is achieved with the code I have below.

import appdaemon.appapi as appapi

class SliderBrightnessLevel(appapi.AppDaemon):

    def initialize(self):
        self.listen_state(self.adjust_bright_level, 'input_slider.lights_brightness_level')
                                    #entity can be just "light" to listen to all lights

    def adjust_bright_level(self, entity, attribute, old, new, kwargs):
        self.log("{}.{} changed from {} to {}.".format(entity, attribute, old, new))

        for entity_id, state in self.get_datapoint_from_all(datapoint='state', service='light'):
            if state == 'on':

                # brightness is in 0 - 255, constrain to 255 to get percentage-of
                blevel = round(int(float(new)) * 255 / 100)
                
                self.log("light.{} set to {}.".format(entity_id, blevel))
                self.turn_on(entity_id, brightness=blevel)
    
    # helper functions
    def get_datapoint_from_all(self, datapoint, service):
        all_things = self.get_state(service)

        for entity_id in all_things:
            for data in all_things[entity_id]:
                if data == datapoint:
                    yield entity_id, all_things[entity_id][data]

Ninja edit: It should be noted that the logging done here is unnecessary, as HomeAssistant keeps a record of everything that is done already. I was using logs to debug what is/isn’t happening as I learn the framework that the plugin class requires.

Python is about being beautiful, and so I wrote a helper function that allows my code to stay descriptive and well, beautiful. It pulls out specific datapoints from a group of like-entities. In this case, you can see that on line 12, I am pulling all the lights. You’ll see the adjust_brightness does just that - it loops through all the lights, and if they are on, then they will have their brightness level set to blevel!

This is still a very, very basic automation … so it is just the beginning for me! Be on the lookout, as I will surely share more projects and scripts as I learn to grow with the platform.

Thanks for reading,
SupahNoob

7 Likes

very cool, I want to add it, but not for all lights but certain group.

I am new to python and few weeks with HA, how you add the second part … (!!?)

nice, @SupahNoob
please share your next apps with Appdeamon in the topic title, so appdaemon fans dont miss it. :wink:

@anon35356645 you have to install appdaemon.


to be able to use appdaemon apps.

not really familiar with python, but not afraid to give it a try with appdaemon?
appdaemon for beginner is here:
http://appdaemon.reot.org

Unfortunately no time to start serious programming. Is just a weekend hobby, for few hours

I’m sure this can be done! In doing some other notification-based automations, it seemed to be that a group was just a collection of entities. I will port over my notification-based automations to AppDaemon next and then come back and share an alternative helper function to grab a datapoint or entity_id in a group of your choosing. That should be very simple!

ed//
In further testing, you can actually call this already by specifying the group’s entity_id instead of light! I will eventually rename my helper function to be more descriptive, but it should work just fine for you once you change that :slight_smile:

Great suggestion. I’ve tagged the title as such.

1 Like

thanks a lot !

Hello,

I know this is an old topic that I’m digging up, but I’m currently trying to do something similar with AppDaemon.

I want to have a slider to control various dimmers. That part is well explained in the post above and works fine for me.
The problem I have is that I can also control those dimmers via other means (switchs on the wall among others). I would like the slider to de updated based on these changes (so that the slider does not show a none zero value when all light are off for instance).
I cannot find a way to update the slider value without triggering again a change in slider which triggers the change in dimming value, which starts an infinte loop of updates.

Is their any way to do a “silent” update the slider value (i.e. without triggering automations listening to this slider changes) ?

Any idea welcome…

thats alway the trouble when trying to keep several things at the same state.
there is no way to change a state without triggering a state change event.

the only thing i can think of it to avoid that it changes to zero at all.

i guess you listen to a light and then in the callback you change the slider.
make sure in the callback that when its zero it doesnt do the change.

Thanks for taking the time to answer.
Actually it’s not only the special case of zero, I was also trying to update the slider each time a dimmer change by calculating the average dim level of all lights and mapping it to the slider…I’ll have to find another way to do this then :frowning:

please share all code that changes your slider.
maybe i can help out to make it work without creating a loop.

Thanks a lot for offering to help on this.

My current version of code is below (Warning it doesn’t not work properly and creates loops in Appdaemon…).

It’s one of my first code in python so might not be the most efficient or pretty. I tried to put some comments to help you udnerstand what I’m trying to do.
As you can see also I took some inspiration from some of your code snippets you share here and there on the forum :wink:

import appdaemon.plugins.hass.hassapi as hass

##############################################################################################
# Code to create a single slider to control a group of lights with mixed status (lights and switches)
# If the light suport dimming, the dimme value is used
# If the light or switch do not support dimming, off is considered as brightness 0 and on as brightness 100
# Changing the input slider send commands to control all the lights and switch in the groups
# Changing any light or switch in the group updates the input slider so that it reflects accurately the status of the group
#
# Args:
## input: Name of the input slider used (should be of type input_number)
# entities: list of entities which are part of the group (should be of type lights or switches)

# TODO: remove infinite loop (call to set slider to updated level triggers slider change function
#       add handling of light based on input_select switch (Level 1 for < 50% and Level 2 for > 50%)
#

class MixGroupLight(hass.Hass):

  def initialize(self):
      # make sure we ignore any initial state update at startup
      self.set_state("sensor.notify_message_mixgroup",state="silent_update")
      # after 5 seconds we remove the flag silent_update (hoping that initial startup is finished!)
      self.run_in(self.set_state("sensor.notify_message_mixgroup",state=" "),5)

      # listen to state change of the slider
      self.listen_state(self.on_control_change, self.args["input"])
      # listen to state change of each entity in the mixed group
      for entity in self.args["entities"]:
          self.listen_state(self.on_entity_change, entity)


  #change all entities in the group based on the change of the input
  def on_control_change(self, entity, attribute, old, new, kwargs):
      self.log("Entering on_control_change {}.{} changed from {} to {}.".format(entity, attribute, old, new))

      # check if we are currently doing a silent_update. If yes return without sending commands to the entities
      message = self.get_state("sensor.notify_message_mixgroup")
      if "silent_update" in message:
          self.log("on_control_change was called without changing the states of entities")
          # remove silent_update flag after 2 seconds as this fnction is called mulitple times in a row when entity changes
          self.run_in(self.set_state("sensor.notify_message_mixgroup",state=" "),2)
          return

      for entity in self.args["entities"]:
          if float(new) == 0:
              self.turn_off(entity)
          else:
              if "switch" in entity:
                  self.turn_on(entity)
              else:
                  self.turn_on(entity,brightness=new)

  # map any entity change on the input slider
  def on_entity_change(self, entity, attribute, old, new, kwargs):
      self.log("Entering on_entity_change. {}.{} changed from {} to {}.".format(entity, attribute, old, new))

      # calculate the average brightness of the group of lights
      nb_entities = 0
      total_brightness = 0
      for entity in self.args["entities"]:
        brightness = self.get_state(entity, attribute='brightness')
        state = self.get_state(entity)
        self.log("brightness of {} is {} and state is {}".format(entity,brightness,state))
        # handle cases where no brightness attribute is found
        if brightness == None and state == "on":
          brightness = 255
        elif brightness == None and state == "off":
          brightness = 0
        total_brightness += float(brightness)
        nb_entities += 1

      #put silent_update flag on to make sure the update on the slider does not create an infinite loop by updating the entities
      self.set_state("sensor.notify_message_mixgroup",state="silent_update")
      self.set_state(self.args["input"],state= (round(total_brightness/nb_entities)))
      self.log("putting slider to position {}".format(round(total_brightness/nb_entities)))

In case you don’t want to use AppDeamon for this, there is a Lovelace custom card which adds a dimmer slider next to lights.

o boy.

that wont work.
you need a callback

  self.run_in(self.callback,5)
def callback(self,kwargs):
  #do what you want

and why do you want to set the state to silent update in the initialise and set it to " "5 seconds later?
initialise gets only called when you start the app or restart AD.

the biggest problem that you have is that several things start to happen after you changed 1 thing.
that cant be helped by just setting 1 var (1 sensor)

i did copy your code and cleaned it up so i could work with it and added some stuff, i hope you understand what i did, but feel free to ask.

import appdaemon.plugins.hass.hassapi as hass
import datetime

class MixGroupLight(hass.Hass):

  def initialize(self):
      self.threshhold = 5
      self.last_control_change = datetime.datetime.now()
      self.last_entity_change = {}
      self.listen_state(self.on_control_change, self.args["input"])

      for entity in self.args["entities"]:
          self.listen_state(self.on_entity_change, entity)
          self.last_entity_change[entity] = datetime.datetime.now()


  def on_control_change(self, entity, attribute, old, new, kwargs):
      if self.last_control_change > datetime.datetime.now() - datetime.timedelta(seconds=self.treshhold):
          for entity in self.args["entities"]:
              if float(new) == 0:
                  self.turn_off(entity)
              else:
                  if "switch" in entity:
                      self.turn_on(entity)
                  else:
                      self.turn_on(entity,brightness=new)

  def on_entity_change(self, entity, attribute, old, new, kwargs):
      if self.last_entity_change[entity] > datetime.datetime.now() - datetime.timedelta(seconds=self.treshhold):
          nb_entities = 0
          total_brightness = 0
          for entity in self.args["entities"]:
              brightness = self.get_state(entity, attribute='brightness')
              state = self.get_state(entity)
              if brightness == None and state == "on":
                  brightness = 255
              elif brightness == None and state == "off":
                  brightness = 0
              total_brightness += float(brightness)
              nb_entities += 1
          self.set_state(self.args["input"],state= (round(total_brightness/nb_entities)))

Waw … much nicer. I did not think at all to use the time of change to avoid the loop effect. It’s much cleaner and easier to understand this way.

Concerning the run_in, I usually use it with callback … I was so deep in my looping issue that I didn’t event realise I used it wrongly in this code :frowning:

Concerning the code to ignore state updates at start up, I had introduced that in the initialize because when I restart HA, I have my zwave dimmer states which get updated and it was triggering the infinite loop I had created straight away…it was a bit of a deseperate attempt :stuck_out_tongue:

Once more thanks a lot for spending time on this to help me.

your welcome.
remember, now you cant change 2 lights inside the timeframe from 5 seconds.
you might want to try out to shorten the treshhold

In case anyone is interested in the future by this code, I put below the final version. It’s the same as the one from @ReneTode, but I corrected some small problems and added more places with time checks to avoid some small issue with loss of sync between sliders and light states.

In the end I use a threshold of 15 seconds, so quite high, as the only consequence of this is to have a slider which is not in sync with the states of lights in case I swtich several time the same entity within 15s. The position gets back in sync at the next entity change anyway so this is a minor inconvenience. On the other hand going over the threshold can start infinite loops so I really want to avoid this.
In the group I have create I have between 5 and 10 z wave instances and I have seen some callbacks after more than 9 seconds while testing (I guess due to z wave delay or retransmissions) so I took 15 s as a safe value.

import appdaemon.plugins.hass.hassapi as hass
import datetime

class MixGroupLight(hass.Hass):

  def initialize(self):
      self.threshhold = 15
      self.last_control_change = datetime.datetime.now()
      self.last_entity_change = {}
      self.listen_state(self.on_control_change, self.args["input"])

      for entity in self.args["entities"]:
          self.listen_state(self.on_entity_change, entity)
          self.last_entity_change[entity] = datetime.datetime.now()


  def on_control_change(self, entity, attribute, old, new, kwargs):
 
      if datetime.datetime.now() > self.last_control_change + datetime.timedelta(seconds=self.threshhold):
          self.last_control_change = datetime.datetime.now()
          for entity_id in self.args["entities"]:
              if float(new) == 0:
                  self.turn_off(entity_id)
                  self.last_entity_change[entity_id] = datetime.datetime.now()
              else:
                  if "switch" in entity_id:
                      self.turn_on(entity_id)
                      self.last_entity_change[entity_id] = datetime.datetime.now()
                  else:
                      self.turn_on(entity_id,brightness=float(new))
                      self.last_entity_change[entity_id] = datetime.datetime.now()

  def on_entity_change(self, entity, attribute, old, new, kwargs):
      if  datetime.datetime.now() > self.last_entity_change[entity] + datetime.timedelta(seconds=self.threshhold):
          self.last_entity_change[entity] = datetime.datetime.now()
          nb_entities = 0
          total_brightness = 0
          for entity in self.args["entities"]:
              brightness = self.get_state(entity, attribute='brightness')
              state = self.get_state(entity)
              if brightness == None and state == "on":
                  brightness = 255
              elif brightness == None and state == "off":
                  brightness = 0
              total_brightness += float(brightness)
              nb_entities += 1
              # test time of last slider update to avoid updating the slider if the entity update was triggered by a slider change itself
              if datetime.datetime.now() > self.last_control_change + datetime.timedelta(seconds=self.threshhold):
                  self.set_state(self.args["input"],state= (round(total_brightness/nb_entities)))
                  self.last_control_change = datetime.datetime.now()
1 Like