AppDaemon, light on with color and brightness variables with pulsing function

So I’ve written this app specifically for my LIFX lights at home but there are some things that I’m not really across.

It’s works fine so far, I got sick of having to pass meaningless numbers to change my lights to a certain setting. I like to have it as it’s laid out in the LIFX app where brightness’s are in percentage and color_temp’s are in the K values.

I also added in a pulsing feature for light notifications.

Questions:

  1. How can I use this app within other apps using AppDaemon? imports and use etc. I’ve given it a go and couldn’t get it working.
  2. How can I make multiple entity_id’s passed to the app run simultaneous? This is only an issue for the pulsing because I’ve got a delay in between state transitions. I’d like to be able to send multiple lights through to the app and them all pulse at the same time to notify if someone is at the front door or whatever else can be dreamed up.
  3. This is my first app, so any tip or tricks would be appreciated!
  4. Oh and for some reason when I sent a color_temp under 153.85 it wouldn’t work, however that’s the state value that was in HA when changed the lights using the LIFX app? Perhaps it’s something to do with the rgb_color value?
import appdaemon.appapi as appapi
import time


# Comments:
#
# App to easily turn on LIFX lights with brightness, color_temp and rgb_color variables. Also allows light pulsing
# to use for notification triggers.
#
# Args:
#
# entity_id      = entity_id of light to be turned on. You can send multiple entities
#                  eg. light.study or light.study,light.lounge
# brightness     = light brightness in percentage
#                  eg. 100
#                  default: 75
# color_temp     = light color temp
#                  eg. neutral or 3200
#                  default: neutral
# rgb_color      = light rgb color
#                  eg. red or 255,0,0
#                  default: white
# pulse_count    = pulse cycles to pulse between the original light states and the ones specified above. If the light is
#                  already off the light will pulse between passed variables and off state.
#                  eg. 2
#                  default: 0 - which is no pulse
# pulse_time_on  = time in seconds to pulse on
#                  eg. 1 or 1.5
#                  defaults: 1 second
# pulse_time_off = time in seconds to pulse off
#                  eg. 1 or 1.5
#                  default: 1 second
#
# Version 1.0:
#   Initial Version



