Light Schedule - My take using the new YAML config

I have followed @ReneTode 's light schedule closely. I even implemented a similar version using a csv file to maintain the schedule for each light.

With the new updates to the appdaemon config and moving to yaml, I’ve taken another stab at it in an attempt to make it easier to maintain. Here is what my current version looks like:

The App



import appdaemon.appapi as appapi
import re
import datetime


# App to schedule lights on and off
#
# EXAMPLE appdaemon.yaml entry below
# 
# # Apps
# 
# Lights V2:
#   class: lights_v2
#   module: lights_v2
#   lights: 
#     - light: light.eave_lights
#       on_time: "sunset - 00:15:00"
#       on_random_start: 10
#       on_random_end: 20
#       off_time: "23:00:00"
#       off_random_start: -5
#       off_random_end: 0
#       constrain_days: "mon,tue,wed,thu,fri,sat,sun"
#     - light: light.eave_lights
#       on_time: "05:40:00"
#       on_random_start: -5
#       on_random_end: 5
#       off_time: "sunrise"
#       off_random_start: -15
#       off_random_end: 0
#       constrain_days: "mon,tue,wed,thu,fri"


class lights_v2(appapi.AppDaemon):

    def initialize(self):
        # self.log("test")
        # self.log(self.args["lights"])

        if "lights" in self.args:
            for lightconfig in self.args["lights"]:

                light = lightconfig["light"]
                constrain_days = lightconfig["constrain_days"]

                ## Light On Detail
                on_time = lightconfig["on_time"]
                on_random_start = int(lightconfig["on_random_start"]) * 60
                on_random_end = int(lightconfig["on_random_end"]) * 60

                self.setup_on_time(light, on_time, on_random_start, on_random_end, constrain_days)


                ## Light Off Detail
                off_time = lightconfig["off_time"]
                off_random_start = int(lightconfig["off_random_start"]) * 60
                off_random_end = int(lightconfig["off_random_end"]) * 60

                self.setup_off_time(light, off_time, off_random_start, off_random_end, constrain_days)

    
    


    def setup_on_time(self, light, on_time, start, end, constrain_days):
        on_time_parsed = self.parse_time(on_time)
        if int(start) >= int(end):
            self.log("{}: on_random_start must be numerically lower than on_random_end".format(light), "ERROR")
        elif type(on_time_parsed) is not datetime.time:
            self.log("{}: on_time must be in string format".format(light), "ERROR")
        else:
            sun_on = False
            on_time_set = False

            if on_time == "sunset":
                sun_on = True
                on_time_set = True
                self.run_at_sunset(self.set_lights_on, random_start=start, random_end=end, constrain_days=constrain_days, switch=light)

            if on_time == "sunrise":
                sun_on = True
                on_time_set = True
                self.run_at_sunrise(self.set_lights_on, random_start=start, random_end=end, constrain_days=constrain_days, switch=light)

            
            if not sun_on:
                on_time_set = True
                self.run_daily(self.set_lights_on, on_time_parsed, random_start=start, random_end=end, constrain_days=constrain_days, switch=light)

            if on_time_set:
                msg = "{} set to turn on at {}".format(light, on_time)
                if on_time != str(on_time_parsed):
                    msg += " ({})".format(on_time_parsed)
                self.log(msg, "INFO")

    
    def setup_off_time(self, light, off_time, start, end, constrain_days):
        off_time_parsed = self.parse_time(off_time)
        if int(start) >= int(end):
            self.log("{}: off_random_start must be numerically lower than off_random_end".format(light), "ERROR")
        elif type(off_time_parsed) is not datetime.time:
            self.log("{}: off_time must be in string format".format(light), "ERROR")
        else:
            sun_off = False
            off_time_set = False
            
            if off_time == "sunset":
                sun_off = True
                off_time_set = True
                self.run_at_sunset(self.set_lights_off, random_start=start, random_end=end, constrain_days=constrain_days, switch=light)

            if off_time == "sunrise":
                sun_off = True
                off_time_set = True
                self.run_at_sunrise(self.set_lights_off, random_start=start, random_end=end, constrain_days=constrain_days, switch=light)
                  
            if not sun_off:
                off_time_set = True
                self.run_daily(self.set_lights_off, off_time_parsed, random_start=start, random_end=end, constrain_days=constrain_days, switch=light)

            if off_time_set:
                msg = "{} set to turn off at {}".format(light, off_time)
                if off_time != str(off_time_parsed):
                    msg += " ({})".format(off_time_parsed)
                self.log(msg, "INFO")
    
    def set_lights_on(self, kwargs):
        msg = "Turned {} on.".format(self.friendly_name(kwargs["switch"]))
        self.log(msg, "INFO")
        self.call_service("notify/notify", message=msg)
        self.turn_on(kwargs["switch"])

    def set_lights_off(self, kwargs):
        msg = "Turned {} off.".format(self.friendly_name(kwargs["switch"]))
        self.log(msg, "INFO")
        self.call_service("notify/notify", message=msg)
        self.turn_off(kwargs["switch"])
