From f507951f8d7fff17edf1918e3a001e96748ce16c Mon Sep 17 00:00:00 2001 From: haberda Date: Mon, 28 Dec 2020 18:25:18 -0800 Subject: [PATCH 1/8] Update README.md --- README.md | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4fb7326..58a6032 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,22 @@ -# update_lights -## This code acts on a list of lights to primarily do two things: +# Periodic lights -1) Gradually change the brightness level and the color temperature of the lights from the start to end time ONLY if the lights are currently on -2) When a light in the list is turned on and its brightness is not equal to the current level it immediately changes the settings to match other lights (if watch_light_state is True) +## Introduction -What makes this code different than others (namely custom component Circadian Lighting or Flux component) is the implementation of change thresholds; -so if someone manually adjusts a light outside of the threshold range the code will skip that light. -The brightness/color temp is calculated by determining how far from the middle point of the start and end time the current time is -in other words at start time and end time there is 100% brightness, at the exact middle point there is 0% brightness (or whatever the configured max and min are) -All lights can be mixed e.g. you can have a list with RGB, color temp, and brightness only lights together -There are numerous options that can be configured to adjust the gradient of the change and constrain whether or not the code is active +Periodic lights (formally update lights) is an automatic light brightness and color temperature adjustment tool for AppDaemon. This code will act on a provided list of lights to keep them in sync with the current light parameters. What makes this app different from others (namely custom components Circadian Lighting, Adaptive Lighting or the built-in Flux component) is the implementation of change thresholds; this allows manual adjustment of a light outside of the threshold range that the app will then ignore, until and unless the light is either manually adjusted to the current threshold range, or the light is toggled. The brightness/color temp is calculated by determining how far from the middle point of the start and end time the current time is. All lights can be mixed e.g. you can have a list with RGB, color temp, and brightness only lights together There are numerous options that can be configured to adjust the gradient of the change and constrain whether or not the app is active -Options: +## Options: --- Key | Required | Description | Default | Unit ------------ | ------------- | ------------- | ------------- | ------------- entities | True | List of lights | None | List -run_every | False | Time interval in seconds to run the code, set to 0 to disable time-based updates | 60 | Seconds +run_every | False | Time interval in seconds to run the code, set to 0 to disable time-based updates | 180 | Seconds event_subscription | False | Home-assistant event to listen for, forces lights to update, can take transition and threshold variables | None | string start_time | False | Time in format 'HH:MM:SS' to start; also can be 'sunset - HH:MM:SS' | sunset | Time end_time | False | Time in format 'HH:MM:SS' to start; also can be 'sunrise - HH:MM:SS' | sunrise | Time start_index | False | With this option you can push the middle point left or right and increase or decrease the brightness change gradient and point of minimum brightness/temp, takes time the same was as start/end times | None | Time end_index | False | Same as start index but changes the end time rather than start both can be configured to have 2 minimum brightness points | None | Time -brightness_threshold | False | Residual threshold between calculated brightness and current brightness if residual > this no change | 255 or 100 | Bit or percent +brightness_threshold | False | Residual threshold between calculated brightness and current brightness if residual > threshold no change | 255 or 100 | Bit or percent brightness_unit | False | percent or bit | bit | None max_brightness_level | False | Max brightness level | 255 or 100 | Bit or percent min_brightness_level | False | Max brightness level | 3 or 1 | Bit or percent @@ -35,17 +28,38 @@ disable_condition | False | Override default condition check for disable_entity sleep_entity | False | List of entities that track whether a 'sleep mode' has been enabled this immediatly brings lights to the lowest brightness and color temp defined. Can take a comma separated condition rather than disable condition key below (e.g. input_boolean.sleep_mode,on) | None | List sleep_condition | False | Override default condition check for sleep_entity | on, True, or Home | Boolean or string in list form red_hour | False | Time in format 'HH:MM:SS' during the start and stop times that the RGB lights turn red if sleep conditions are met | None | Time +sleep_color | False | Color in string format (e.g. 'red') | red | String transition | False | Light transition time in seconds | 5 | Seconds companion_script | False | Script to execute before changing lights, useful to force Zwave lights to update state | None | sensor_log | False | Creates a sensor to track the dimming percentage, mostly for diagnostic purposes, format: sensor.my_sensor | None | +sensor_only | False | Only creates a sensor that tracks the brightness and color temperature, will not adjust lights. | None | watch_light_state | False | Whether or not to watch individual lights and adjust them when they are turned on | True | Boolean keep_lights_on | False | Forces the light to turn on, in other words ignores that it is off | False | Boolean start_lights_on | False | Turn on the lights at the start time | False | Boolean stop_lights_off | False | Turn off the lights at the stop time | False | Boolean - AppDaemon constraints can be used as well, see AppDaemon API Docs https://appdaemon.readthedocs.io/en/latest/APPGUIDE.html#callback-constraints +## Algorithm explanation + +This works by calculating the time that occurs exactly in the middle of the start_time and end_time. The current time is then compared to the middle point to determine at what point in the day the current time is relative to the middle point. This is then fit to a sin function (running from 0-1, aka sin from 0 to pi). This returns a percentage, if your defined light percentage is from 0-100 then the direct reading from the sin function will be used. In practice this is calculated by: + +![equation.png](equation.png) + +The color temperature is calculated the same way as the brightness. + +The values are updated 24 hours a day, therefore, setting the run_every variable to a very short time will not result in the lights updating faster. There are 256 brightness bit values, assuming the maximum is set to 100% brightness and the minimum is set to 0%. This means there are only 512 brightness steps that can be taken in 1 day, or in other words, about 1 every 3 minutes. Therefore, in terms of brightness, the app should be set to update about once every 3 minutes. A similar calculation can be made for color temperature, assuming the default range. Using the default settings, the brightness of the lights would look like this over the course of a day: + +![default_settings.png](default_settings.png) + +Both brightness and color temperature are subject to the ranges provided by the user or the default values described above. + +## Understanding start/end index + +This is a fine tuning feature that allows for the lights to be dimmer or brighter close to the start or end than they otherwise would by extending the middle-point. Using these effectively creates two middle points during the start and end window. A start_index value effects the result after the middle point, an end_index effects the result before the middle-point. If the lights are watched closely one would observe the lights dim to the lowest point from the start->middlepoint1 then hold at the minimum between middlepoint1->middlepoint2; after middlepoint2 the lights will become brighter as usual. + +This should be tested by setting up a template light not connected to anything and observing the behavior to see if the desired result is achieved. + ## Example apps.yaml: ``` @@ -111,7 +125,9 @@ exterior_update_lights: start_lights_on: True stop_lights_off: True ``` + ## Example script/automation for event subscription: + ``` script: - force_light_update: From dd7a88a50ec4ab61045372d3da834462037f830c Mon Sep 17 00:00:00 2001 From: haberda Date: Mon, 28 Dec 2020 18:27:31 -0800 Subject: [PATCH 2/8] Add files via upload --- apps/update_lights/update_lights.py | 173 ++++++++++++++++------------ 1 file changed, 101 insertions(+), 72 deletions(-) diff --git a/apps/update_lights/update_lights.py b/apps/update_lights/update_lights.py index f2e689a..6e7f38b 100644 --- a/apps/update_lights/update_lights.py +++ b/apps/update_lights/update_lights.py @@ -20,18 +20,29 @@ def initialize(self): self.transition = int(self.args.get('transition', 5)) self.start_time = str(self.args.get('start_time', 'sunset')) self.end_time = str(self.args.get('end_time', 'sunrise')) - self.red_hour = str(self.args.get('red_hour', 'None')) + self.red_hour = self.args.get('red_hour', None) self.start_index = str(self.args.get('start_index', self.start_time)) self.end_index = str(self.args.get('end_index', self.end_time)) self.color_temp_unit = str(self.args.get('color_temp_unit', 'kelvin')) self.color_temp_max = int(self.args.get('color_temp_max', 4000)) self.color_temp_min = int(self.args.get('color_temp_min', 2200)) - self.watch_light_state = bool(self.args.get('watch_light_state', True)) - self.keep_lights_on = bool(self.args.get('keep_lights_on', False)) - self.start_lights_on = bool(self.args.get('start_lights_on', False)) - self.stop_lights_off = bool(self.args.get('stop_lights_off', False)) + self.watch_light_state = self.args.get('watch_light_state', True) + self.keep_lights_on = self.args.get('keep_lights_on', False) + self.start_lights_on = self.args.get('start_lights_on', False) + self.stop_lights_off = self.args.get('stop_lights_off', False) + self.sensor_only = self.args.get('sensor_only', False) self.event = self.args.get('event_subscription', None) + interval = int(self.args.get('run_every', 180)) + target = now + timedelta(seconds=interval) + + if self.sensor_only and self.sensor_only != 'false': + #Sensor only is specified + self.run_every(self.time_change, target, interval) + return + else: + self.sensor_only = False + if isinstance(self.all_lights, str): self.all_lights = self.all_lights.split(',') if isinstance(self.disable_entity, str): @@ -59,26 +70,26 @@ def initialize(self): if not isinstance(self.min_brightness_level, int) or self.min_brightness_level > 255 or self.min_brightness_level > self.max_brightness_level: self.min_brightness_level = 3 - if self.keep_lights_on or str(self.keep_lights_on).lower() == 'true': - self.keep_lights_on = True - else: + if str(self.keep_lights_on).lower() == 'false': self.keep_lights_on = False + else: + self.keep_lights_on = True - if self.start_lights_on or str(self.start_lights_on).lower() == 'true': + if str(self.start_lights_on).lower() == 'false': + self.start_lights_on = False + else: self.start_lights_on = True self.run_daily(self.lights_on, self.parse_time(self.start_time)) - else: - self.start_lights_on = False - if self.stop_lights_off or str(self.stop_lights_off).lower() == 'true': + if str(self.stop_lights_off).lower() == 'false': + self.stop_lights_off = False + else: self.stop_lights_off = True self.run_daily(self.lights_off, self.parse_time(self.end_time)) - else: - self.stop_lights_off = False + #Set callbacks for time interval, and subscribe to individual lights and disable/sleep entities - interval = int(self.args.get('run_every', 60)) - target = now + timedelta(seconds=interval) + if self.all_lights is not None: if self.disable_entity is not None: for entity in self.disable_entity: @@ -92,7 +103,7 @@ def initialize(self): self.listen_state(self.state_change, entity) if self.watch_light_state: for light in self.all_lights: - self.listen_state(self.state_change, light) + self.listen_state(self.state_change, light, oneshot = True) if interval > 0: self.run_every(self.time_change, target, interval) if self.event is not None: @@ -119,8 +130,10 @@ def event_subscription(self, event, data, kwargs): def state_change(self, entity, attribute, old, new, kwargs): threshold = 255 transition = 0 - if entity in self.all_lights and new == "on": - self.adjust_light(entity, threshold, transition) + if entity in self.all_lights: + if new == "on": + self.adjust_light(entity, threshold, transition) + self.run_in(self.resubscribe, 2, entity = entity) return if self.disable_entity is not None: for check_entity in self.disable_entity: @@ -133,6 +146,9 @@ def state_change(self, entity, attribute, old, new, kwargs): self.adjust_light(self.all_lights, threshold, transition) return + def resubscribe (self, kwargs): + self.listen_state(self.state_change, kwargs['entity'], oneshot = True) + def lights_on(self, kwargs): #Turn on all lights check = self.condition_query(self.disable_entity, self.disable_condition) @@ -154,45 +170,51 @@ def pct(self): dt = datetime.datetime.now() now_time = dt.timestamp() - start_ts = datetime.datetime.combine(self.date(), self.parse_time(self.start_time)) - end_ts = datetime.datetime.combine(self.date(), self.parse_time(self.end_time)) + start = datetime.datetime.combine(self.date(), self.parse_time(self.start_time)) + end = datetime.datetime.combine(self.date(), self.parse_time(self.end_time)) midnight = '0:00:00' - if self.now_is_between(self.start_time, self.end_time): - #We are in between the start and end times - if self.now_is_between(midnight, self.end_time) and int(start_ts.timestamp()) > int(end_ts.timestamp()): - #We are past midnight and the start time was the day before - start_ts = start_ts + timedelta(days=-1) - elif int(start_ts.timestamp()) > int(end_ts.timestamp()): - #We are before midnight and the end time is after midnight - end_ts = end_ts + timedelta(days=1) - - start_i_ts = datetime.datetime.combine(start_ts.date(), self.parse_time(self.start_index)) - end_i_ts = datetime.datetime.combine(end_ts.date(), self.parse_time(self.end_index)) - - start_ts = int(start_ts.timestamp()) - end_ts = int(end_ts.timestamp()) - start_i_ts = int(start_i_ts.timestamp()) - end_i_ts = int(end_i_ts.timestamp()) + if self.now_is_between(midnight, self.end_time) and not self.now_is_between(self.start_time, midnight): + #We are past midnight and the start time was the day before + self.log('Time delta start -1 day') + start = start + timedelta(days=-1) + elif self.now_is_between(self.end_time, midnight) and start > end: + #We are before midnight and the end time is after midnight + self.log('Time delta end +1 day') + end = end + timedelta(days=1) + #Get index times + start_i = datetime.datetime.combine(start.date(), self.parse_time(self.start_index)) + end_i = datetime.datetime.combine(end.date(), self.parse_time(self.end_index)) + if start_i > end_i: + #End is before midnight but end index is after + end_i = end_i + timedelta(days=1) + #Figure out midpoint + half_seconds = (end - start).total_seconds() / 2 + half = start + timedelta(seconds=half_seconds) + #Figure out start index midpoint + half_seconds = (end - start_i).total_seconds() / 2 + midpoint_start = start_i + timedelta(seconds=half_seconds) + #Figure out end index midpoint + half_seconds = (end_i - start).total_seconds() / 2 + midpoint_end = start + timedelta(seconds=half_seconds) + #Calculate the midpoint between start and end time incorpertaing the indexed times - midpoint_start = (start_i_ts + end_ts) / 2 - midpoint_end = (start_ts + end_i_ts) / 2 - if now_time < midpoint_start: - midpoint = midpoint_start - else: - midpoint = midpoint_end - - if now_time < start_ts and now_time > end_ts: - #We are outside of the start and end time so 0 dimming + if (dt > midpoint_start and dt < midpoint_end) or (dt < midpoint_start and dt > midpoint_end): + self.log('In the middle of the midpoints') pct = 0 + # elif self.now_is_between(self.end_time, self.start_time): + # midpoint = half.timestamp() else: - if now_time < midpoint: - #We are after start time but before midpoint (ramp down) - pct = float((now_time - start_ts) / (midpoint - start_ts)) + if dt < midpoint_start: + # midpoint = midpoint_start.timestamp() + midpoint = midpoint_end.timestamp() else: - #We are after midpoint but before end time (ramp up) - pct = 1 - float((now_time - midpoint) / (end_ts - midpoint)) - return pct + midpoint = midpoint_start.timestamp() + # midpoint = midpoint_end.timestamp() + + pct = abs(float(math.sin(math.pi*((now_time - midpoint) / (86400))))) + + return pct, half, midpoint_start, midpoint_end def color(self, pct): color_max = self.color_temp_max @@ -211,7 +233,7 @@ def color(self, pct): if sleep_state == False: #Calculate desired color temp - desired_temp_kelvin = round(int(color_max) - (abs(int(color_max) - int(color_min))* float(pct))) + desired_temp_kelvin = round(int(color_min) + (abs(int(color_max) - int(color_min))* float(pct))) else: desired_temp_kelvin = color_min desired_temp_mired = self.color_temperature_kelvin_to_mired(desired_temp_kelvin) @@ -260,27 +282,27 @@ def rgb_color(self, desired_temp): elif tmp_blue > 255: tmp_blue = 255 return tmp_red, tmp_green, tmp_blue - + def brightness(self, pct): max_brightness_level = self.max_brightness_level min_brightness_level = self.min_brightness_level brightness_unit = self.brightness_unit #Calculate brightness level in the defined range - brightness_level = int(max_brightness_level) - round(int(max_brightness_level - min_brightness_level) * pct) + brightness_level = int(min_brightness_level) + round(int(max_brightness_level - min_brightness_level) * pct) sleep_state = self.condition_query(self.sleep_entity, self.sleep_condition) if int(brightness_level) > int(max_brightness_level) and sleep_state != True: #If we are above 255 correct for that - brightness_level = int(max_brightness_level) + return int(max_brightness_level) elif int(brightness_level) < int(min_brightness_level) or sleep_state == True: #If we are below min or are in sleep state return int(min_brightness_level) return brightness_level def red_hour_query (self): - if self.red_hour is not 'None': + if self.red_hour is not None: try: if self.now_is_between(self.red_hour, self.end_time): return True @@ -315,25 +337,32 @@ def color_temperature_kelvin_to_mired(self, kelvin_temperature: float) -> float: return math.floor(1000000 / kelvin_temperature) def adjust_light(self, entities, threshold, transition): + #Calculate our percentage and midpoints + pct, half, midpoint_start, midpoint_end = self.pct() + #Calculate brightness and temp based on percentage + brightness_level = self.brightness(pct) + desired_temp_kelvin, desired_temp_mired = self.color(pct) + tmp_red, tmp_green, tmp_blue = self.rgb_color(desired_temp_kelvin) + #Output sensor log + if 'sensor_log' in self.args: + sensor_log = self.args['sensor_log'] + # self.set_state(self.args['sensor_log'], state=(pct*100), attributes = {"unit_of_measurement":"%", "note":"Percentage of dimming, inverted to brightness percent"}) + else: + sensor_log = 'sensor.' + self.name + self.set_state(sensor_log, state=(pct*100), attributes = {"unit_of_measurement":"%", "note":"Light brightness", "Kelvin temperature": desired_temp_kelvin, "Mired temperature": desired_temp_mired, "RGB": [tmp_red, tmp_green, tmp_blue], "Midpoint": half, "Start index midpoint": midpoint_start, "End index midpoint": midpoint_end}) + #Check if any disable entities are blocking override = self.condition_query(self.disable_entity, self.disable_condition) - if override: + if override or self.sensor_only: return None + #Run companion script if defined if 'companion_script' in self.args: self.turn_on(entity_id=self.args['companion_script']) - - dt = datetime.datetime.now() - - pct = self.pct() - - if 'sensor_log' in self.args: - self.set_state(self.args['sensor_log'], state=(pct*100), attributes = {"unit_of_measurement":"%", "note":"Percentage of dimming, inverted to brightness percent"}) - - brightness_level = self.brightness(pct) - desired_temp_kelvin, desired_temp_mired = self.color(pct) + #Check if sleep conditions are met sleep_state = self.condition_query(self.sleep_entity, self.sleep_condition) + #Check if red hour conditions are met red_hour = self.red_hour_query() ########################## @@ -346,15 +375,16 @@ def adjust_light(self, entities, threshold, transition): kelvin_list = [] rgb_list = [] brightness_only_list = [] - + #Create service data structures for each light type rgb_service_data = {"brightness": brightness_level, "transition": transition} color_temp_service_data = {"brightness": brightness_level, "transition": transition} kelvin_service_data = {"brightness": brightness_level, "transition": transition} brightness_only_service_data = {"brightness": brightness_level, "transition": transition} for entity_id in entities: + #Loop through lights, checking the condition for each one. Append each compliant light to a list depending on what type of adjustment the light is capable of. cur_state = self.get_state(entity_id) - if (cur_state == 'on' or self.keep_lights_on): + if (cur_state == 'on' or (self.keep_lights_on and self.now_is_between(self.start_time, self.end_time))): brightness = self.get_state(entity_id, attribute="brightness") if (brightness is not None and (abs(int(brightness) - int(brightness_level)) < int(threshold)) and int(brightness) != int(brightness_level)) or self.keep_lights_on or (red_hour and sleep_state): color_temp = self.get_state(entity_id, attribute='color_temp') @@ -374,10 +404,9 @@ def adjust_light(self, entities, threshold, transition): tmp_red = 255 tmp_green = 0 tmp_blue = 0 - rgb_service_data['brightness'] = int((self.max_brightness_level + self.min_brightness_level) / 2) + # rgb_service_data['brightness'] = int((self.max_brightness_level + self.min_brightness_level) / 2) rgb_service_data['color_name'] = self.sleep_color else: - tmp_red, tmp_green, tmp_blue = self.rgb_color(desired_temp_kelvin) rgb_service_data['rgb_color'] = [int(tmp_red), int(tmp_green), int(tmp_blue)] rgb_service_data['entity_id'] = rgb_list self.call_service("light/turn_on", **rgb_service_data) From e09efcbd144df32ba5059b8ab9ee71e9364f9d45 Mon Sep 17 00:00:00 2001 From: haberda Date: Mon, 28 Dec 2020 18:45:26 -0800 Subject: [PATCH 3/8] Update README.md --- README.md | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 58a6032..c0411ae 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Introduction -Periodic lights (formally update lights) is an automatic light brightness and color temperature adjustment tool for AppDaemon. This code will act on a provided list of lights to keep them in sync with the current light parameters. What makes this app different from others (namely custom components Circadian Lighting, Adaptive Lighting or the built-in Flux component) is the implementation of change thresholds; this allows manual adjustment of a light outside of the threshold range that the app will then ignore, until and unless the light is either manually adjusted to the current threshold range, or the light is toggled. The brightness/color temp is calculated by determining how far from the middle point of the start and end time the current time is. All lights can be mixed e.g. you can have a list with RGB, color temp, and brightness only lights together There are numerous options that can be configured to adjust the gradient of the change and constrain whether or not the app is active +Periodic lights (formally update lights) is an automatic light brightness and color temperature adjustment tool for AppDaemon. This code will act on a provided list of lights to keep them in sync with the current light parameters. What makes this app different from others (namely custom components Circadian Lighting, Adaptive Lighting or the built-in Flux component) is the implementation of brightness change thresholds; this allows for manual adjustment of a light outside of the threshold range that the app will then ignore until and unless the light is either manually adjusted to the current threshold range, or the light is toggled. The brightness/color temperature is calculated by determining how far from the middle point of the start and end time the current time is. All light types can be mixed (e.g. you can have a list with RGB, color temp, and brightness only lights together). There are numerous options that can be configured to suit your needs. ## Options: --- @@ -42,13 +42,13 @@ AppDaemon constraints can be used as well, see AppDaemon API Docs https://appdae ## Algorithm explanation -This works by calculating the time that occurs exactly in the middle of the start_time and end_time. The current time is then compared to the middle point to determine at what point in the day the current time is relative to the middle point. This is then fit to a sin function (running from 0-1, aka sin from 0 to pi). This returns a percentage, if your defined light percentage is from 0-100 then the direct reading from the sin function will be used. In practice this is calculated by: +This app works by calculating the time that occurs exactly in the middle of the start_time and end_time. The current time is then compared to the middle point to determine at what point in the day the current time is relative to the middle point. This is then fit to a sin function (running from 0 to pi in the x direction). This returns a percentage, if your defined light brightness is from 0-100 then the direct reading from the sin function will be used. In practice this is calculated by: ![equation.png](equation.png) The color temperature is calculated the same way as the brightness. -The values are updated 24 hours a day, therefore, setting the run_every variable to a very short time will not result in the lights updating faster. There are 256 brightness bit values, assuming the maximum is set to 100% brightness and the minimum is set to 0%. This means there are only 512 brightness steps that can be taken in 1 day, or in other words, about 1 every 3 minutes. Therefore, in terms of brightness, the app should be set to update about once every 3 minutes. A similar calculation can be made for color temperature, assuming the default range. Using the default settings, the brightness of the lights would look like this over the course of a day: +The values are updated 24 hours a day, therefore, setting the run_every variable to a very short time will not result in the lights updating faster. There are 256 brightness bit values, assuming the maximum is set to 100% brightness and the minimum is set to 0%. This means there are only 512 brightness steps that can be taken in 1 day, or in other words, about 1 every 3 minutes. Therefore, in terms of brightness, the app should be set to update about once every 3 minutes. A similar calculation can be made for color temperature, assuming the default range in mired. Using the default settings, the brightness of the lights would look like this over the course of a day: ![default_settings.png](default_settings.png) @@ -60,10 +60,12 @@ This is a fine tuning feature that allows for the lights to be dimmer or brighte This should be tested by setting up a template light not connected to anything and observing the behavior to see if the desired result is achieved. +Check back here in a couple of days for a plot showing this behavior. + ## Example apps.yaml: ``` -main_update_lights: +main_periodic_lights: module: update_lights class: update_lights run_every: 180 @@ -111,7 +113,7 @@ main_update_lights: - sensor.arbitrary_sensor,arbitrary_condition sensor_log: sensor.main_lights -exterior_update_lights: +exterior_periodic_lights: module: update_lights class: update_lights run_every: 180 @@ -132,8 +134,18 @@ exterior_update_lights: script: - force_light_update: sequence: - - event: main_update_lights + - event: main_periodic_lights event_data: threshold: 255 transition: 0 ``` +## Example sensor only configuration: +``` +sensor_only_periodic_lights: + module: update_lights + class: update_lights + run_every: 30 + sensor_only: True + start_index: sunset + 02:00:00 + end_index: sunrise - 02:00:00 +``` From 3d7e2021c219bddaf7f70eb40c14dd76db507893 Mon Sep 17 00:00:00 2001 From: haberda Date: Mon, 28 Dec 2020 18:46:16 -0800 Subject: [PATCH 4/8] Add files via upload --- apps/update_lights/update_lights.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/update_lights/update_lights.py b/apps/update_lights/update_lights.py index 6e7f38b..c1f3e3d 100644 --- a/apps/update_lights/update_lights.py +++ b/apps/update_lights/update_lights.py @@ -349,7 +349,7 @@ def adjust_light(self, entities, threshold, transition): # self.set_state(self.args['sensor_log'], state=(pct*100), attributes = {"unit_of_measurement":"%", "note":"Percentage of dimming, inverted to brightness percent"}) else: sensor_log = 'sensor.' + self.name - self.set_state(sensor_log, state=(pct*100), attributes = {"unit_of_measurement":"%", "note":"Light brightness", "Kelvin temperature": desired_temp_kelvin, "Mired temperature": desired_temp_mired, "RGB": [tmp_red, tmp_green, tmp_blue], "Midpoint": half, "Start index midpoint": midpoint_start, "End index midpoint": midpoint_end}) + self.set_state(sensor_log, state=(brightness_level/2.55), attributes = {"unit_of_measurement":"%", "note":"Light brightness", "Kelvin temperature": desired_temp_kelvin, "Mired temperature": desired_temp_mired, "RGB": [tmp_red, tmp_green, tmp_blue], "Midpoint": half, "Start index midpoint": midpoint_start, "End index midpoint": midpoint_end}) #Check if any disable entities are blocking override = self.condition_query(self.disable_entity, self.disable_condition) From c07baf2c9646ffb8911833834259a99b033c8a22 Mon Sep 17 00:00:00 2001 From: haberda Date: Tue, 29 Dec 2020 09:52:39 -0800 Subject: [PATCH 5/8] Add files via upload --- index_demo.png | Bin 0 -> 42609 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 index_demo.png diff --git a/index_demo.png b/index_demo.png new file mode 100644 index 0000000000000000000000000000000000000000..d1da05022ab4b3b9b693c53b12870d1715e47cbb GIT binary patch literal 42609 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

