Changing to run every

Hey there everyone,

So I wrote this to compensate for lack of ambient light outside

import appdaemon.appapi as appapi
import datetime

class living_room_daytime_light(appapi.AppDaemon):

  def initialize(self):
      self.living_room_lights = 'light.living_room_lights'
      self.luxsensor = 'sensor.average_outside_lux'

      self.run_every(self.adjust_light_function, self.luxsensor)

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

  def adjust_light_function(self, entity, attribute, old, new, kwargs):
      lux=float(new)
      self.log(lux)
      if lux < 500:
          brightness = self.brightness_to_percent(80)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif 501 <= lux <= 800:
          brightness = self.brightness_to_percent(70)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif 801 <= lux <= 1000:
          brightness = self.brightness_to_percent(60)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif 1001 <= lux <= 1200:
          brightness = self.brightness_to_percent(50)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif 1201 <= lux <= 1500:
          brightness = self.brightness_to_percent(40)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif lux > 1500 :
          self.turn_off(self.living_room_lights)

Right now I have it set to run with a state change so naturally everytime my lux sensors report in and HA figures out the AVG through a sensor template the script is called. So needless to say it gets called a lot.

I’d like to change it instead to run like every 2 mins or every 5 just to give the system a small break.

I’ve been looking at the examples of the self.run_every and I’m confused on the time aspect.

So I see this in the docs

self.run_every(self.run_every_c, time, 17 * 60)

Which says that it should run every 17 mins in 2 hours which I don’t quite understand how that works.

So I want to double check the syntax of this, should I set it up like

self.run_every(self.adjust_light_function, time 5 * 300, self.luxsensor)

Thank you all for your help with wrapping my head around this.

EDIT: Realized my math was screwy

1 Like

time is a datetime object when the first callback will occur. While it doesn’t show it in the example, the datetime object has + 2 hours added to it. I think it’s missing from the documentation. To do that 2 hour offset (FYI there is many ways you can do this, this is one option):

from datetime import datetime, timedelta
two_hours_from_now = datetime.now() + timedelta(hours=2)
self.run_every(self.run_every_c, two_hours_from_now.time(), 17 * 60)

The second peice is in seconds, so converting 17 minutes to seconds would be 17*60. This would start running in 2 hours and then execute every 17 minutes from then on.

So if you want it to run every 5 minutes starting now:

import datetime
self.run_every(self.adjust_light_function, datetime.now(), 5*60, **kwargs)

**kwargs being whatever keyword arguments you want to supply, example: entity_name=‘domain.device’

2 Likes

Thank you very much for that wonderful explanation that makes perfect sense.

No problem, You may want to test out the time portion. To me, the doc’s are a bit unclear.

The example uses datetime.time(0,0,0) and implies that the function will start immediately. But datetime.time(0,0,0) is midnight. So the time that is supplied to the function might be an offset of datetime.time().

If thats the case, then the 2 hour example would be:

from datetime import datetime
time = datetime.time(2,0,0)
self.run_every(self.run_every_c, time, 17 * 60)

Yeah for me I just have this constrained to run during daylight hours and only if someone is home. So I don’t really care when it decides to start running but I want it to check the lux reading every 5 mins and adjust my lights accordingly. It works really nice on heavy cloud days or during a heavy thunderstorm. Took me some serious tinkering to figure out the lux ranges that I wanted to use for this.

I used to use the lux reading that came from the airport that is about 2 miles north of me but they only update their reading once an hour so this is a lot nicer.

1 Like

Nice! glad it works.

yeah i also noticed that some parts of the docs can be a little confusing.

the time that you set is the first time the callback is started. and always in the future.
so with this:

time = datetime.time(2,0,0)
self.run_every(self.run_every_c, time, 17 * 60)

it will start at 2 and then every 17 minutes after that untill eternity.
to make it start immediatly, you need to use now() (and at least 1 second added, to make sure its the future)
the interval is completely set by the last part.

if you want it to start 2 hours from now, you need a now() + 2 hours

i hope this makes it more clear.

Does the function use a datetime.time or datetime.datetime object?

So it threw an error with datetime.now() so I changed it over to datetime.datetime.now() and now I get an error that the run_every expects 4 args but 5 were given.

However I’m not sure how its getting 5 args as I’m only giving it 4 based on this

      self.run_every(self.adjust_light_function, datetime.datetime.now(), 2*60, self.luxsensor)

self is always the first argument. Your problem is that the function does not except extra arguements, only extra keyword arguments.

extra arguments are denoted by a *. example: *args

This means that you can add a ton of arguments and the function will still work:

>>> def something(*args):
	print(args)

>>> something(1,2,3,4)
(1, 2, 3, 4)

when you see 2 stars (**) it means the function accepts keyword arguments. Example: **kwargs

>>> def something(**kwargs):
	print(kwargs)

>>> something(a=1,b=2,c=3,d=4)
{'a': 1, 'c': 3, 'b': 2, 'd': 4}

you can convert a list into *arguments:

>>> def something(*args):
    	print(args)
>>> foo = [1,2,3]
>>> something(*foo)
(1, 2, 3)

you can convert a dictionary into **kwargs:

>>> def something(**kwargs):
	print(kwargs)
>>> foo = {'f':1, 'oo':2}
>>> something(**foo)
{'oo': 2, 'f': 1}