3 Likes

Super clean looking! As someone who hasn’t been following your and @ReneTode’s app development, what exactly is the off_random_* supposed to do? It’s hard to tell exactly. Maybe include that in your documentation section!

1 Like

Thanks! Admittedly a lot of my improvements were influenced by your awesome guides.

Yeah, good idea. Basically it’s straight from the API Schedule Randomization section.

Using these arguments it is possible to randomize the firing of callbacks to the degree desired by setting the appropriate number of seconds with the parameter

I have one of each for randomizing both on and off times.

So, I can say… Turn on at sunset but randomize between 10 minutes early (-10) and 10 minutes after (10 – API takes seconds and that’s why I convert in the app). Ultimately, giving me a twenty minute window at random.

1 Like

This is what I had assumed. :slight_smile: Admittedly, I don’t care much for randomization in something like my lights schedule, however it’s a good example to usage! I do like how you give examples in your documentation, but I think you should show what each parameter’s usage is for.

# Arguments
#
# sequence:: "light": light.entity_name
#
# on_time: the time value to turn `light.entity_name` ON
# on_random_start: maximum negative offset of `on_time` in minutes
# on_random_end: maximum positive offset of `on_time` in minutes
#       
# off_time: the time value to turn `light.enity_name` OFF
# off_random_start: maximum negative offset of `off_time` in minutes
# off_random_end: maximum positive offset of `off_time` in minutes
#
# constrain_days: comma-separated string of weekdays ala AppDaemon API
#
#
# Notes
# - random on/off parameters above allow lights to switch in a random time between
#       start and end values to allow for a less regimented automation!

This way, you’ve documented that things like on_random_start will always be assumed to be a negative value and can tighten up your code a bit. Then your users don’t need to remember to put this sort of thing, they only need to work about the time value.

To take this app to the next level, you could make the off_random_* and on_random_* params optional. :slight_smile: Great work @kylerw!

1 Like

That’s motivation enough… :slight_smile:

import appdaemon.appapi as appapi
import re
import datetime


# App to schedule lights on and off
#
# EXAMPLE appdaemon.yaml entry below
# 
# # Apps
# 
# Lights V2:
#   class: lights_v2
#   module: lights_v2
#   lights: 
#     - light: light.first_light
#       on_time: "sunset - 00:15:00"
#       off_time: "23:00:00"
#       constrain_days: "mon,wed,fri"
#     - light: light.second_light
#       on_time: "sunset"
#       on_random_start: 10
#       on_random_end: 20
#       off_time: "23:00:00"
#       off_random_start: -5
#       off_random_end: 0
#       constrain_days: "mon,tue,wed,thu,fri,sat,sun"
#
#
# Arguments
# 
# light: light.entity_name
#
# Required
#   on_time: the time value to turn `light.entity_name` ON. 
#   off_time: the time value to turn `light.enity_name` OFF
# 
# Optional Day Constraint
#   constrain_days: Comma-separated string of weekdays. Defaults to everyday.
#
# Optional Randomization:
#   on_random_start: Offset start of random window for `on_time`, in minutes
#   on_random_end: Offset end of random window for `on_time`, in minutes
#
#   off_random_start: Offset start of random window for `off_time`, in minutes
#   off_random_end: Offset end of random window for `off_time`, in minutes
#
# Notes
# - on/off time can use valid AppDaemon timestrings
#       Example: "22:00:00" or "sunset" or "sunrise" or "sunset + 00:10:00"
#
# - Random on/off parameters above allow lights to switch in a random time between
#       start and end values to allow for a less regimented automation!
#
# - Randomization will generally be a negative number for _start and positive number for _end
#       if not: _start must be numerically lower than _end
#
#
#