?BNrnng*7OUT z({u(bc1K6dYF7vk#V-4VL`%F@=3KmYPw+13Lzc>icWlksl5H{;QQ zKaRIT3j=PVMxq|lz*CcZ#IUUBnGaRsP!7uz2dZ(r$68sa26QLhep&F9;MJ!DY(pruJOi z>v?y{{I-mUmCRFsK^O$n2F!F$CvG)w%(@?sPecl;xnInvIM90r9&nuBUkcZbK+il& z_v{F|Lliy4%**SFDA09VLT_3^MG>g1N>WkNu-p zvbBm{;+IKyoxOAWbmq7CO09RMYsd=|)lTV+r|*|qeVZw(!2VDnQ7fl`tmtPV6E(|JEoI-Lm)vksgZ}%}(1>u^rh2Y)#Vk^=6XoJsDpK(F$gk=^Y*UNQUJd8xOs= zTFr?eG%f4G&06m;QiPMYh?AFn5GZh^fv~Cn~zkTaUyZVvr9WR>AEC*M)4VsN9QFf zuM3i&FgF?}Qr;TealTiSb?w#lHj(kWO`51_{#3pxXCKvN;s?=gF~+SWE{9&m$~Ao`Rm}`!Si1k2V2+=t_1bHf!#U8 zqJ+Aa;9qx9jhuHS_qQwMGp zeE_X*fFzrdN%yccb`?^|R*P40<4XdPZ{3&tIcoz4`s^U_JiPm>AN{@G|NlLT%Kkra z4)U)Zphwtc$4*pKRKNcvt8NXQ{^ZG% zk9u>aYAIZ+p2)_XMRVVxM890%9YprCJp2{+*)wNgj^ny7@%5lZ(cU~0QWBEh5Bd`N zJ7@Tu=WkCmM`fLW5`+4UK%(mgm{=GB!REM##%){Zm1fneJO67!W4U7wd(Hisph1aR5&;`t85i~_BCDtsLOy6U` z7u0xk-Y>Fx0CNzn=ntoHUBP|79~2N9`#qA|5{X29S%aY=^N?=wPSW9=Y)|?MxfmE< zl?luZ#sRvOp4wcQ8z6lsM|k^;K_A3SfQTTN6w_T#$ru?;LRAB+E0oPCD|bUfM0wQzca=fIZZbIGhwK?UxJw}3>qaw zK8egdFu}px))pcF`_M=(F%e{Yz+PCYV2hXn2-(L8(Aa$@E3UQ zDj>@_7)1cnfT*P+ADf%w1d0o>ZrdI!yeQyuQEYX7NE1e%K$m=qTHz8*Ja~CSWD!C% z5j3c$jYUFeQjsZ*D1}fV6%vU`SdNiDpyC z(4Y)SD9TKvLMxI&3MDIJM3Iu}yuYik_I~zrUgvq8bNsQay|&Wt`~7_G;kvH-x~VDC z?+zs1+X~hVkdLBm}*sC^6uAV94F0U z=BKEK%md+2#{o-{^<1?|9kQr(KxW@5{HsW@=kX#^D*9`-V&ms8UkaUcn0mJ_jQ!;a zX8H&#L_U9_!Kp!)M(}J0n@2z8gmOo^DjN7;`2bMoc32xzPXZY4w!5rm8Nmb-mh#2| zrHbR?052euOpNZZo{JvisTSTrsoW$9ep$hd3SP?@tCHITXkK)5 z3QJ4<7?-#8{c45wDsycshMCY#cSZT;y?Y-MHs+pl6-+#&D~@P?zbb0VjF+O0lVBUQ zMZC8#vrkT-;hF;s?ds|Z9m%V>%+H(d=lA*BIk972ou!(>%i4=}!*JjW{v4M#&W~cG zJZa9^ox64&&~e^$Q@m@}tb317&Kt$AfdkvuoL~CTwDK(6CO;A8D_srFTDzK;q_-D* z85wtq=rMm=9zmI2{1W~P46K{HYx)rx=>d{(u(N_Y@9O2tm$*mx@iA9FU-Oy^af;(c z`$BkAdo`$a7HOA+oSfS83GF*JCkRn@o8q|e4D2xCK*5MIIUQbpS=T)xZW4MlysH|B zcsObZ@dd||S%YPJC@MBHv~8Ve`4-$tNqtRo=5LoKDxw6X&j@~87+S}VN&Wj#$)U@o?E5Q3 zBC_Cu`ys=o70S8$Qn2l$0q`r#5`PrHC&?qS-F{>wGWKZa8yb3Nv-` zbtG@w`ZZ~WY>Z^P=JSh|_~|M1@rSbyFKV3Mf?t+DaRY#zy_uJ4*RCDbTsoLhYrVq~uVSw)k-3)m7$ND8 z8xZH1RI&T>0GpHXp%GJCOsPk$S9{qSTNcd}0Aw@>NFt@A((t-mzI?fbf!*~DjM3B6qnQz06t)Hgd?-y<+(qaSGH}RBE6nArZRGmMnK$@6;_7mT>T$bSL0T}) z-&JyUi8JUV{6c{Bj7KO0HpcXm1rG6`<@G?Iqzh zclM8PDO{#GA-D3RzhKu@vw7>*N$-U#Aa8ic<{|eiWvjiF?x-Slafb>VLmr>ZYu9jg z;Y|@Ueb&OF!Xg z+2MUk(*2qf5)y>OOgz6!(LiBRg4x;R>o1tl8PLt52qTH4q@)WM zbhWH1-n@D9_HBLsB*h`P+hk=a*(zI5HFA3-2H3TUXH1sZot&C_P{;WJW-pwg!m+W* zYA3B5p0BGC0z#)A4gRsEm>lyGC=$Hh6;8?Ba`NC9Pq7L_QG$uFmb{q%rcJz@RWz6f zH&B?2aDHIqmqvzIkZgB@>@p*8)&&eODPeJjI)`nR4d%di)MuYEGdI6wE*^3uG~ae! zfB9DHp8?K`fucFwWVAz143-wE$jIy!YzTgn7E`$Cf}s0Msg>?xETliB;>f z2hswxorhy)U_Sms_oBw$%E~%&q6U~ zJ$u&W!Qr6cb6#^ln03Xg(`XEgmhhB4-fSQKVFz9 zF?tDypuo z?s(?j=a;vtxVJxQ^3vDM@g(ei!|w>V+*nL4@-GU32|w z++185iu&A}Z8J)H9enqvaDx*!kB{hcEZ>xe|FSQ|hcAR=o9jSRY@)wF+~jZGFaCt3LF! za(#NC$0QYxH}@CTS6Afd2xKx)V6UykJkh65pKj{DL@EzDZpYCx4@bJu*7{O^I~Mmk zlagYw#pByolBLNU=f4X=>%nh&_&S>!xd7Fzv9QFwQIizOR014XEBuK zv#pZALc}=?jr#7gf1f^muxl=_sF3R0cWAUthCAg2229_dtu75(V>*$V8-J%y+8P=f z{rew+VJ+9h;)OHq>*M2bR!?t`f0c-V=er2ReQ>TqpYZ8LlKpir{9R*wYv*VMhrL^U zK)=W41(voCaFES&RqfI~zi#EX6R%=v4qt))foZ}uZ>(NNsX$RHCAI%!$cwKky+_~tb@P2{a_G>Q8z;;>eJC8Vc$)8CluDuGoflcag4n&ul-_6u_GKU2fKPU}O#Oc#jSj6N16cG`z*+hSTEypktVB$|wT;S_n zGyz~0CH%GT(@%QiHHTfNMBbYXO^Ts2U#WC9f0*8$WLbR4^91!VZd0$Q9jg4A8^}pi zdzm{t-W3YAgB@4P%%Q`1MT#`9aeo9tn|u)?XHQNd4FyFrZNA^@sMUhCDYtCldw5o> z#X~mX<9C8Q2lcn~@u_Em+tuQ`a*{$vhp|#o4nAnGtuKZCDmt&a;!h?&22Vd&M@eCO zai!AOu_e!+_pMMik2bOzFUjJCn^?}eG!qf15AH_mU#1v}b2uG4VA;?6YHH=}T}KLG z4+VV0IIa{~S#YCO*%laxiy}6>Upek09CISb@xyGA`(dcpuU{-9AxHNa~-9$cTj$Kgow{JqPf;AnHeRrI44+)86ob~A%`slik|6LKb8$Nt^H1io`C!l~2 zPtG3Ww3zOgK4ZpxTp3dt8r{BKB6K9a-<(g{?OwB{1{Zy&9o_wTr8C{-?Y3Z7t!+i( zcoA{gWZbVQLX9R(0w2I*?dR4JV`Jlm4|v*~bfp~P2Y2)$nU{itqhl6hFh62%ti3@D zmTh1KE{;Zsoo$<~V@cW~qN00i!ev|kk~rm_3JQ8Sz;TYndk|d-r}q)e;#go3s$qRr?(~XOdg!%LFs~53wu3J&?DO zDirm+`C^aV-ts5K#Rh-u z=$qvB@##e}6puNJB7+)xhLgLMeF)F4M}^G??8S*K3qq;KdWXDuv?PUhu{^y|T1lmp zwsxdi&O+`nk;3TBve56DCZcPoUB)##V&*{P6IUUio{qs5R)*I8(kEn>(XuSg~?t z&p#9QP{v?^3Y6D}_K{;i=c;{94ajCRMJI?*&=Z`$`pC*&z^S^qv9$T_pMMtkoe{qr zwkV~T-5dC=Of<`ghCyguHX&*KfLMy{>6F~le*J9LASDrcX22DK&}tkib@yVKn;rBY z3%t0M_IwWDlkm{cOm}`%=F4z4^&?)+f;?1gG+XJc(|+^)e$FZg`WM=2}HO?LiCDHy3zlRY>nPi{q<|s+Wpwb zPn=JE3NknfUtc&e$AvG&Pnnu!eDwYLaJJ1u%2nh^5*|iInS76-3$EB;$-1JdJ124R zlQWJ0A5HY3t5&VTif)Vk8YBq7)NHaU;lNy`*0%KSN>Slle40)2lwya z_Jr?)kE0sXq|R8hUNE)UY~|_EtCuz}2;AgzUHFDqS7dJ&^tc(@4VO5Vw7FV;YyyjQ zSh7S$Uj9Av0!-jbNU&wcpGw6Mo!_o5Xq$UHvn>1gkqn+jCg|763Bw!wRbebiD}uA@ z%S^rcnVQ*UiJ5~oD_I9jbFjHPO3_mt+yCpT$LFRc7WGOpP3on7YjEMiMA1#Vk=}vnm&!e$9k90p7{P_rRnf903$ICrYF$v%`f>OjB|j)9+3-CFjV@dE zYjt(cU4?CZi>zCOX{#%XHs)tkrA^QXsIuwt>)t!M@TEBRckb5BI@>c3eU0NG=WQ`4 zkKzOjAD|9&M>gA>L!JLX@a*JOG-C?-><^`*b&X@)A>oi$f>mc_X9sUR6kJXl3~NtY z-bGv-Mx>F3jPDbWB>+Yfqcj&9l4eD_%2^6YvaLQa`)ijt)#s>T;_s>&Hn)AL~&6IDyh~> zLj!Y%=)HSq$D4FDlZw|p4h9ZJa~J%xvaGD%;)2FgQ^SJ3U47X3!DV4} zOEsVs3P7h~#v*W7_5;zzp%)AFB|oj?xox=6lD9St_5)bJ_Hg<^-TsfPXA1D=@Zmkz z`2ibpi`9*dlb7U0^TUH}@B+VKt$8mu_m8uSyXLSE%7+C%ef8IB*dz;k6XHVDW9TZ3 zW*wUzWo}S^nBphf+RCG-o?rwTQvdz?GIA1Vno4GL*?k^(8dmu(8cnI9WMwP)YQmN|tDz7A2$daBu0G^B{8}g9=kCot4&u{t>{QEM` z1CPadlq>O^&hIEYR`@)0eu0-4T*E28uC9)iLc`{qIZWUtQH}O^Bay?_lKA-eiH3&v z|Kg_BQxPWje3oMxYI1eOZSj)}uF4&Ik>Zziu(ilZcxpOj%9_OA$W=n_KUJXnzW%Ok zj)wnZi=193%<^ zcEU_nQKIw6)_}4D7Dobs{g56qm%6#Sntt>Vy~wg>V=N3?Yq3{!{jGRj{?asJK2(f_ zhlh?}wW4emS@!<@iF_wo8v^zxAkjEbn-BcHEI%%Q(Owf1k;QC;$TObpKOx|BI)FZ| zmRv!hiWRuC@UN@iK2X9p?AEQX^MhHI_F%ln9PUiG|=(T z2P-^0jMnh!Nww>Uh@8ts99rt-%KZ3SAT*gXSz2(CzqkL0+zqoHP%UwiRSSN;<+v_g zyF&2Pp^k!Fsl#2Cvdftu53-C$#gy|6W!$Ix@8`k2k0GJgE^ zu~k?5o{9fkYose$3?!yRINnvZ%x0;%HXBKrn=iTD%+f_k# zhQ`d9(eLUt7NvmsAcHDQnp1$EsnYEVu3U)Bu;)|>2w&Yz-3?&PvOXt(du!b!FmT^? zF8@i#TiKP@?g0qVf_CCL@!VLxu%CQ3e*6Z+FRVehM0_}Wcu+)1QIQUA<00N_*RsW` z4}2h90zLXF{F3{d59~c}+=a`RZ=q!;k>9%Ipe`3i-!;Z&5H_-u36!-!JCf?EMuvtN zG*%5?-qt|4sT+?0TlHZ_6vAYH%#hvS9gn%F!~k$1*}6QxCjV+IyUSIQ=Yz&Nru3ZI zuB!A7%a-_c_Uy!&C~v91xZv9(Bch`XR{nKc*k!|+?x~arzJLw6q_W69wV#_EE%QM? zj!~g;w0g#K7V&ef)#{KzacjIPZQr6O@O+J^I-zyCxNlP$8-z=eo%bIuMH z!7lzgpQu*=Z<#XQk8@0|Jb1CXDs+gpeP-bG>(_~CeaCnJ=jIm|%Nwk7*3SX*9n=ihO9_v$=Ts&F=%$)XySSSdBN|vYVdsWJc5i>mC z282h$HFY5H0^HOCIfT@AneE=V>HPWC?(U!HO1PwlB=sRIQj?N2z@fC3y15PcxTQxt zaR_JVxzuJ<8zTvzp6{(&*y{pp-esdu7eISj`TCzRtvnwo+Fn+wqP%e|#n!%&ZgMMq zSPfS=r8WF4GRO>BNp;0erzFWaHf+?xV%U*F8j448)UrR=-y}wgc~@1{3=GGo^P_mB z%#k1Ckr0_KA@aEUzrakv>r3uAWMyX34QOd~&;Rue-Bh{PagG_z;FJanZ7N5-x^eK5QuSEcX8;hoil?-1 zM_M0wVD3a3Ng4w7%A`ZuZ;H|&9h3Xco;kCWMH(i8Ycf{zyaZu4ABYx;d`iZitK=M9 z8!4h^9X0!0U5$8Qw(YQ4Gr@4Yh$f?ee{Gw;%Xn4uBbC<=vm$ix#7s=G@!LKvxqJJu zwFWME(fUL>7gQR;ik^II#ruEc00x~m|7pT3MevFTIK)Ycqb`gzO44dBr&9|L5rFH=4Qi!|r*dwUeZPSdI`O^s>sRP~P`zC<=dOfI$2MB1Rs?o{(tx}|gmmI> zrtW+At*KK$`qQJb(Y1|Z1CdR~bLMV$il^zyd_$Ep((XFYyO0|mo|-TJI@WIXAKY$O zw0%6XD$jz}lVG_K^9b`8gJ*Z{yrn(LZ$2=U^#Hest3q=N=v!4q7nz5-LcX&*xIPHd zHee-+{T4Qg8eBYwP;%De?aUvAC&w>zZ4~g+B*D!d$bNRPn4^2+wuh{BA#bK*Xh`*` z?p5~Jon0J%=i*e=T7`y3=a<`Z&=qzJU)_04aZ?mV#3uR!s!GBbbXlW5HtfjRx%f5HnDz*Mg{-myQIxKRiz&mi z_BHTHg6r15i7Sw-mU;QpIWy-ZTfi>!xtp85r9)aRIg((syY~B3>du^k-~8YpTaIw_ znpgWkjME~<*qW|uR6^SFtJ=~0{YMx5Kii+%Y6z(Ujlf}|4O@`De!l91&Ye2hP_qDK zbyLudmDKO-aN;MeG2EQq!rZ5MI{6j6f>Tq#8$L2wSw-ayxN|Wta`NO(!WDf)H9>tC zbX@6($_c4;y229M*77eQV%mNkXKoseTM;z_P$)p-pd5Jcb5v3h^g>|6a&Aj3K2}w+ zn0CuPts+5r9JKxe=xjGM`fo!u|z`-YqghuYdwDMiawt-q$hFm` zM6m3wsw3fBq{NN^Q2F&=ZVPN$-<~};ZI&mG?Aql(5?nmAQ%Gq#8i?{N`#A~?L|BVu z8a1)8v6Nc_RVsT9pD?^UFf__~{F)ZH&Y~9vJ`KQ>tfz{QsYo?AB4AF+$^vGwR8*>) zh*TFXeeX}>Bg%^4_J@Q^%)-PD-dXAw`WC##z1=yK<#$Jwwz9W$J>&Mnz&4JNF|uibA~TbQB!-= zf_Z|X8;+ivLkT?TW-V_<5IcBPMl$I|#;4EM+~8#(mw|dG@^HQrNyJ}()q>)Nv;#xtqX!3fBg>f3aS#ig>7Wb zqD-MAwK}ooWGam<`#3Ou;1mz>>O5^F+8Cei_a)DCMyOL+-$MEZjj+4GPp{IwTQ_j6 zAAjvK_fYrlS3`bk|BH@NcY#a^RX zhkIfBw20VW@L}1EEGNRry-R^vb3gRwY$inqAZvf@>s{Mg(e zta$CV-d=6-WskQ#w;-ya+Mdnp->UtVb&WE8dez&BCd(J~)(Y;Je_wiCT=$Z!M=9Oy zCC`GnEDRVrbSMk~(KSw?YcuE(%79>tL))jZ?ras0)oIJXjld!c-5~hX2 zpVE~lRYvU@owFks37O}rIncW|p+N86y(_$!TzATFi|~5rH;eEWaR(nAvjqA+cu?3z z)UiW{Kox3yUm8Nli;&~rH762pSBRs58L!VOHh+x`w=RFQC{aWAJ%SZ`OT`D0S8ZeSoo3WzYPKBX z9X>=#HiENMSO7M6#~D%$rhj|B&dGk*ymMx$`@0$zkVl_sjWJ#`@FaR(T5;y;E-!tk zmU=vF$j}9u{@Ha1P_%-NgH?%(i=)x8}Ip2sp5^9zt^?S{IWVg^bCanr<3aEH3tX4dIE&ytgCzL^rXFHZ(W`%s4qGE?;NH` zjsI`JS8(gEhR+?o*y#Fk8Na=Dww~Hj<_?;`?M0CHkVgouV5s`Q;y`MB@s)oz0`PR( zXC{u4Uwq$5N0v-4$bIF7+!t~@AaaanVtRTrH#%#+wunr3F)=y)wI0Y|kWU}4J*+YB zTqWn0RZOqao4&H;DT0UgcHE};VWoq=EOaA3>#|T2xU=S}<6`h^72e;m@9w({95%{I zDk{e8+ou}6UTq5>+c|Bn=jToPx~R-nuDEqvJ<$gLK~BsKG6nfhBd+y{UcWF%v={4ODq7_y#!+UbK8{Ka(GzU z)K45ppXxixOILTEd%;O{-MH`Z#ZGdR*F^MLJXZ&PY?w*!i(6nv3=+QB*vrBI{% z(v_=L5%7+MzDad?+-(vzt{33x;Bg0Z_#t^-vUeAwQuaYZ;tbu=V+UrnDJ3Cd8Qqo> zcZAL8#*R{$4V=F97z4%|PO^nr+-ZkgC}j*1K|{)?%U-KbUYY z!Q^<%h*<F!q93s=!XDTh$?xgK7C#mgC z>c*+lrezIA_qEQ%es@~_Dn z4`>~4EsN`*Z`C}V^~beM;6aF-X9tVVx#Inq0**~*DQvBzvJbqb{(|dIZfpMjINhMG zti8CPoWEi&Xid>{6e5qHjoa$9}r}wwrB=kyRyHsi$0+5YXWP zsD{SSzMpv@S{aO4bRU^d=}Qf4AAdck^%=@9AffhIw)LRjs9~44l zb1KG;8Kd@cn_h*1?7ShZKjn@IS#L|}Uckj`txAS`o3Iv!NByu57 zf|o&C^G!w;^?yJCe*gXq$1M)Z zD)s#~-+y&?Zn+)gjQwe}1P`zF44GtX9CTHX&!{|}eY3O!)t;gPOk z9plVB#@bz0+uU!kCyX?sBf?TRa;WA3m0Z!14C&oU4)*txr1Pa-3Vn}5TWO((RQ^~x z!3;Eu`niGPJbRnswkVT8;*9m+y{V8g>F9)@)N;Jn3Ho&W6=l;re> zL(jQ4kO+j4MCsQUzyR5V^0PSAy=x8X@ne9#M3IprN17h*w*NI1lhCfDc)Pl4NDG@g zBb>Lzx^p7|Pp&_=KP@bz@H%6+Tz#c)u-t|2-7b#y$acOn&c8G!E-%??H*o`T|A7-Hj20~$HugN>BCeezb_ z3LCLMPt7?iGNSv`ijtwV*7`FY8n!RD;>1O<1}<{*tdLpZ#^cj3Tl%Gmbiep1v*p}R zJ_9Ocz|PFBx&PXQuAvA|oP9c6Dh_GmPC*?7=}&esmM4&`;0xuSx9egx;*o+zhl0x{(opgm&Ew8%1(Y(%K4%%l>GDO z&(H4!C7p2&u=D)N3RAPs82&pB18hv?h8cK_WYD)iW&0x9APYmt19IE?xn+Q~?x zThSc~Zl;splS<+aZy!&*4_Tb{MhSHYynMD-is6K(-Ol(muh%JIpvj$cwDp8`J6h4( zlQcq3Vz}3v2ZKVakfV%{gX9~qZHZYEvjCheD5ef+MMjS!s#TSJZor&%h z6n0y-Y+;owIc_!I7)>4qB{J$1AUAFxS@J_;EJ#ImIOx~a7d31klY#I)2Z)3ZhzI)< z#+zOsc(E9rTn9V5bqu=$hb?B=r5w41qXOu2mHv{Gcmt)X0&zb} zN!o%5hNWq9lO)#Jpcbqg{jT@9ki__9vtv?x+jv^lZCGdp{VgY^rTu8jlf`dgRgj>m zsz`>sQTM)W9>H#I;n(hS=bx`V!OMe+%G500K!Y)T>eQR=bgB0@nar{swKt2im|wPf zoC+DV~_p&$C^0PLeoNjUKS zMY)aqqkWN)b3%2W=+~*nfuSvMT}}e)Q;^UvpD=-#{1!dwrEr z(vuA+k90>AiAk3tOoVeBeXOt zSV~Jvi*oPg8YyjF5q#2GF0ai}aD{}^_M#aX88fs|E^O>QJ;vZF6|z>kK~jek8DfGA zb=IbdHpzBv&xQDBi~&hMoqKH7ZG#P$)XcKdrePA$8z7%*Blz;>FjtO=U&!EdP1Xh z%-jqMQ0d?o$~$-OJ|aHH1`DXlHrDUU%G{CZTO_>d!fOY7KcKy&TcU>D63uZrJu2wr z)U6U1xw%~h

R~ujz53=SBaNC$ZaoB?@;^wFWQH*Qlm7M|l*qvQTr;CPS^n(`EhD z2evenmiNGH#k&5*&;ifgUVb%?ZEN!*pAH$kM46LMSU zGk}Q@`$iVg>lqn}8pRp~7c`*EsEmk_u4KOjQ>IM|3k;kQ+;r;Gcyt3}8T&*X=)=JR zJefRw`m)4tJ!5Ve4(2ra)k#M_Jb_4H!xkSn>j>wgr*#|0ZbyS_s)@p&xjv@(h7;7C z@x^Ok_mGzABS(6H7*hfgBlirS5C>L+OjyLqdc3|wMFO__)^cGc%@fo*ySlkOoIcbS z1;5CjEi}b#OQv+^{n!}W(m_uMF&F>Kn%sv%8Oat1TTk0Sp7Yqx|hL`wRToY&NHr5?YPyc9vUf+~N-Za8nL0hp9BWJEPQ| z`dwK4OXQKN)II@q3(><%*;KHnD#27@`V<&p!m+GXO#q@uBt}-gjyu>UY?!%$cSukP z?O0WsUdpJjMV*e=ldV3<-614VSJ)73OnmR1pzEv)GSq4nMIJpurNgTj#|Sa|nd6>& zZ(6K6x4~(1u!T-%tSd6Rthg`f`>UZYT(Y;#EUg1U3n~YWv@PU*72s zP}|?^_~7-4RSqT-9}?8c*knXJ6k6b^JKBbXc!B_e2t@kPB@=B1HaBhc5A7JY93cu^ znas_iwyD>;e2mt*y@;An*Gp?IYM!ru=)D!5UqA;Z*Ir6*f*SsS{7;+v0jV8I@TSVM zc>+q4nMZ9v%3>8+Y)_yt(GDH#(zX5o=cn?ab?&U3gw)i&YsdL8LV8hOj7C{^CZ^nc zEo@JwD_mM|G;F=!-G55Yi>Qleed$QLR8Sck=fJ>}UR^ipUI9TP^GBBTZG$#j7_D-I zZQ|?f&5^m77rsPk8f(~ZVx_EsWn|lU70W$q4j%hi!kb#d1fy&N;c3W{v3>K+DYhft z_|>4jaazAy@3<*}QzFxOS8&WQSXU$GjDK02mE{O*DxmN0AF$t+iItqx0kUJgG>Wgl z{zDRm&9rQ@{}Zs2(^dwi9TrzUB>0fFc>tTee97|pn+3faq$4OO`&<(>Y6DaBGY>|l z!wcJZ!O1XCO1=s@Nna8#$dX07x<8w5)V-qN=7j8a6L4T({k%t%D%I)D>wpuemj-X#-rgC^2YOYiBUWuHkyo|Q6CIyG}%+R`KQijxi7IU`|~mhp3Of`uyb zKv$ildYcOM5a*#4Ynnps|TtzoEjf?0t!nUDh#a&;~>F~ykiXlbF@D^gGT zV+eis;Lrd&3G1FxQX>}U%4#juknrueZcyRJKv+_N%|}o16uKhifM)U#SBp8r2QTJ= z4Y*`De9{aQu{|bjUgTaG_q51a>V=(pxys)|2qE5cV@$NFN(I(PUY}yZda$HSx9%xT zdCfnzwLFvEuP3(bV~>lI|KIi6W*lKE2dBS6K8i<}|2ML>DWsp}d@#lxp<5j6?Sbn4 zzmvHcHl0Me_8G%)*wLKHhc>>sIdsn4xwOGG@sqd@n^)?b+Tjim28PcL^Q_#C3qJ$= z4MSV55#oT8rEX4`V_!*09ZJ9s0rL$9>ki0&zo;^`T~2G4FY)ix?M@#97ZiYk_+G#q z`4tx@^d0X+V^5W7qCsFjMEannMM8IF2i{QJ%dQtu7WWAOZk7pwwlMk7dEyYs`Jp!Q z_YSIbaDB7O+@ME|gGga}Nk-9D5x^64WKB*!;~u_Re0f`7(d;hvxz{ngd*ph-_Z=-Dx|8p*V2DB+3WTGLzDe~$n}1hMLdOqQ4M(u-Rsc5gQ#eHedx%` z%Q|*n*p>W?+UybimC=;`8*`(^%6xFlpIZLcUsTXxAM69##f`5gxaZd@Qg%YPE1tU7 zNh5rS{p+kVMLFC(fu|>W7Zn%B>4)>SseX+OaESODy1zksISDG=|6lmsp6i>tP48Rl z4U7CvLZ5&!fnjrI}u1=NA2~ACUOLBxaQ{4<_14P zh>DE%O%lg1S=7Z5TfHqbhjjAk6_|SE|vhp#C zIi9r}$1+ag?2Ffw!Jm4~(kekAuBo7?=trG{VAbH@6!W!fFST~nGpa^j-$dF@l0MsrKtjc(Fm%RnOk%zJ$;-B zFGHRk6H7Oa7fdjcK<~F)(uJ-%XjpkHaJXV+_gb+AuQ&Jm9~@mMpX5hRShVcMvgr;D z{Sr=O+$diJtSN3aL{n3u1UHSowXXpKZ<5Mk?QjMe_27uzSFz3>q9Itc zwAp{hetgU?_2zwH*NZDky}2i?#mA|pj#>BhohMP5Vpr98ORG+FqPFyhN2#egI>Bt4 z`pGq?p3ZWb5a4P!u0!f3?#Gb6hubN*;?gLWj^1sr${l6fe%Q81Omh}@m`6@3BCjB( z3KsF^K@dhx%}bb!Trs&=O5Iw6?)pZzzIrykb8p+cf=fKKZg+t#RWuUaQy?%m%%B?#R0ZSLD z2s&H=N39*1m1%~d%|i=~YFby`OckOxU0?}D_f=lyO?sv%-mg4kbzYG{r*~&aMdPmu zlQ%6hIx8x5{ZwRpgH=?$2TJc5;@w(HEHWf_O3BH|_30D#C}F0h3Iskxbl$yt%Meb+ zJ#pIZ?Yq^f2Az$q>WF*)-cYAxWrcXR2wFoe%&vNWwzsoGhT5JJGY*A5hS!_UH+QNZ zc_`wYWY2bc*3xIAu19zf5tpv6w_V_O`s&po%@G!uK|u;WD(|jlzD`NLsr&rl7#_U4 zyE~9y!0@|Yc7Ljw7D=W)JG^|Lm zr8-NrV_sU2+|x-3Mgpcg!oO$RPW>}I?MA+R)NsdOZYCq6h$JIsUVWnJZxVlFgYaoT zO^?=_V>Tp{j0E*YU8C`Fzt#ose=1+6rYSq}d^ix^z*WuYAejusdCFXGkkyQ9(-J}h z`iaZwB%pJl*_HhE@-GJs34g4Vg-vM%=2UbxKWKEn`EP}zO z>4mX=L^SEgqs z3?WZ77F6lAv=+NXQoH6qMP&7xr|uyyA2RY8JaynJcPA$&*r|M@u>7mLMQVtiUUcg9 zO%nG>TFf%tPcW*H{@?@<;IFTdEdcy{^WSaq+`UXWg9&Ho)%QKw^L|+rMFal3pLqGw z2O&a312)`6eJeD-HqtKFKQT|itxX9kE3@Q_JwvdVLLO1YyzaOg)inSK78dMYC zX?TUOnjY=f+)%1(b-UZwSUMuaEg`5t-8sav{E2Z$v+1bs!jh?Uk(}MXp$!G3iB#t< zU2e{yBQ2rpo)MUjGJwhot=_|gDMgAAf%PHfun6IoZppz7CrGxu9E&xZYWHJUzHM9g zV&AxZ3M~zZPF?=yOmU}f={5JpI%yA`cTQZG18}ltCSP2Bq*-ov8|huJiIl^_Sk)D` z=T}DW-;=$-%3%iM70Jc;MyabeHk2Cm+Y5VMV3p7SuhV?g(l9-5C$_-(yhz;N*2pjR40K5pfiS!U(&P zk|X7~AWYu1s{p-W<}ADH;kPU+Cvv87)fvuc1?MB=tg}$$15og6!;LAgu7W9KjI!9I z)3Mn|QmXsCG1He?L~s6SD5ZvMUv+r&uP!(4!T_Gq5! zIY8}-h}l@}2t!fDgW8?6#>rohZPVuV(W*B5T2!!F`0dQwvBGapzVzXr8(hD4 Date: Tue, 29 Dec 2020 09:58:12 -0800 Subject: [PATCH 6/8] Update README.md --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c0411ae..4c31909 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,15 @@ This is a fine tuning feature that allows for the lights to be dimmer or brighte This should be tested by setting up a template light not connected to anything and observing the behavior to see if the desired result is achieved. -Check back here in a couple of days for a plot showing this behavior. +This is an example of the behavior of the light brighness with sunset->sunrise start and end times and the index settings below: + +![index_demo.png](index_demo.png) + +``` + start_index: sunset + 02:00:00 + end_index: sunrise - 02:00:00 +``` +Note the step change in brightness at the start and end times as the code switches from one mid-point to another, and the two hour minimum point in the middle of the night. ## Example apps.yaml: From 9fd06e6566c6b5005be96523d7650c3a0b380236 Mon Sep 17 00:00:00 2001 From: haberda Date: Tue, 29 Dec 2020 10:02:42 -0800 Subject: [PATCH 7/8] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c31909..cfeb236 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ This is an example of the behavior of the light brighness with sunset->sunrise s start_index: sunset + 02:00:00 end_index: sunrise - 02:00:00 ``` -Note the step change in brightness at the start and end times as the code switches from one mid-point to another, and the two hour minimum point in the middle of the night. +Note the step change in brightness at the start and end times as the code switches from one mid-point to another, and the two hour minimum point in the middle of the night. The brightness step change should be accounted for in your threshold settings if you want the lights that are on to smoothly transition at that point. In the case of this example the step change is about 15%, therefore a brightness threshold above 15% should be sufficient to account for this change. ## Example apps.yaml: From 41054597eb5ea10d93617f37dc13eabe20426556 Mon Sep 17 00:00:00 2001 From: haberda Date: Tue, 29 Dec 2020 10:05:03 -0800 Subject: [PATCH 8/8] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cfeb236..4daea77 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ sleep_color | False | Color in string format (e.g. 'red') | red | String transition | False | Light transition time in seconds | 5 | Seconds companion_script | False | Script to execute before changing lights, useful to force Zwave lights to update state | None | sensor_log | False | Creates a sensor to track the dimming percentage, mostly for diagnostic purposes, format: sensor.my_sensor | None | -sensor_only | False | Only creates a sensor that tracks the brightness and color temperature, will not adjust lights. | None | +sensor_only | False | Only creates a sensor that tracks the brightness and color temperature, will not adjust lights. | False | Boolean watch_light_state | False | Whether or not to watch individual lights and adjust them when they are turned on | True | Boolean keep_lights_on | False | Forces the light to turn on, in other words ignores that it is off | False | Boolean start_lights_on | False | Turn on the lights at the start time | False | Boolean