class LIFX_Lights(appapi.AppDaemon):

    def initialize(self):
        self.log("LIFX Lights App Started.")

        if "pulse_count" in self.args:
            pulse_count = int(self.args["pulse_count"])
        else:
            pulse_count = "0"           # 0 = always on, 1+ = the amount of times to pulse

        if "pulse_time_on" in self.args:
            pulse_time_on = float(self.args["pulse_time_on"])
        else:
            pulse_time_on = "1"         # 1 second

        if "pulse_time_off" in self.args:
            pulse_time_off = float(self.args["pulse_time_off"])
        else:
            pulse_time_off = "1"        # default to 1 second

        if "brightness" in self.args:
            brightness = self.args["brightness"]
        else:
            brightness = "75"           # default to 75%
        if "color_temp" in self.args:
            color_temp = self.args["color_temp"]
        else:
            color_temp = "3500"         # default to 3500K - Neutral

        if "rgb_color" in self.args:
            rgb_color = self.args["rgb_color"]
        else:
            rgb_color = "white"         # default to white

        if "entity_id" in self.args:
            for entity in self.split_device_list(self.args["entity_id"]):
                entity_id = entity

                if (pulse_count > 0):
                    self.pulse_light_on_off(entity_id, brightness, color_temp, rgb_color, pulse_count, pulse_time_on, pulse_time_off)
                else:
                    self.turn_light_on(entity_id, brightness, color_temp, rgb_color)
        else:
            self.log("No entity to turn on.")


    def turn_light_on(self, entity_id, brightness, color_temp, rgb_color, from_cfg="yes"):
        #brightness = 0-255, color_temp = , rgb_color = [0,0,0]-[255,255,255]
        if from_cfg == "yes":
            brightness  = self.brightness_to_percent(brightness)
            color_temp  = self.color_temperature_to_value(color_temp)
            rgb_color   = self.rgb_to_value(rgb_color)
        self.log("brightness {}, color_temp {}, rgb_color {}".format(brightness, color_temp, rgb_color))
        self.turn_on(entity_id, brightness=brightness, color_temp=color_temp, rgb_color=rgb_color)


    def pulse_light_on_off(self, entity_id, brightness, color_temp, rgb_color, pulse_count, pulse_time_on, pulse_time_off):
        start_state         = self.get_state(entity_id)
        if start_state == "on":
            start_brightness    = float(self.get_state(entity_id, "brightness"))
            start_color_temp    = float(self.get_state(entity_id, "color_temp"))
            start_rgb_color     = self.get_state(entity_id, "rgb_color")
        else:
            start_brightness    = None
            start_color_temp    = None
            start_rgb_color     = None

        for x in range(0,pulse_count):
            self.turn_light_on(entity_id, brightness, color_temp, rgb_color)
            time.sleep(pulse_time_on)
            if start_state == "on":
                self.turn_light_on(entity_id, start_brightness, start_color_temp, start_rgb_color, "no")
            else:
                self.turn_off(entity_id)
            time.sleep(pulse_time_off)


    def brightness_to_percent(self, brightness):
        return 255/100 * int(brightness)


    def color_temperature_to_value(self, color_temp):    # Doesn't work above 6000K? Why?
        color_temp = str.lower(str(color_temp))

        if(color_temp == "2500" or color_temp == "ultra warm"):
            return 400                 # Ultra Warm - 2500K
        elif(color_temp == "2750" or color_temp == "incandescent"):
            return 363.63              # Incandescent - 2750K
        elif(color_temp == "3000" or color_temp == "warm"):
            return 333.33              # Warm - 3000K
        elif (color_temp == "3200" or color_temp == "neutral warm"):
            return 312.5               # Neutral Warm - 3200K
        elif (color_temp == "3500" or color_temp == "neutral"):
            return 285.71              # Neutral - 3500K
        elif (color_temp == "4000" or color_temp == "cool"):
            return 250                 # Cool - 4000K
        elif (color_temp == "4500" or color_temp == "cool daylight"):
            return 222.22              # Cool Daylight - 4500K
        elif (color_temp == "5000" or color_temp == "soft daylight"):
            return 200                 # Soft Daylight - 5000K
        elif (color_temp == "5500" or color_temp == "daylight"):
            return 181.82              # Daylight - 5500K
        elif (color_temp == "6000" or color_temp == "noon daylight"):
            return 166.67              # Noon Daylight - 6000K
        #elif (color_temp == 6500 or color_temp == "bright daylight"):
        #    return 153.85             # Bright Daylight - 6500K
        #elif (color_temp == 7000 or color_temp == "cloudy daylight"):
        #    return 142.86             # Cloudy Daylight - 7000K
        #elif (color_temp == 7500 or color_temp == "blue daylight"):
        #    return 133.33             # Blue Daylight - 7500K
        #elif (color_temp == 8000 or color_temp == "blue overcast"):
        #    return 125                # Blue Overcast - 8000K
        #elif (color_temp == 8500 or color_temp == "blue water"):
        #    return 117.65             # Blue Water - 8500K
        #elif (color_temp == 9000 or color_temp == "blue ice"):
        #    return 111.11             # Blue Ice - 9000K
        else:
            return 285.71             # Neutral - 3500K


    def rgb_to_value(self, rgb_color):
        if (rgb_color == "red"):
          return [255,0,0]
        elif (rgb_color == "green"):
            return [0, 255, 0]
        elif (rgb_color == "blue"):
            return [0, 0, 255]
        elif (rgb_color == "white"):
            return [255, 255, 255]
        elif (rgb_color == "yellow"):
            return [255, 255, 0]
        elif (rgb_color == "purple" or rgb_color == "pink"):
            return [255, 0, 255]
        elif (rgb_color == "teal" or rgb_color == "light_blue"):
            return [0, 255, 255]
        else:
            rgb_return = []
            for x in self.split_device_list(rgb_color):
                rgb_return.append(x)
            return rgb_return
1 Like

Cool. I have no AppDaemon experience, so my comments may be a little off.

Have you seen the recently added LIFX effects? They handle the initial state for you, so I guess you can synchronize your lights just by not splitting the entity_id.

The color_temp problem below 154 should be fixed in 0.44 with PR #7206.

Rather than starting your own list of color names, maybe you should pass the color to color_name if it is not an RGB list?