So your error is complaining because you aren’t using a keyword for self.luxsensor. You can do something like:

self.run_every(self.adjust_light_function, datetime.datetime.now(), 2*60, myluxsensor = self.luxsensor)

Then self.adjust_light_function needs to use ‘myluxsensor’ by doing:

value = kwargs['myluxsensor']

2 Likes

So are you saying something like this?

import appdaemon.appapi as appapi
import datetime

class living_room_daytime_light(appapi.AppDaemon):

  def initialize(self):
      self.living_room_lights = 'light.living_room_lights'
      self.luxsensor = 'sensor.average_outside_lux'
      self.run_every(self.adjust_light_function, datetime.datetime.now(), 2*60, luxsensor = self.luxsensor)

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

  def adjust_light_function(self, entity, attribute, old, new, kwargs):
      lux = kwargs['luxsensor']
      lux=float(new)
      self.log(lux)
      if lux < 500:
          brightness = self.brightness_to_percent(80)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif 501 <= lux <= 800:
          brightness = self.brightness_to_percent(70)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif 801 <= lux <= 1000:
          brightness = self.brightness_to_percent(60)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif 1001 <= lux <= 1200:
          brightness = self.brightness_to_percent(50)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif 1201 <= lux <= 1500:
          brightness = self.brightness_to_percent(40)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif lux > 1500 :
          self.turn_off(self.living_room_lights)

Actually, you don’t even need kwargs for what you are doing. You are running this every 5 minutes, it doesn’t need to have the attributes that the listeners require. This can be your own function, so you can decide which attributes to supply. Because you are using self.luxsesnor, you don’t need to pass anything to the function because your AppDaemon object already has .luxsensor.

import appdaemon.appapi as appapi
import datetime

class living_room_daytime_light(appapi.AppDaemon):

  def initialize(self):
      self.living_room_lights = 'light.living_room_lights'
      self.luxsensor = 'sensor.average_outside_lux'
      self.run_every(self.adjust_light_function, datetime.datetime.now(), 2*60)

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

  def adjust_light_function(self, kwargs):
      lux = self.get_state(self.luxsensor, attribute='lux') #CHANGE THIS TO GET ACTUAL ATTRIBUTE
      self.log(lux)
      if lux < 500:
          brightness = self.brightness_to_percent(80)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif 501 <= lux <= 800:
          brightness = self.brightness_to_percent(70)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif 801 <= lux <= 1000:
          brightness = self.brightness_to_percent(60)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif 1001 <= lux <= 1200:
          brightness = self.brightness_to_percent(50)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif 1201 <= lux <= 1500:
          brightness = self.brightness_to_percent(40)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif lux > 1500 :
          self.turn_off(self.living_room_lights)

So I think that my actual attribute is just the state since I’m using SmartThings at the moment with the MQTT bridge so every sensor on my AeonLabs multi is an individual device. so for the attribute it’d just be the state if I understand this correctly.

EDIT: Or no attribute at all since the only attribute is the lux reading itself.

yeah, just

lux = get_state(self.luxsensor)

then.

as an aside, technically the name on this func is incorrect:

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

because you are converting a percent (0 to 100) to a brightness (0 to 255). Also, You don’t need the int on percent because it’s not a string but you do need it on the return:

  def percent_to_brightness(self, percent):
    return int(255/100 * percent)

Oh good to know. I found that in an example with the way I was using it.

One last thing too:

      if lux < 500:
          brightness = self.brightness_to_percent(80)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif 500 <= lux < 800:
          brightness = self.brightness_to_percent(70)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif 800 <= lux < 1000:
          brightness = self.brightness_to_percent(60)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif 1000 <= lux < 1200:
          brightness = self.brightness_to_percent(50)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif 1200 <= lux < 1500:
          brightness = self.brightness_to_percent(40)
          self.turn_on(self.living_room_lights, brightness=brightness)
      elif lux > 1500 :
          self.turn_off(self.living_room_lights)

You weren’t covering between 500 and 501, 800 and 801, etc

Oh good catch. I hope that all of my questions that I’ve been raising these last few days will help others. I am just learning python (I’m sure its obvious) I can bash the heck out of something but this is new to me but I"m getting the hang of it. Here soon I’m going to write up the app that will control my whole house fan but before I make that go live I will be posting it in here for sure because I need several eyes to over look it due to the safety aspects of it before I implement it.

it does use a datetime.time

actually the name was good because he was turning a brightness to a percent
he just used the wrong function.
the function percent_to brightness would be different:

def percent_to_brightness(self,percent):
  return int((255*percent)/255)

I’m not trying to undermine you here or start a huge debate…

>>> def percent_to_brightness(self,percent):
  return int((255*percent)/255)

>>> percent_to_brightness(None, 80)
80

that doesn’t convert anything. His original eq was good, just needed the int

>>> def percent_to_brightness(self, percent):
    return int(255/100 * percent)

>>> percent_to_brightness(None, 80)
204

The equation looks odd because typically it would be percent/100 * 255, but math is math so you can divide either number by 100 and get the same result

yeah you are right, i did read and react to fast on my way to diner.
and i always have trouble when people leave ( ) away.

i come from a math background and have learned always to use () even if they are obsolete.

1 Like