class lights_v2(appapi.AppDaemon):

    def initialize(self):
        # self.log("test")
        # self.log(self.args["lights"])

        if "lights" in self.args:
            for lightconfig in self.args["lights"]:

                ## Optional Fields
                on_random_start = 0
                on_random_end = 0
                off_random_start = 0
                off_random_end = 0
                constrain_days = "mon,tue,wed,thu,fri,sat,sun"

                ## Required Fields
                light = lightconfig["light"]
                on_time = lightconfig["on_time"]
                off_time = lightconfig["off_time"]                

                if "constrain_days" in lightconfig:
                    constrain_days = lightconfig["constrain_days"]

                ## Light On Randomization
                if "on_random_start" in lightconfig:
                    on_random_start = int(lightconfig["on_random_start"]) * 60
                if "on_random_end" in lightconfig:
                    on_random_end = int(lightconfig["on_random_end"]) * 60

                self.setup_on_time(light, on_time, on_random_start, on_random_end, constrain_days)


                ## Light Off Randomization
                if "off_random_start" in lightconfig:
                    off_random_start = int(lightconfig["off_random_start"]) * 60
                if "off_random_end" in lightconfig:
                    off_random_end = int(lightconfig["off_random_end"]) * 60

                self.setup_off_time(light, off_time, off_random_start, off_random_end, constrain_days)

    
    


    def setup_on_time(self, light, on_time, start, end, constrain_days):
        on_time_parsed = self.parse_time(on_time)
        if int(start) >= int(end):
            self.log("{}: on_random_start must be numerically lower than on_random_end".format(light), "ERROR")
        elif type(on_time_parsed) is not datetime.time:
            self.log("{}: on_time must be in string format".format(light), "ERROR")
        else:
            sun_on = False
            on_time_set = False

            if on_time == "sunset":
                sun_on = True
                on_time_set = True
                self.run_at_sunset(self.set_lights_on, random_start=start, random_end=end, constrain_days=constrain_days, switch=light)

            if on_time == "sunrise":
                sun_on = True
                on_time_set = True
                self.run_at_sunrise(self.set_lights_on, random_start=start, random_end=end, constrain_days=constrain_days, switch=light)

            
            if not sun_on:
                on_time_set = True
                self.run_daily(self.set_lights_on, on_time_parsed, random_start=start, random_end=end, constrain_days=constrain_days, switch=light)

            if on_time_set:
                msg = "{} set to turn on at {}".format(light, on_time)
                if on_time != str(on_time_parsed):
                    msg += " ({})".format(on_time_parsed)
                self.log(msg, "INFO")

    
    def setup_off_time(self, light, off_time, start, end, constrain_days):
        off_time_parsed = self.parse_time(off_time)
        if int(start) >= int(end):
            self.log("{}: off_random_start must be numerically lower than off_random_end".format(light), "ERROR")
        elif type(off_time_parsed) is not datetime.time:
            self.log("{}: off_time must be in string format".format(light), "ERROR")
        else:
            sun_off = False
            off_time_set = False
            
            if off_time == "sunset":
                sun_off = True
                off_time_set = True
                self.run_at_sunset(self.set_lights_off, random_start=start, random_end=end, constrain_days=constrain_days, switch=light)

            if off_time == "sunrise":
                sun_off = True
                off_time_set = True
                self.run_at_sunrise(self.set_lights_off, random_start=start, random_end=end, constrain_days=constrain_days, switch=light)
                  
            if not sun_off:
                off_time_set = True
                self.run_daily(self.set_lights_off, off_time_parsed, random_start=start, random_end=end, constrain_days=constrain_days, switch=light)

            if off_time_set:
                msg = "{} set to turn off at {}".format(light, off_time)
                if off_time != str(off_time_parsed):
                    msg += " ({})".format(off_time_parsed)
                self.log(msg, "INFO")
    
    def set_lights_on(self, kwargs):
        msg = "Turned {} on.".format(self.friendly_name(kwargs["switch"]))
        self.log(msg, "INFO")
        self.call_service("notify/notify", message=msg)
        self.turn_on(kwargs["switch"])

    def set_lights_off(self, kwargs):
        msg = "Turned {} off.".format(self.friendly_name(kwargs["switch"]))
        self.log(msg, "INFO")
        self.call_service("notify/notify", message=msg)
        self.turn_off(kwargs["switch"])

Next improvement would probably be allowing groups of lights… quicker/easier setup for lights you want on the same schedule…

2 Likes

My use doesn’t require them either but I feel that it gives a bit more of an occusim feel to the automation.

1 Like

Feels more natural when its random and is another deterrent for a burglar.