Hi @adamja! This is really cool. Love it and your code is pretty clean too, awesome.

appdaemon.cfg

[AppDaemon]
...
# Apps

[lights]
module = lifx_lights
class = LIFX_Lights

[hello_world]
module = hello_world
class = HelloWorld
dependencies = lights

#hello_world.py

import appdaemon.appapi as appapi

class HelloWorld(appapi.AppDaemon):

    def initialize(self):
        lifx_lights = self.get_app('lifx_lights')
        lifx_lights.turn_light_on(entity_id='light.bulb1', brightness='255', color_temp='2500', rgb_color='blue')
        self.log('Hello World!')

It’s as simple as that. The key is making your lights app a dependency to the app that requires it.

This is mainly your problem, from what I can tell. The API used to have a pretty explicit warning against using time.sleep(), but to put it simply, AppDaemon is a threaded application. A call to time.sleep() is a blocking operation, which means it will cause the entire thread to “hang” for that amount of time. This is why you see lights pulsing sequentially. Instead, you should use self.run_in() … which is going to change your code a bit. This is how I would rewrite it.

    def initialize(self):
        ...
        ...
        if "entity_id" in self.args:
            for entity in self.split_device_list(self.args["entity_id"]):
                entity_id = entity

                if (pulse_count > 0):
                    self.pulse_setup(entity_id, brightness, color_temp, rgb_color, pulse_count, pulse_time_on, pulse_time_off)
                else:
                    self.turn_light_on(entity_id, brightness, color_temp, rgb_color)
        else:
            self.log("No entity to turn on.")

    def pulse_setup(self, entity_id, brightness, color_temp, rgb_color, pulse_count, pulse_time_on, pulse_time_off):
        start_state         = self.get_state(entity_id)
        
        if start_state == "on":
            start_brightness    = float(self.get_state(entity_id, "brightness"))
            start_color_temp    = float(self.get_state(entity_id, "color_temp"))
            start_rgb_color     = self.get_state(entity_id, "rgb_color")
        else:
            start_brightness    = None
            start_color_temp    = None
            start_rgb_color     = None

        kwargs = {
            "pulse_remaining": pulse_count, 
            "entity_id": entity_id, 
            "state": start_state,
            "brightness": start_brightness, 
            "color_temp": start_color_temp, 
            "rgb_color": start_rgb_color, 
            "on_off": (pulse_time_on, pulse_time_off)
       }

        self.pulse(kwargs)

    def pulse(self, kwargs):

        pulse_remaining = kwargs['pulse_remaining'], 
        entity_id = kwargs['entity_id'], 
        state = kwargs['state'],
        brightness = kwargs['brightness'], 
        color_temp = kwargs['color_temp'], 
        rgb_color =  kwargs['rgb_color'], 
        on_off = kwargs['on_off']

        # pulse
        # setup state/sleep values
        if state == 'on':
            self.turn_off(entity_id)
            state = 'off'
            sleep = on_off[1]
        else:
            self.turn_light_on(entity_id, brightness, color_temp, rgb_color, "no")
            state = 'on'
            sleep = on_off[0]

        # manipulating data for next pulse iteration
        data = {
            "pulse_remaining": pulse_remaining-1,
            "entity_id": entity_id,
            "state": state,
            "brightness": brightness,
            "color_temp": color_temp,
            "rgb_color": rgb_color,
            "on_off": on_off
        }

        # queue up next pulse
        self.run_in(self.pulse, delay=sleep, **data)

And then any time you’d want to call it from another app, you’d make the call to lifx_lights.pulse_setup() instead.

We can take the idea one further, however, and move your for-loop from def initialize() to def pulse_setup(). Would look something like pseudocode below.

def pulse_setup(self, entities, brightness, color_temp, rgb_color, ....):
    if isinstance(entities, str):
        entities = [entities]

    for entity in entities:
        #
        # get setup values here
        #
        if pulse_count > 0:
            self.pulse()
        else:
            self.turn_light_on()

This has the added benefit of cleaning up your code in other apps. Now, all you need to do is supply either a single entity or a list of entities. If you want to split up a comma separated list of entities, you’d simply use self.split_device_list() like you did in your original initiatlize. :slight_smile:

There are some other comments I’d make, but they’re really more style-related and not very important in the grand scheme of things. You’ve done good work here!

This might be an issue with the LIFX bulbs. I don’t really know, as I’m not familiar with the product. You could rewrite these lines to look a bit cleaner, though … something like this would work:

        if color_temp in ["2500", "ultra warm"]:
            return 400                 # Ultra Warm - 2500K
        elif color_temp in ["2750", "incandescent"]:
            return 363.63              # Incandescent - 2750K
        elif color_temp in ["3000", "warm"]:
            return 333.33              # Warm - 3000K
        elif color_temp in ["3200", "neutral warm"]:
            return 312.5               # Neutral Warm - 3200K
        elif color_temp in ["3500", "neutral"]:
            return 285.71              # Neutral - 3500K
        elif color_temp in ["4000", "cool"]:
            return 250                 # Cool - 4000K
        elif color_temp in ["4500", "cool daylight"]:
            return 222.22              # Cool Daylight - 4500K
        elif color_temp in ["5000", "soft daylight"]:
            return 200                 # Soft Daylight - 5000K
        elif color_temp in ["5500", "daylight"]:
            return 181.82              # Daylight - 5500K
        elif color_temp in ["6000", "noon daylight"]:
            return 166.67              # Noon Daylight - 6000K

To me, even this is a bit messy. Sure there are more readable ways than that even, but hey, as long as you know what’s happening. :slight_smile:

2 Likes

I hadn’t checked out the new effects, will get onto that. I think I’m still running an older version so hopefully the color_temp issue is resolved on update. I’m glad the color_temp is a know issue, I didn’t know what was going on.
That’s a good idea with the existing RGB list. I’ll check that out.
Cheers!

Woah, that’s an amazing response, I’ll try and hack these suggestions together into my code. Thank you so much!
I didn’t feel right about using sleep, but I didn’t know how else to get the result I was after!

I’ve tried to copy what you I thought you were doing when using the run_in() function but I can’t figure out how to pass variables to the callback function. I’ve done it a little different so the code makes more sense for me, but it’s not working at the moment.

This is what I’ve tried to do:

    def pulse_light(self, entity_id, brightness, color_temp, rgb_color, pulse_count, pulse_time_on, pulse_time_off):
        start_state = self.get_state(entity_id)

        data_0 = {
            "entity_id": entity_id,
            "brightness": brightness,
            "color_temp": color_temp,
            "rgb_color": rgb_color,
        }

        if start_state == "on":
            start_brightness    = float(self.get_state(entity_id, "brightness"))
            start_color_temp    = float(self.get_state(entity_id, "color_temp"))
            start_rgb_color     = self.get_state(entity_id, "rgb_color")

            data_1 = {
                "entity_id": entity_id,
                "brightness": start_brightness,
                "color_temp": start_color_temp,
                "rgb_color": start_rgb_color,
                "from_cfg" : "no",
            }
        else:
            start_brightness    = None
            start_color_temp    = None
            start_rgb_color     = None

            data_1 = {
                "entity_id": entity_id,
            }

        sleep = 0

        for x in range(0,pulse_count):
            #self.turn_light_on(entity_id, brightness, color_temp, rgb_color)
            self.run_in(self.turn_light_on, seconds=sleep, **data_0)
            sleep = sleep + pulse_time_on

            if start_state == "on":
                self.run_in(self.turn_light_on, seconds=sleep, **data_1)
                sleep = sleep + pulse_time_off
            else:
                self.run_in(self.turn_off, seconds=sleep, **data_1)
                sleep = sleep + pulse_time_off

This is the error:

2017-05-07 11:44:18.012619 INFO lights: LIFX Lights App Started.
2017-05-07 11:44:18.016495 WARNING ------------------------------------------------------------
2017-05-07 11:44:18.016813 WARNING Unexpected error in worker for App lights:
2017-05-07 11:44:18.017179 WARNING Worker Ags: {'kwargs': {'rgb_color': 'green', 'brightness': '50', 'color_temp': '4000', 'entity_id': 'light.adams_study'}, 'name': 'lights', 'id': UUID('e221ea57-b65e-4eeb-9e84-326c9f644ed1'), 'function': <bound method LIFX_Lights.turn_light_on of <lights.LIFX_Lights object at 0x7f51f055e198>>, 'type': 'timer'}
2017-05-07 11:44:18.017477 WARNING ------------------------------------------------------------
2017-05-07 11:44:18.018049 WARNING Traceback (most recent call last):
  File "/usr/local/lib/python3.4/site-packages/appdaemon/appdaemon.py", line 602, in worker
    function(ha.sanitize_timer_kwargs(args["kwargs"]))
TypeError: turn_light_on() missing 3 required positional arguments: 'brightness', 'color_temp', and 'rgb_color'

2017-05-07 11:44:18.018326 WARNING ------------------------------------------------------------
2017-05-07 11:44:19.004849 WARNING ------------------------------------------------------------
2017-05-07 11:44:19.005209 WARNING Unexpected error in worker for App lights:
2017-05-07 11:44:19.005614 WARNING Worker Ags: {'kwargs': {'from_cfg': 'no', 'rgb_color': [126, 0, 0], 'brightness': 127.0, 'color_temp': 250.0, 'entity_id': 'light.adams_study'}, 'name': 'lights', 'id': UUID('e221ea57-b65e-4eeb-9e84-326c9f644ed1'), 'function': <bound method LIFX_Lights.turn_light_on of <lights.LIFX_Lights object at 0x7f51f055e198>>, 'type': 'timer'}
2017-05-07 11:44:19.005909 WARNING ------------------------------------------------------------
2017-05-07 11:44:19.006539 WARNING Traceback (most recent call last):
  File "/usr/local/lib/python3.4/site-packages/appdaemon/appdaemon.py", line 602, in worker
    function(ha.sanitize_timer_kwargs(args["kwargs"]))
TypeError: turn_light_on() missing 3 required positional arguments: 'brightness', 'color_temp', and 'rgb_color'

2017-05-07 11:44:19.006852 WARNING ------------------------------------------------------------

Hmmm, probably because I don’t think I gave you correct/full feedback. :slight_smile:

The callback function to self.run_in() should look like func_name(self, kwargs) … which will mean you have to throw in some sort of wrapper for turn_light_on() that calls turn_light_on() appropriately.

    def _turn_light_on(self, kwargs):
        entity_id = kwargs['entity_id']
        brightness = kwargs['brightness']
        color_temp = kwargs['color_temp']
        rgb_color = kwargs['rgb_color']
        from_cfg = kwargs['from_cfg']

        self.turn_light_on(entity_id, brightness, color_temp, rgb_color, from_cfg)

    def pulse_light(....):
        ...
        self.run_in(self._turn_light_on, seconds=sleep, **data_1)

** I also edited my original response to show how it would be done in that sense as well.

Thanks for this!

I’m really not understanding the whole **kwargs vs kwargs passing between functions. I’ve got it all working however I had to do two different turn_light_on() methods for it to work from the appdaemon passed variables and the run_in() function.

The turn_light_on() and turn_light_on_start() functions below are what I mean. I’ve tried to find a reason why I can’t use the ** when it’s passed from run_in() to the callback method, but I don’t get it…

class LIFX_Lights(appapi.AppDaemon):

    def initialize(self):
        self.log("LIFX Lights App Started.")

        if "entity_id" in self.args:

            if (int(self.args["pulse_count"]) > 0):
                self.pulse_light(entity_id=self.args["entity_id"], brightness=self.args["brightness"],
                                 color_temp=self.args["color_temp"], rgb_color=self.args["rgb_color"],
                                 pulse_count=self.args["pulse_count"], pulse_time_on=self.args["pulse_time_on"],
                                 pulse_time_off=self.args["pulse_time_off"])
            else:
                self.turn_light_on_star(entity_id=self.args["entity_id"], brightness=self.args["brightness"],
                                   color_temp=self.args["color_temp"], rgb_color=self.args["rgb_color"])

        else:
            self.log("No entity to turn on.")


    def turn_light_on(self, kwargs):
        # brightness = 0-255, color_temp = , rgb_color = [0,0,0]-[255,255,255]
        if isinstance(kwargs["rgb_color"], str):
            rgb_color = kwargs["rgb_color"].split(",")
        else:
            rgb_color = kwargs["rgb_color"]

        self.log("turned on: {} with brightness: {}, color_temp: {}, rgb_color: {}".format(kwargs["entity_id"], kwargs["brightness"], kwargs["color_temp"], rgb_color))
        self.turn_on(kwargs["entity_id"], brightness=kwargs["brightness"], color_temp=kwargs["color_temp"], rgb_color=rgb_color)


    def turn_light_on_star(self, **kwargs):
        # brightness = 0-255, color_temp = , rgb_color = [0,0,0]-[255,255,255]
        if isinstance(kwargs["rgb_color"], str):
            rgb_color = kwargs["rgb_color"].split(",")
        else:
            rgb_color = kwargs["rgb_color"]

        self.log("turned on: {} with brightness: {}, color_temp: {}, rgb_color: {}".format(kwargs["entity_id"], kwargs["brightness"], kwargs["color_temp"], rgb_color))
        self.turn_on(kwargs["entity_id"], brightness=kwargs["brightness"], color_temp=kwargs["color_temp"], rgb_color=rgb_color)


    def pulse_light(self, **kwargs):
        start_state = self.get_state(kwargs["entity_id"])

        new ={
            "entity_id": kwargs["entity_id"],
            "brightness": kwargs["brightness"],
            "color_temp": kwargs["color_temp"],
            "rgb_color": kwargs["rgb_color"],
        }

        start = {}

        if start_state == "on":
            start_brightness    = float(self.get_state(kwargs["entity_id"], "brightness"))
            start_color_temp    = float(self.get_state(kwargs["entity_id"], "color_temp"))
            start_rgb_color     = self.get_state(kwargs["entity_id"], "rgb_color")

            start = {
                "entity_id": kwargs["entity_id"],
                "brightness": start_brightness,
                "color_temp": start_color_temp,
                "rgb_color": start_rgb_color,
            }

        sleep = 0

        for x in range(0, int(kwargs["pulse_count"])):
            #self.turn_light_on(entity_id, brightness, color_temp, rgb_color)
            self.run_in(self.turn_light_on, sleep, **new)
            sleep = sleep + float(kwargs["pulse_time_on"])

            if start_state == "on":
                self.run_in(self.turn_light_on, sleep, **start)
                sleep = sleep + float(kwargs["pulse_time_off"])
            else:
                self.run_in(self.turn_off, sleep, {"entity": kwargs["entity_id"],})
                sleep = sleep + float(kwargs["pulse_time_off"])

I suppose the way you did it is not-wrong, great job for figuring it out! :slight_smile:

This is an oversimplification, but “star star kwargs” just essentially means “unpack this dictionary”. When done in the context of parameters, it turns a dictionary into keyword arguments and their values. So these would be equivalent options.

sleep = 0
start = {
    "entity_id": kwargs['entity_id'],
    "brightness": start_brightness,
    "color_temp": start_color_temp,
    "rgb_color": start_rgb_color
}

self.run_in(self.turn_light_on, sleep, **start)

and…

sleep = 0
self.run_in(self.turn_light_on, sleep, entity_id=kwargs['entity_id'], brightness=start_brightness, color_temp=start_color_temp, rgb_color=start_rgb_color)

however the second approach does not let us dynamically add these arguments if one of them is not yet-defined … in your case, the variable from_cfg. “start start kwargs” allows us to handle this inconsistency beautifully.

Now, "why does the callback require only a single parameter called kwargs?" That’s a question for @aimc. It’s not a terrible way of doing things, but I believe we do it this way so function arguments are sanitized. I honestly haven’t looked into it too in-depth because it doesn’t both me too terribly much.

3 Likes

Ok, thank you for all your help, i really appreciate it! I’ve spent a lot of time trying to learning more about the python programming language over the past week through trial and error with these apps, i knew the basics but there is certainly no shortage of concepts to wrap your head around! :slight_smile:

No problem! Any time you’ve got a question, don’t be afraid to ask! There are many here in the community that will offer up their knowledge and help. :slight_smile: It’s important to remember that there are many ways to achieve the same thing in Python, so you’ll get a different style from everyone … and hopefully they’re always correct! ha ha

1 Like

Incidentally, Home Assistant 0.45 added brightness_pct and kelvin attributes to help with this.

Very nice, that’s great!

The more I learn the more I realise this little project wasn’t that useful other than to improve my python/appdaemon/ha skills. There are a lot of features added to the LIFX module in HA that didn’t exist when I started using it as well. :slight_smile: