From f3a1a7388517d8d48e2095bcc7cce9373c64e937 Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Fri, 14 Jun 2019 16:27:28 -0400 Subject: [PATCH 0001/1077] Remove configuration options from entity attributes The biggest concern is that apparently any device_class with longitude, latitude, and elevation will show up on the HA map (not just device_tracker). In general, however, configuration options in attributes is extraneous and the community voted to remove them (https://community.home-assistant.io/t/circadian-lighting-custom-component/61246/355) --- .../circadian_lighting/__init__.py | 2 +- .../circadian_lighting/sensor.py | 11 +++- .../circadian_lighting/switch.py | 58 +++++++++---------- custom_updater.json | 4 +- 4 files changed, 40 insertions(+), 35 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 8976e285..6458e57c 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -48,7 +48,7 @@ from homeassistant.util.dt import utcnow as dt_utcnow, as_local from datetime import datetime, timedelta -VERSION = '1.0.5' +VERSION = '1.0.6' _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py index 3395142b..756d5799 100644 --- a/custom_components/circadian_lighting/sensor.py +++ b/custom_components/circadian_lighting/sensor.py @@ -45,7 +45,10 @@ class CircadianSensor(Entity): self._unit_of_measurement = '%' self._icon = ICON self._hs_color = self._cl.data['hs_color'] - self._attributes = self._cl.data + self._attributes = {} + self._attributes['colortemp'] = self._cl.data['colortemp'] + self._attributes['rgb_color'] = self._cl.data['rgb_color'] + self._attributes['xy_color'] = self._cl.data['xy_color'] """Register callbacks.""" dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self.update_sensor) @@ -82,7 +85,7 @@ class CircadianSensor(Entity): @property def device_state_attributes(self): """Return the attributes of the sensor.""" - return dict((k,str(v) if isinstance(v, datetime.time) or isinstance(v, datetime.timedelta) else v) for k,v in self._attributes.items()) + return self._attributes def update(self): """Fetch new state data for the sensor. @@ -95,5 +98,7 @@ class CircadianSensor(Entity): if self._cl.data is not None: self._state = self._cl.data['percent'] self._hs_color = self._cl.data['hs_color'] - self._attributes = self._cl.data + self._attributes['colortemp'] = self._cl.data['colortemp'] + self._attributes['rgb_color'] = self._cl.data['rgb_color'] + self._attributes['xy_color'] = self._cl.data['xy_color'] _LOGGER.debug("Circadian Lighting Sensor Updated") \ No newline at end of file diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 380e03fe..07153680 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -115,20 +115,20 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): self._state = None self._icon = ICON self._hs_color = None + self._lights_ct = lights_ct + self._lights_rgb = lights_rgb + self._lights_xy = lights_xy + self._lights_brightness = lights_brightness + self._disable_brightness_adjust = disable_brightness_adjust + self._min_brightness = min_brightness + self._max_brightness = max_brightness + self._sleep_entity = sleep_entity + self._sleep_state = sleep_state + self._sleep_colortemp = sleep_colortemp + self._sleep_brightness = sleep_brightness + self._disable_entity = disable_entity + self._disable_state = disable_state self._attributes = {} - self._attributes['lights_ct'] = lights_ct - self._attributes['lights_rgb'] = lights_rgb - self._attributes['lights_xy'] = lights_xy - self._attributes['lights_brightness'] = lights_brightness - self._attributes['disable_brightness_adjust'] = disable_brightness_adjust - self._attributes['min_brightness'] = min_brightness - self._attributes['max_brightness'] = max_brightness - self._attributes['sleep_entity'] = sleep_entity - self._attributes['sleep_state'] = sleep_state - self._attributes['sleep_colortemp'] = sleep_colortemp - self._attributes['sleep_brightness'] = sleep_brightness - self._attributes['disable_entity'] = disable_entity - self._attributes['disable_state'] = disable_state self._attributes['hs_color'] = self._hs_color self._attributes['brightness'] = None @@ -145,8 +145,8 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): """Register callbacks.""" dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self.update_switch) track_state_change(hass, self._lights, self.light_state_changed) - if self._attributes['sleep_entity'] is not None: - track_state_change(hass, self._attributes['sleep_entity'], self.sleep_state_changed) + if self._sleep_entity is not None: + track_state_change(hass, self._sleep_entity, self.sleep_state_changed) @property def entity_id(self): @@ -205,19 +205,19 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): self._attributes['brightness'] = None def is_sleep(self): - return self._attributes['sleep_entity'] is not None and self.hass.states.get(self._attributes['sleep_entity']).state == self._attributes['sleep_state'] + return self._sleep_entity is not None and self.hass.states.get(self._sleep_entity).state == self._sleep_state def calc_ct(self): if self.is_sleep(): _LOGGER.debug(self._name + " in Sleep mode") - return color_temperature_kelvin_to_mired(self._attributes['sleep_colortemp']) + return color_temperature_kelvin_to_mired(self._sleep_colortemp) else: return color_temperature_kelvin_to_mired(self._cl.data['colortemp']) def calc_rgb(self): if self.is_sleep(): _LOGGER.debug(self._name + " in Sleep mode") - return color_temperature_to_rgb(self._attributes['sleep_colortemp']) + return color_temperature_to_rgb(self._sleep_colortemp) else: return color_temperature_to_rgb(self._cl.data['colortemp']) @@ -228,17 +228,17 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): return color_xy_to_hs(*self.calc_xy()) def calc_brightness(self): - if self._attributes['disable_brightness_adjust'] is True: + if self._disable_brightness_adjust is True: return None else: if self.is_sleep(): _LOGGER.debug(self._name + " in Sleep mode") - return self._attributes['sleep_brightness'] + return self._sleep_brightness else: if self._cl.data['percent'] > 0: - return self._attributes['max_brightness'] + return self._max_brightness else: - return ((self._attributes['max_brightness'] - self._attributes['min_brightness']) * ((100+self._cl.data['percent']) / 100)) + self._attributes['min_brightness'] + return ((self._max_brightness - self._min_brightness) * ((100+self._cl.data['percent']) / 100)) + self._min_brightness def update_switch(self, transition=None): if self._cl.data is not None: @@ -256,8 +256,8 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): elif self._cl.data is None: _LOGGER.debug(self._name + " could not retrieve Circadian Lighting data") return False - elif self._attributes['disable_entity'] is not None and self.hass.states.get(self._attributes['disable_entity']).state == self._attributes['disable_state']: - _LOGGER.debug(self._name + " disabled by " + str(self._attributes['disable_entity'])) + elif self._disable_entity is not None and self.hass.states.get(self._disable_entity).state == self._disable_state: + _LOGGER.debug(self._name + " disabled by " + str(self._disable_entity)) return False else: return True @@ -271,7 +271,7 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): for light in lights: """Set color of array of ct light.""" - if self._attributes['lights_ct'] is not None and light in self._attributes['lights_ct']: + if self._lights_ct is not None and light in self._lights_ct: mired = int(self.calc_ct()) if is_on(self.hass, light): service_data = {ATTR_ENTITY_ID: light} @@ -286,7 +286,7 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): _LOGGER.debug(light + " CT Adjusted - color_temp: " + str(mired) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) """Set color of array of rgb light.""" - if self._attributes['lights_rgb'] is not None and light in self._attributes['lights_rgb']: + if self._lights_rgb is not None and light in self._lights_rgb: rgb = self.calc_rgb() if is_on(self.hass, light): service_data = {ATTR_ENTITY_ID: light} @@ -301,7 +301,7 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): _LOGGER.debug(light + " RGB Adjusted - rgb_color: " + str(rgb) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) """Set color of array of xy light.""" - if self._attributes['lights_xy'] is not None and light in self._attributes['lights_xy']: + if self._lights_xy is not None and light in self._lights_xy: x_val, y_val = self.calc_xy() if is_on(self.hass, light): service_data = {ATTR_ENTITY_ID: light} @@ -317,7 +317,7 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): _LOGGER.debug(light + " XY Adjusted - xy_color: [" + str(x_val) + ", " + str(y_val) + "], brightness: " + str(brightness) + ", transition: " + str(transition) + ", white_value: " + str(brightness)) """Set color of array of brightness light.""" - if self._attributes['lights_brightness'] is not None and light in self._attributes['lights_brightness']: + if self._lights_brightness is not None and light in self._lights_brightness: if is_on(self.hass, light): service_data = {ATTR_ENTITY_ID: light} if brightness is not None: @@ -332,5 +332,5 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): self.adjust_lights([entity_id], 1) def sleep_state_changed(self, entity_id, from_state, to_state): - if to_state.state == self._attributes['sleep_state'] or from_state.state == self._attributes['sleep_state']: + if to_state.state == self._sleep_state or from_state.state == self._sleep_state: self.update_switch(1) \ No newline at end of file diff --git a/custom_updater.json b/custom_updater.json index f2a32ea4..4eda9353 100644 --- a/custom_updater.json +++ b/custom_updater.json @@ -1,7 +1,7 @@ { "circadian_lighting": { - "updated_at": "2019-06-01", - "version": "1.0.5", + "updated_at": "2019-06-14", + "version": "1.0.6", "local_location": "/custom_components/circadian_lighting/__init__.py", "remote_location": "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/__init__.py", "visit_repo": "https://github.com/claytonjn/hass-circadian_lighting", From fbafcebe6634a6ce84710f46cdf4d01e126e8786 Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Thu, 27 Jun 2019 14:35:07 -0400 Subject: [PATCH 0002/1077] Add instructions and info for HACS --- README.md | 4 +++- info.md | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 info.md diff --git a/README.md b/README.md index 97a3cd46..e900af06 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,9 @@ In addition, Circadian Lighting can set your lights to a nice cool white at 1% i ## Basic Installation/Configuration Instructions: #### Installation: -Install `custom_component` files automatically using [Custom Updater](https://github.com/claytonjn/hass-circadian_lighting/wiki/Installation-Instructions#automatic-installation) or [Manually](https://github.com/claytonjn/hass-circadian_lighting/wiki/Installation-Instructions#manual-installation). +Install `custom_component` files automatically using [HACS](https://github.com/claytonjn/hass-circadian_lighting/wiki/Installation-Instructions#hacs) or [Custom Updater](https://github.com/claytonjn/hass-circadian_lighting/wiki/Installation-Instructions#custom-updater), or install [Manually](https://github.com/claytonjn/hass-circadian_lighting/wiki/Installation-Instructions#manual-installation). + +[![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs) #### Component Configuration: ```yaml diff --git a/info.md b/info.md new file mode 100644 index 00000000..57de8ace --- /dev/null +++ b/info.md @@ -0,0 +1,7 @@ +## Stay healthier and sleep better by syncing your lights with natural daylight to maintain your circadian rhythm! + +![Circadian Light Rhythm|690x287](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/f/5fe7a780e9f8905fea4d1cbb66cdbe35858a6e36.jpg) + +Circadian Lighting slowly synchronizes your color changing lights with the regular naturally occuring color temperature of the sky throughout the day. This gives your environment a more natural feel, with cooler hues during the midday and warmer tints near twilight and dawn. + +In addition, Circadian Lighting can set your lights to a nice cool white at 1% in “Sleep” mode, which is far brighter than starlight but won’t reset your circadian rhythm or break down too much rhodopsin in your eyes. \ No newline at end of file From 7c7b5ec07227b3b6b7ab5d386ef0406f37d8c2df Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Sat, 29 Jun 2019 21:18:50 -0400 Subject: [PATCH 0003/1077] Repo is now included in default repositories Update HACS badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e900af06..4e9dfccc 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ In addition, Circadian Lighting can set your lights to a nice cool white at 1% i #### Installation: Install `custom_component` files automatically using [HACS](https://github.com/claytonjn/hass-circadian_lighting/wiki/Installation-Instructions#hacs) or [Custom Updater](https://github.com/claytonjn/hass-circadian_lighting/wiki/Installation-Instructions#custom-updater), or install [Manually](https://github.com/claytonjn/hass-circadian_lighting/wiki/Installation-Instructions#manual-installation). -[![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs) +[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs) #### Component Configuration: ```yaml From 50ae5208ca1de11308c5768c4df3bbf06f0c1df2 Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Sat, 29 Jun 2019 21:19:05 -0400 Subject: [PATCH 0004/1077] Attempt to fix info.md for HACS --- info.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/info.md b/info.md index 57de8ace..7c4f799f 100644 --- a/info.md +++ b/info.md @@ -1,6 +1,6 @@ ## Stay healthier and sleep better by syncing your lights with natural daylight to maintain your circadian rhythm! -![Circadian Light Rhythm|690x287](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/f/5fe7a780e9f8905fea4d1cbb66cdbe35858a6e36.jpg) + Circadian Lighting slowly synchronizes your color changing lights with the regular naturally occuring color temperature of the sky throughout the day. This gives your environment a more natural feel, with cooler hues during the midday and warmer tints near twilight and dawn. From 36115d4fce1078f349af9ecb99b52893b104a5f9 Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Tue, 30 Jul 2019 16:46:28 -0400 Subject: [PATCH 0005/1077] Check light values before setting them Hopefully this eliminates possible "infinite loop" of adjustment, where the SERVICE_TURN_ON call triggers CL to adjust and so on. --- .../circadian_lighting/switch.py | 56 +++++++++++++------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 07153680..759eaa91 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -267,7 +267,7 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): if transition == None: transition = self._cl.data['transition'] - brightness = (self._attributes['brightness'] / 100) * 255 if self._attributes['brightness'] is not None else None + brightness = int((self._attributes['brightness'] / 100) * 255) if self._attributes['brightness'] is not None else None for light in lights: """Set color of array of ct light.""" @@ -276,18 +276,23 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): if is_on(self.hass, light): service_data = {ATTR_ENTITY_ID: light} if mired is not None: - service_data[ATTR_COLOR_TEMP] = int(mired) + service_data[ATTR_COLOR_TEMP] = mired if brightness is not None: service_data[ATTR_BRIGHTNESS] = brightness if transition is not None: service_data[ATTR_TRANSITION] = transition - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " CT Adjusted - color_temp: " + str(mired) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) + lightAttrs = self.hass.states.get(light).attributes + if ( (ATTR_COLOR_TEMP in lightAttrs and lightAttrs[ATTR_COLOR_TEMP] == mired) and + (ATTR_BRIGHTNESS in lightAttrs and lightAttrs[ATTR_BRIGHTNESS] == brightness) ): + _LOGGER.debug(light + " already set to the proper values, not adjusting") + else: + self.hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug(light + " CT Adjusted - color_temp: " + str(mired) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) """Set color of array of rgb light.""" if self._lights_rgb is not None and light in self._lights_rgb: - rgb = self.calc_rgb() + rgb = tuple(map(int, self.calc_rgb())) if is_on(self.hass, light): service_data = {ATTR_ENTITY_ID: light} if rgb is not None: @@ -296,25 +301,36 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): service_data[ATTR_BRIGHTNESS] = brightness if transition is not None: service_data[ATTR_TRANSITION] = transition - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " RGB Adjusted - rgb_color: " + str(rgb) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) + lightAttrs = self.hass.states.get(light).attributes + if ( (ATTR_RGB_COLOR in lightAttrs and lightAttrs[ATTR_RGB_COLOR] == rgb) and + (ATTR_BRIGHTNESS in lightAttrs and lightAttrs[ATTR_BRIGHTNESS] == brightness) ): + _LOGGER.debug(light + " already set to the proper values, not adjusting") + else: + self.hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug(light + " RGB Adjusted - rgb_color: " + str(rgb) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) """Set color of array of xy light.""" if self._lights_xy is not None and light in self._lights_xy: - x_val, y_val = self.calc_xy() + xy = self.calc_xy() if is_on(self.hass, light): service_data = {ATTR_ENTITY_ID: light} - if x_val is not None and y_val is not None: - service_data[ATTR_XY_COLOR] = [x_val, y_val] + if xy is not None: + service_data[ATTR_XY_COLOR] = xy if brightness is not None: service_data[ATTR_BRIGHTNESS] = brightness service_data[ATTR_WHITE_VALUE] = brightness if transition is not None: service_data[ATTR_TRANSITION] = transition - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " XY Adjusted - xy_color: [" + str(x_val) + ", " + str(y_val) + "], brightness: " + str(brightness) + ", transition: " + str(transition) + ", white_value: " + str(brightness)) + lightAttrs = self.hass.states.get(light).attributes + if ( (ATTR_XY_COLOR in lightAttrs and lightAttrs[ATTR_XY_COLOR] == xy) and + (ATTR_BRIGHTNESS in lightAttrs and lightAttrs[ATTR_BRIGHTNESS] == brightness) and + (ATTR_WHITE_VALUE in lightAttrs and lightAttrs[ATTR_WHITE_VALUE] == brightness) ): + _LOGGER.debug(light + " already set to the proper values, not adjusting") + else: + self.hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug(light + " XY Adjusted - xy_color: " + str(xy) + ", brightness: " + str(brightness) + ", transition: " + str(transition) + ", white_value: " + str(brightness)) """Set color of array of brightness light.""" if self._lights_brightness is not None and light in self._lights_brightness: @@ -324,9 +340,13 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): service_data[ATTR_BRIGHTNESS] = brightness if transition is not None: service_data[ATTR_TRANSITION] = transition - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " Brightness Adjusted - brightness: " + str(brightness) + ", transition: " + str(transition)) + lightAttrs = self.hass.states.get(light).attributes + if (ATTR_BRIGHTNESS in lightAttrs and lightAttrs[ATTR_BRIGHTNESS] == brightness): + _LOGGER.debug(light + " already set to the proper values, not adjusting") + else: + self.hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug(light + " Brightness Adjusted - brightness: " + str(brightness) + ", transition: " + str(transition)) def light_state_changed(self, entity_id, from_state, to_state): self.adjust_lights([entity_id], 1) From 40568296ed9e26b5a75caba701145e41292eb257 Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Tue, 30 Jul 2019 17:15:46 -0400 Subject: [PATCH 0006/1077] Slight efficiency tweaks Don't calculate values for every light, and only set service_data if it's going to be used --- .../circadian_lighting/switch.py | 94 +++++++++---------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 759eaa91..8264fc49 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -268,12 +268,19 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): transition = self._cl.data['transition'] brightness = int((self._attributes['brightness'] / 100) * 255) if self._attributes['brightness'] is not None else None + mired = int(self.calc_ct()) if self._lights_ct is not None else None + rgb = tuple(map(int, self.calc_rgb())) if self._lights_rgb is not None else None + xy = self.calc_xy() if self._lights_xy is not None else None for light in lights: - """Set color of array of ct light.""" - if self._lights_ct is not None and light in self._lights_ct: - mired = int(self.calc_ct()) - if is_on(self.hass, light): + """Set color of array of ct light if on.""" + if self._lights_ct is not None and light in self._lights_ct and is_on(self.hass, light): + """Check to see if light is already set properly""" + lightAttrs = self.hass.states.get(light).attributes + if ( (ATTR_COLOR_TEMP in lightAttrs and lightAttrs[ATTR_COLOR_TEMP] == mired) and + (ATTR_BRIGHTNESS in lightAttrs and lightAttrs[ATTR_BRIGHTNESS] == brightness) ): + _LOGGER.debug(light + " already set to the proper values, not adjusting") + else: service_data = {ATTR_ENTITY_ID: light} if mired is not None: service_data[ATTR_COLOR_TEMP] = mired @@ -281,19 +288,18 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): service_data[ATTR_BRIGHTNESS] = brightness if transition is not None: service_data[ATTR_TRANSITION] = transition - lightAttrs = self.hass.states.get(light).attributes - if ( (ATTR_COLOR_TEMP in lightAttrs and lightAttrs[ATTR_COLOR_TEMP] == mired) and - (ATTR_BRIGHTNESS in lightAttrs and lightAttrs[ATTR_BRIGHTNESS] == brightness) ): - _LOGGER.debug(light + " already set to the proper values, not adjusting") - else: - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " CT Adjusted - color_temp: " + str(mired) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) + self.hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug(light + " CT Adjusted - color_temp: " + str(mired) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) - """Set color of array of rgb light.""" - if self._lights_rgb is not None and light in self._lights_rgb: - rgb = tuple(map(int, self.calc_rgb())) - if is_on(self.hass, light): + """Set color of array of rgb light if on.""" + if self._lights_rgb is not None and light in self._lights_rgb and is_on(self.hass, light): + """Check to see if light is already set properly""" + lightAttrs = self.hass.states.get(light).attributes + if ( (ATTR_RGB_COLOR in lightAttrs and lightAttrs[ATTR_RGB_COLOR] == rgb) and + (ATTR_BRIGHTNESS in lightAttrs and lightAttrs[ATTR_BRIGHTNESS] == brightness) ): + _LOGGER.debug(light + " already set to the proper values, not adjusting") + else: service_data = {ATTR_ENTITY_ID: light} if rgb is not None: service_data[ATTR_RGB_COLOR] = rgb @@ -301,19 +307,19 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): service_data[ATTR_BRIGHTNESS] = brightness if transition is not None: service_data[ATTR_TRANSITION] = transition - lightAttrs = self.hass.states.get(light).attributes - if ( (ATTR_RGB_COLOR in lightAttrs and lightAttrs[ATTR_RGB_COLOR] == rgb) and - (ATTR_BRIGHTNESS in lightAttrs and lightAttrs[ATTR_BRIGHTNESS] == brightness) ): - _LOGGER.debug(light + " already set to the proper values, not adjusting") - else: - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " RGB Adjusted - rgb_color: " + str(rgb) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) + self.hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug(light + " RGB Adjusted - rgb_color: " + str(rgb) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) - """Set color of array of xy light.""" - if self._lights_xy is not None and light in self._lights_xy: - xy = self.calc_xy() - if is_on(self.hass, light): + """Set color of array of xy light if on.""" + if self._lights_xy is not None and light in self._lights_xy and is_on(self.hass, light): + """Check to see if light is already set properly""" + lightAttrs = self.hass.states.get(light).attributes + if ( (ATTR_XY_COLOR in lightAttrs and lightAttrs[ATTR_XY_COLOR] == xy) and + (ATTR_BRIGHTNESS in lightAttrs and lightAttrs[ATTR_BRIGHTNESS] == brightness) and + (ATTR_WHITE_VALUE in lightAttrs and lightAttrs[ATTR_WHITE_VALUE] == brightness) ): + _LOGGER.debug(light + " already set to the proper values, not adjusting") + else: service_data = {ATTR_ENTITY_ID: light} if xy is not None: service_data[ATTR_XY_COLOR] = xy @@ -322,31 +328,25 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): service_data[ATTR_WHITE_VALUE] = brightness if transition is not None: service_data[ATTR_TRANSITION] = transition - lightAttrs = self.hass.states.get(light).attributes - if ( (ATTR_XY_COLOR in lightAttrs and lightAttrs[ATTR_XY_COLOR] == xy) and - (ATTR_BRIGHTNESS in lightAttrs and lightAttrs[ATTR_BRIGHTNESS] == brightness) and - (ATTR_WHITE_VALUE in lightAttrs and lightAttrs[ATTR_WHITE_VALUE] == brightness) ): - _LOGGER.debug(light + " already set to the proper values, not adjusting") - else: - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " XY Adjusted - xy_color: " + str(xy) + ", brightness: " + str(brightness) + ", transition: " + str(transition) + ", white_value: " + str(brightness)) + self.hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug(light + " XY Adjusted - xy_color: " + str(xy) + ", brightness: " + str(brightness) + ", transition: " + str(transition) + ", white_value: " + str(brightness)) - """Set color of array of brightness light.""" - if self._lights_brightness is not None and light in self._lights_brightness: - if is_on(self.hass, light): + """Set color of array of brightness light if on.""" + if self._lights_brightness is not None and light in self._lights_brightness and is_on(self.hass, light): + """Check to see if light is already set properly""" + lightAttrs = self.hass.states.get(light).attributes + if (ATTR_BRIGHTNESS in lightAttrs and lightAttrs[ATTR_BRIGHTNESS] == brightness): + _LOGGER.debug(light + " already set to the proper values, not adjusting") + else: service_data = {ATTR_ENTITY_ID: light} if brightness is not None: service_data[ATTR_BRIGHTNESS] = brightness if transition is not None: service_data[ATTR_TRANSITION] = transition - lightAttrs = self.hass.states.get(light).attributes - if (ATTR_BRIGHTNESS in lightAttrs and lightAttrs[ATTR_BRIGHTNESS] == brightness): - _LOGGER.debug(light + " already set to the proper values, not adjusting") - else: - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " Brightness Adjusted - brightness: " + str(brightness) + ", transition: " + str(transition)) + self.hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug(light + " Brightness Adjusted - brightness: " + str(brightness) + ", transition: " + str(transition)) def light_state_changed(self, entity_id, from_state, to_state): self.adjust_lights([entity_id], 1) From 14f272e8cd197ecfd347d85abe201d03d616b88b Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Tue, 30 Jul 2019 18:25:02 -0400 Subject: [PATCH 0007/1077] Change brightness scale to max at 254 Supposedly the max light brightness is 255, but my Hue and Lightify lights both max at 254. 0 turns off the light, so the range is probably (0, 255) not [0, 255] --- custom_components/circadian_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 8264fc49..8186d346 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -267,7 +267,7 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): if transition == None: transition = self._cl.data['transition'] - brightness = int((self._attributes['brightness'] / 100) * 255) if self._attributes['brightness'] is not None else None + brightness = int((self._attributes['brightness'] / 100) * 254) if self._attributes['brightness'] is not None else None mired = int(self.calc_ct()) if self._lights_ct is not None else None rgb = tuple(map(int, self.calc_rgb())) if self._lights_rgb is not None else None xy = self.calc_xy() if self._lights_xy is not None else None From 6dcd192f4eaa1814854681a5b589100ba04c895d Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Tue, 30 Jul 2019 18:25:42 -0400 Subject: [PATCH 0008/1077] Bump version number --- custom_components/circadian_lighting/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 6458e57c..4a9cea2e 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -48,7 +48,7 @@ from homeassistant.util.dt import utcnow as dt_utcnow, as_local from datetime import datetime, timedelta -VERSION = '1.0.6' +VERSION = '1.0.7' _LOGGER = logging.getLogger(__name__) From 7185fe3febbf45b1ca2b486c3df27328b837a00e Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Wed, 31 Jul 2019 14:53:34 -0400 Subject: [PATCH 0009/1077] Only react to light state change to 'on' And only when light isn't already on. This should allow for users to disable CL when they adjust a light from the frontend, without CL immediately readjusting. This also should remove the need for checking state of each light before adjusting, because infinite loops should no longer trigger. --- .../circadian_lighting/__init__.py | 2 +- .../circadian_lighting/switch.py | 107 +++++++----------- 2 files changed, 44 insertions(+), 65 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 4a9cea2e..95f19301 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -48,7 +48,7 @@ from homeassistant.util.dt import utcnow as dt_utcnow, as_local from datetime import datetime, timedelta -VERSION = '1.0.7' +VERSION = '1.0.8' _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 8186d346..8c5b25a5 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -275,82 +275,61 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): for light in lights: """Set color of array of ct light if on.""" if self._lights_ct is not None and light in self._lights_ct and is_on(self.hass, light): - """Check to see if light is already set properly""" - lightAttrs = self.hass.states.get(light).attributes - if ( (ATTR_COLOR_TEMP in lightAttrs and lightAttrs[ATTR_COLOR_TEMP] == mired) and - (ATTR_BRIGHTNESS in lightAttrs and lightAttrs[ATTR_BRIGHTNESS] == brightness) ): - _LOGGER.debug(light + " already set to the proper values, not adjusting") - else: - service_data = {ATTR_ENTITY_ID: light} - if mired is not None: - service_data[ATTR_COLOR_TEMP] = mired - if brightness is not None: - service_data[ATTR_BRIGHTNESS] = brightness - if transition is not None: - service_data[ATTR_TRANSITION] = transition - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " CT Adjusted - color_temp: " + str(mired) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) + service_data = {ATTR_ENTITY_ID: light} + if mired is not None: + service_data[ATTR_COLOR_TEMP] = mired + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition + self.hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug(light + " CT Adjusted - color_temp: " + str(mired) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) """Set color of array of rgb light if on.""" if self._lights_rgb is not None and light in self._lights_rgb and is_on(self.hass, light): - """Check to see if light is already set properly""" - lightAttrs = self.hass.states.get(light).attributes - if ( (ATTR_RGB_COLOR in lightAttrs and lightAttrs[ATTR_RGB_COLOR] == rgb) and - (ATTR_BRIGHTNESS in lightAttrs and lightAttrs[ATTR_BRIGHTNESS] == brightness) ): - _LOGGER.debug(light + " already set to the proper values, not adjusting") - else: - service_data = {ATTR_ENTITY_ID: light} - if rgb is not None: - service_data[ATTR_RGB_COLOR] = rgb - if brightness is not None: - service_data[ATTR_BRIGHTNESS] = brightness - if transition is not None: - service_data[ATTR_TRANSITION] = transition - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " RGB Adjusted - rgb_color: " + str(rgb) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) + service_data = {ATTR_ENTITY_ID: light} + if rgb is not None: + service_data[ATTR_RGB_COLOR] = rgb + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition + self.hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug(light + " RGB Adjusted - rgb_color: " + str(rgb) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) """Set color of array of xy light if on.""" if self._lights_xy is not None and light in self._lights_xy and is_on(self.hass, light): - """Check to see if light is already set properly""" - lightAttrs = self.hass.states.get(light).attributes - if ( (ATTR_XY_COLOR in lightAttrs and lightAttrs[ATTR_XY_COLOR] == xy) and - (ATTR_BRIGHTNESS in lightAttrs and lightAttrs[ATTR_BRIGHTNESS] == brightness) and - (ATTR_WHITE_VALUE in lightAttrs and lightAttrs[ATTR_WHITE_VALUE] == brightness) ): - _LOGGER.debug(light + " already set to the proper values, not adjusting") - else: - service_data = {ATTR_ENTITY_ID: light} - if xy is not None: - service_data[ATTR_XY_COLOR] = xy - if brightness is not None: - service_data[ATTR_BRIGHTNESS] = brightness - service_data[ATTR_WHITE_VALUE] = brightness - if transition is not None: - service_data[ATTR_TRANSITION] = transition - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " XY Adjusted - xy_color: " + str(xy) + ", brightness: " + str(brightness) + ", transition: " + str(transition) + ", white_value: " + str(brightness)) + service_data = {ATTR_ENTITY_ID: light} + if xy is not None: + service_data[ATTR_XY_COLOR] = xy + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + service_data[ATTR_WHITE_VALUE] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition + self.hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug(light + " XY Adjusted - xy_color: " + str(xy) + ", brightness: " + str(brightness) + ", transition: " + str(transition) + ", white_value: " + str(brightness)) """Set color of array of brightness light if on.""" if self._lights_brightness is not None and light in self._lights_brightness and is_on(self.hass, light): - """Check to see if light is already set properly""" - lightAttrs = self.hass.states.get(light).attributes - if (ATTR_BRIGHTNESS in lightAttrs and lightAttrs[ATTR_BRIGHTNESS] == brightness): - _LOGGER.debug(light + " already set to the proper values, not adjusting") - else: - service_data = {ATTR_ENTITY_ID: light} - if brightness is not None: - service_data[ATTR_BRIGHTNESS] = brightness - if transition is not None: - service_data[ATTR_TRANSITION] = transition - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " Brightness Adjusted - brightness: " + str(brightness) + ", transition: " + str(transition)) + service_data = {ATTR_ENTITY_ID: light} + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition + self.hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug(light + " Brightness Adjusted - brightness: " + str(brightness) + ", transition: " + str(transition)) def light_state_changed(self, entity_id, from_state, to_state): - self.adjust_lights([entity_id], 1) + _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) + if to_state.state == 'on' and from_state.state != 'on': + self.adjust_lights([entity_id], 1) def sleep_state_changed(self, entity_id, from_state, to_state): + _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) if to_state.state == self._sleep_state or from_state.state == self._sleep_state: self.update_switch(1) \ No newline at end of file From be01be243dd011e5d23f4b92c2a882e77717da9f Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Sun, 4 Aug 2019 14:16:59 -0400 Subject: [PATCH 0010/1077] Define "initial transition" and use it when CL switch is turned on A 1 sec transition was used when a light is turned on or when sleep state changes. The short transition should also be used when the CL switch is turned on. Also, because this is used in multiple spots, better to define it in one spot (this will also make it easier to make it configurable, if desired) --- custom_components/circadian_lighting/__init__.py | 2 +- custom_components/circadian_lighting/switch.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 95f19301..c3f993ba 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -48,7 +48,7 @@ from homeassistant.util.dt import utcnow as dt_utcnow, as_local from datetime import datetime, timedelta -VERSION = '1.0.8' +VERSION = '1.0.9' _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 8c5b25a5..420a3d90 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -45,6 +45,7 @@ CONF_SLEEP_CT = 'sleep_colortemp' CONF_SLEEP_BRIGHT = 'sleep_brightness' CONF_DISABLE_ENTITY = 'disable_entity' CONF_DISABLE_STATE = 'disable_state' +DEFAULT_INITIAL_TRANSITION = 1 PLATFORM_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): 'circadian_lighting', @@ -192,7 +193,7 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): self._state = True # Make initial update - self.update_switch() + self.update_switch(DEFAULT_INITIAL_TRANSITION) self.schedule_update_ha_state() @@ -327,9 +328,9 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): def light_state_changed(self, entity_id, from_state, to_state): _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) if to_state.state == 'on' and from_state.state != 'on': - self.adjust_lights([entity_id], 1) + self.adjust_lights([entity_id], DEFAULT_INITIAL_TRANSITION) def sleep_state_changed(self, entity_id, from_state, to_state): _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) if to_state.state == self._sleep_state or from_state.state == self._sleep_state: - self.update_switch(1) \ No newline at end of file + self.update_switch(DEFAULT_INITIAL_TRANSITION) \ No newline at end of file From abc151601b43c7df213bcf319413dcdba9b29163 Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Sun, 4 Aug 2019 14:49:14 -0400 Subject: [PATCH 0011/1077] Adjust lights immediately when disable entity state changes I'm thinking about removing the disable entity option, but while it remains this is the proper behavior --- custom_components/circadian_lighting/switch.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 420a3d90..9aa6c3a9 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -148,6 +148,8 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): track_state_change(hass, self._lights, self.light_state_changed) if self._sleep_entity is not None: track_state_change(hass, self._sleep_entity, self.sleep_state_changed) + if self._disable_entity is not None: + track_state_change(hass, self._disable_entity, self.disable_state_changed) @property def entity_id(self): @@ -333,4 +335,9 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): def sleep_state_changed(self, entity_id, from_state, to_state): _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) if to_state.state == self._sleep_state or from_state.state == self._sleep_state: + self.update_switch(DEFAULT_INITIAL_TRANSITION) + + def disable_state_changed(self, entity_id, from_state, to_state): + _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) + if from_state.state == self._disable_state: self.update_switch(DEFAULT_INITIAL_TRANSITION) \ No newline at end of file From ca4b4fc7bd9f7eab51dee9d2d5861f3df66b99ff Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Sun, 4 Aug 2019 15:15:33 -0400 Subject: [PATCH 0012/1077] Exemption handling Handles some unexpected state behavior --- .../circadian_lighting/switch.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 9aa6c3a9..6fef883b 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -328,16 +328,25 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): _LOGGER.debug(light + " Brightness Adjusted - brightness: " + str(brightness) + ", transition: " + str(transition)) def light_state_changed(self, entity_id, from_state, to_state): - _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) - if to_state.state == 'on' and from_state.state != 'on': - self.adjust_lights([entity_id], DEFAULT_INITIAL_TRANSITION) + try: + _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) + if to_state.state == 'on' and from_state.state != 'on': + self.adjust_lights([entity_id], DEFAULT_INITIAL_TRANSITION) + except: + pass def sleep_state_changed(self, entity_id, from_state, to_state): - _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) - if to_state.state == self._sleep_state or from_state.state == self._sleep_state: - self.update_switch(DEFAULT_INITIAL_TRANSITION) + try: + _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) + if to_state.state == self._sleep_state or from_state.state == self._sleep_state: + self.update_switch(DEFAULT_INITIAL_TRANSITION) + except: + pass def disable_state_changed(self, entity_id, from_state, to_state): - _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) - if from_state.state == self._disable_state: - self.update_switch(DEFAULT_INITIAL_TRANSITION) \ No newline at end of file + try: + _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) + if from_state.state == self._disable_state: + self.update_switch(DEFAULT_INITIAL_TRANSITION) + except: + pass \ No newline at end of file From 108f13acedf19d727b559db728896ac5320a6980 Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Tue, 20 Aug 2019 19:05:53 -0400 Subject: [PATCH 0013/1077] Bump custom_updater.json to 1.0.9 I'm an idiot and forgot people are still using Custom Updater...doh. --- custom_updater.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_updater.json b/custom_updater.json index 4eda9353..e3cee255 100644 --- a/custom_updater.json +++ b/custom_updater.json @@ -1,7 +1,7 @@ { "circadian_lighting": { - "updated_at": "2019-06-14", - "version": "1.0.6", + "updated_at": "2019-08-17", + "version": "1.0.9", "local_location": "/custom_components/circadian_lighting/__init__.py", "remote_location": "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/__init__.py", "visit_repo": "https://github.com/claytonjn/hass-circadian_lighting", @@ -13,4 +13,4 @@ "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/switch.py" ] } -} \ No newline at end of file +} From c2b0ef0e3b6bda8de251533121c22c0aa96b120e Mon Sep 17 00:00:00 2001 From: "Kevin T. Berstene" Date: Wed, 12 Feb 2020 22:06:08 -0500 Subject: [PATCH 0014/1077] Fixed services to conform to Home Assistant specs --- custom_components/circadian_lighting/services.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/circadian_lighting/services.yaml b/custom_components/circadian_lighting/services.yaml index e4f0b86b..586f06ea 100644 --- a/custom_components/circadian_lighting/services.yaml +++ b/custom_components/circadian_lighting/services.yaml @@ -1,3 +1,2 @@ values_update: description: Updates values for Circadian Lighting. - fields: From db7d0574dd7e4fdad5bd9b9c08db24f85bdddedb Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Wed, 12 Feb 2020 23:04:10 -0500 Subject: [PATCH 0015/1077] Bump version for services hotfix, and fix typo --- README.md | 2 +- custom_components/circadian_lighting/__init__.py | 2 +- custom_updater.json | 4 ++-- info.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4e9dfccc..efb07d56 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![Circadian Light Rhythm|690x287](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/f/5fe7a780e9f8905fea4d1cbb66cdbe35858a6e36.jpg) -Circadian Lighting slowly synchronizes your color changing lights with the regular naturally occuring color temperature of the sky throughout the day. This gives your environment a more natural feel, with cooler hues during the midday and warmer tints near twilight and dawn. +Circadian Lighting slowly synchronizes your color changing lights with the regular naturally occurring color temperature of the sky throughout the day. This gives your environment a more natural feel, with cooler hues during the midday and warmer tints near twilight and dawn. In addition, Circadian Lighting can set your lights to a nice cool white at 1% in “Sleep” mode, which is far brighter than starlight but won’t reset your circadian rhythm or break down too much rhodopsin in your eyes. diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index c3f993ba..1dc0596d 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -48,7 +48,7 @@ from homeassistant.util.dt import utcnow as dt_utcnow, as_local from datetime import datetime, timedelta -VERSION = '1.0.9' +VERSION = '1.0.10b' _LOGGER = logging.getLogger(__name__) diff --git a/custom_updater.json b/custom_updater.json index e3cee255..4c05dae6 100644 --- a/custom_updater.json +++ b/custom_updater.json @@ -1,7 +1,7 @@ { "circadian_lighting": { - "updated_at": "2019-08-17", - "version": "1.0.9", + "updated_at": "2020-02-12", + "version": "1.0.10b", "local_location": "/custom_components/circadian_lighting/__init__.py", "remote_location": "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/__init__.py", "visit_repo": "https://github.com/claytonjn/hass-circadian_lighting", diff --git a/info.md b/info.md index 7c4f799f..564c61b7 100644 --- a/info.md +++ b/info.md @@ -2,6 +2,6 @@ -Circadian Lighting slowly synchronizes your color changing lights with the regular naturally occuring color temperature of the sky throughout the day. This gives your environment a more natural feel, with cooler hues during the midday and warmer tints near twilight and dawn. +Circadian Lighting slowly synchronizes your color changing lights with the regular naturally occurring color temperature of the sky throughout the day. This gives your environment a more natural feel, with cooler hues during the midday and warmer tints near twilight and dawn. In addition, Circadian Lighting can set your lights to a nice cool white at 1% in “Sleep” mode, which is far brighter than starlight but won’t reset your circadian rhythm or break down too much rhodopsin in your eyes. \ No newline at end of file From 05f98fa673b77eb492fadb74d3d657e81ee1841a Mon Sep 17 00:00:00 2001 From: mouth4war <30283635+mouth4war@users.noreply.github.com> Date: Thu, 20 Feb 2020 15:22:07 +0530 Subject: [PATCH 0016/1077] Initial transition configurable and defaulted --- .../circadian_lighting/switch.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 6fef883b..368b9d00 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -45,6 +45,7 @@ CONF_SLEEP_CT = 'sleep_colortemp' CONF_SLEEP_BRIGHT = 'sleep_brightness' CONF_DISABLE_ENTITY = 'disable_entity' CONF_DISABLE_STATE = 'disable_state' +CONF_INITIAL_TRANSITION = 'initial_transition' DEFAULT_INITIAL_TRANSITION = 1 PLATFORM_SCHEMA = vol.Schema({ @@ -66,7 +67,9 @@ PLATFORM_SCHEMA = vol.Schema({ vol.Optional(CONF_SLEEP_BRIGHT): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, - vol.Optional(CONF_DISABLE_STATE): cv.string + vol.Optional(CONF_DISABLE_STATE): cv.string, + vol.Optional(CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION): + vol.All(vol.Coerce(int), vol.Range(min=1, max=1000)) }) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -87,10 +90,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sleep_brightness = config.get(CONF_SLEEP_BRIGHT) disable_entity = config.get(CONF_DISABLE_ENTITY) disable_state = config.get(CONF_DISABLE_STATE) + initial_transition = config.get(CONF_INITIAL_TRANSITION) cs = CircadianSwitch(hass, cl, name, lights_ct, lights_rgb, lights_xy, lights_brightness, disable_brightness_adjust, min_brightness, max_brightness, sleep_entity, sleep_state, sleep_colortemp, sleep_brightness, - disable_entity, disable_state) + disable_entity, disable_state, initial_transition) add_devices([cs]) def update(call=None): @@ -107,7 +111,7 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): def __init__(self, hass, cl, name, lights_ct, lights_rgb, lights_xy, lights_brightness, disable_brightness_adjust, min_brightness, max_brightness, sleep_entity, sleep_state, sleep_colortemp, sleep_brightness, - disable_entity, disable_state): + disable_entity, disable_state, initial_transition): """Initialize the Circadian Lighting switch.""" self.hass = hass self._cl = cl @@ -129,6 +133,7 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): self._sleep_brightness = sleep_brightness self._disable_entity = disable_entity self._disable_state = disable_state + self._initial_transition = initial_transition self._attributes = {} self._attributes['hs_color'] = self._hs_color self._attributes['brightness'] = None @@ -195,7 +200,7 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): self._state = True # Make initial update - self.update_switch(DEFAULT_INITIAL_TRANSITION) + self.update_switch(CONF_INITIAL_TRANSITION) self.schedule_update_ha_state() @@ -331,7 +336,7 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): try: _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) if to_state.state == 'on' and from_state.state != 'on': - self.adjust_lights([entity_id], DEFAULT_INITIAL_TRANSITION) + self.adjust_lights([entity_id], CONF_INITIAL_TRANSITION) except: pass @@ -339,7 +344,7 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): try: _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) if to_state.state == self._sleep_state or from_state.state == self._sleep_state: - self.update_switch(DEFAULT_INITIAL_TRANSITION) + self.update_switch(CONF_INITIAL_TRANSITION) except: pass @@ -347,6 +352,6 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): try: _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) if from_state.state == self._disable_state: - self.update_switch(DEFAULT_INITIAL_TRANSITION) + self.update_switch(CONF_INITIAL_TRANSITION) except: - pass \ No newline at end of file + pass From 61741f65017bcf2b9e8d8ae28db496b46cee9f74 Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Fri, 21 Feb 2020 22:05:08 -0500 Subject: [PATCH 0017/1077] Fix getting initial transition from config --- custom_components/circadian_lighting/switch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 368b9d00..431f944e 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -200,7 +200,7 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): self._state = True # Make initial update - self.update_switch(CONF_INITIAL_TRANSITION) + self.update_switch(self._initial_transition) self.schedule_update_ha_state() @@ -336,7 +336,7 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): try: _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) if to_state.state == 'on' and from_state.state != 'on': - self.adjust_lights([entity_id], CONF_INITIAL_TRANSITION) + self.adjust_lights([entity_id], self._initial_transition) except: pass @@ -344,7 +344,7 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): try: _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) if to_state.state == self._sleep_state or from_state.state == self._sleep_state: - self.update_switch(CONF_INITIAL_TRANSITION) + self.update_switch(self._initial_transition) except: pass @@ -352,6 +352,6 @@ class CircadianSwitch(SwitchDevice, RestoreEntity): try: _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) if from_state.state == self._disable_state: - self.update_switch(CONF_INITIAL_TRANSITION) + self.update_switch(self._initial_transition) except: pass From 6c26f5dbe19aed59c237dfdb187fc41a7ff1e573 Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Fri, 21 Feb 2020 22:16:31 -0500 Subject: [PATCH 0018/1077] Version bump for (deprecated) custom updater --- custom_components/circadian_lighting/__init__.py | 2 +- custom_updater.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 1dc0596d..360819cd 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -48,7 +48,7 @@ from homeassistant.util.dt import utcnow as dt_utcnow, as_local from datetime import datetime, timedelta -VERSION = '1.0.10b' +VERSION = '1.0.11b' _LOGGER = logging.getLogger(__name__) diff --git a/custom_updater.json b/custom_updater.json index 4c05dae6..16b4a1cf 100644 --- a/custom_updater.json +++ b/custom_updater.json @@ -1,7 +1,7 @@ { "circadian_lighting": { - "updated_at": "2020-02-12", - "version": "1.0.10b", + "updated_at": "2020-02-21", + "version": "1.0.11b", "local_location": "/custom_components/circadian_lighting/__init__.py", "remote_location": "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/__init__.py", "visit_repo": "https://github.com/claytonjn/hass-circadian_lighting", From abe47adc8678cacebb470bada623a290c936c2fb Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Sat, 7 Mar 2020 10:16:13 -0500 Subject: [PATCH 0019/1077] Version bump for (deprecated) custom updater --- custom_components/circadian_lighting/__init__.py | 2 +- custom_updater.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 360819cd..62667398 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -48,7 +48,7 @@ from homeassistant.util.dt import utcnow as dt_utcnow, as_local from datetime import datetime, timedelta -VERSION = '1.0.11b' +VERSION = '1.0.11' _LOGGER = logging.getLogger(__name__) diff --git a/custom_updater.json b/custom_updater.json index 16b4a1cf..867cd59d 100644 --- a/custom_updater.json +++ b/custom_updater.json @@ -1,7 +1,7 @@ { "circadian_lighting": { - "updated_at": "2020-02-21", - "version": "1.0.11b", + "updated_at": "2020-03-07", + "version": "1.0.11", "local_location": "/custom_components/circadian_lighting/__init__.py", "remote_location": "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/__init__.py", "visit_repo": "https://github.com/claytonjn/hass-circadian_lighting", From 3fb9fcc9c350e54249f27a958c7bfd8da82fe9bb Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Sat, 7 Mar 2020 10:31:52 -0500 Subject: [PATCH 0020/1077] Use HA defined VALID_TRANSITION for initial transition rather than defining our own --- custom_components/circadian_lighting/switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 431f944e..f8da28da 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -16,7 +16,7 @@ from homeassistant.helpers.event import track_state_change from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.components.light import ( is_on, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN) + VALID_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN) from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( ATTR_ENTITY_ID, CONF_NAME, CONF_PLATFORM, STATE_ON, @@ -69,7 +69,7 @@ PLATFORM_SCHEMA = vol.Schema({ vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, vol.Optional(CONF_DISABLE_STATE): cv.string, vol.Optional(CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION): - vol.All(vol.Coerce(int), vol.Range(min=1, max=1000)) + VALID_TRANSITION }) def setup_platform(hass, config, add_devices, discovery_info=None): From 9e1d140e191f7f8fdc4ea8e77ab6cd8c845eedc1 Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Thu, 19 Mar 2020 15:48:52 -0400 Subject: [PATCH 0021/1077] Use the same timezone for "now" time and configured lat/long --- .../circadian_lighting/__init__.py | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 360819cd..a2d60951 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -44,9 +44,9 @@ from homeassistant.helpers.event import track_sunrise, track_sunset, track_time_ from homeassistant.util.color import ( color_temperature_to_rgb, color_RGB_to_xy, color_xy_to_hs) -from homeassistant.util.dt import utcnow as dt_utcnow, as_local +from homeassistant.util.dt import now as dt_now -from datetime import datetime, timedelta +from datetime import timedelta VERSION = '1.0.11b' @@ -135,6 +135,7 @@ class CircadianLighting(object): self.data['elevation'] = elevation self.data['interval'] = interval self.data['transition'] = transition + self.data['timezone'] = self.get_timezone() self.data['percent'] = self.calc_percent() self.data['colortemp'] = self.calc_colortemp() self.data['rgb_color'] = self.calc_rgb() @@ -152,35 +153,41 @@ class CircadianLighting(object): else: track_sunset(self.hass, self._update, self.data['sunset_offset']) + def get_astral_location(self): + import astral + location = astral.Location() + location.name = 'name' + location.region = 'region' + location.latitude = self.data['latitude'] + location.longitude = self.data['longitude'] + location.elevation = self.data['elevation'] + _LOGGER.debug("Astral location: " + str(location)) + return location + + def get_timezone(self): + timezone = self.get_astral_location().tz + _LOGGER.debug("Timezone: " + str(timezone)) + return timezone + def get_sunrise_sunset(self, date = None): if self.data['sunrise_time'] is not None and self.data['sunset_time'] is not None: if date is None: - utcdate = dt_utcnow() - date = as_local(utcdate) + date = dt_now(self.data['timezone']) sunrise = date.replace(hour=int(self.data['sunrise_time'].strftime("%H")), minute=int(self.data['sunrise_time'].strftime("%M")), second=int(self.data['sunrise_time'].strftime("%S")), microsecond=int(self.data['sunrise_time'].strftime("%f"))) sunset = date.replace(hour=int(self.data['sunset_time'].strftime("%H")), minute=int(self.data['sunset_time'].strftime("%M")), second=int(self.data['sunset_time'].strftime("%S")), microsecond=int(self.data['sunset_time'].strftime("%f"))) solar_noon = sunrise + (sunset - sunrise)/2 solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset)/2 else: - import astral - location = astral.Location() - location.name = 'name' - location.region = 'region' - location.latitude = self.data['latitude'] - location.longitude = self.data['longitude'] - location.elevation = self.data['elevation'] - _LOGGER.debug("Astral location: " + str(location)) + location = self.get_astral_location() if self.data['sunrise_time'] is not None: if date is None: - utcdate = dt_utcnow() - date = as_local(utcdate) + date = dt_now(self.data['timezone']) sunrise = date.replace(hour=int(self.data['sunrise_time'].strftime("%H")), minute=int(self.data['sunrise_time'].strftime("%M")), second=int(self.data['sunrise_time'].strftime("%S")), microsecond=int(self.data['sunrise_time'].strftime("%f"))) else: sunrise = location.sunrise(date) if self.data['sunset_time'] is not None: if date is None: - utcdate = dt_utcnow() - date = as_local(utcdate) + date = dt_now(self.data['timezone']) sunset = date.replace(hour=int(self.data['sunset_time'].strftime("%H")), minute=int(self.data['sunset_time'].strftime("%M")), second=int(self.data['sunset_time'].strftime("%S")), microsecond=int(self.data['sunset_time'].strftime("%f"))) else: sunset = location.sunset(date) @@ -198,8 +205,7 @@ class CircadianLighting(object): } def calc_percent(self): - utcnow = dt_utcnow() - now = as_local(utcnow) + now = dt_now(self.data['timezone']) _LOGGER.debug("now: " + str(now)) today_sun_times = self.get_sunrise_sunset(now) From 03718f0e3c9bd8b34cacae9de3e3515f45f6338a Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Thu, 19 Mar 2020 15:49:30 -0400 Subject: [PATCH 0022/1077] Version bump for (deprecated) custom updater --- custom_components/circadian_lighting/__init__.py | 2 +- custom_updater.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index a2d60951..68486800 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -48,7 +48,7 @@ from homeassistant.util.dt import now as dt_now from datetime import timedelta -VERSION = '1.0.11b' +VERSION = '1.0.12b' _LOGGER = logging.getLogger(__name__) diff --git a/custom_updater.json b/custom_updater.json index 16b4a1cf..91225bd0 100644 --- a/custom_updater.json +++ b/custom_updater.json @@ -1,7 +1,7 @@ { "circadian_lighting": { - "updated_at": "2020-02-21", - "version": "1.0.11b", + "updated_at": "2020-03-19", + "version": "1.0.12b", "local_location": "/custom_components/circadian_lighting/__init__.py", "remote_location": "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/__init__.py", "visit_repo": "https://github.com/claytonjn/hass-circadian_lighting", From 498da0e0db45886a5f5eba6ad72300f61df58efa Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Thu, 19 Mar 2020 17:07:27 -0400 Subject: [PATCH 0023/1077] Use actual timezone to fix configured sunrise/sunset --- .../circadian_lighting/__init__.py | 37 +++++++++---------- .../circadian_lighting/manifest.json | 2 +- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 68486800..c6fcecfc 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -44,9 +44,9 @@ from homeassistant.helpers.event import track_sunrise, track_sunset, track_time_ from homeassistant.util.color import ( color_temperature_to_rgb, color_RGB_to_xy, color_xy_to_hs) -from homeassistant.util.dt import now as dt_now +from homeassistant.util.dt import now as dt_now, get_time_zone -from datetime import timedelta +from datetime import datetime, timedelta VERSION = '1.0.12b' @@ -153,19 +153,11 @@ class CircadianLighting(object): else: track_sunset(self.hass, self._update, self.data['sunset_offset']) - def get_astral_location(self): - import astral - location = astral.Location() - location.name = 'name' - location.region = 'region' - location.latitude = self.data['latitude'] - location.longitude = self.data['longitude'] - location.elevation = self.data['elevation'] - _LOGGER.debug("Astral location: " + str(location)) - return location - def get_timezone(self): - timezone = self.get_astral_location().tz + from timezonefinder import TimezoneFinder + tf = TimezoneFinder() + timezone_string = tf.timezone_at(lng=self.data['longitude'], lat=self.data['latitude']) + timezone = get_time_zone(timezone_string) _LOGGER.debug("Timezone: " + str(timezone)) return timezone @@ -178,7 +170,14 @@ class CircadianLighting(object): solar_noon = sunrise + (sunset - sunrise)/2 solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset)/2 else: - location = self.get_astral_location() + import astral + location = astral.Location() + location.name = 'name' + location.region = 'region' + location.latitude = self.data['latitude'] + location.longitude = self.data['longitude'] + location.elevation = self.data['elevation'] + _LOGGER.debug("Astral location: " + str(location)) if self.data['sunrise_time'] is not None: if date is None: date = dt_now(self.data['timezone']) @@ -198,10 +197,10 @@ class CircadianLighting(object): if self.data['sunset_offset'] is not None: sunset = sunset + self.data['sunset_offset'] return { - SUN_EVENT_SUNRISE: sunrise, - SUN_EVENT_SUNSET: sunset, - 'solar_noon': solar_noon, - 'solar_midnight': solar_midnight + SUN_EVENT_SUNRISE: sunrise.astimezone(self.data['timezone']), + SUN_EVENT_SUNSET: sunset.astimezone(self.data['timezone']), + 'solar_noon': solar_noon.astimezone(self.data['timezone']), + 'solar_midnight': solar_midnight.astimezone(self.data['timezone']) } def calc_percent(self): diff --git a/custom_components/circadian_lighting/manifest.json b/custom_components/circadian_lighting/manifest.json index b45b39e2..4832a25b 100644 --- a/custom_components/circadian_lighting/manifest.json +++ b/custom_components/circadian_lighting/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://github.com/claytonjn/hass-circadian_lighting", "dependencies": [], "codeowners": ["@claytonjn"], - "requirements": [] + "requirements": ["timezonefinder==4.2.0"] } From eaa5c0e3dd909941bb7f82104c93f7eaf678b184 Mon Sep 17 00:00:00 2001 From: LJU Date: Thu, 14 May 2020 08:26:29 +0200 Subject: [PATCH 0024/1077] Update SwitchDevice to SwitchEntity --- custom_components/circadian_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index f8da28da..2ac2fc9c 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -17,7 +17,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.components.light import ( is_on, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, VALID_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN) -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( ATTR_ENTITY_ID, CONF_NAME, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON) From d7c27cd107d745fef74897fbc1f99745372338e4 Mon Sep 17 00:00:00 2001 From: LJU Date: Thu, 14 May 2020 08:28:41 +0200 Subject: [PATCH 0025/1077] Update SwitchDevice to SwitchEntity --- custom_components/circadian_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 2ac2fc9c..e85f5041 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -105,7 +105,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False -class CircadianSwitch(SwitchDevice, RestoreEntity): +class CircadianSwitch(SwitchEntity, RestoreEntity): """Representation of a Circadian Lighting switch.""" def __init__(self, hass, cl, name, lights_ct, lights_rgb, lights_xy, lights_brightness, From 5f3700343b6953847de12bdde6c54ea777400c9d Mon Sep 17 00:00:00 2001 From: LJU Date: Thu, 21 May 2020 15:33:49 +0200 Subject: [PATCH 0026/1077] Update import Update import and add switchdevice for earlier versions --- custom_components/circadian_lighting/switch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index e85f5041..332c91e1 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -17,6 +17,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.components.light import ( is_on, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, VALID_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN) +from homeassistant.components.switch import SwitchDevice from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( ATTR_ENTITY_ID, CONF_NAME, CONF_PLATFORM, STATE_ON, @@ -105,7 +106,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False -class CircadianSwitch(SwitchEntity, RestoreEntity): +class CircadianSwitch(SwitchEntity, SwitchDevice, RestoreEntity): """Representation of a Circadian Lighting switch.""" def __init__(self, hass, cl, name, lights_ct, lights_rgb, lights_xy, lights_brightness, From 836e2922a6c66db0b290a9a76f10a6be28913b9f Mon Sep 17 00:00:00 2001 From: LJU Date: Fri, 22 May 2020 23:36:16 +0200 Subject: [PATCH 0027/1077] Update with try --- custom_components/circadian_lighting/switch.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 332c91e1..3e00037e 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -16,9 +16,13 @@ from homeassistant.helpers.event import track_state_change from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.components.light import ( is_on, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - VALID_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN) -from homeassistant.components.switch import SwitchDevice -from homeassistant.components.switch import SwitchEntity + VALID_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN + +try: + from homeassistant.components.switch import SwitchEntity +except ImportError: + from homeassistant.components.switch import SwitchDevice as SwitchEntity + from homeassistant.const import ( ATTR_ENTITY_ID, CONF_NAME, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON) @@ -106,7 +110,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False -class CircadianSwitch(SwitchEntity, SwitchDevice, RestoreEntity): +class CircadianSwitch(SwitchEntity, RestoreEntity): """Representation of a Circadian Lighting switch.""" def __init__(self, hass, cl, name, lights_ct, lights_rgb, lights_xy, lights_brightness, From a2a1c4574454497e2f7852e87ad299fa43e70b31 Mon Sep 17 00:00:00 2001 From: LJU Date: Fri, 22 May 2020 23:36:45 +0200 Subject: [PATCH 0028/1077] Update switch.py --- custom_components/circadian_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 3e00037e..3b58c1c5 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -16,7 +16,7 @@ from homeassistant.helpers.event import track_state_change from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.components.light import ( is_on, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - VALID_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN + VALID_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN) try: from homeassistant.components.switch import SwitchEntity From ff4854e7b72db62252b10a773163588299e06cdc Mon Sep 17 00:00:00 2001 From: Clayton Nummer Date: Thu, 11 Jun 2020 13:09:31 -0400 Subject: [PATCH 0029/1077] Version bump for master --- custom_components/circadian_lighting/__init__.py | 2 +- custom_updater.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 219c7333..3acf0473 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -48,7 +48,7 @@ from homeassistant.util.dt import now as dt_now, get_time_zone from datetime import datetime, timedelta -VERSION = '1.0.13b' +VERSION = '1.0.13' _LOGGER = logging.getLogger(__name__) diff --git a/custom_updater.json b/custom_updater.json index a3c454e0..73a04bb6 100644 --- a/custom_updater.json +++ b/custom_updater.json @@ -1,7 +1,7 @@ { "circadian_lighting": { - "updated_at": "2020-06-03", - "version": "1.0.13b", + "updated_at": "2020-06-11", + "version": "1.0.13", "local_location": "/custom_components/circadian_lighting/__init__.py", "remote_location": "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/__init__.py", "visit_repo": "https://github.com/claytonjn/hass-circadian_lighting", From 9b8a4ad51f991730b675eb4d40f934d0bea100f8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:08:33 +0200 Subject: [PATCH 0030/1077] run black, pyupgrade, and isort --- .../circadian_lighting/__init__.py | 336 +++++++++++------- .../circadian_lighting/sensor.py | 44 ++- .../circadian_lighting/switch.py | 331 +++++++++++------ 3 files changed, 470 insertions(+), 241 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 3acf0473..79d8de1d 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -28,64 +28,78 @@ Technical notes: I had to make a lot of assumptions when writing this app """ import logging - -import voluptuous as vol +from datetime import datetime, timedelta import homeassistant.helpers.config_validation as cv -from homeassistant.components.light import ( - VALID_TRANSITION, ATTR_TRANSITION) +import voluptuous as vol +from homeassistant.components.light import ATTR_TRANSITION, VALID_TRANSITION from homeassistant.const import ( - CONF_LATITUDE, CONF_LONGITUDE, CONF_ELEVATION, - SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) -from homeassistant.util import Throttle + CONF_ELEVATION, + CONF_LATITUDE, + CONF_LONGITUDE, + SUN_EVENT_SUNRISE, + SUN_EVENT_SUNSET, +) from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import track_sunrise, track_sunset, track_time_change +from homeassistant.util import Throttle from homeassistant.util.color import ( - color_temperature_to_rgb, color_RGB_to_xy, - color_xy_to_hs) -from homeassistant.util.dt import now as dt_now, get_time_zone + color_RGB_to_xy, + color_temperature_to_rgb, + color_xy_to_hs, +) +from homeassistant.util.dt import get_time_zone +from homeassistant.util.dt import now as dt_now -from datetime import datetime, timedelta - -VERSION = '1.0.13' +VERSION = "1.0.13" _LOGGER = logging.getLogger(__name__) -DOMAIN = 'circadian_lighting' -CIRCADIAN_LIGHTING_PLATFORMS = ['sensor', 'switch'] -CIRCADIAN_LIGHTING_UPDATE_TOPIC = '{0}_update'.format(DOMAIN) -DATA_CIRCADIAN_LIGHTING = 'data_cl' +DOMAIN = "circadian_lighting" +CIRCADIAN_LIGHTING_PLATFORMS = ["sensor", "switch"] +CIRCADIAN_LIGHTING_UPDATE_TOPIC = "{}_update".format(DOMAIN) +DATA_CIRCADIAN_LIGHTING = "data_cl" -CONF_MIN_CT = 'min_colortemp' +CONF_MIN_CT = "min_colortemp" DEFAULT_MIN_CT = 2500 -CONF_MAX_CT = 'max_colortemp' +CONF_MAX_CT = "max_colortemp" DEFAULT_MAX_CT = 5500 -CONF_SUNRISE_OFFSET = 'sunrise_offset' -CONF_SUNSET_OFFSET = 'sunset_offset' -CONF_SUNRISE_TIME = 'sunrise_time' -CONF_SUNSET_TIME = 'sunset_time' -CONF_INTERVAL = 'interval' +CONF_SUNRISE_OFFSET = "sunrise_offset" +CONF_SUNSET_OFFSET = "sunset_offset" +CONF_SUNRISE_TIME = "sunrise_time" +CONF_SUNSET_TIME = "sunset_time" +CONF_INTERVAL = "interval" DEFAULT_INTERVAL = 300 DEFAULT_TRANSITION = 60 -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): - vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): - vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_SUNRISE_OFFSET): cv.time_period_str, - vol.Optional(CONF_SUNSET_OFFSET): cv.time_period_str, - vol.Optional(CONF_SUNRISE_TIME): cv.time, - vol.Optional(CONF_SUNSET_TIME): cv.time, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_ELEVATION): float, - vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.positive_int, - vol.Optional(ATTR_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION - }), -}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_SUNRISE_OFFSET): cv.time_period_str, + vol.Optional(CONF_SUNSET_OFFSET): cv.time_period_str, + vol.Optional(CONF_SUNRISE_TIME): cv.time, + vol.Optional(CONF_SUNSET_TIME): cv.time, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_ELEVATION): float, + vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.positive_int, + vol.Optional( + ATTR_TRANSITION, default=DEFAULT_TRANSITION + ): VALID_TRANSITION, + } + ), + }, + extra=vol.ALLOW_EXTRA, +) + def setup(hass, config): """Set up the Circadian Lighting component.""" @@ -101,110 +115,171 @@ def setup(hass, config): longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) elevation = conf.get(CONF_ELEVATION, hass.config.elevation) - load_platform(hass, 'sensor', DOMAIN, {}, config) + load_platform(hass, "sensor", DOMAIN, {}, config) interval = conf.get(CONF_INTERVAL) transition = conf.get(ATTR_TRANSITION) - cl = CircadianLighting(hass, min_colortemp, max_colortemp, - sunrise_offset, sunset_offset, sunrise_time, sunset_time, - latitude, longitude, elevation, - interval, transition) + cl = CircadianLighting( + hass, + min_colortemp, + max_colortemp, + sunrise_offset, + sunset_offset, + sunrise_time, + sunset_time, + latitude, + longitude, + elevation, + interval, + transition, + ) hass.data[DATA_CIRCADIAN_LIGHTING] = cl return True + class CircadianLighting(object): """Calculate universal Circadian values.""" - def __init__(self, hass, min_colortemp, max_colortemp, - sunrise_offset, sunset_offset, sunrise_time, sunset_time, - latitude, longitude, elevation, - interval, transition): + def __init__( + self, + hass, + min_colortemp, + max_colortemp, + sunrise_offset, + sunset_offset, + sunrise_time, + sunset_time, + latitude, + longitude, + elevation, + interval, + transition, + ): self.hass = hass self.data = {} - self.data['min_colortemp'] = min_colortemp - self.data['max_colortemp'] = max_colortemp - self.data['sunrise_offset'] = sunrise_offset - self.data['sunset_offset'] = sunset_offset - self.data['sunrise_time'] = sunrise_time - self.data['sunset_time'] = sunset_time - self.data['latitude'] = latitude - self.data['longitude'] = longitude - self.data['elevation'] = elevation - self.data['interval'] = interval - self.data['transition'] = transition - self.data['timezone'] = self.get_timezone() - self.data['percent'] = self.calc_percent() - self.data['colortemp'] = self.calc_colortemp() - self.data['rgb_color'] = self.calc_rgb() - self.data['xy_color'] = self.calc_xy() - self.data['hs_color'] = self.calc_hs() + self.data["min_colortemp"] = min_colortemp + self.data["max_colortemp"] = max_colortemp + self.data["sunrise_offset"] = sunrise_offset + self.data["sunset_offset"] = sunset_offset + self.data["sunrise_time"] = sunrise_time + self.data["sunset_time"] = sunset_time + self.data["latitude"] = latitude + self.data["longitude"] = longitude + self.data["elevation"] = elevation + self.data["interval"] = interval + self.data["transition"] = transition + self.data["timezone"] = self.get_timezone() + self.data["percent"] = self.calc_percent() + self.data["colortemp"] = self.calc_colortemp() + self.data["rgb_color"] = self.calc_rgb() + self.data["xy_color"] = self.calc_xy() + self.data["hs_color"] = self.calc_hs() self.update = Throttle(timedelta(seconds=interval))(self._update) - if self.data['sunrise_time'] is not None: - track_time_change(self.hass, self._update, hour=int(self.data['sunrise_time'].strftime("%H")), minute=int(self.data['sunrise_time'].strftime("%M")), second=int(self.data['sunrise_time'].strftime("%S"))) + if self.data["sunrise_time"] is not None: + track_time_change( + self.hass, + self._update, + hour=int(self.data["sunrise_time"].strftime("%H")), + minute=int(self.data["sunrise_time"].strftime("%M")), + second=int(self.data["sunrise_time"].strftime("%S")), + ) else: - track_sunrise(self.hass, self._update, self.data['sunrise_offset']) - if self.data['sunset_time'] is not None: - track_time_change(self.hass, self._update, hour=int(self.data['sunset_time'].strftime("%H")), minute=int(self.data['sunset_time'].strftime("%M")), second=int(self.data['sunset_time'].strftime("%S"))) + track_sunrise(self.hass, self._update, self.data["sunrise_offset"]) + if self.data["sunset_time"] is not None: + track_time_change( + self.hass, + self._update, + hour=int(self.data["sunset_time"].strftime("%H")), + minute=int(self.data["sunset_time"].strftime("%M")), + second=int(self.data["sunset_time"].strftime("%S")), + ) else: - track_sunset(self.hass, self._update, self.data['sunset_offset']) + track_sunset(self.hass, self._update, self.data["sunset_offset"]) def get_timezone(self): from timezonefinder import TimezoneFinder + tf = TimezoneFinder() - timezone_string = tf.timezone_at(lng=self.data['longitude'], lat=self.data['latitude']) + timezone_string = tf.timezone_at( + lng=self.data["longitude"], lat=self.data["latitude"] + ) timezone = get_time_zone(timezone_string) _LOGGER.debug("Timezone: " + str(timezone)) return timezone - - def get_sunrise_sunset(self, date = None): - if self.data['sunrise_time'] is not None and self.data['sunset_time'] is not None: + + def get_sunrise_sunset(self, date=None): + if ( + self.data["sunrise_time"] is not None + and self.data["sunset_time"] is not None + ): if date is None: - date = dt_now(self.data['timezone']) - sunrise = date.replace(hour=int(self.data['sunrise_time'].strftime("%H")), minute=int(self.data['sunrise_time'].strftime("%M")), second=int(self.data['sunrise_time'].strftime("%S")), microsecond=int(self.data['sunrise_time'].strftime("%f"))) - sunset = date.replace(hour=int(self.data['sunset_time'].strftime("%H")), minute=int(self.data['sunset_time'].strftime("%M")), second=int(self.data['sunset_time'].strftime("%S")), microsecond=int(self.data['sunset_time'].strftime("%f"))) - solar_noon = sunrise + (sunset - sunrise)/2 - solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset)/2 + date = dt_now(self.data["timezone"]) + sunrise = date.replace( + hour=int(self.data["sunrise_time"].strftime("%H")), + minute=int(self.data["sunrise_time"].strftime("%M")), + second=int(self.data["sunrise_time"].strftime("%S")), + microsecond=int(self.data["sunrise_time"].strftime("%f")), + ) + sunset = date.replace( + hour=int(self.data["sunset_time"].strftime("%H")), + minute=int(self.data["sunset_time"].strftime("%M")), + second=int(self.data["sunset_time"].strftime("%S")), + microsecond=int(self.data["sunset_time"].strftime("%f")), + ) + solar_noon = sunrise + (sunset - sunrise) / 2 + solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset) / 2 else: import astral + location = astral.Location() - location.name = 'name' - location.region = 'region' - location.latitude = self.data['latitude'] - location.longitude = self.data['longitude'] - location.elevation = self.data['elevation'] + location.name = "name" + location.region = "region" + location.latitude = self.data["latitude"] + location.longitude = self.data["longitude"] + location.elevation = self.data["elevation"] _LOGGER.debug("Astral location: " + str(location)) - if self.data['sunrise_time'] is not None: + if self.data["sunrise_time"] is not None: if date is None: - date = dt_now(self.data['timezone']) - sunrise = date.replace(hour=int(self.data['sunrise_time'].strftime("%H")), minute=int(self.data['sunrise_time'].strftime("%M")), second=int(self.data['sunrise_time'].strftime("%S")), microsecond=int(self.data['sunrise_time'].strftime("%f"))) + date = dt_now(self.data["timezone"]) + sunrise = date.replace( + hour=int(self.data["sunrise_time"].strftime("%H")), + minute=int(self.data["sunrise_time"].strftime("%M")), + second=int(self.data["sunrise_time"].strftime("%S")), + microsecond=int(self.data["sunrise_time"].strftime("%f")), + ) else: sunrise = location.sunrise(date) - if self.data['sunset_time'] is not None: + if self.data["sunset_time"] is not None: if date is None: - date = dt_now(self.data['timezone']) - sunset = date.replace(hour=int(self.data['sunset_time'].strftime("%H")), minute=int(self.data['sunset_time'].strftime("%M")), second=int(self.data['sunset_time'].strftime("%S")), microsecond=int(self.data['sunset_time'].strftime("%f"))) + date = dt_now(self.data["timezone"]) + sunset = date.replace( + hour=int(self.data["sunset_time"].strftime("%H")), + minute=int(self.data["sunset_time"].strftime("%M")), + second=int(self.data["sunset_time"].strftime("%S")), + microsecond=int(self.data["sunset_time"].strftime("%f")), + ) else: sunset = location.sunset(date) solar_noon = location.solar_noon(date) solar_midnight = location.solar_midnight(date) - if self.data['sunrise_offset'] is not None: - sunrise = sunrise + self.data['sunrise_offset'] - if self.data['sunset_offset'] is not None: - sunset = sunset + self.data['sunset_offset'] + if self.data["sunrise_offset"] is not None: + sunrise = sunrise + self.data["sunrise_offset"] + if self.data["sunset_offset"] is not None: + sunset = sunset + self.data["sunset_offset"] return { - SUN_EVENT_SUNRISE: sunrise.astimezone(self.data['timezone']), - SUN_EVENT_SUNSET: sunset.astimezone(self.data['timezone']), - 'solar_noon': solar_noon.astimezone(self.data['timezone']), - 'solar_midnight': solar_midnight.astimezone(self.data['timezone']) + SUN_EVENT_SUNRISE: sunrise.astimezone(self.data["timezone"]), + SUN_EVENT_SUNSET: sunset.astimezone(self.data["timezone"]), + "solar_noon": solar_noon.astimezone(self.data["timezone"]), + "solar_midnight": solar_midnight.astimezone(self.data["timezone"]), } def calc_percent(self): - now = dt_now(self.data['timezone']) + now = dt_now(self.data["timezone"]) _LOGGER.debug("now: " + str(now)) today_sun_times = self.get_sunrise_sunset(now) @@ -214,25 +289,41 @@ class CircadianLighting(object): now_seconds = now.timestamp() sunrise_seconds = today_sun_times[SUN_EVENT_SUNRISE].timestamp() sunset_seconds = today_sun_times[SUN_EVENT_SUNSET].timestamp() - solar_noon_seconds = today_sun_times['solar_noon'].timestamp() - solar_midnight_seconds = today_sun_times['solar_midnight'].timestamp() + solar_noon_seconds = today_sun_times["solar_noon"].timestamp() + solar_midnight_seconds = today_sun_times["solar_midnight"].timestamp() - if now < today_sun_times[SUN_EVENT_SUNRISE]: # It's before sunrise (after midnight) + if ( + now < today_sun_times[SUN_EVENT_SUNRISE] + ): # It's before sunrise (after midnight) # Because it's before sunrise (and after midnight) sunset must have happend yesterday yesterday_sun_times = self.get_sunrise_sunset(now - timedelta(days=1)) _LOGGER.debug("yesterday_sun_times: " + str(yesterday_sun_times)) sunset_seconds = yesterday_sun_times[SUN_EVENT_SUNSET].timestamp() - if today_sun_times['solar_midnight'] > today_sun_times[SUN_EVENT_SUNSET] and yesterday_sun_times['solar_midnight'] > yesterday_sun_times[SUN_EVENT_SUNSET]: + if ( + today_sun_times["solar_midnight"] > today_sun_times[SUN_EVENT_SUNSET] + and yesterday_sun_times["solar_midnight"] + > yesterday_sun_times[SUN_EVENT_SUNSET] + ): # Solar midnight is after sunset so use yesterdays's time - solar_midnight_seconds = yesterday_sun_times['solar_midnight'].timestamp() - elif now > today_sun_times[SUN_EVENT_SUNSET]: # It's after sunset (before midnight) + solar_midnight_seconds = yesterday_sun_times[ + "solar_midnight" + ].timestamp() + elif ( + now > today_sun_times[SUN_EVENT_SUNSET] + ): # It's after sunset (before midnight) # Because it's after sunset (and before midnight) sunrise should happen tomorrow tomorrow_sun_times = self.get_sunrise_sunset(now + timedelta(days=1)) _LOGGER.debug("tomorrow_sun_times: " + str(tomorrow_sun_times)) sunrise_seconds = tomorrow_sun_times[SUN_EVENT_SUNRISE].timestamp() - if today_sun_times['solar_midnight'] < today_sun_times[SUN_EVENT_SUNRISE] and tomorrow_sun_times['solar_midnight'] < tomorrow_sun_times[SUN_EVENT_SUNRISE]: + if ( + today_sun_times["solar_midnight"] < today_sun_times[SUN_EVENT_SUNRISE] + and tomorrow_sun_times["solar_midnight"] + < tomorrow_sun_times[SUN_EVENT_SUNRISE] + ): # Solar midnight is before sunrise so use tomorrow's time - solar_midnight_seconds = tomorrow_sun_times['solar_midnight'].timestamp() + solar_midnight_seconds = tomorrow_sun_times[ + "solar_midnight" + ].timestamp() _LOGGER.debug("now_seconds: " + str(now_seconds)) _LOGGER.debug("sunrise_seconds: " + str(sunrise_seconds)) @@ -269,8 +360,8 @@ class CircadianLighting(object): x = sunrise_seconds y = 0 - a = (y-k)/(h-x)**2 - percentage = a*(now_seconds-h)**2+k + a = (y - k) / (h - x) ** 2 + percentage = a * (now_seconds - h) ** 2 + k _LOGGER.debug("h: " + str(h)) _LOGGER.debug("k: " + str(k)) @@ -282,13 +373,16 @@ class CircadianLighting(object): return percentage def calc_colortemp(self): - if self.data['percent'] > 0: - return ((self.data['max_colortemp'] - self.data['min_colortemp']) * (self.data['percent'] / 100)) + self.data['min_colortemp'] + if self.data["percent"] > 0: + return ( + (self.data["max_colortemp"] - self.data["min_colortemp"]) + * (self.data["percent"] / 100) + ) + self.data["min_colortemp"] else: - return self.data['min_colortemp'] + return self.data["min_colortemp"] def calc_rgb(self): - return color_temperature_to_rgb(self.data['colortemp']) + return color_temperature_to_rgb(self.data["colortemp"]) def calc_xy(self): rgb = self.calc_rgb() @@ -307,10 +401,10 @@ class CircadianLighting(object): def _update(self, *args, **kwargs): """Update Circadian Values.""" - self.data['percent'] = self.calc_percent() - self.data['colortemp'] = self.calc_colortemp() - self.data['rgb_color'] = self.calc_rgb() - self.data['xy_color'] = self.calc_xy() - self.data['hs_color'] = self.calc_hs() + self.data["percent"] = self.calc_percent() + self.data["colortemp"] = self.calc_colortemp() + self.data["rgb_color"] = self.calc_rgb() + self.data["xy_color"] = self.calc_xy() + self.data["hs_color"] = self.calc_hs() dispatcher_send(self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC) _LOGGER.debug("Circadian Lighting Component Updated") diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py index 756d5799..dc6cd738 100644 --- a/custom_components/circadian_lighting/sensor.py +++ b/custom_components/circadian_lighting/sensor.py @@ -2,20 +2,24 @@ Circadian Lighting Sensor for Home-Assistant. """ -DEPENDENCIES = ['circadian_lighting'] +DEPENDENCIES = ["circadian_lighting"] +import datetime import logging -from custom_components.circadian_lighting import DOMAIN, CIRCADIAN_LIGHTING_UPDATE_TOPIC, DATA_CIRCADIAN_LIGHTING - from homeassistant.helpers.dispatcher import dispatcher_connect from homeassistant.helpers.entity import Entity -import datetime +from custom_components.circadian_lighting import ( + CIRCADIAN_LIGHTING_UPDATE_TOPIC, + DATA_CIRCADIAN_LIGHTING, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) -ICON = 'mdi:theme-light-dark' +ICON = "mdi:theme-light-dark" + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Circadian Lighting sensor.""" @@ -27,28 +31,30 @@ def setup_platform(hass, config, add_devices, discovery_info=None): def update(call=None): """Update component.""" cl._update() + service_name = "values_update" hass.services.register(DOMAIN, service_name, update) return True else: return False + class CircadianSensor(Entity): """Representation of a Circadian Lighting sensor.""" def __init__(self, hass, cl): """Initialize the Circadian Lighting sensor.""" self._cl = cl - self._name = 'Circadian Values' - self._entity_id = 'sensor.circadian_values' - self._state = self._cl.data['percent'] - self._unit_of_measurement = '%' + self._name = "Circadian Values" + self._entity_id = "sensor.circadian_values" + self._state = self._cl.data["percent"] + self._unit_of_measurement = "%" self._icon = ICON - self._hs_color = self._cl.data['hs_color'] + self._hs_color = self._cl.data["hs_color"] self._attributes = {} - self._attributes['colortemp'] = self._cl.data['colortemp'] - self._attributes['rgb_color'] = self._cl.data['rgb_color'] - self._attributes['xy_color'] = self._cl.data['xy_color'] + self._attributes["colortemp"] = self._cl.data["colortemp"] + self._attributes["rgb_color"] = self._cl.data["rgb_color"] + self._attributes["xy_color"] = self._cl.data["xy_color"] """Register callbacks.""" dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self.update_sensor) @@ -96,9 +102,9 @@ class CircadianSensor(Entity): def update_sensor(self): if self._cl.data is not None: - self._state = self._cl.data['percent'] - self._hs_color = self._cl.data['hs_color'] - self._attributes['colortemp'] = self._cl.data['colortemp'] - self._attributes['rgb_color'] = self._cl.data['rgb_color'] - self._attributes['xy_color'] = self._cl.data['xy_color'] - _LOGGER.debug("Circadian Lighting Sensor Updated") \ No newline at end of file + self._state = self._cl.data["percent"] + self._hs_color = self._cl.data["hs_color"] + self._attributes["colortemp"] = self._cl.data["colortemp"] + self._attributes["rgb_color"] = self._cl.data["rgb_color"] + self._attributes["xy_color"] = self._cl.data["xy_color"] + _LOGGER.debug("Circadian Lighting Sensor Updated") diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 3b58c1c5..59aca5a9 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -2,80 +2,105 @@ Circadian Lighting Switch for Home-Assistant. """ -DEPENDENCIES = ['circadian_lighting', 'light'] +DEPENDENCIES = ["circadian_lighting", "light"] import logging -from custom_components.circadian_lighting import DOMAIN, CIRCADIAN_LIGHTING_UPDATE_TOPIC, DATA_CIRCADIAN_LIGHTING - -import voluptuous as vol - import homeassistant.helpers.config_validation as cv +import voluptuous as vol +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_RGB_COLOR, + ATTR_TRANSITION, + ATTR_WHITE_VALUE, + ATTR_XY_COLOR, +) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.light import VALID_TRANSITION, is_on +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_NAME, + CONF_PLATFORM, + SERVICE_TURN_ON, + STATE_ON, +) from homeassistant.helpers.dispatcher import dispatcher_connect from homeassistant.helpers.event import track_state_change from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.components.light import ( - is_on, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - VALID_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN) - +from homeassistant.util import slugify +from homeassistant.util.color import ( + color_RGB_to_xy, + color_temperature_kelvin_to_mired, + color_temperature_to_rgb, + color_xy_to_hs, +) + +from custom_components.circadian_lighting import ( + CIRCADIAN_LIGHTING_UPDATE_TOPIC, + DATA_CIRCADIAN_LIGHTING, + DOMAIN, +) + try: from homeassistant.components.switch import SwitchEntity except ImportError: from homeassistant.components.switch import SwitchDevice as SwitchEntity - -from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_NAME, CONF_PLATFORM, STATE_ON, - SERVICE_TURN_ON) -from homeassistant.util import slugify -from homeassistant.util.color import ( - color_RGB_to_xy, color_temperature_kelvin_to_mired, - color_temperature_to_rgb, color_xy_to_hs) + _LOGGER = logging.getLogger(__name__) -ICON = 'mdi:theme-light-dark' +ICON = "mdi:theme-light-dark" -CONF_LIGHTS_CT = 'lights_ct' -CONF_LIGHTS_RGB = 'lights_rgb' -CONF_LIGHTS_XY = 'lights_xy' -CONF_LIGHTS_BRIGHT = 'lights_brightness' -CONF_DISABLE_BRIGHTNESS_ADJUST = 'disable_brightness_adjust' -CONF_MIN_BRIGHT = 'min_brightness' +CONF_LIGHTS_CT = "lights_ct" +CONF_LIGHTS_RGB = "lights_rgb" +CONF_LIGHTS_XY = "lights_xy" +CONF_LIGHTS_BRIGHT = "lights_brightness" +CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust" +CONF_MIN_BRIGHT = "min_brightness" DEFAULT_MIN_BRIGHT = 1 -CONF_MAX_BRIGHT = 'max_brightness' +CONF_MAX_BRIGHT = "max_brightness" DEFAULT_MAX_BRIGHT = 100 -CONF_SLEEP_ENTITY = 'sleep_entity' -CONF_SLEEP_STATE = 'sleep_state' -CONF_SLEEP_CT = 'sleep_colortemp' -CONF_SLEEP_BRIGHT = 'sleep_brightness' -CONF_DISABLE_ENTITY = 'disable_entity' -CONF_DISABLE_STATE = 'disable_state' -CONF_INITIAL_TRANSITION = 'initial_transition' +CONF_SLEEP_ENTITY = "sleep_entity" +CONF_SLEEP_STATE = "sleep_state" +CONF_SLEEP_CT = "sleep_colortemp" +CONF_SLEEP_BRIGHT = "sleep_brightness" +CONF_DISABLE_ENTITY = "disable_entity" +CONF_DISABLE_STATE = "disable_state" +CONF_INITIAL_TRANSITION = "initial_transition" DEFAULT_INITIAL_TRANSITION = 1 -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): 'circadian_lighting', - vol.Optional(CONF_NAME, default="Circadian Lighting"): cv.string, - vol.Optional(CONF_LIGHTS_CT): cv.entity_ids, - vol.Optional(CONF_LIGHTS_RGB): cv.entity_ids, - vol.Optional(CONF_LIGHTS_XY): cv.entity_ids, - vol.Optional(CONF_LIGHTS_BRIGHT): cv.entity_ids, - vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=False): cv.boolean, - vol.Optional(CONF_MIN_BRIGHT, default=DEFAULT_MIN_BRIGHT): - vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional(CONF_MAX_BRIGHT, default=DEFAULT_MAX_BRIGHT): - vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, - vol.Optional(CONF_SLEEP_STATE): cv.string, - vol.Optional(CONF_SLEEP_CT): - vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_SLEEP_BRIGHT): - vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, - vol.Optional(CONF_DISABLE_STATE): cv.string, - vol.Optional(CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION): - VALID_TRANSITION -}) +PLATFORM_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "circadian_lighting", + vol.Optional(CONF_NAME, default="Circadian Lighting"): cv.string, + vol.Optional(CONF_LIGHTS_CT): cv.entity_ids, + vol.Optional(CONF_LIGHTS_RGB): cv.entity_ids, + vol.Optional(CONF_LIGHTS_XY): cv.entity_ids, + vol.Optional(CONF_LIGHTS_BRIGHT): cv.entity_ids, + vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=False): cv.boolean, + vol.Optional(CONF_MIN_BRIGHT, default=DEFAULT_MIN_BRIGHT): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_MAX_BRIGHT, default=DEFAULT_MAX_BRIGHT): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, + vol.Optional(CONF_SLEEP_STATE): cv.string, + vol.Optional(CONF_SLEEP_CT): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_SLEEP_BRIGHT): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, + vol.Optional(CONF_DISABLE_STATE): cv.string, + vol.Optional( + CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION + ): VALID_TRANSITION, + } +) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Circadian Lighting switches.""" @@ -96,15 +121,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None): disable_entity = config.get(CONF_DISABLE_ENTITY) disable_state = config.get(CONF_DISABLE_STATE) initial_transition = config.get(CONF_INITIAL_TRANSITION) - cs = CircadianSwitch(hass, cl, name, lights_ct, lights_rgb, lights_xy, lights_brightness, - disable_brightness_adjust, min_brightness, max_brightness, - sleep_entity, sleep_state, sleep_colortemp, sleep_brightness, - disable_entity, disable_state, initial_transition) + cs = CircadianSwitch( + hass, + cl, + name, + lights_ct, + lights_rgb, + lights_xy, + lights_brightness, + disable_brightness_adjust, + min_brightness, + max_brightness, + sleep_entity, + sleep_state, + sleep_colortemp, + sleep_brightness, + disable_entity, + disable_state, + initial_transition, + ) add_devices([cs]) def update(call=None): """Update lights.""" cs.update_switch() + return True else: return False @@ -113,15 +154,33 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class CircadianSwitch(SwitchEntity, RestoreEntity): """Representation of a Circadian Lighting switch.""" - def __init__(self, hass, cl, name, lights_ct, lights_rgb, lights_xy, lights_brightness, - disable_brightness_adjust, min_brightness, max_brightness, - sleep_entity, sleep_state, sleep_colortemp, sleep_brightness, - disable_entity, disable_state, initial_transition): + def __init__( + self, + hass, + cl, + name, + lights_ct, + lights_rgb, + lights_xy, + lights_brightness, + disable_brightness_adjust, + min_brightness, + max_brightness, + sleep_entity, + sleep_state, + sleep_colortemp, + sleep_brightness, + disable_entity, + disable_state, + initial_transition, + ): """Initialize the Circadian Lighting switch.""" self.hass = hass self._cl = cl self._name = name - self._entity_id = "switch." + slugify("{} {}".format('circadian_lighting', name)) + self._entity_id = "switch." + slugify( + "{} {}".format("circadian_lighting", name) + ) self._state = None self._icon = ICON self._hs_color = None @@ -140,8 +199,8 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._disable_state = disable_state self._initial_transition = initial_transition self._attributes = {} - self._attributes['hs_color'] = self._hs_color - self._attributes['brightness'] = None + self._attributes["hs_color"] = self._hs_color + self._attributes["brightness"] = None self._lights = [] if lights_ct != None: @@ -214,25 +273,28 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._state = False self.schedule_update_ha_state() self._hs_color = None - self._attributes['hs_color'] = self._hs_color - self._attributes['brightness'] = None + self._attributes["hs_color"] = self._hs_color + self._attributes["brightness"] = None def is_sleep(self): - return self._sleep_entity is not None and self.hass.states.get(self._sleep_entity).state == self._sleep_state + return ( + self._sleep_entity is not None + and self.hass.states.get(self._sleep_entity).state == self._sleep_state + ) def calc_ct(self): if self.is_sleep(): _LOGGER.debug(self._name + " in Sleep mode") return color_temperature_kelvin_to_mired(self._sleep_colortemp) else: - return color_temperature_kelvin_to_mired(self._cl.data['colortemp']) + return color_temperature_kelvin_to_mired(self._cl.data["colortemp"]) def calc_rgb(self): if self.is_sleep(): _LOGGER.debug(self._name + " in Sleep mode") return color_temperature_to_rgb(self._sleep_colortemp) else: - return color_temperature_to_rgb(self._cl.data['colortemp']) + return color_temperature_to_rgb(self._cl.data["colortemp"]) def calc_xy(self): return color_RGB_to_xy(*self.calc_rgb()) @@ -248,16 +310,19 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug(self._name + " in Sleep mode") return self._sleep_brightness else: - if self._cl.data['percent'] > 0: + if self._cl.data["percent"] > 0: return self._max_brightness else: - return ((self._max_brightness - self._min_brightness) * ((100+self._cl.data['percent']) / 100)) + self._min_brightness + return ( + (self._max_brightness - self._min_brightness) + * ((100 + self._cl.data["percent"]) / 100) + ) + self._min_brightness def update_switch(self, transition=None): if self._cl.data is not None: self._hs_color = self.calc_hs() - self._attributes['hs_color'] = self._hs_color - self._attributes['brightness'] = self.calc_brightness() + self._attributes["hs_color"] = self._hs_color + self._attributes["brightness"] = self.calc_brightness() _LOGGER.debug(self._name + " Switch Updated") self.adjust_lights(self._lights, transition) @@ -269,7 +334,10 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): elif self._cl.data is None: _LOGGER.debug(self._name + " could not retrieve Circadian Lighting data") return False - elif self._disable_entity is not None and self.hass.states.get(self._disable_entity).state == self._disable_state: + elif ( + self._disable_entity is not None + and self.hass.states.get(self._disable_entity).state == self._disable_state + ): _LOGGER.debug(self._name + " disabled by " + str(self._disable_entity)) return False else: @@ -278,16 +346,28 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def adjust_lights(self, lights, transition=None): if self.should_adjust(): if transition == None: - transition = self._cl.data['transition'] + transition = self._cl.data["transition"] - brightness = int((self._attributes['brightness'] / 100) * 254) if self._attributes['brightness'] is not None else None + brightness = ( + int((self._attributes["brightness"] / 100) * 254) + if self._attributes["brightness"] is not None + else None + ) mired = int(self.calc_ct()) if self._lights_ct is not None else None - rgb = tuple(map(int, self.calc_rgb())) if self._lights_rgb is not None else None + rgb = ( + tuple(map(int, self.calc_rgb())) + if self._lights_rgb is not None + else None + ) xy = self.calc_xy() if self._lights_xy is not None else None for light in lights: """Set color of array of ct light if on.""" - if self._lights_ct is not None and light in self._lights_ct and is_on(self.hass, light): + if ( + self._lights_ct is not None + and light in self._lights_ct + and is_on(self.hass, light) + ): service_data = {ATTR_ENTITY_ID: light} if mired is not None: service_data[ATTR_COLOR_TEMP] = mired @@ -295,12 +375,23 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_BRIGHTNESS] = brightness if transition is not None: service_data[ATTR_TRANSITION] = transition - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " CT Adjusted - color_temp: " + str(mired) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) + self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug( + light + + " CT Adjusted - color_temp: " + + str(mired) + + ", brightness: " + + str(brightness) + + ", transition: " + + str(transition) + ) """Set color of array of rgb light if on.""" - if self._lights_rgb is not None and light in self._lights_rgb and is_on(self.hass, light): + if ( + self._lights_rgb is not None + and light in self._lights_rgb + and is_on(self.hass, light) + ): service_data = {ATTR_ENTITY_ID: light} if rgb is not None: service_data[ATTR_RGB_COLOR] = rgb @@ -308,12 +399,23 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_BRIGHTNESS] = brightness if transition is not None: service_data[ATTR_TRANSITION] = transition - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " RGB Adjusted - rgb_color: " + str(rgb) + ", brightness: " + str(brightness) + ", transition: " + str(transition)) + self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug( + light + + " RGB Adjusted - rgb_color: " + + str(rgb) + + ", brightness: " + + str(brightness) + + ", transition: " + + str(transition) + ) """Set color of array of xy light if on.""" - if self._lights_xy is not None and light in self._lights_xy and is_on(self.hass, light): + if ( + self._lights_xy is not None + and light in self._lights_xy + and is_on(self.hass, light) + ): service_data = {ATTR_ENTITY_ID: light} if xy is not None: service_data[ATTR_XY_COLOR] = xy @@ -322,40 +424,67 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_WHITE_VALUE] = brightness if transition is not None: service_data[ATTR_TRANSITION] = transition - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " XY Adjusted - xy_color: " + str(xy) + ", brightness: " + str(brightness) + ", transition: " + str(transition) + ", white_value: " + str(brightness)) + self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug( + light + + " XY Adjusted - xy_color: " + + str(xy) + + ", brightness: " + + str(brightness) + + ", transition: " + + str(transition) + + ", white_value: " + + str(brightness) + ) """Set color of array of brightness light if on.""" - if self._lights_brightness is not None and light in self._lights_brightness and is_on(self.hass, light): + if ( + self._lights_brightness is not None + and light in self._lights_brightness + and is_on(self.hass, light) + ): service_data = {ATTR_ENTITY_ID: light} if brightness is not None: service_data[ATTR_BRIGHTNESS] = brightness if transition is not None: service_data[ATTR_TRANSITION] = transition - self.hass.services.call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(light + " Brightness Adjusted - brightness: " + str(brightness) + ", transition: " + str(transition)) + self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug( + light + + " Brightness Adjusted - brightness: " + + str(brightness) + + ", transition: " + + str(transition) + ) def light_state_changed(self, entity_id, from_state, to_state): try: - _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) - if to_state.state == 'on' and from_state.state != 'on': + _LOGGER.debug( + entity_id + " change from " + str(from_state) + " to " + str(to_state) + ) + if to_state.state == "on" and from_state.state != "on": self.adjust_lights([entity_id], self._initial_transition) except: pass def sleep_state_changed(self, entity_id, from_state, to_state): try: - _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) - if to_state.state == self._sleep_state or from_state.state == self._sleep_state: + _LOGGER.debug( + entity_id + " change from " + str(from_state) + " to " + str(to_state) + ) + if ( + to_state.state == self._sleep_state + or from_state.state == self._sleep_state + ): self.update_switch(self._initial_transition) except: pass - + def disable_state_changed(self, entity_id, from_state, to_state): try: - _LOGGER.debug(entity_id + " change from " + str(from_state) + " to " + str(to_state)) + _LOGGER.debug( + entity_id + " change from " + str(from_state) + " to " + str(to_state) + ) if from_state.state == self._disable_state: self.update_switch(self._initial_transition) except: From f7119aebaf49462abcad528dd973f28385a7ec09 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:10:41 +0200 Subject: [PATCH 0031/1077] use 'is None' and 'is not None' --- custom_components/circadian_lighting/switch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 59aca5a9..c184fb22 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -203,13 +203,13 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._attributes["brightness"] = None self._lights = [] - if lights_ct != None: + if lights_ct is not None: self._lights += lights_ct - if lights_rgb != None: + if lights_rgb is not None: self._lights += lights_rgb - if lights_xy != None: + if lights_xy is not None: self._lights += lights_xy - if lights_brightness != None: + if lights_brightness is not None: self._lights += lights_brightness """Register callbacks.""" From 5448caced2d641e0e8d7b951215f524e1889990f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:16:20 +0200 Subject: [PATCH 0032/1077] use f-strings in logs --- .../circadian_lighting/switch.py | 64 ++++++------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index c184fb22..c031b3d9 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -284,14 +284,14 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def calc_ct(self): if self.is_sleep(): - _LOGGER.debug(self._name + " in Sleep mode") + _LOGGER.debug(f"{self._name} in Sleep mode") return color_temperature_kelvin_to_mired(self._sleep_colortemp) else: return color_temperature_kelvin_to_mired(self._cl.data["colortemp"]) def calc_rgb(self): if self.is_sleep(): - _LOGGER.debug(self._name + " in Sleep mode") + _LOGGER.debug(f"{self._name} in Sleep mode") return color_temperature_to_rgb(self._sleep_colortemp) else: return color_temperature_to_rgb(self._cl.data["colortemp"]) @@ -307,7 +307,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): return None else: if self.is_sleep(): - _LOGGER.debug(self._name + " in Sleep mode") + _LOGGER.debug(f"{self._name} in Sleep mode") return self._sleep_brightness else: if self._cl.data["percent"] > 0: @@ -323,29 +323,29 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._hs_color = self.calc_hs() self._attributes["hs_color"] = self._hs_color self._attributes["brightness"] = self.calc_brightness() - _LOGGER.debug(self._name + " Switch Updated") + _LOGGER.debug(f"{self._name} Switch Updated") self.adjust_lights(self._lights, transition) def should_adjust(self): if self._state is not True: - _LOGGER.debug(self._name + " off - not adjusting") + _LOGGER.debug(f"{self._name} off - not adjusting") return False elif self._cl.data is None: - _LOGGER.debug(self._name + " could not retrieve Circadian Lighting data") + _LOGGER.debug(f"{self._name} could not retrieve Circadian Lighting data") return False elif ( self._disable_entity is not None and self.hass.states.get(self._disable_entity).state == self._disable_state ): - _LOGGER.debug(self._name + " disabled by " + str(self._disable_entity)) + _LOGGER.debug(f"{self._name} disabled by " + str(self._disable_entity)) return False else: return True def adjust_lights(self, lights, transition=None): if self.should_adjust(): - if transition == None: + if transition is None: transition = self._cl.data["transition"] brightness = ( @@ -377,13 +377,8 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_TRANSITION] = transition self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) _LOGGER.debug( - light - + " CT Adjusted - color_temp: " - + str(mired) - + ", brightness: " - + str(brightness) - + ", transition: " - + str(transition) + f"{light} CT Adjusted - color_temp: {mired}, " + f"brightness: {brightness}, transition: {transition}" ) """Set color of array of rgb light if on.""" @@ -401,13 +396,8 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_TRANSITION] = transition self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) _LOGGER.debug( - light - + " RGB Adjusted - rgb_color: " - + str(rgb) - + ", brightness: " - + str(brightness) - + ", transition: " - + str(transition) + f"{light} RGB Adjusted - rgb_color: {rgb}, " + f"brightness: {brightness}, transition: {transition}" ) """Set color of array of xy light if on.""" @@ -426,15 +416,8 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_TRANSITION] = transition self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) _LOGGER.debug( - light - + " XY Adjusted - xy_color: " - + str(xy) - + ", brightness: " - + str(brightness) - + ", transition: " - + str(transition) - + ", white_value: " - + str(brightness) + f"{light} XY Adjusted - xy_color: {xy}, brightness: {brightness}, " + f"transition: {transition}, white_value: {brightness}" ) """Set color of array of brightness light if on.""" @@ -450,18 +433,13 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_TRANSITION] = transition self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) _LOGGER.debug( - light - + " Brightness Adjusted - brightness: " - + str(brightness) - + ", transition: " - + str(transition) + f"{light} Brightness Adjusted - brightness: {brightness}, " + f"transition: {transition}" ) def light_state_changed(self, entity_id, from_state, to_state): try: - _LOGGER.debug( - entity_id + " change from " + str(from_state) + " to " + str(to_state) - ) + _LOGGER.debug(f"{entity_id} change from {from_state} to {to_state}") if to_state.state == "on" and from_state.state != "on": self.adjust_lights([entity_id], self._initial_transition) except: @@ -469,9 +447,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def sleep_state_changed(self, entity_id, from_state, to_state): try: - _LOGGER.debug( - entity_id + " change from " + str(from_state) + " to " + str(to_state) - ) + _LOGGER.debug(f"{entity_id} change from {from_state} to {to_state}") if ( to_state.state == self._sleep_state or from_state.state == self._sleep_state @@ -482,9 +458,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def disable_state_changed(self, entity_id, from_state, to_state): try: - _LOGGER.debug( - entity_id + " change from " + str(from_state) + " to " + str(to_state) - ) + _LOGGER.debug("{entity_id} change from {from_state} to {to_state}") if from_state.state == self._disable_state: self.update_switch(self._initial_transition) except: From 26796a449d8cb7267983c32610d6449dd5e1ebc5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:17:16 +0200 Subject: [PATCH 0033/1077] use contextlib.suppress --- custom_components/circadian_lighting/switch.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index c031b3d9..3486e4f5 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -5,6 +5,7 @@ Circadian Lighting Switch for Home-Assistant. DEPENDENCIES = ["circadian_lighting", "light"] import logging +from contextlib import suppress import homeassistant.helpers.config_validation as cv import voluptuous as vol @@ -438,28 +439,22 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): ) def light_state_changed(self, entity_id, from_state, to_state): - try: + with suppress(Exception): _LOGGER.debug(f"{entity_id} change from {from_state} to {to_state}") if to_state.state == "on" and from_state.state != "on": self.adjust_lights([entity_id], self._initial_transition) - except: - pass def sleep_state_changed(self, entity_id, from_state, to_state): - try: + with suppress(Exception): _LOGGER.debug(f"{entity_id} change from {from_state} to {to_state}") if ( to_state.state == self._sleep_state or from_state.state == self._sleep_state ): self.update_switch(self._initial_transition) - except: - pass def disable_state_changed(self, entity_id, from_state, to_state): - try: + with suppress(Exception): _LOGGER.debug("{entity_id} change from {from_state} to {to_state}") if from_state.state == self._disable_state: self.update_switch(self._initial_transition) - except: - pass From ce7e08d8249f8b5ee33e76173d80e004083a738b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:18:21 +0200 Subject: [PATCH 0034/1077] fix last f-string --- custom_components/circadian_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 3486e4f5..3aaa4951 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -339,7 +339,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._disable_entity is not None and self.hass.states.get(self._disable_entity).state == self._disable_state ): - _LOGGER.debug(f"{self._name} disabled by " + str(self._disable_entity)) + _LOGGER.debug(f"{self._name} disabled by {self._disable_entity}") return False else: return True From 5f1c7ad82760dcca67880a304935512062f8deb4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:18:46 +0200 Subject: [PATCH 0035/1077] remove unused import --- custom_components/circadian_lighting/switch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 3aaa4951..7750ea6e 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -40,7 +40,6 @@ from homeassistant.util.color import ( from custom_components.circadian_lighting import ( CIRCADIAN_LIGHTING_UPDATE_TOPIC, DATA_CIRCADIAN_LIGHTING, - DOMAIN, ) try: From 860557830178cc96419e09c5a3711c23daa8c75d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:21:48 +0200 Subject: [PATCH 0036/1077] more simplifications --- custom_components/circadian_lighting/switch.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 7750ea6e..e33053e9 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -178,9 +178,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self.hass = hass self._cl = cl self._name = name - self._entity_id = "switch." + slugify( - "{} {}".format("circadian_lighting", name) - ) + self._entity_id = "switch." + slugify(f"circadian_lighting {name}") self._state = None self._icon = ICON self._hs_color = None @@ -198,9 +196,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._disable_entity = disable_entity self._disable_state = disable_state self._initial_transition = initial_transition - self._attributes = {} - self._attributes["hs_color"] = self._hs_color - self._attributes["brightness"] = None + self._attributes = {"hs_color": self._hs_color, "brightness": None} self._lights = [] if lights_ct is not None: From 56a5a49f64e04d99d9d9256b3557acc10b994a87 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:23:12 +0200 Subject: [PATCH 0037/1077] simplify calc_brightness --- .../circadian_lighting/switch.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index e33053e9..b7c1f3c4 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -301,18 +301,16 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def calc_brightness(self): if self._disable_brightness_adjust is True: return None + elif self.is_sleep(): + _LOGGER.debug(f"{self._name} in Sleep mode") + return self._sleep_brightness + elif self._cl.data["percent"] > 0: + return self._max_brightness else: - if self.is_sleep(): - _LOGGER.debug(f"{self._name} in Sleep mode") - return self._sleep_brightness - else: - if self._cl.data["percent"] > 0: - return self._max_brightness - else: - return ( - (self._max_brightness - self._min_brightness) - * ((100 + self._cl.data["percent"]) / 100) - ) + self._min_brightness + return ( + (self._max_brightness - self._min_brightness) + * ((100 + self._cl.data["percent"]) / 100) + ) + self._min_brightness def update_switch(self, transition=None): if self._cl.data is not None: From 97598cfaac7060117f809518174c313bac134ff2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:25:36 +0200 Subject: [PATCH 0038/1077] reduce indentation of adjust_lights --- .../circadian_lighting/switch.py | 172 +++++++++--------- 1 file changed, 85 insertions(+), 87 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index b7c1f3c4..6af78d16 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -338,98 +338,96 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): return True def adjust_lights(self, lights, transition=None): - if self.should_adjust(): - if transition is None: - transition = self._cl.data["transition"] + if not self.should_adjust(): + return - brightness = ( - int((self._attributes["brightness"] / 100) * 254) - if self._attributes["brightness"] is not None - else None - ) - mired = int(self.calc_ct()) if self._lights_ct is not None else None - rgb = ( - tuple(map(int, self.calc_rgb())) - if self._lights_rgb is not None - else None - ) - xy = self.calc_xy() if self._lights_xy is not None else None + if transition is None: + transition = self._cl.data["transition"] - for light in lights: - """Set color of array of ct light if on.""" - if ( - self._lights_ct is not None - and light in self._lights_ct - and is_on(self.hass, light) - ): - service_data = {ATTR_ENTITY_ID: light} - if mired is not None: - service_data[ATTR_COLOR_TEMP] = mired - if brightness is not None: - service_data[ATTR_BRIGHTNESS] = brightness - if transition is not None: - service_data[ATTR_TRANSITION] = transition - self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug( - f"{light} CT Adjusted - color_temp: {mired}, " - f"brightness: {brightness}, transition: {transition}" - ) + brightness = ( + int((self._attributes["brightness"] / 100) * 254) + if self._attributes["brightness"] is not None + else None + ) + mired = int(self.calc_ct()) if self._lights_ct is not None else None + rgb = tuple(map(int, self.calc_rgb())) if self._lights_rgb is not None else None + xy = self.calc_xy() if self._lights_xy is not None else None - """Set color of array of rgb light if on.""" - if ( - self._lights_rgb is not None - and light in self._lights_rgb - and is_on(self.hass, light) - ): - service_data = {ATTR_ENTITY_ID: light} - if rgb is not None: - service_data[ATTR_RGB_COLOR] = rgb - if brightness is not None: - service_data[ATTR_BRIGHTNESS] = brightness - if transition is not None: - service_data[ATTR_TRANSITION] = transition - self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug( - f"{light} RGB Adjusted - rgb_color: {rgb}, " - f"brightness: {brightness}, transition: {transition}" - ) + for light in lights: + """Set color of array of ct light if on.""" + if ( + self._lights_ct is not None + and light in self._lights_ct + and is_on(self.hass, light) + ): + service_data = {ATTR_ENTITY_ID: light} + if mired is not None: + service_data[ATTR_COLOR_TEMP] = mired + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition + self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug( + f"{light} CT Adjusted - color_temp: {mired}, " + f"brightness: {brightness}, transition: {transition}" + ) - """Set color of array of xy light if on.""" - if ( - self._lights_xy is not None - and light in self._lights_xy - and is_on(self.hass, light) - ): - service_data = {ATTR_ENTITY_ID: light} - if xy is not None: - service_data[ATTR_XY_COLOR] = xy - if brightness is not None: - service_data[ATTR_BRIGHTNESS] = brightness - service_data[ATTR_WHITE_VALUE] = brightness - if transition is not None: - service_data[ATTR_TRANSITION] = transition - self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug( - f"{light} XY Adjusted - xy_color: {xy}, brightness: {brightness}, " - f"transition: {transition}, white_value: {brightness}" - ) + """Set color of array of rgb light if on.""" + if ( + self._lights_rgb is not None + and light in self._lights_rgb + and is_on(self.hass, light) + ): + service_data = {ATTR_ENTITY_ID: light} + if rgb is not None: + service_data[ATTR_RGB_COLOR] = rgb + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition + self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug( + f"{light} RGB Adjusted - rgb_color: {rgb}, " + f"brightness: {brightness}, transition: {transition}" + ) - """Set color of array of brightness light if on.""" - if ( - self._lights_brightness is not None - and light in self._lights_brightness - and is_on(self.hass, light) - ): - service_data = {ATTR_ENTITY_ID: light} - if brightness is not None: - service_data[ATTR_BRIGHTNESS] = brightness - if transition is not None: - service_data[ATTR_TRANSITION] = transition - self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug( - f"{light} Brightness Adjusted - brightness: {brightness}, " - f"transition: {transition}" - ) + """Set color of array of xy light if on.""" + if ( + self._lights_xy is not None + and light in self._lights_xy + and is_on(self.hass, light) + ): + service_data = {ATTR_ENTITY_ID: light} + if xy is not None: + service_data[ATTR_XY_COLOR] = xy + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + service_data[ATTR_WHITE_VALUE] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition + self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug( + f"{light} XY Adjusted - xy_color: {xy}, brightness: {brightness}, " + f"transition: {transition}, white_value: {brightness}" + ) + + """Set color of array of brightness light if on.""" + if ( + self._lights_brightness is not None + and light in self._lights_brightness + and is_on(self.hass, light) + ): + service_data = {ATTR_ENTITY_ID: light} + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition + self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + _LOGGER.debug( + f"{light} Brightness Adjusted - brightness: {brightness}, " + f"transition: {transition}" + ) def light_state_changed(self, entity_id, from_state, to_state): with suppress(Exception): From e381d0c0e395f67736a2da55d0225eeef1f12b15 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:27:48 +0200 Subject: [PATCH 0039/1077] simplify conditions in adjust_lights --- .../circadian_lighting/switch.py | 35 ++++++------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 6af78d16..34c97121 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -354,12 +354,11 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): xy = self.calc_xy() if self._lights_xy is not None else None for light in lights: - """Set color of array of ct light if on.""" - if ( - self._lights_ct is not None - and light in self._lights_ct - and is_on(self.hass, light) - ): + if not is_on(self.hass, light): + continue + + # Set color of array of ct. + if self._lights_ct is not None and light in self._lights_ct: service_data = {ATTR_ENTITY_ID: light} if mired is not None: service_data[ATTR_COLOR_TEMP] = mired @@ -373,12 +372,8 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): f"brightness: {brightness}, transition: {transition}" ) - """Set color of array of rgb light if on.""" - if ( - self._lights_rgb is not None - and light in self._lights_rgb - and is_on(self.hass, light) - ): + # Set color of array of rgb. + if self._lights_rgb is not None and light in self._lights_rgb: service_data = {ATTR_ENTITY_ID: light} if rgb is not None: service_data[ATTR_RGB_COLOR] = rgb @@ -392,12 +387,8 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): f"brightness: {brightness}, transition: {transition}" ) - """Set color of array of xy light if on.""" - if ( - self._lights_xy is not None - and light in self._lights_xy - and is_on(self.hass, light) - ): + # Set color of array of xy. + if self._lights_xy is not None and light in self._lights_xy: service_data = {ATTR_ENTITY_ID: light} if xy is not None: service_data[ATTR_XY_COLOR] = xy @@ -412,12 +403,8 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): f"transition: {transition}, white_value: {brightness}" ) - """Set color of array of brightness light if on.""" - if ( - self._lights_brightness is not None - and light in self._lights_brightness - and is_on(self.hass, light) - ): + # Set color of array of brightness. + if self._lights_brightness is not None and light in self._lights_brightness: service_data = {ATTR_ENTITY_ID: light} if brightness is not None: service_data[ATTR_BRIGHTNESS] = brightness From 34c4fdb71a770d26bfcf0095bc1e847619414350 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:30:08 +0200 Subject: [PATCH 0040/1077] don't run if statements that will never run --- custom_components/circadian_lighting/switch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 34c97121..6cf89b52 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -373,7 +373,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): ) # Set color of array of rgb. - if self._lights_rgb is not None and light in self._lights_rgb: + elif self._lights_rgb is not None and light in self._lights_rgb: service_data = {ATTR_ENTITY_ID: light} if rgb is not None: service_data[ATTR_RGB_COLOR] = rgb @@ -388,7 +388,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): ) # Set color of array of xy. - if self._lights_xy is not None and light in self._lights_xy: + elif self._lights_xy is not None and light in self._lights_xy: service_data = {ATTR_ENTITY_ID: light} if xy is not None: service_data[ATTR_XY_COLOR] = xy @@ -404,7 +404,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): ) # Set color of array of brightness. - if self._lights_brightness is not None and light in self._lights_brightness: + elif self._lights_brightness is not None and light in self._lights_brightness: service_data = {ATTR_ENTITY_ID: light} if brightness is not None: service_data[ATTR_BRIGHTNESS] = brightness From 38dc367c565c5f738941db4351162be6ff7f26e8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:39:13 +0200 Subject: [PATCH 0041/1077] simplify adjust_lights even more --- .../circadian_lighting/switch.py | 48 +++++++------------ 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 6cf89b52..474e2ed1 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -357,64 +357,50 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): if not is_on(self.hass, light): continue + which = None + service_data = {ATTR_ENTITY_ID: light} + if transition is not None: + service_data[ATTR_TRANSITION] = transition + # Set color of array of ct. if self._lights_ct is not None and light in self._lights_ct: - service_data = {ATTR_ENTITY_ID: light} + which = "CT" if mired is not None: service_data[ATTR_COLOR_TEMP] = mired if brightness is not None: service_data[ATTR_BRIGHTNESS] = brightness - if transition is not None: - service_data[ATTR_TRANSITION] = transition - self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug( - f"{light} CT Adjusted - color_temp: {mired}, " - f"brightness: {brightness}, transition: {transition}" - ) # Set color of array of rgb. elif self._lights_rgb is not None and light in self._lights_rgb: - service_data = {ATTR_ENTITY_ID: light} + which = "RGB" if rgb is not None: service_data[ATTR_RGB_COLOR] = rgb if brightness is not None: service_data[ATTR_BRIGHTNESS] = brightness - if transition is not None: - service_data[ATTR_TRANSITION] = transition - self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug( - f"{light} RGB Adjusted - rgb_color: {rgb}, " - f"brightness: {brightness}, transition: {transition}" - ) # Set color of array of xy. elif self._lights_xy is not None and light in self._lights_xy: - service_data = {ATTR_ENTITY_ID: light} + which = "XY" if xy is not None: service_data[ATTR_XY_COLOR] = xy if brightness is not None: service_data[ATTR_BRIGHTNESS] = brightness service_data[ATTR_WHITE_VALUE] = brightness - if transition is not None: - service_data[ATTR_TRANSITION] = transition - self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug( - f"{light} XY Adjusted - xy_color: {xy}, brightness: {brightness}, " - f"transition: {transition}, white_value: {brightness}" - ) # Set color of array of brightness. - elif self._lights_brightness is not None and light in self._lights_brightness: - service_data = {ATTR_ENTITY_ID: light} + elif ( + self._lights_brightness is not None and light in self._lights_brightness + ): + which = "Brightness" if brightness is not None: service_data[ATTR_BRIGHTNESS] = brightness - if transition is not None: - service_data[ATTR_TRANSITION] = transition + + if which is not None: self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug( - f"{light} Brightness Adjusted - brightness: {brightness}, " - f"transition: {transition}" + msg = ", ".join( + [f"{k}: v" for k, v in d.items() if k != ATTR_ENTITY_ID] ) + _LOGGER.debug(f"{light} {which} Adjusted - {msg}") def light_state_changed(self, entity_id, from_state, to_state): with suppress(Exception): From 8a49a6fe87edd011766808b634b6213a5b92a0da Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:41:58 +0200 Subject: [PATCH 0042/1077] use extend instead of += --- custom_components/circadian_lighting/switch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 474e2ed1..01c1ad66 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -200,13 +200,13 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._lights = [] if lights_ct is not None: - self._lights += lights_ct + self._lights.extend(lights_ct) if lights_rgb is not None: - self._lights += lights_rgb + self._lights.extend(lights_rgb) if lights_xy is not None: - self._lights += lights_xy + self._lights.extend(lights_xy) if lights_brightness is not None: - self._lights += lights_brightness + self._lights.extend(lights_brightness) """Register callbacks.""" dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self.update_switch) From 3ea906ea2e058fdf68628a44af939618fe63ee71 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:43:07 +0200 Subject: [PATCH 0043/1077] use comment when inline --- custom_components/circadian_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 01c1ad66..c42a531e 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -208,7 +208,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): if lights_brightness is not None: self._lights.extend(lights_brightness) - """Register callbacks.""" + # Register callbacks dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self.update_switch) track_state_change(hass, self._lights, self.light_state_changed) if self._sleep_entity is not None: From 643d69bbf855f6789baac2aa07c28f5e4bd5f1fd Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:45:10 +0200 Subject: [PATCH 0044/1077] do is sleep logging in is_sleep method --- custom_components/circadian_lighting/switch.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index c42a531e..139a6c06 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -273,21 +273,23 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._attributes["brightness"] = None def is_sleep(self): - return ( + is_sleep = ( self._sleep_entity is not None and self.hass.states.get(self._sleep_entity).state == self._sleep_state ) + if is_sleep: + _LOGGER.debug(f"{self._name} in Sleep mode") + + return is_sleep def calc_ct(self): if self.is_sleep(): - _LOGGER.debug(f"{self._name} in Sleep mode") return color_temperature_kelvin_to_mired(self._sleep_colortemp) else: return color_temperature_kelvin_to_mired(self._cl.data["colortemp"]) def calc_rgb(self): if self.is_sleep(): - _LOGGER.debug(f"{self._name} in Sleep mode") return color_temperature_to_rgb(self._sleep_colortemp) else: return color_temperature_to_rgb(self._cl.data["colortemp"]) @@ -302,7 +304,6 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): if self._disable_brightness_adjust is True: return None elif self.is_sleep(): - _LOGGER.debug(f"{self._name} in Sleep mode") return self._sleep_brightness elif self._cl.data["percent"] > 0: return self._max_brightness From 3be5f4448876f0ace7f712154d5d621bfeca99ce Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:46:52 +0200 Subject: [PATCH 0045/1077] simplify calc_ct and calc_rgb --- custom_components/circadian_lighting/switch.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 139a6c06..76aaa6f7 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -283,16 +283,11 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): return is_sleep def calc_ct(self): - if self.is_sleep(): - return color_temperature_kelvin_to_mired(self._sleep_colortemp) - else: - return color_temperature_kelvin_to_mired(self._cl.data["colortemp"]) + col_temp = self._sleep_colortemp if self.is_sleep() else self._cl.data["colortemp"] + return color_temperature_kelvin_to_mired(col_temp) def calc_rgb(self): - if self.is_sleep(): - return color_temperature_to_rgb(self._sleep_colortemp) - else: - return color_temperature_to_rgb(self._cl.data["colortemp"]) + return self.calc_ct() def calc_xy(self): return color_RGB_to_xy(*self.calc_rgb()) From c0082175b6679e4ca4850ffac7dff4aadbc4d011 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:52:23 +0200 Subject: [PATCH 0046/1077] simplify debug logging call --- .../circadian_lighting/__init__.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 79d8de1d..2aca343d 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -325,11 +325,13 @@ class CircadianLighting(object): "solar_midnight" ].timestamp() - _LOGGER.debug("now_seconds: " + str(now_seconds)) - _LOGGER.debug("sunrise_seconds: " + str(sunrise_seconds)) - _LOGGER.debug("sunset_seconds: " + str(sunset_seconds)) - _LOGGER.debug("solar_midnight_seconds: " + str(solar_midnight_seconds)) - _LOGGER.debug("solar_noon_seconds: " + str(solar_noon_seconds)) + _LOGGER.debug( + f"now_seconds: {now_seconds}, " + f"sunrise_seconds: {sunrise_seconds}, " + f"sunset_seconds: {sunset_seconds}, " + f"solar_midnight_seconds: {solar_midnight_seconds}, " + f"solar_noon_seconds: {solar_noon_seconds}" + ) # Figure out where we are in time so we know which half of the parabola to calculate # We're generating a different sunset-sunrise parabola for before and after solar midnight @@ -363,12 +365,9 @@ class CircadianLighting(object): a = (y - k) / (h - x) ** 2 percentage = a * (now_seconds - h) ** 2 + k - _LOGGER.debug("h: " + str(h)) - _LOGGER.debug("k: " + str(k)) - _LOGGER.debug("x: " + str(x)) - _LOGGER.debug("y: " + str(y)) - _LOGGER.debug("a: " + str(a)) - _LOGGER.debug("percentage: " + str(percentage)) + _LOGGER.debug( + f"h: {h}, k: {k}, x: {x}, y: {y}, a: {a}, percentage: {percentage}" + ) return percentage From d2dda06802f64d7636aff33650b50a00c314bce5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:52:48 +0200 Subject: [PATCH 0047/1077] remove unused import --- custom_components/circadian_lighting/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 2aca343d..af76c78e 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -28,7 +28,7 @@ Technical notes: I had to make a lot of assumptions when writing this app """ import logging -from datetime import datetime, timedelta +from datetime import timedelta import homeassistant.helpers.config_validation as cv import voluptuous as vol From 5b98fedf1264c31ede7245a40987436dfd1de6e0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:52:58 +0200 Subject: [PATCH 0048/1077] black --- custom_components/circadian_lighting/switch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 76aaa6f7..5491758f 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -283,7 +283,9 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): return is_sleep def calc_ct(self): - col_temp = self._sleep_colortemp if self.is_sleep() else self._cl.data["colortemp"] + col_temp = ( + self._sleep_colortemp if self.is_sleep() else self._cl.data["colortemp"] + ) return color_temperature_kelvin_to_mired(col_temp) def calc_rgb(self): From 1053e749562259f53368a14924c89682f0256944 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:55:33 +0200 Subject: [PATCH 0049/1077] don't assign variables but directly pass them --- .../circadian_lighting/switch.py | 45 +++++++------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 5491758f..7d2fc80d 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -106,39 +106,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Circadian Lighting switches.""" cl = hass.data.get(DATA_CIRCADIAN_LIGHTING) if cl: - lights_ct = config.get(CONF_LIGHTS_CT) - lights_rgb = config.get(CONF_LIGHTS_RGB) - lights_xy = config.get(CONF_LIGHTS_XY) - lights_brightness = config.get(CONF_LIGHTS_BRIGHT) - disable_brightness_adjust = config.get(CONF_DISABLE_BRIGHTNESS_ADJUST) - name = config.get(CONF_NAME) - min_brightness = config.get(CONF_MIN_BRIGHT) - max_brightness = config.get(CONF_MAX_BRIGHT) - sleep_entity = config.get(CONF_SLEEP_ENTITY) - sleep_state = config.get(CONF_SLEEP_STATE) - sleep_colortemp = config.get(CONF_SLEEP_CT) - sleep_brightness = config.get(CONF_SLEEP_BRIGHT) - disable_entity = config.get(CONF_DISABLE_ENTITY) - disable_state = config.get(CONF_DISABLE_STATE) - initial_transition = config.get(CONF_INITIAL_TRANSITION) cs = CircadianSwitch( hass, cl, - name, - lights_ct, - lights_rgb, - lights_xy, - lights_brightness, - disable_brightness_adjust, - min_brightness, - max_brightness, - sleep_entity, - sleep_state, - sleep_colortemp, - sleep_brightness, - disable_entity, - disable_state, - initial_transition, + name=config.get(CONF_NAME), + lights_ct=config.get(CONF_LIGHTS_CT), + lights_rgb=config.get(CONF_LIGHTS_RGB), + lights_xy=config.get(CONF_LIGHTS_XY), + lights_brightness=config.get(CONF_LIGHTS_BRIGHT), + disable_brightness_adjust=config.get(CONF_DISABLE_BRIGHTNESS_ADJUST), + min_brightness=config.get(CONF_MIN_BRIGHT), + max_brightness=config.get(CONF_MAX_BRIGHT), + sleep_entity=config.get(CONF_SLEEP_ENTITY), + sleep_state=config.get(CONF_SLEEP_STATE), + sleep_colortemp=config.get(CONF_SLEEP_CT), + sleep_brightness=config.get(CONF_SLEEP_BRIGHT), + disable_entity=config.get(CONF_DISABLE_ENTITY), + disable_state=config.get(CONF_DISABLE_STATE), + initial_transition=config.get(CONF_INITIAL_TRANSITION), ) add_devices([cs]) From 2d064e519500fa5b5728bb933524586efb5900c3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 18:59:04 +0200 Subject: [PATCH 0050/1077] fix in calc_rgb --- custom_components/circadian_lighting/switch.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 7d2fc80d..96afdfbd 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -267,14 +267,15 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): return is_sleep + @property + def _color_temperature(self): + return self._sleep_colortemp if self.is_sleep() else self._cl.data["colortemp"] + def calc_ct(self): - col_temp = ( - self._sleep_colortemp if self.is_sleep() else self._cl.data["colortemp"] - ) - return color_temperature_kelvin_to_mired(col_temp) + return color_temperature_kelvin_to_mired(self._color_temperature) def calc_rgb(self): - return self.calc_ct() + return color_temperature_to_rgb(self._color_temperature) def calc_xy(self): return color_RGB_to_xy(*self.calc_rgb()) From 0780879a0b9cd99f7323d21cc02d33dec2b3a185 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 19:02:18 +0200 Subject: [PATCH 0051/1077] simplify setup() --- .../circadian_lighting/__init__.py | 40 ++++++------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index af76c78e..ec13e7ac 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -104,39 +104,23 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Circadian Lighting component.""" conf = config[DOMAIN] - min_colortemp = conf.get(CONF_MIN_CT) - max_colortemp = conf.get(CONF_MAX_CT) - sunrise_offset = conf.get(CONF_SUNRISE_OFFSET) - sunset_offset = conf.get(CONF_SUNSET_OFFSET) - sunrise_time = conf.get(CONF_SUNRISE_TIME) - sunset_time = conf.get(CONF_SUNSET_TIME) - - latitude = conf.get(CONF_LATITUDE, hass.config.latitude) - longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) - elevation = conf.get(CONF_ELEVATION, hass.config.elevation) - load_platform(hass, "sensor", DOMAIN, {}, config) - interval = conf.get(CONF_INTERVAL) - transition = conf.get(ATTR_TRANSITION) - - cl = CircadianLighting( + hass.data[DATA_CIRCADIAN_LIGHTING] = CircadianLighting( hass, - min_colortemp, - max_colortemp, - sunrise_offset, - sunset_offset, - sunrise_time, - sunset_time, - latitude, - longitude, - elevation, - interval, - transition, + min_colortemp=conf.get(CONF_MIN_CT), + max_colortemp=conf.get(CONF_MAX_CT), + sunrise_offset=conf.get(CONF_SUNRISE_OFFSET), + sunset_offset=conf.get(CONF_SUNSET_OFFSET), + sunrise_time=conf.get(CONF_SUNRISE_TIME), + sunset_time=conf.get(CONF_SUNSET_TIME), + latitude=conf.get(CONF_LATITUDE, hass.config.latitude), + longitude=conf.get(CONF_LONGITUDE, hass.config.longitude), + elevation=conf.get(CONF_ELEVATION, hass.config.elevation), + interval=conf.get(CONF_INTERVAL), + transition=conf.get(ATTR_TRANSITION), ) - hass.data[DATA_CIRCADIAN_LIGHTING] = cl - return True From b3e48344212dadaeaa77558e753cecf0927f2e24 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 19:06:00 +0200 Subject: [PATCH 0052/1077] simplify time.replace calls --- .../circadian_lighting/__init__.py | 36 +++++++------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index ec13e7ac..b7e196c4 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -196,6 +196,14 @@ class CircadianLighting(object): _LOGGER.debug("Timezone: " + str(timezone)) return timezone + def _time_dict(self, key): + return dict( + hour=int(self.data[key].strftime("%H")), + minute=int(self.data[key].strftime("%M")), + second=int(self.data[key].strftime("%S")), + microsecond=int(self.data[key].strftime("%f")), + ) + def get_sunrise_sunset(self, date=None): if ( self.data["sunrise_time"] is not None @@ -203,18 +211,8 @@ class CircadianLighting(object): ): if date is None: date = dt_now(self.data["timezone"]) - sunrise = date.replace( - hour=int(self.data["sunrise_time"].strftime("%H")), - minute=int(self.data["sunrise_time"].strftime("%M")), - second=int(self.data["sunrise_time"].strftime("%S")), - microsecond=int(self.data["sunrise_time"].strftime("%f")), - ) - sunset = date.replace( - hour=int(self.data["sunset_time"].strftime("%H")), - minute=int(self.data["sunset_time"].strftime("%M")), - second=int(self.data["sunset_time"].strftime("%S")), - microsecond=int(self.data["sunset_time"].strftime("%f")), - ) + sunrise = date.replace(**self._time_dict("sunrise_time")) + sunset = date.replace(**self._time_dict("sunset_time")) solar_noon = sunrise + (sunset - sunrise) / 2 solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset) / 2 else: @@ -230,23 +228,13 @@ class CircadianLighting(object): if self.data["sunrise_time"] is not None: if date is None: date = dt_now(self.data["timezone"]) - sunrise = date.replace( - hour=int(self.data["sunrise_time"].strftime("%H")), - minute=int(self.data["sunrise_time"].strftime("%M")), - second=int(self.data["sunrise_time"].strftime("%S")), - microsecond=int(self.data["sunrise_time"].strftime("%f")), - ) + sunrise = date.replace(**self._time_dict("sunrise_time")) else: sunrise = location.sunrise(date) if self.data["sunset_time"] is not None: if date is None: date = dt_now(self.data["timezone"]) - sunset = date.replace( - hour=int(self.data["sunset_time"].strftime("%H")), - minute=int(self.data["sunset_time"].strftime("%M")), - second=int(self.data["sunset_time"].strftime("%S")), - microsecond=int(self.data["sunset_time"].strftime("%f")), - ) + sunset = date.replace(**self._time_dict("sunset_time")) else: sunset = location.sunset(date) solar_noon = location.solar_noon(date) From c75b1238d84d6c851fe060b12e056e510fe67842 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 19:12:37 +0200 Subject: [PATCH 0053/1077] simplify track_time_change setup --- .../circadian_lighting/__init__.py | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index b7e196c4..38f48dae 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -164,26 +164,20 @@ class CircadianLighting(object): self.update = Throttle(timedelta(seconds=interval))(self._update) - if self.data["sunrise_time"] is not None: - track_time_change( - self.hass, - self._update, - hour=int(self.data["sunrise_time"].strftime("%H")), - minute=int(self.data["sunrise_time"].strftime("%M")), - second=int(self.data["sunrise_time"].strftime("%S")), - ) - else: - track_sunrise(self.hass, self._update, self.data["sunrise_offset"]) - if self.data["sunset_time"] is not None: - track_time_change( - self.hass, - self._update, - hour=int(self.data["sunset_time"].strftime("%H")), - minute=int(self.data["sunset_time"].strftime("%M")), - second=int(self.data["sunset_time"].strftime("%S")), - ) - else: - track_sunset(self.hass, self._update, self.data["sunset_offset"]) + for which in ["sunrise", "sunrise"]: + time = self.data[f"{which}_time"] + if time is not None: + track_time_change( + self.hass, + self._update, + hour=int(time.strftime("%H")), + minute=int(time.strftime("%M")), + second=int(time.strftime("%S")), + ) + elif which == "sunrise": + track_sunrise(self.hass, self._update, self.data["sunrise_offset"]) + elif which == "sunset": + track_sunset(self.hass, self._update, self.data["sunset_offset"]) def get_timezone(self): from timezonefinder import TimezoneFinder From be0e58c3f01b0e2b8f0405f6c8e6a4cc962b5ca7 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Aug 2020 19:14:02 +0200 Subject: [PATCH 0054/1077] move comments --- custom_components/circadian_lighting/__init__.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 38f48dae..715125c6 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -258,9 +258,8 @@ class CircadianLighting(object): solar_noon_seconds = today_sun_times["solar_noon"].timestamp() solar_midnight_seconds = today_sun_times["solar_midnight"].timestamp() - if ( - now < today_sun_times[SUN_EVENT_SUNRISE] - ): # It's before sunrise (after midnight) + if now < today_sun_times[SUN_EVENT_SUNRISE]: + # It's before sunrise (after midnight) # Because it's before sunrise (and after midnight) sunset must have happend yesterday yesterday_sun_times = self.get_sunrise_sunset(now - timedelta(days=1)) _LOGGER.debug("yesterday_sun_times: " + str(yesterday_sun_times)) @@ -274,9 +273,8 @@ class CircadianLighting(object): solar_midnight_seconds = yesterday_sun_times[ "solar_midnight" ].timestamp() - elif ( - now > today_sun_times[SUN_EVENT_SUNSET] - ): # It's after sunset (before midnight) + elif now > today_sun_times[SUN_EVENT_SUNSET]: + # It's after sunset (before midnight) # Because it's after sunset (and before midnight) sunrise should happen tomorrow tomorrow_sun_times = self.get_sunrise_sunset(now + timedelta(days=1)) _LOGGER.debug("tomorrow_sun_times: " + str(tomorrow_sun_times)) From 2f5b92f80a96a85717a43551815ba686fed361bd Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Aug 2020 22:23:31 +0200 Subject: [PATCH 0055/1077] fix variable d -> service_data --- custom_components/circadian_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 96afdfbd..aae9bad8 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -382,7 +382,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): if which is not None: self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) msg = ", ".join( - [f"{k}: v" for k, v in d.items() if k != ATTR_ENTITY_ID] + [f"{k}: {v}" for k, v in service_data.items() if k != ATTR_ENTITY_ID] ) _LOGGER.debug(f"{light} {which} Adjusted - {msg}") From 6050e991892711ff013004e9a79279181fc71841 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Aug 2020 23:18:04 +0200 Subject: [PATCH 0056/1077] move lines into if statement --- custom_components/circadian_lighting/switch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index aae9bad8..6ee48d0d 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -333,9 +333,6 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): if self._attributes["brightness"] is not None else None ) - mired = int(self.calc_ct()) if self._lights_ct is not None else None - rgb = tuple(map(int, self.calc_rgb())) if self._lights_rgb is not None else None - xy = self.calc_xy() if self._lights_xy is not None else None for light in lights: if not is_on(self.hass, light): @@ -349,6 +346,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): # Set color of array of ct. if self._lights_ct is not None and light in self._lights_ct: which = "CT" + mired = int(self.calc_ct()) if self._lights_ct is not None else None if mired is not None: service_data[ATTR_COLOR_TEMP] = mired if brightness is not None: @@ -357,6 +355,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): # Set color of array of rgb. elif self._lights_rgb is not None and light in self._lights_rgb: which = "RGB" + rgb = tuple(map(int, self.calc_rgb())) if self._lights_rgb is not None else None if rgb is not None: service_data[ATTR_RGB_COLOR] = rgb if brightness is not None: @@ -365,6 +364,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): # Set color of array of xy. elif self._lights_xy is not None and light in self._lights_xy: which = "XY" + xy = self.calc_xy() if self._lights_xy is not None else None if xy is not None: service_data[ATTR_XY_COLOR] = xy if brightness is not None: From 5491c7dcf173a9b34166c1d07475808bc8bb2417 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Aug 2020 23:22:38 +0200 Subject: [PATCH 0057/1077] simplify color settings --- custom_components/circadian_lighting/switch.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 6ee48d0d..9c5f1dd8 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -346,27 +346,21 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): # Set color of array of ct. if self._lights_ct is not None and light in self._lights_ct: which = "CT" - mired = int(self.calc_ct()) if self._lights_ct is not None else None - if mired is not None: - service_data[ATTR_COLOR_TEMP] = mired + service_data[ATTR_COLOR_TEMP] = int(self.calc_ct()) if brightness is not None: service_data[ATTR_BRIGHTNESS] = brightness # Set color of array of rgb. elif self._lights_rgb is not None and light in self._lights_rgb: which = "RGB" - rgb = tuple(map(int, self.calc_rgb())) if self._lights_rgb is not None else None - if rgb is not None: - service_data[ATTR_RGB_COLOR] = rgb + service_data[ATTR_RGB_COLOR] = tuple(map(int, self.calc_rgb())) if brightness is not None: service_data[ATTR_BRIGHTNESS] = brightness # Set color of array of xy. elif self._lights_xy is not None and light in self._lights_xy: which = "XY" - xy = self.calc_xy() if self._lights_xy is not None else None - if xy is not None: - service_data[ATTR_XY_COLOR] = xy + service_data[ATTR_XY_COLOR] = self.calc_xy() if brightness is not None: service_data[ATTR_BRIGHTNESS] = brightness service_data[ATTR_WHITE_VALUE] = brightness From c5e4486f00ff1f47e8d3f93fe222ee473bfa6536 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Aug 2020 23:32:32 +0200 Subject: [PATCH 0058/1077] simplify setting brightness --- .../circadian_lighting/switch.py | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 9c5f1dd8..536ac024 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -328,18 +328,16 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): if transition is None: transition = self._cl.data["transition"] - brightness = ( - int((self._attributes["brightness"] / 100) * 254) - if self._attributes["brightness"] is not None - else None - ) - for light in lights: if not is_on(self.hass, light): continue which = None service_data = {ATTR_ENTITY_ID: light} + if self._attributes["brightness"] is not None: + service_data[ATTR_BRIGHTNESS] = int( + (self._attributes["brightness"] / 100) * 254 + ) if transition is not None: service_data[ATTR_TRANSITION] = transition @@ -347,37 +345,31 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): if self._lights_ct is not None and light in self._lights_ct: which = "CT" service_data[ATTR_COLOR_TEMP] = int(self.calc_ct()) - if brightness is not None: - service_data[ATTR_BRIGHTNESS] = brightness # Set color of array of rgb. elif self._lights_rgb is not None and light in self._lights_rgb: which = "RGB" service_data[ATTR_RGB_COLOR] = tuple(map(int, self.calc_rgb())) - if brightness is not None: - service_data[ATTR_BRIGHTNESS] = brightness # Set color of array of xy. elif self._lights_xy is not None and light in self._lights_xy: which = "XY" service_data[ATTR_XY_COLOR] = self.calc_xy() - if brightness is not None: - service_data[ATTR_BRIGHTNESS] = brightness - service_data[ATTR_WHITE_VALUE] = brightness + if service_data.get(ATTR_BRIGHTNESS, False): + service_data[ATTR_WHITE_VALUE] = service_data[ATTR_BRIGHTNESS] # Set color of array of brightness. elif ( self._lights_brightness is not None and light in self._lights_brightness ): which = "Brightness" - if brightness is not None: - service_data[ATTR_BRIGHTNESS] = brightness if which is not None: self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - msg = ", ".join( - [f"{k}: {v}" for k, v in service_data.items() if k != ATTR_ENTITY_ID] - ) + key_value_strings = [ + f"{k}: {v}" for k, v in service_data.items() if k != ATTR_ENTITY_ID + ] + msg = ", ".join(key_value_strings) _LOGGER.debug(f"{light} {which} Adjusted - {msg}") def light_state_changed(self, entity_id, from_state, to_state): From b7daa6c05c9617b890475941811304fb3d8d3572 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Aug 2020 23:35:22 +0200 Subject: [PATCH 0059/1077] make light_* empty lists instead of None --- .../circadian_lighting/switch.py | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 536ac024..e64ce37b 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -110,10 +110,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass, cl, name=config.get(CONF_NAME), - lights_ct=config.get(CONF_LIGHTS_CT), - lights_rgb=config.get(CONF_LIGHTS_RGB), - lights_xy=config.get(CONF_LIGHTS_XY), - lights_brightness=config.get(CONF_LIGHTS_BRIGHT), + lights_ct=config.get(CONF_LIGHTS_CT, []), + lights_rgb=config.get(CONF_LIGHTS_RGB, []), + lights_xy=config.get(CONF_LIGHTS_XY, []), + lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []), disable_brightness_adjust=config.get(CONF_DISABLE_BRIGHTNESS_ADJUST), min_brightness=config.get(CONF_MIN_BRIGHT), max_brightness=config.get(CONF_MAX_BRIGHT), @@ -183,15 +183,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._initial_transition = initial_transition self._attributes = {"hs_color": self._hs_color, "brightness": None} - self._lights = [] - if lights_ct is not None: - self._lights.extend(lights_ct) - if lights_rgb is not None: - self._lights.extend(lights_rgb) - if lights_xy is not None: - self._lights.extend(lights_xy) - if lights_brightness is not None: - self._lights.extend(lights_brightness) + self._lights = lights_ct + lights_rgb + lights_xy + lights_brightness # Register callbacks dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self.update_switch) @@ -342,26 +334,24 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_TRANSITION] = transition # Set color of array of ct. - if self._lights_ct is not None and light in self._lights_ct: + if light in self._lights_ct: which = "CT" service_data[ATTR_COLOR_TEMP] = int(self.calc_ct()) # Set color of array of rgb. - elif self._lights_rgb is not None and light in self._lights_rgb: + elif light in self._lights_rgb: which = "RGB" service_data[ATTR_RGB_COLOR] = tuple(map(int, self.calc_rgb())) # Set color of array of xy. - elif self._lights_xy is not None and light in self._lights_xy: + elif light in self._lights_xy: which = "XY" service_data[ATTR_XY_COLOR] = self.calc_xy() if service_data.get(ATTR_BRIGHTNESS, False): service_data[ATTR_WHITE_VALUE] = service_data[ATTR_BRIGHTNESS] # Set color of array of brightness. - elif ( - self._lights_brightness is not None and light in self._lights_brightness - ): + elif light in self._lights_brightness: which = "Brightness" if which is not None: From 88d355561422d1df6f402f0e397a829a05126d1c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Aug 2020 23:40:59 +0200 Subject: [PATCH 0060/1077] assign brightness variable --- custom_components/circadian_lighting/switch.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index e64ce37b..080f14d9 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -326,10 +326,9 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): which = None service_data = {ATTR_ENTITY_ID: light} - if self._attributes["brightness"] is not None: - service_data[ATTR_BRIGHTNESS] = int( - (self._attributes["brightness"] / 100) * 254 - ) + brightness = self._attributes["brightness"] + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = int((brightness / 100) * 254) if transition is not None: service_data[ATTR_TRANSITION] = transition From 7ef47b19c8b285444dddd0f2ff4878fab02a551c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Aug 2020 23:47:28 +0200 Subject: [PATCH 0061/1077] which can never not be set --- custom_components/circadian_lighting/switch.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 080f14d9..e3fc2fea 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -324,7 +324,6 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): if not is_on(self.hass, light): continue - which = None service_data = {ATTR_ENTITY_ID: light} brightness = self._attributes["brightness"] if brightness is not None: @@ -353,13 +352,12 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): elif light in self._lights_brightness: which = "Brightness" - if which is not None: - self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - key_value_strings = [ - f"{k}: {v}" for k, v in service_data.items() if k != ATTR_ENTITY_ID - ] - msg = ", ".join(key_value_strings) - _LOGGER.debug(f"{light} {which} Adjusted - {msg}") + self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) + key_value_strings = [ + f"{k}: {v}" for k, v in service_data.items() if k != ATTR_ENTITY_ID + ] + msg = ", ".join(key_value_strings) + _LOGGER.debug(f"{light} {which} Adjusted - {msg}") def light_state_changed(self, entity_id, from_state, to_state): with suppress(Exception): From 2f9a6d7706cf526716095da9b8f463f81ad1b8b9 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 00:06:34 +0200 Subject: [PATCH 0062/1077] implement once_only and remove _attributes --- .../circadian_lighting/switch.py | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index e3fc2fea..e3b8f951 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -6,6 +6,7 @@ DEPENDENCIES = ["circadian_lighting", "light"] import logging from contextlib import suppress +from typing import Optional import homeassistant.helpers.config_validation as cv import voluptuous as vol @@ -69,6 +70,7 @@ CONF_DISABLE_ENTITY = "disable_entity" CONF_DISABLE_STATE = "disable_state" CONF_INITIAL_TRANSITION = "initial_transition" DEFAULT_INITIAL_TRANSITION = 1 +CONF_ONCE_ONLY = "once_only" PLATFORM_SCHEMA = vol.Schema( { @@ -98,6 +100,7 @@ PLATFORM_SCHEMA = vol.Schema( vol.Optional( CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION ): VALID_TRANSITION, + vol.Optional(CONF_ONCE_ONLY): cv.bool, } ) @@ -124,13 +127,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): disable_entity=config.get(CONF_DISABLE_ENTITY), disable_state=config.get(CONF_DISABLE_STATE), initial_transition=config.get(CONF_INITIAL_TRANSITION), + once_only=config.get(CONF_ONCE_ONLY), ) add_devices([cs]) - def update(call=None): - """Update lights.""" - cs.update_switch() - return True else: return False @@ -158,6 +158,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): disable_entity, disable_state, initial_transition, + once_only, ): """Initialize the Circadian Lighting switch.""" self.hass = hass @@ -181,12 +182,12 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._disable_entity = disable_entity self._disable_state = disable_state self._initial_transition = initial_transition - self._attributes = {"hs_color": self._hs_color, "brightness": None} + self._once_only = once_only self._lights = lights_ct + lights_rgb + lights_xy + lights_brightness # Register callbacks - dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self.update_switch) + dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self._update_switch) track_state_change(hass, self._lights, self.light_state_changed) if self._sleep_entity is not None: track_state_change(hass, self._sleep_entity, self.sleep_state_changed) @@ -230,14 +231,14 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): @property def device_state_attributes(self): """Return the attributes of the switch.""" - return self._attributes + return {"hs_color": self._hs_color, "brightness": self._brightness} def turn_on(self, **kwargs): """Turn on circadian lighting.""" self._state = True # Make initial update - self.update_switch(self._initial_transition) + self._update_switch(self._initial_transition, force=True) self.schedule_update_ha_state() @@ -246,8 +247,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._state = False self.schedule_update_ha_state() self._hs_color = None - self._attributes["hs_color"] = self._hs_color - self._attributes["brightness"] = None + self._brightness = None def is_sleep(self): is_sleep = ( @@ -283,16 +283,16 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): elif self._cl.data["percent"] > 0: return self._max_brightness else: - return ( - (self._max_brightness - self._min_brightness) - * ((100 + self._cl.data["percent"]) / 100) - ) + self._min_brightness + delta_brightness = self._max_brightness - self._min_brightness + procent = (100 + self._cl.data["percent"]) / 100 + return (delta_brightness * procent) + self._min_brightness - def update_switch(self, transition=None): + def _update_switch(self, transition=None, force=False): + if self._once_only and not force: + return if self._cl.data is not None: self._hs_color = self.calc_hs() - self._attributes["hs_color"] = self._hs_color - self._attributes["brightness"] = self.calc_brightness() + self._brightness = self.calc_brightness() _LOGGER.debug(f"{self._name} Switch Updated") self.adjust_lights(self._lights, transition) @@ -325,7 +325,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): continue service_data = {ATTR_ENTITY_ID: light} - brightness = self._attributes["brightness"] + brightness = self._brightness if brightness is not None: service_data[ATTR_BRIGHTNESS] = int((brightness / 100) * 254) if transition is not None: @@ -372,10 +372,10 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): to_state.state == self._sleep_state or from_state.state == self._sleep_state ): - self.update_switch(self._initial_transition) + self._update_switch(self._initial_transition, force=True) def disable_state_changed(self, entity_id, from_state, to_state): with suppress(Exception): _LOGGER.debug("{entity_id} change from {from_state} to {to_state}") if from_state.state == self._disable_state: - self.update_switch(self._initial_transition) + self._update_switch(self._initial_transition, force=True) From dfa0f8cc609d6f1500c3ff253f86c4ffebf80d6b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 00:15:32 +0200 Subject: [PATCH 0063/1077] renames --- custom_components/circadian_lighting/__init__.py | 6 ++---- custom_components/circadian_lighting/sensor.py | 3 +-- custom_components/circadian_lighting/switch.py | 9 ++++----- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 715125c6..d4398cc8 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -57,9 +57,7 @@ VERSION = "1.0.13" _LOGGER = logging.getLogger(__name__) DOMAIN = "circadian_lighting" -CIRCADIAN_LIGHTING_PLATFORMS = ["sensor", "switch"] -CIRCADIAN_LIGHTING_UPDATE_TOPIC = "{}_update".format(DOMAIN) -DATA_CIRCADIAN_LIGHTING = "data_cl" +CIRCADIAN_LIGHTING_UPDATE_TOPIC = f"{DOMAIN}_update" CONF_MIN_CT = "min_colortemp" DEFAULT_MIN_CT = 2500 @@ -106,7 +104,7 @@ def setup(hass, config): conf = config[DOMAIN] load_platform(hass, "sensor", DOMAIN, {}, config) - hass.data[DATA_CIRCADIAN_LIGHTING] = CircadianLighting( + hass.data[DOMAIN] = CircadianLighting( hass, min_colortemp=conf.get(CONF_MIN_CT), max_colortemp=conf.get(CONF_MAX_CT), diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py index dc6cd738..0d3016c1 100644 --- a/custom_components/circadian_lighting/sensor.py +++ b/custom_components/circadian_lighting/sensor.py @@ -12,7 +12,6 @@ from homeassistant.helpers.entity import Entity from custom_components.circadian_lighting import ( CIRCADIAN_LIGHTING_UPDATE_TOPIC, - DATA_CIRCADIAN_LIGHTING, DOMAIN, ) @@ -23,7 +22,7 @@ ICON = "mdi:theme-light-dark" def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Circadian Lighting sensor.""" - cl = hass.data.get(DATA_CIRCADIAN_LIGHTING) + cl = hass.data.get(DOMAIN) if cl: cs = CircadianSensor(hass, cl) add_devices([cs]) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index e3b8f951..189c30fd 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -6,7 +6,6 @@ DEPENDENCIES = ["circadian_lighting", "light"] import logging from contextlib import suppress -from typing import Optional import homeassistant.helpers.config_validation as cv import voluptuous as vol @@ -40,7 +39,7 @@ from homeassistant.util.color import ( from custom_components.circadian_lighting import ( CIRCADIAN_LIGHTING_UPDATE_TOPIC, - DATA_CIRCADIAN_LIGHTING, + DOMAIN, ) try: @@ -107,9 +106,9 @@ PLATFORM_SCHEMA = vol.Schema( def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Circadian Lighting switches.""" - cl = hass.data.get(DATA_CIRCADIAN_LIGHTING) + cl = hass.data.get(DOMAIN) if cl: - cs = CircadianSwitch( + switch = CircadianSwitch( hass, cl, name=config.get(CONF_NAME), @@ -129,7 +128,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): initial_transition=config.get(CONF_INITIAL_TRANSITION), once_only=config.get(CONF_ONCE_ONLY), ) - add_devices([cs]) + add_devices([switch]) return True else: From d4551746c62cb93811db2e32643c1e93910d7b45 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 00:20:25 +0200 Subject: [PATCH 0064/1077] do not define a .data dict --- .../circadian_lighting/__init__.py | 88 +++++++++---------- .../circadian_lighting/sensor.py | 20 ++--- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index d4398cc8..7207fa2c 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -142,23 +142,23 @@ class CircadianLighting(object): ): self.hass = hass self.data = {} - self.data["min_colortemp"] = min_colortemp - self.data["max_colortemp"] = max_colortemp - self.data["sunrise_offset"] = sunrise_offset - self.data["sunset_offset"] = sunset_offset + self._min_colortemp = min_colortemp + self._max_colortemp = max_colortemp + self._sunrise_offset = sunrise_offset + self._sunset_offset = sunset_offset self.data["sunrise_time"] = sunrise_time self.data["sunset_time"] = sunset_time - self.data["latitude"] = latitude - self.data["longitude"] = longitude - self.data["elevation"] = elevation - self.data["interval"] = interval - self.data["transition"] = transition - self.data["timezone"] = self.get_timezone() - self.data["percent"] = self.calc_percent() - self.data["colortemp"] = self.calc_colortemp() - self.data["rgb_color"] = self.calc_rgb() - self.data["xy_color"] = self.calc_xy() - self.data["hs_color"] = self.calc_hs() + self._latitude = latitude + self._longitude = longitude + self._elevation = elevation + self._interval = interval + self._transition = transition + self._timezone = self.get_timezone() + self._percent = self.calc_percent() + self._colortemp = self.calc_colortemp() + self._rgb_color = self.calc_rgb() + self._xy_color = self.calc_xy() + self._hs_color = self.calc_hs() self.update = Throttle(timedelta(seconds=interval))(self._update) @@ -173,16 +173,16 @@ class CircadianLighting(object): second=int(time.strftime("%S")), ) elif which == "sunrise": - track_sunrise(self.hass, self._update, self.data["sunrise_offset"]) + track_sunrise(self.hass, self._update, self._sunrise_offset) elif which == "sunset": - track_sunset(self.hass, self._update, self.data["sunset_offset"]) + track_sunset(self.hass, self._update, self._sunset_offset) def get_timezone(self): from timezonefinder import TimezoneFinder tf = TimezoneFinder() timezone_string = tf.timezone_at( - lng=self.data["longitude"], lat=self.data["latitude"] + lng=self._longitude, lat=self._latitude ) timezone = get_time_zone(timezone_string) _LOGGER.debug("Timezone: " + str(timezone)) @@ -202,7 +202,7 @@ class CircadianLighting(object): and self.data["sunset_time"] is not None ): if date is None: - date = dt_now(self.data["timezone"]) + date = dt_now(self._timezone) sunrise = date.replace(**self._time_dict("sunrise_time")) sunset = date.replace(**self._time_dict("sunset_time")) solar_noon = sunrise + (sunset - sunrise) / 2 @@ -213,37 +213,37 @@ class CircadianLighting(object): location = astral.Location() location.name = "name" location.region = "region" - location.latitude = self.data["latitude"] - location.longitude = self.data["longitude"] - location.elevation = self.data["elevation"] + location.latitude = self._latitude + location.longitude = self._longitude + location.elevation = self._elevation _LOGGER.debug("Astral location: " + str(location)) if self.data["sunrise_time"] is not None: if date is None: - date = dt_now(self.data["timezone"]) + date = dt_now(self._timezone) sunrise = date.replace(**self._time_dict("sunrise_time")) else: sunrise = location.sunrise(date) if self.data["sunset_time"] is not None: if date is None: - date = dt_now(self.data["timezone"]) + date = dt_now(self._timezone) sunset = date.replace(**self._time_dict("sunset_time")) else: sunset = location.sunset(date) solar_noon = location.solar_noon(date) solar_midnight = location.solar_midnight(date) - if self.data["sunrise_offset"] is not None: - sunrise = sunrise + self.data["sunrise_offset"] - if self.data["sunset_offset"] is not None: - sunset = sunset + self.data["sunset_offset"] + if self._sunrise_offset is not None: + sunrise = sunrise + self._sunrise_offset + if self._sunset_offset is not None: + sunset = sunset + self._sunset_offset return { - SUN_EVENT_SUNRISE: sunrise.astimezone(self.data["timezone"]), - SUN_EVENT_SUNSET: sunset.astimezone(self.data["timezone"]), - "solar_noon": solar_noon.astimezone(self.data["timezone"]), - "solar_midnight": solar_midnight.astimezone(self.data["timezone"]), + SUN_EVENT_SUNRISE: sunrise.astimezone(self._timezone), + SUN_EVENT_SUNSET: sunset.astimezone(self._timezone), + "solar_noon": solar_noon.astimezone(self._timezone), + "solar_midnight": solar_midnight.astimezone(self._timezone), } def calc_percent(self): - now = dt_now(self.data["timezone"]) + now = dt_now(self._timezone) _LOGGER.debug("now: " + str(now)) today_sun_times = self.get_sunrise_sunset(now) @@ -334,16 +334,16 @@ class CircadianLighting(object): return percentage def calc_colortemp(self): - if self.data["percent"] > 0: + if self._percent > 0: return ( - (self.data["max_colortemp"] - self.data["min_colortemp"]) - * (self.data["percent"] / 100) - ) + self.data["min_colortemp"] + (self._max_colortemp - self._min_colortemp) + * (self._percent / 100) + ) + self._min_colortemp else: - return self.data["min_colortemp"] + return self._min_colortemp def calc_rgb(self): - return color_temperature_to_rgb(self.data["colortemp"]) + return color_temperature_to_rgb(self._colortemp) def calc_xy(self): rgb = self.calc_rgb() @@ -362,10 +362,10 @@ class CircadianLighting(object): def _update(self, *args, **kwargs): """Update Circadian Values.""" - self.data["percent"] = self.calc_percent() - self.data["colortemp"] = self.calc_colortemp() - self.data["rgb_color"] = self.calc_rgb() - self.data["xy_color"] = self.calc_xy() - self.data["hs_color"] = self.calc_hs() + self._percent = self.calc_percent() + self._colortemp = self.calc_colortemp() + self._rgb_color = self.calc_rgb() + self._xy_color = self.calc_xy() + self._hs_color = self.calc_hs() dispatcher_send(self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC) _LOGGER.debug("Circadian Lighting Component Updated") diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py index 0d3016c1..6cad713d 100644 --- a/custom_components/circadian_lighting/sensor.py +++ b/custom_components/circadian_lighting/sensor.py @@ -46,14 +46,14 @@ class CircadianSensor(Entity): self._cl = cl self._name = "Circadian Values" self._entity_id = "sensor.circadian_values" - self._state = self._cl.data["percent"] + self._state = self._cl._percent self._unit_of_measurement = "%" self._icon = ICON - self._hs_color = self._cl.data["hs_color"] + self._hs_color = self._cl._hs_color self._attributes = {} - self._attributes["colortemp"] = self._cl.data["colortemp"] - self._attributes["rgb_color"] = self._cl.data["rgb_color"] - self._attributes["xy_color"] = self._cl.data["xy_color"] + self._attributes["colortemp"] = self._cl._colortemp + self._attributes["rgb_color"] = self._cl._rgb_color + self._attributes["xy_color"] = self._cl._xy_color """Register callbacks.""" dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self.update_sensor) @@ -101,9 +101,9 @@ class CircadianSensor(Entity): def update_sensor(self): if self._cl.data is not None: - self._state = self._cl.data["percent"] - self._hs_color = self._cl.data["hs_color"] - self._attributes["colortemp"] = self._cl.data["colortemp"] - self._attributes["rgb_color"] = self._cl.data["rgb_color"] - self._attributes["xy_color"] = self._cl.data["xy_color"] + self._state = self._cl._percent + self._hs_color = self._cl._hs_color + self._attributes["colortemp"] = self._cl._colortemp + self._attributes["rgb_color"] = self._cl._rgb_color + self._attributes["xy_color"] = self._cl._xy_color _LOGGER.debug("Circadian Lighting Sensor Updated") From a59b03eb6781f6726867ce33e3cf3fc23d5b60c4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 00:22:32 +0200 Subject: [PATCH 0065/1077] remove ._attributes --- .../circadian_lighting/sensor.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py index 6cad713d..f3e98113 100644 --- a/custom_components/circadian_lighting/sensor.py +++ b/custom_components/circadian_lighting/sensor.py @@ -50,12 +50,11 @@ class CircadianSensor(Entity): self._unit_of_measurement = "%" self._icon = ICON self._hs_color = self._cl._hs_color - self._attributes = {} - self._attributes["colortemp"] = self._cl._colortemp - self._attributes["rgb_color"] = self._cl._rgb_color - self._attributes["xy_color"] = self._cl._xy_color + self._colortemp = self._cl._colortemp + self._rgb_color = self._cl._rgb_color + self._xy_color = self._cl._xy_color - """Register callbacks.""" + # Register callbacks dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self.update_sensor) @property @@ -90,7 +89,11 @@ class CircadianSensor(Entity): @property def device_state_attributes(self): """Return the attributes of the sensor.""" - return self._attributes + return { + "colortemp": self._cl._colortemp, + "rgb_color": self._cl._rgb_color, + "xy_color": self._cl._xy_color, + } def update(self): """Fetch new state data for the sensor. @@ -103,7 +106,7 @@ class CircadianSensor(Entity): if self._cl.data is not None: self._state = self._cl._percent self._hs_color = self._cl._hs_color - self._attributes["colortemp"] = self._cl._colortemp - self._attributes["rgb_color"] = self._cl._rgb_color - self._attributes["xy_color"] = self._cl._xy_color + self._colortemp = self._cl._colortemp + self._rgb_color = self._cl._rgb_color + self._xy_color = self._cl._xy_color _LOGGER.debug("Circadian Lighting Sensor Updated") From 5ff9be00905ba1bfb8cc6c8899cd71b89a21271b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 00:26:19 +0200 Subject: [PATCH 0066/1077] create ._time dict --- .../circadian_lighting/__init__.py | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 7207fa2c..934c31cb 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -141,13 +141,14 @@ class CircadianLighting(object): transition, ): self.hass = hass - self.data = {} self._min_colortemp = min_colortemp self._max_colortemp = max_colortemp self._sunrise_offset = sunrise_offset self._sunset_offset = sunset_offset - self.data["sunrise_time"] = sunrise_time - self.data["sunset_time"] = sunset_time + self._time = { + "sunrise": sunrise_time, + "sunset": sunset_time, + } self._latitude = latitude self._longitude = longitude self._elevation = elevation @@ -163,7 +164,7 @@ class CircadianLighting(object): self.update = Throttle(timedelta(seconds=interval))(self._update) for which in ["sunrise", "sunrise"]: - time = self.data[f"{which}_time"] + time = self._time[which] if time is not None: track_time_change( self.hass, @@ -181,30 +182,28 @@ class CircadianLighting(object): from timezonefinder import TimezoneFinder tf = TimezoneFinder() - timezone_string = tf.timezone_at( - lng=self._longitude, lat=self._latitude - ) + timezone_string = tf.timezone_at(lng=self._longitude, lat=self._latitude) timezone = get_time_zone(timezone_string) _LOGGER.debug("Timezone: " + str(timezone)) return timezone def _time_dict(self, key): return dict( - hour=int(self.data[key].strftime("%H")), - minute=int(self.data[key].strftime("%M")), - second=int(self.data[key].strftime("%S")), - microsecond=int(self.data[key].strftime("%f")), + hour=int(self._time[key].strftime("%H")), + minute=int(self._time[key].strftime("%M")), + second=int(self._time[key].strftime("%S")), + microsecond=int(self._time[key].strftime("%f")), ) def get_sunrise_sunset(self, date=None): if ( - self.data["sunrise_time"] is not None - and self.data["sunset_time"] is not None + self._time["sunrise"] is not None + and self._time["sunset"] is not None ): if date is None: date = dt_now(self._timezone) - sunrise = date.replace(**self._time_dict("sunrise_time")) - sunset = date.replace(**self._time_dict("sunset_time")) + sunrise = date.replace(**self._time_dict("sunrise")) + sunset = date.replace(**self._time_dict("sunset")) solar_noon = sunrise + (sunset - sunrise) / 2 solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset) / 2 else: @@ -217,16 +216,16 @@ class CircadianLighting(object): location.longitude = self._longitude location.elevation = self._elevation _LOGGER.debug("Astral location: " + str(location)) - if self.data["sunrise_time"] is not None: + if self._time["sunrise"] is not None: if date is None: date = dt_now(self._timezone) - sunrise = date.replace(**self._time_dict("sunrise_time")) + sunrise = date.replace(**self._time_dict("sunrise")) else: sunrise = location.sunrise(date) - if self.data["sunset_time"] is not None: + if self._time["sunset"] is not None: if date is None: date = dt_now(self._timezone) - sunset = date.replace(**self._time_dict("sunset_time")) + sunset = date.replace(**self._time_dict("sunset")) else: sunset = location.sunset(date) solar_noon = location.solar_noon(date) @@ -336,8 +335,7 @@ class CircadianLighting(object): def calc_colortemp(self): if self._percent > 0: return ( - (self._max_colortemp - self._min_colortemp) - * (self._percent / 100) + (self._max_colortemp - self._min_colortemp) * (self._percent / 100) ) + self._min_colortemp else: return self._min_colortemp From 875542650e5e324e7766f923354eb11c148b7f91 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 00:39:41 +0200 Subject: [PATCH 0067/1077] more simplifications and renames --- .../circadian_lighting/__init__.py | 26 +++-------- .../circadian_lighting/sensor.py | 45 +++++++++---------- .../circadian_lighting/switch.py | 33 +++++++------- 3 files changed, 45 insertions(+), 59 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 934c31cb..5492511e 100644 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -122,7 +122,7 @@ def setup(hass, config): return True -class CircadianLighting(object): +class CircadianLighting: """Calculate universal Circadian values.""" def __init__( @@ -196,10 +196,7 @@ class CircadianLighting(object): ) def get_sunrise_sunset(self, date=None): - if ( - self._time["sunrise"] is not None - and self._time["sunset"] is not None - ): + if self._time["sunrise"] is not None and self._time["sunset"] is not None: if date is None: date = dt_now(self._timezone) sunrise = date.replace(**self._time_dict("sunrise")) @@ -334,9 +331,9 @@ class CircadianLighting(object): def calc_colortemp(self): if self._percent > 0: - return ( - (self._max_colortemp - self._min_colortemp) * (self._percent / 100) - ) + self._min_colortemp + delta = self._max_colortemp - self._min_colortemp + percent = self._percent / 100 + return (delta * percent) + self._min_colortemp else: return self._min_colortemp @@ -344,19 +341,10 @@ class CircadianLighting(object): return color_temperature_to_rgb(self._colortemp) def calc_xy(self): - rgb = self.calc_rgb() - iR = rgb[0] - iG = rgb[1] - iB = rgb[2] - - return color_RGB_to_xy(iR, iG, iB) + return color_RGB_to_xy(*self.calc_rgb()) def calc_hs(self): - xy = self.calc_xy() - vX = xy[0] - vY = xy[1] - - return color_xy_to_hs(vX, vY) + return color_xy_to_hs(*self.calc_xy()) def _update(self, *args, **kwargs): """Update Circadian Values.""" diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py index f3e98113..e5f67282 100644 --- a/custom_components/circadian_lighting/sensor.py +++ b/custom_components/circadian_lighting/sensor.py @@ -22,14 +22,14 @@ ICON = "mdi:theme-light-dark" def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Circadian Lighting sensor.""" - cl = hass.data.get(DOMAIN) - if cl: - cs = CircadianSensor(hass, cl) - add_devices([cs]) + circadian_lighting = hass.data.get(DOMAIN) + if circadian_lighting is not None: + sensor = CircadianSensor(hass, circadian_lighting) + add_devices([sensor]) def update(call=None): """Update component.""" - cl._update() + circadian_lighting._update() service_name = "values_update" hass.services.register(DOMAIN, service_name, update) @@ -41,18 +41,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class CircadianSensor(Entity): """Representation of a Circadian Lighting sensor.""" - def __init__(self, hass, cl): + def __init__(self, hass, circadian_lighting): """Initialize the Circadian Lighting sensor.""" - self._cl = cl + self._circadian_lighting = circadian_lighting self._name = "Circadian Values" self._entity_id = "sensor.circadian_values" - self._state = self._cl._percent + self._state = self._circadian_lighting._percent self._unit_of_measurement = "%" self._icon = ICON - self._hs_color = self._cl._hs_color - self._colortemp = self._cl._colortemp - self._rgb_color = self._cl._rgb_color - self._xy_color = self._cl._xy_color + self._hs_color = self._circadian_lighting._hs_color + self._colortemp = self._circadian_lighting._colortemp + self._rgb_color = self._circadian_lighting._rgb_color + self._xy_color = self._circadian_lighting._xy_color # Register callbacks dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self.update_sensor) @@ -90,9 +90,9 @@ class CircadianSensor(Entity): def device_state_attributes(self): """Return the attributes of the sensor.""" return { - "colortemp": self._cl._colortemp, - "rgb_color": self._cl._rgb_color, - "xy_color": self._cl._xy_color, + "colortemp": self._circadian_lighting._colortemp, + "rgb_color": self._circadian_lighting._rgb_color, + "xy_color": self._circadian_lighting._xy_color, } def update(self): @@ -100,13 +100,12 @@ class CircadianSensor(Entity): This is the only method that should fetch new data for Home Assistant. """ - self._cl.update() + self._circadian_lighting.update() def update_sensor(self): - if self._cl.data is not None: - self._state = self._cl._percent - self._hs_color = self._cl._hs_color - self._colortemp = self._cl._colortemp - self._rgb_color = self._cl._rgb_color - self._xy_color = self._cl._xy_color - _LOGGER.debug("Circadian Lighting Sensor Updated") + self._state = self._circadian_lighting._percent + self._hs_color = self._circadian_lighting._hs_color + self._colortemp = self._circadian_lighting._colortemp + self._rgb_color = self._circadian_lighting._rgb_color + self._xy_color = self._circadian_lighting._xy_color + _LOGGER.debug("Circadian Lighting Sensor Updated") diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 189c30fd..93dd4457 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -106,11 +106,11 @@ PLATFORM_SCHEMA = vol.Schema( def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Circadian Lighting switches.""" - cl = hass.data.get(DOMAIN) - if cl: + circadian_lighting = hass.data.get(DOMAIN) + if circadian_lighting is not None: switch = CircadianSwitch( hass, - cl, + circadian_lighting, name=config.get(CONF_NAME), lights_ct=config.get(CONF_LIGHTS_CT, []), lights_rgb=config.get(CONF_LIGHTS_RGB, []), @@ -141,7 +141,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def __init__( self, hass, - cl, + circadian_lighting, name, lights_ct, lights_rgb, @@ -161,7 +161,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): ): """Initialize the Circadian Lighting switch.""" self.hass = hass - self._cl = cl + self._circadian_lighting = circadian_lighting self._name = name self._entity_id = "switch." + slugify(f"circadian_lighting {name}") self._state = None @@ -260,7 +260,11 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): @property def _color_temperature(self): - return self._sleep_colortemp if self.is_sleep() else self._cl.data["colortemp"] + return ( + self._sleep_colortemp + if self.is_sleep() + else self._circadian_lighting._colortemp + ) def calc_ct(self): return color_temperature_kelvin_to_mired(self._color_temperature) @@ -279,30 +283,25 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): return None elif self.is_sleep(): return self._sleep_brightness - elif self._cl.data["percent"] > 0: + elif self._circadian_lighting._percent > 0: return self._max_brightness else: delta_brightness = self._max_brightness - self._min_brightness - procent = (100 + self._cl.data["percent"]) / 100 + procent = (100 + self._circadian_lighting._percent) / 100 return (delta_brightness * procent) + self._min_brightness def _update_switch(self, transition=None, force=False): if self._once_only and not force: return - if self._cl.data is not None: - self._hs_color = self.calc_hs() - self._brightness = self.calc_brightness() - _LOGGER.debug(f"{self._name} Switch Updated") - + self._hs_color = self.calc_hs() + self._brightness = self.calc_brightness() + _LOGGER.debug(f"{self._name} Switch Updated") self.adjust_lights(self._lights, transition) def should_adjust(self): if self._state is not True: _LOGGER.debug(f"{self._name} off - not adjusting") return False - elif self._cl.data is None: - _LOGGER.debug(f"{self._name} could not retrieve Circadian Lighting data") - return False elif ( self._disable_entity is not None and self.hass.states.get(self._disable_entity).state == self._disable_state @@ -317,7 +316,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): return if transition is None: - transition = self._cl.data["transition"] + transition = self._circadian_lighting._transition for light in lights: if not is_on(self.hass, light): From b9ca03be9cba2df303ca166a2d222077a14ecf86 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 00:40:53 +0200 Subject: [PATCH 0068/1077] simplify self._entity_id --- custom_components/circadian_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 93dd4457..256289c3 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -163,7 +163,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self.hass = hass self._circadian_lighting = circadian_lighting self._name = name - self._entity_id = "switch." + slugify(f"circadian_lighting {name}") + self._entity_id = f"switch.circadian_lighting_{slugify(name)}" self._state = None self._icon = ICON self._hs_color = None From 1150c586585131a434851d8d7c5b2b88bff0de00 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 00:57:46 +0200 Subject: [PATCH 0069/1077] Fix syntax --- custom_components/circadian_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 256289c3..987ee748 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -99,7 +99,7 @@ PLATFORM_SCHEMA = vol.Schema( vol.Optional( CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION ): VALID_TRANSITION, - vol.Optional(CONF_ONCE_ONLY): cv.bool, + vol.Optional(CONF_ONCE_ONLY): cv.boolean, } ) From c1af45d46a79734a48170a3d7b321577ca4bb29d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 08:36:04 +0200 Subject: [PATCH 0070/1077] set self._brightness in __init__ --- custom_components/circadian_lighting/switch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 987ee748..bb90bce8 100644 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -167,6 +167,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._state = None self._icon = ICON self._hs_color = None + self._brightness = None self._lights_ct = lights_ct self._lights_rgb = lights_rgb self._lights_xy = lights_xy From d9deee9e52eaa342a152de2fe3a6376402bb23bc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 08:37:04 +0200 Subject: [PATCH 0071/1077] simplify calc_percent --- .../circadian_lighting/__init__.py | 104 +++++++++--------- .../circadian_lighting/sensor.py | 0 .../circadian_lighting/switch.py | 23 ++-- 3 files changed, 59 insertions(+), 68 deletions(-) mode change 100644 => 100755 custom_components/circadian_lighting/__init__.py mode change 100644 => 100755 custom_components/circadian_lighting/sensor.py mode change 100644 => 100755 custom_components/circadian_lighting/switch.py diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py old mode 100644 new mode 100755 index 5492511e..5d12a934 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -58,6 +58,8 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "circadian_lighting" CIRCADIAN_LIGHTING_UPDATE_TOPIC = f"{DOMAIN}_update" +SUN_EVENT_NOON = "solar_noon" +SUN_EVENT_MIDNIGHT = "solar_midnight" CONF_MIN_CT = "min_colortemp" DEFAULT_MIN_CT = 2500 @@ -195,7 +197,7 @@ class CircadianLighting: microsecond=int(self._time[key].strftime("%f")), ) - def get_sunrise_sunset(self, date=None): + def get_sunrise_sunset(self, date=None, as_timestamps=False): if self._time["sunrise"] is not None and self._time["sunset"] is not None: if date is None: date = dt_now(self._timezone) @@ -231,65 +233,57 @@ class CircadianLighting: sunrise = sunrise + self._sunrise_offset if self._sunset_offset is not None: sunset = sunset + self._sunset_offset - return { + datetimes = { SUN_EVENT_SUNRISE: sunrise.astimezone(self._timezone), SUN_EVENT_SUNSET: sunset.astimezone(self._timezone), - "solar_noon": solar_noon.astimezone(self._timezone), - "solar_midnight": solar_midnight.astimezone(self._timezone), + SUN_EVENT_NOON: solar_noon.astimezone(self._timezone), + SUN_EVENT_MIDNIGHT: solar_midnight.astimezone(self._timezone), } + if as_timestamps: + return {k: dt.timestamp() for k, dt in datetimes.items()} + else: + return datetimes def calc_percent(self): now = dt_now(self._timezone) _LOGGER.debug("now: " + str(now)) - today_sun_times = self.get_sunrise_sunset(now) - _LOGGER.debug("today_sun_times: " + str(today_sun_times)) + today = self.get_sunrise_sunset(now, as_timestamps=True) + _LOGGER.debug("today: " + str(today)) # Convert everything to epoch timestamps for easy calculation - now_seconds = now.timestamp() - sunrise_seconds = today_sun_times[SUN_EVENT_SUNRISE].timestamp() - sunset_seconds = today_sun_times[SUN_EVENT_SUNSET].timestamp() - solar_noon_seconds = today_sun_times["solar_noon"].timestamp() - solar_midnight_seconds = today_sun_times["solar_midnight"].timestamp() + now_ts = now.timestamp() - if now < today_sun_times[SUN_EVENT_SUNRISE]: + if now_ts < today[SUN_EVENT_SUNRISE]: # It's before sunrise (after midnight) # Because it's before sunrise (and after midnight) sunset must have happend yesterday - yesterday_sun_times = self.get_sunrise_sunset(now - timedelta(days=1)) - _LOGGER.debug("yesterday_sun_times: " + str(yesterday_sun_times)) - sunset_seconds = yesterday_sun_times[SUN_EVENT_SUNSET].timestamp() + yesterday = self.get_sunrise_sunset( + now - timedelta(days=1), as_timestamps=True + ) + _LOGGER.debug("yesterday: " + str(yesterday)) + today[SUN_EVENT_SUNSET] = yesterday[SUN_EVENT_SUNSET] if ( - today_sun_times["solar_midnight"] > today_sun_times[SUN_EVENT_SUNSET] - and yesterday_sun_times["solar_midnight"] - > yesterday_sun_times[SUN_EVENT_SUNSET] + today[SUN_EVENT_MIDNIGHT] > today[SUN_EVENT_SUNSET] + and yesterday[SUN_EVENT_MIDNIGHT] > yesterday[SUN_EVENT_SUNSET] ): # Solar midnight is after sunset so use yesterdays's time - solar_midnight_seconds = yesterday_sun_times[ - "solar_midnight" - ].timestamp() - elif now > today_sun_times[SUN_EVENT_SUNSET]: + today[SUN_EVENT_MIDNIGHT] = yesterday[SUN_EVENT_MIDNIGHT] + elif now_ts > today[SUN_EVENT_SUNSET]: # It's after sunset (before midnight) # Because it's after sunset (and before midnight) sunrise should happen tomorrow - tomorrow_sun_times = self.get_sunrise_sunset(now + timedelta(days=1)) - _LOGGER.debug("tomorrow_sun_times: " + str(tomorrow_sun_times)) - sunrise_seconds = tomorrow_sun_times[SUN_EVENT_SUNRISE].timestamp() + tomorrow = self.get_sunrise_sunset( + now + timedelta(days=1), as_timestamps=True + ) + _LOGGER.debug("tomorrow: " + str(tomorrow)) + today[SUN_EVENT_SUNRISE] = tomorrow[SUN_EVENT_SUNRISE] if ( - today_sun_times["solar_midnight"] < today_sun_times[SUN_EVENT_SUNRISE] - and tomorrow_sun_times["solar_midnight"] - < tomorrow_sun_times[SUN_EVENT_SUNRISE] + today[SUN_EVENT_MIDNIGHT] < today[SUN_EVENT_SUNRISE] + and tomorrow[SUN_EVENT_MIDNIGHT] < tomorrow[SUN_EVENT_SUNRISE] ): # Solar midnight is before sunrise so use tomorrow's time - solar_midnight_seconds = tomorrow_sun_times[ - "solar_midnight" - ].timestamp() + today[SUN_EVENT_MIDNIGHT] = tomorrow[SUN_EVENT_MIDNIGHT] - _LOGGER.debug( - f"now_seconds: {now_seconds}, " - f"sunrise_seconds: {sunrise_seconds}, " - f"sunset_seconds: {sunset_seconds}, " - f"solar_midnight_seconds: {solar_midnight_seconds}, " - f"solar_noon_seconds: {solar_noon_seconds}" - ) + _LOGGER.debug(f"now_ts: {now_ts}, {today}") # Figure out where we are in time so we know which half of the parabola to calculate # We're generating a different sunset-sunrise parabola for before and after solar midnight @@ -297,31 +291,31 @@ class CircadianLighting: # We're also (obviously) generating a different parabola for sunrise-sunset # sunrise-sunset parabola - if now_seconds > sunrise_seconds and now_seconds < sunset_seconds: - h = solar_noon_seconds + if now_ts > today[SUN_EVENT_SUNRISE] and now_ts < today[SUN_EVENT_SUNSET]: + h = today[SUN_EVENT_NOON] k = 100 - # parabola before solar_noon - if now_seconds < solar_noon_seconds: - x = sunrise_seconds - # parabola after solar_noon - else: - x = sunset_seconds + # parabola before solar_noon else after solar_noon + x = ( + today[SUN_EVENT_SUNRISE] + if now_ts < today[SUN_EVENT_NOON] + else today[SUN_EVENT_SUNSET] + ) y = 0 # sunset_sunrise parabola - elif now_seconds > sunset_seconds and now_seconds < sunrise_seconds: - h = solar_midnight_seconds + elif now_ts > today[SUN_EVENT_SUNSET] and now_ts < today[SUN_EVENT_SUNRISE]: + h = today[SUN_EVENT_MIDNIGHT] k = -100 - # parabola before solar_midnight - if now_seconds < solar_midnight_seconds: - x = sunset_seconds - # parabola after solar_midnight - else: - x = sunrise_seconds + # parabola before solar_midnight else after solar_midnight + x = ( + today[SUN_EVENT_SUNSET] + if now_ts < today[SUN_EVENT_MIDNIGHT] + else today[SUN_EVENT_SUNRISE] + ) y = 0 a = (y - k) / (h - x) ** 2 - percentage = a * (now_seconds - h) ** 2 + k + percentage = a * (now_ts - h) ** 2 + k _LOGGER.debug( f"h: {h}, k: {k}, x: {x}, y: {y}, a: {a}, percentage: {percentage}" diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py old mode 100644 new mode 100755 diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py old mode 100644 new mode 100755 index bb90bce8..d0a781aa --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -99,7 +99,7 @@ PLATFORM_SCHEMA = vol.Schema( vol.Optional( CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION ): VALID_TRANSITION, - vol.Optional(CONF_ONCE_ONLY): cv.boolean, + vol.Optional(CONF_ONCE_ONLY, default=False): cv.boolean, } ) @@ -359,22 +359,19 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug(f"{light} {which} Adjusted - {msg}") def light_state_changed(self, entity_id, from_state, to_state): - with suppress(Exception): - _LOGGER.debug(f"{entity_id} change from {from_state} to {to_state}") - if to_state.state == "on" and from_state.state != "on": - self.adjust_lights([entity_id], self._initial_transition) + _LOGGER.debug(f"{entity_id} change from {from_state} to {to_state}") + if to_state.state == "on" and from_state.state != "on": + self.adjust_lights([entity_id], self._initial_transition) def sleep_state_changed(self, entity_id, from_state, to_state): - with suppress(Exception): - _LOGGER.debug(f"{entity_id} change from {from_state} to {to_state}") - if ( - to_state.state == self._sleep_state + _LOGGER.debug(f"{entity_id} change from {from_state} to {to_state}") + if ( + to_state.state == self._sleep_state or from_state.state == self._sleep_state ): self._update_switch(self._initial_transition, force=True) def disable_state_changed(self, entity_id, from_state, to_state): - with suppress(Exception): - _LOGGER.debug("{entity_id} change from {from_state} to {to_state}") - if from_state.state == self._disable_state: - self._update_switch(self._initial_transition, force=True) + _LOGGER.debug("{entity_id} change from {from_state} to {to_state}") + if from_state.state == self._disable_state: + self._update_switch(self._initial_transition, force=True) From 1f8324c065c45749738f992a3c8fe48e131aeebb Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 17:42:16 +0200 Subject: [PATCH 0072/1077] more simplifications --- .../circadian_lighting/__init__.py | 28 ++++++++----------- .../circadian_lighting/manifest.json | 2 +- .../circadian_lighting/sensor.py | 3 -- .../circadian_lighting/switch.py | 10 ++----- 4 files changed, 15 insertions(+), 28 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 5d12a934..36824779 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -30,8 +30,10 @@ Technical notes: I had to make a lot of assumptions when writing this app import logging from datetime import timedelta -import homeassistant.helpers.config_validation as cv import voluptuous as vol + +import astral +import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ATTR_TRANSITION, VALID_TRANSITION from homeassistant.const import ( CONF_ELEVATION, @@ -51,8 +53,7 @@ from homeassistant.util.color import ( ) from homeassistant.util.dt import get_time_zone from homeassistant.util.dt import now as dt_now - -VERSION = "1.0.13" +from timezonefinder import TimezoneFinder _LOGGER = logging.getLogger(__name__) @@ -61,16 +62,13 @@ CIRCADIAN_LIGHTING_UPDATE_TOPIC = f"{DOMAIN}_update" SUN_EVENT_NOON = "solar_noon" SUN_EVENT_MIDNIGHT = "solar_midnight" -CONF_MIN_CT = "min_colortemp" -DEFAULT_MIN_CT = 2500 -CONF_MAX_CT = "max_colortemp" -DEFAULT_MAX_CT = 5500 +CONF_MIN_CT, DEFAULT_MIN_CT = "min_colortemp", 2500 +CONF_MAX_CT, DEFAULT_MAX_CT = "max_colortemp", 5500 +CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 300 CONF_SUNRISE_OFFSET = "sunrise_offset" CONF_SUNSET_OFFSET = "sunset_offset" CONF_SUNRISE_TIME = "sunrise_time" CONF_SUNSET_TIME = "sunset_time" -CONF_INTERVAL = "interval" -DEFAULT_INTERVAL = 300 DEFAULT_TRANSITION = 60 CONFIG_SCHEMA = vol.Schema( @@ -181,8 +179,6 @@ class CircadianLighting: track_sunset(self.hass, self._update, self._sunset_offset) def get_timezone(self): - from timezonefinder import TimezoneFinder - tf = TimezoneFinder() timezone_string = tf.timezone_at(lng=self._longitude, lat=self._latitude) timezone = get_time_zone(timezone_string) @@ -201,13 +197,13 @@ class CircadianLighting: if self._time["sunrise"] is not None and self._time["sunset"] is not None: if date is None: date = dt_now(self._timezone) - sunrise = date.replace(**self._time_dict("sunrise")) + sunrise = date.replace( + **self._time_dict("sunrise") + ) # XXX: redefine _time_dict to do the replace! sunset = date.replace(**self._time_dict("sunset")) solar_noon = sunrise + (sunset - sunrise) / 2 solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset) / 2 else: - import astral - location = astral.Location() location.name = "name" location.region = "region" @@ -291,7 +287,7 @@ class CircadianLighting: # We're also (obviously) generating a different parabola for sunrise-sunset # sunrise-sunset parabola - if now_ts > today[SUN_EVENT_SUNRISE] and now_ts < today[SUN_EVENT_SUNSET]: + if today[SUN_EVENT_SUNRISE] < now_ts < today[SUN_EVENT_SUNSET]: h = today[SUN_EVENT_NOON] k = 100 # parabola before solar_noon else after solar_noon @@ -303,7 +299,7 @@ class CircadianLighting: y = 0 # sunset_sunrise parabola - elif now_ts > today[SUN_EVENT_SUNSET] and now_ts < today[SUN_EVENT_SUNRISE]: + elif today[SUN_EVENT_SUNSET] < now_ts < today[SUN_EVENT_SUNRISE]: h = today[SUN_EVENT_MIDNIGHT] k = -100 # parabola before solar_midnight else after solar_midnight diff --git a/custom_components/circadian_lighting/manifest.json b/custom_components/circadian_lighting/manifest.json index 4832a25b..0008ce6b 100644 --- a/custom_components/circadian_lighting/manifest.json +++ b/custom_components/circadian_lighting/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://github.com/claytonjn/hass-circadian_lighting", "dependencies": [], "codeowners": ["@claytonjn"], - "requirements": ["timezonefinder==4.2.0"] + "requirements": ["timezonefinder==4.2.0", "astral==1.10.1"] } diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py index e5f67282..6bd28b2a 100755 --- a/custom_components/circadian_lighting/sensor.py +++ b/custom_components/circadian_lighting/sensor.py @@ -2,9 +2,6 @@ Circadian Lighting Sensor for Home-Assistant. """ -DEPENDENCIES = ["circadian_lighting"] - -import datetime import logging from homeassistant.helpers.dispatcher import dispatcher_connect diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index d0a781aa..5788da4e 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -2,10 +2,7 @@ Circadian Lighting Switch for Home-Assistant. """ -DEPENDENCIES = ["circadian_lighting", "light"] - import logging -from contextlib import suppress import homeassistant.helpers.config_validation as cv import voluptuous as vol @@ -365,11 +362,8 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def sleep_state_changed(self, entity_id, from_state, to_state): _LOGGER.debug(f"{entity_id} change from {from_state} to {to_state}") - if ( - to_state.state == self._sleep_state - or from_state.state == self._sleep_state - ): - self._update_switch(self._initial_transition, force=True) + if to_state.state == self._sleep_state or from_state.state == self._sleep_state: + self._update_switch(self._initial_transition, force=True) def disable_state_changed(self, entity_id, from_state, to_state): _LOGGER.debug("{entity_id} change from {from_state} to {to_state}") From e058091f52e9519b3ef29027cd44a7be027c742a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 17:44:16 +0200 Subject: [PATCH 0073/1077] allow CONF_SLEEP_STATE and CONF_DISABLE_STATE to be lists and add defaults --- .../circadian_lighting/__init__.py | 3 +- .../circadian_lighting/switch.py | 33 +++++++++---------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 36824779..0054d4ed 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -102,8 +102,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Circadian Lighting component.""" conf = config[DOMAIN] - load_platform(hass, "sensor", DOMAIN, {}, config) - hass.data[DOMAIN] = CircadianLighting( hass, min_colortemp=conf.get(CONF_MIN_CT), @@ -118,6 +116,7 @@ def setup(hass, config): interval=conf.get(CONF_INTERVAL), transition=conf.get(ATTR_TRANSITION), ) + load_platform(hass, "sensor", DOMAIN, {}, config) return True diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 5788da4e..9f06d883 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -54,18 +54,15 @@ CONF_LIGHTS_RGB = "lights_rgb" CONF_LIGHTS_XY = "lights_xy" CONF_LIGHTS_BRIGHT = "lights_brightness" CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust" -CONF_MIN_BRIGHT = "min_brightness" -DEFAULT_MIN_BRIGHT = 1 -CONF_MAX_BRIGHT = "max_brightness" -DEFAULT_MAX_BRIGHT = 100 +CONF_MIN_BRIGHT, DEFAULT_MIN_BRIGHT = ("min_brightness", 1) +CONF_MAX_BRIGHT, DEFAULT_MAX_BRIGHT = ("max_brightness", 100) CONF_SLEEP_ENTITY = "sleep_entity" CONF_SLEEP_STATE = "sleep_state" -CONF_SLEEP_CT = "sleep_colortemp" -CONF_SLEEP_BRIGHT = "sleep_brightness" +CONF_SLEEP_CT, DEFAULT_SLEEP_CT = ("sleep_colortemp", 1000) +CONF_SLEEP_BRIGHT, DEFAULT_SLEEP_BRIGHT = ("sleep_brightness", 1) CONF_DISABLE_ENTITY = "disable_entity" CONF_DISABLE_STATE = "disable_state" -CONF_INITIAL_TRANSITION = "initial_transition" -DEFAULT_INITIAL_TRANSITION = 1 +CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = ("initial_transition", 1) CONF_ONCE_ONLY = "once_only" PLATFORM_SCHEMA = vol.Schema( @@ -84,15 +81,15 @@ PLATFORM_SCHEMA = vol.Schema( vol.Coerce(int), vol.Range(min=1, max=100) ), vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, - vol.Optional(CONF_SLEEP_STATE): cv.string, - vol.Optional(CONF_SLEEP_CT): vol.All( + vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_SLEEP_CT, default=DEFAULT_SLEEP_CT): vol.All( vol.Coerce(int), vol.Range(min=1000, max=10000) ), - vol.Optional(CONF_SLEEP_BRIGHT): vol.All( + vol.Optional(CONF_SLEEP_BRIGHT, default=DEFAULT_SLEEP_BRIGHT): vol.All( vol.Coerce(int), vol.Range(min=1, max=100) ), vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, - vol.Optional(CONF_DISABLE_STATE): cv.string, + vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), vol.Optional( CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION ): VALID_TRANSITION, @@ -249,7 +246,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def is_sleep(self): is_sleep = ( self._sleep_entity is not None - and self.hass.states.get(self._sleep_entity).state == self._sleep_state + and self.hass.states.get(self._sleep_entity).state in self._sleep_state ) if is_sleep: _LOGGER.debug(f"{self._name} in Sleep mode") @@ -302,7 +299,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): return False elif ( self._disable_entity is not None - and self.hass.states.get(self._disable_entity).state == self._disable_state + and self.hass.states.get(self._disable_entity).state in self._disable_state ): _LOGGER.debug(f"{self._name} disabled by {self._disable_entity}") return False @@ -362,10 +359,10 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def sleep_state_changed(self, entity_id, from_state, to_state): _LOGGER.debug(f"{entity_id} change from {from_state} to {to_state}") - if to_state.state == self._sleep_state or from_state.state == self._sleep_state: - self._update_switch(self._initial_transition, force=True) + if to_state.state in self._sleep_state or from_state.state in self._sleep_state: + self._update_switch(transition=self._initial_transition, force=True) def disable_state_changed(self, entity_id, from_state, to_state): _LOGGER.debug("{entity_id} change from {from_state} to {to_state}") - if from_state.state == self._disable_state: - self._update_switch(self._initial_transition, force=True) + if from_state.state in self._disable_state: + self._update_switch(transition=self._initial_transition, force=True) From e257af03bfd409ed1e7f23ed5d4c72264c85f994 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 17:43:58 +0200 Subject: [PATCH 0074/1077] call self._update_switch in light_state_changed --- custom_components/circadian_lighting/__init__.py | 2 +- custom_components/circadian_lighting/switch.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 0054d4ed..2d2f8253 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -335,7 +335,7 @@ class CircadianLighting: def calc_hs(self): return color_xy_to_hs(*self.calc_xy()) - def _update(self, *args, **kwargs): + def _update(self): """Update Circadian Values.""" self._percent = self.calc_percent() self._colortemp = self.calc_colortemp() diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 9f06d883..2e95a3c4 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -232,7 +232,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._state = True # Make initial update - self._update_switch(self._initial_transition, force=True) + self._update_switch(transition=self._initial_transition, force=True) self.schedule_update_ha_state() @@ -285,15 +285,15 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): procent = (100 + self._circadian_lighting._percent) / 100 return (delta_brightness * procent) + self._min_brightness - def _update_switch(self, transition=None, force=False): + def _update_switch(self, lights=None, transition=None, force=False): if self._once_only and not force: return self._hs_color = self.calc_hs() self._brightness = self.calc_brightness() _LOGGER.debug(f"{self._name} Switch Updated") - self.adjust_lights(self._lights, transition) + self._adjust_lights(lights or self._lights, transition) - def should_adjust(self): + def _should_adjust(self): if self._state is not True: _LOGGER.debug(f"{self._name} off - not adjusting") return False @@ -306,8 +306,8 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): else: return True - def adjust_lights(self, lights, transition=None): - if not self.should_adjust(): + def _adjust_lights(self, lights, transition=None): + if not self._should_adjust(): return if transition is None: @@ -355,7 +355,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def light_state_changed(self, entity_id, from_state, to_state): _LOGGER.debug(f"{entity_id} change from {from_state} to {to_state}") if to_state.state == "on" and from_state.state != "on": - self.adjust_lights([entity_id], self._initial_transition) + self._update_switch([entity_id], self._initial_transition, force=True) def sleep_state_changed(self, entity_id, from_state, to_state): _LOGGER.debug(f"{entity_id} change from {from_state} to {to_state}") From 080aeed541f3f2de7e89dc9e88a335f73d618427 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 22:31:26 +0200 Subject: [PATCH 0075/1077] add _is_disabled property and move logging to more logical place --- .../circadian_lighting/switch.py | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 2e95a3c4..f3be1dd3 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -230,10 +230,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def turn_on(self, **kwargs): """Turn on circadian lighting.""" self._state = True - - # Make initial update self._update_switch(transition=self._initial_transition, force=True) - self.schedule_update_ha_state() def turn_off(self, **kwargs): @@ -293,14 +290,18 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug(f"{self._name} Switch Updated") self._adjust_lights(lights or self._lights, transition) + @property + def _is_disabled(self): + return ( + self._disable_entity is not None + and self.hass.states.get(self._disable_entity).state in self._disable_state + ) + def _should_adjust(self): if self._state is not True: _LOGGER.debug(f"{self._name} off - not adjusting") return False - elif ( - self._disable_entity is not None - and self.hass.states.get(self._disable_entity).state in self._disable_state - ): + elif self._is_disabled: _LOGGER.debug(f"{self._name} disabled by {self._disable_entity}") return False else: @@ -353,16 +354,25 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug(f"{light} {which} Adjusted - {msg}") def light_state_changed(self, entity_id, from_state, to_state): - _LOGGER.debug(f"{entity_id} change from {from_state} to {to_state}") if to_state.state == "on" and from_state.state != "on": + _LOGGER.debug( + f"light_state_changed for {self._name}: {entity_id} " + f"change from {from_state} to {to_state}" + ) self._update_switch([entity_id], self._initial_transition, force=True) def sleep_state_changed(self, entity_id, from_state, to_state): - _LOGGER.debug(f"{entity_id} change from {from_state} to {to_state}") if to_state.state in self._sleep_state or from_state.state in self._sleep_state: + _LOGGER.debug( + f"sleep_state_changed for {self._name}: {entity_id} " + f"change from {from_state} to {to_state}" + ) self._update_switch(transition=self._initial_transition, force=True) def disable_state_changed(self, entity_id, from_state, to_state): - _LOGGER.debug("{entity_id} change from {from_state} to {to_state}") if from_state.state in self._disable_state: + _LOGGER.debug( + f"disable_state_changed for {self._name}: {entity_id} " + f"change from {from_state} to {to_state}" + ) self._update_switch(transition=self._initial_transition, force=True) From 063a2386aff5e672406a474ed964a42f6027102b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 22:47:40 +0200 Subject: [PATCH 0076/1077] add @log decorator --- .../circadian_lighting/__init__.py | 23 +++++++++++- .../circadian_lighting/sensor.py | 3 +- .../circadian_lighting/switch.py | 37 ++++++------------- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 2d2f8253..11342f8f 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -29,6 +29,7 @@ Technical notes: I had to make a lot of assumptions when writing this app import logging from datetime import timedelta +import inspect import voluptuous as vol @@ -99,6 +100,26 @@ CONFIG_SCHEMA = vol.Schema( ) +def log(with_return=False, logger=_LOGGER): + def _log(func): + def wrapper(*args, **kwargs): + func_args = inspect.signature(func).bind(*args, **kwargs).arguments + key_value_pairs = ( + f"{k}={v!r}" for k, v in func_args.items() if k != "self" + ) + func_args_str = ", ".join(key_value_pairs) + out = f"{func.__qualname__}({func_args_str})" + result = func(*args, **kwargs) + if with_return: + out += f" -> {result}" + logger.debug(out) + return result + + return wrapper + + return _log + + def setup(hass, config): """Set up the Circadian Lighting component.""" conf = config[DOMAIN] @@ -177,11 +198,11 @@ class CircadianLighting: elif which == "sunset": track_sunset(self.hass, self._update, self._sunset_offset) + @log(with_return=True) def get_timezone(self): tf = TimezoneFinder() timezone_string = tf.timezone_at(lng=self._longitude, lat=self._latitude) timezone = get_time_zone(timezone_string) - _LOGGER.debug("Timezone: " + str(timezone)) return timezone def _time_dict(self, key): diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py index 6bd28b2a..082250d3 100755 --- a/custom_components/circadian_lighting/sensor.py +++ b/custom_components/circadian_lighting/sensor.py @@ -10,6 +10,7 @@ from homeassistant.helpers.entity import Entity from custom_components.circadian_lighting import ( CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN, + log, ) _LOGGER = logging.getLogger(__name__) @@ -99,10 +100,10 @@ class CircadianSensor(Entity): """ self._circadian_lighting.update() + @log(logger=_LOGGER) def update_sensor(self): self._state = self._circadian_lighting._percent self._hs_color = self._circadian_lighting._hs_color self._colortemp = self._circadian_lighting._colortemp self._rgb_color = self._circadian_lighting._rgb_color self._xy_color = self._circadian_lighting._xy_color - _LOGGER.debug("Circadian Lighting Sensor Updated") diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index f3be1dd3..729046c4 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -37,6 +37,7 @@ from homeassistant.util.color import ( from custom_components.circadian_lighting import ( CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN, + log, ) try: @@ -240,17 +241,13 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._hs_color = None self._brightness = None + @log(with_return=True, logger=_LOGGER) def is_sleep(self): - is_sleep = ( + return ( self._sleep_entity is not None and self.hass.states.get(self._sleep_entity).state in self._sleep_state ) - if is_sleep: - _LOGGER.debug(f"{self._name} in Sleep mode") - return is_sleep - - @property def _color_temperature(self): return ( self._sleep_colortemp @@ -259,10 +256,10 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): ) def calc_ct(self): - return color_temperature_kelvin_to_mired(self._color_temperature) + return color_temperature_kelvin_to_mired(self._color_temperature()) def calc_rgb(self): - return color_temperature_to_rgb(self._color_temperature) + return color_temperature_to_rgb(self._color_temperature()) def calc_xy(self): return color_RGB_to_xy(*self.calc_rgb()) @@ -282,27 +279,26 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): procent = (100 + self._circadian_lighting._percent) / 100 return (delta_brightness * procent) + self._min_brightness + @log(logger=_LOGGER) def _update_switch(self, lights=None, transition=None, force=False): if self._once_only and not force: return self._hs_color = self.calc_hs() self._brightness = self.calc_brightness() - _LOGGER.debug(f"{self._name} Switch Updated") self._adjust_lights(lights or self._lights, transition) - @property + @log(with_return=True, logger=_LOGGER) def _is_disabled(self): return ( self._disable_entity is not None and self.hass.states.get(self._disable_entity).state in self._disable_state ) + @log(with_return=True, logger=_LOGGER) def _should_adjust(self): if self._state is not True: - _LOGGER.debug(f"{self._name} off - not adjusting") return False - elif self._is_disabled: - _LOGGER.debug(f"{self._name} disabled by {self._disable_entity}") + elif self._is_disabled(): return False else: return True @@ -353,26 +349,17 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): msg = ", ".join(key_value_strings) _LOGGER.debug(f"{light} {which} Adjusted - {msg}") + @log(with_return=True, logger=_LOGGER) def light_state_changed(self, entity_id, from_state, to_state): if to_state.state == "on" and from_state.state != "on": - _LOGGER.debug( - f"light_state_changed for {self._name}: {entity_id} " - f"change from {from_state} to {to_state}" - ) self._update_switch([entity_id], self._initial_transition, force=True) + @log(with_return=True, logger=_LOGGER) def sleep_state_changed(self, entity_id, from_state, to_state): if to_state.state in self._sleep_state or from_state.state in self._sleep_state: - _LOGGER.debug( - f"sleep_state_changed for {self._name}: {entity_id} " - f"change from {from_state} to {to_state}" - ) self._update_switch(transition=self._initial_transition, force=True) + @log(with_return=True, logger=_LOGGER) def disable_state_changed(self, entity_id, from_state, to_state): if from_state.state in self._disable_state: - _LOGGER.debug( - f"disable_state_changed for {self._name}: {entity_id} " - f"change from {from_state} to {to_state}" - ) self._update_switch(transition=self._initial_transition, force=True) From 13bbdd4ad4dca960606eb70178c2bf7ec38906cd Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 23:13:31 +0200 Subject: [PATCH 0077/1077] create _replace_time method --- .../circadian_lighting/__init__.py | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 11342f8f..c14aec27 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -205,22 +205,21 @@ class CircadianLighting: timezone = get_time_zone(timezone_string) return timezone - def _time_dict(self, key): - return dict( - hour=int(self._time[key].strftime("%H")), - minute=int(self._time[key].strftime("%M")), - second=int(self._time[key].strftime("%S")), - microsecond=int(self._time[key].strftime("%f")), + def _replace_time(self, date, key): + other_date = self._time[key] + return date.replace( + hour=other_date.hour, + minute=other_date.minute, + second=other_date.second, + microsecond=other_date.microsecond, ) def get_sunrise_sunset(self, date=None, as_timestamps=False): if self._time["sunrise"] is not None and self._time["sunset"] is not None: if date is None: date = dt_now(self._timezone) - sunrise = date.replace( - **self._time_dict("sunrise") - ) # XXX: redefine _time_dict to do the replace! - sunset = date.replace(**self._time_dict("sunset")) + sunrise = self._replace_time(date, "sunrise") + sunset = self._replace_time(date, "sunset") solar_noon = sunrise + (sunset - sunrise) / 2 solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset) / 2 else: @@ -231,24 +230,29 @@ class CircadianLighting: location.longitude = self._longitude location.elevation = self._elevation _LOGGER.debug("Astral location: " + str(location)) + if self._time["sunrise"] is not None: if date is None: date = dt_now(self._timezone) - sunrise = date.replace(**self._time_dict("sunrise")) + sunrise = self._replace_time(date, "sunrise") else: sunrise = location.sunrise(date) + if self._time["sunset"] is not None: if date is None: date = dt_now(self._timezone) - sunset = date.replace(**self._time_dict("sunset")) + sunset = self._replace_time(date, "sunset") else: sunset = location.sunset(date) + solar_noon = location.solar_noon(date) solar_midnight = location.solar_midnight(date) + if self._sunrise_offset is not None: sunrise = sunrise + self._sunrise_offset if self._sunset_offset is not None: sunset = sunset + self._sunset_offset + datetimes = { SUN_EVENT_SUNRISE: sunrise.astimezone(self._timezone), SUN_EVENT_SUNSET: sunset.astimezone(self._timezone), From 3475bd9b90ecd958e3d9ce863232ea7434d714c7 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 23:38:01 +0200 Subject: [PATCH 0078/1077] date is never None and renames --- .../circadian_lighting/__init__.py | 82 +++++++------------ 1 file changed, 29 insertions(+), 53 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index c14aec27..f5390072 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -165,7 +165,7 @@ class CircadianLighting: self._max_colortemp = max_colortemp self._sunrise_offset = sunrise_offset self._sunset_offset = sunset_offset - self._time = { + self._manual_time = { "sunrise": sunrise_time, "sunset": sunset_time, } @@ -184,29 +184,27 @@ class CircadianLighting: self.update = Throttle(timedelta(seconds=interval))(self._update) for which in ["sunrise", "sunrise"]: - time = self._time[which] + time = self._manual_time[which] if time is not None: track_time_change( self.hass, self._update, - hour=int(time.strftime("%H")), - minute=int(time.strftime("%M")), - second=int(time.strftime("%S")), + hour=time.hour, + minute=time.minute, + second=time.second, ) - elif which == "sunrise": - track_sunrise(self.hass, self._update, self._sunrise_offset) - elif which == "sunset": - track_sunset(self.hass, self._update, self._sunset_offset) + + track_sunrise(self.hass, self._update, self._sunrise_offset) + track_sunset(self.hass, self._update, self._sunset_offset) @log(with_return=True) def get_timezone(self): tf = TimezoneFinder() timezone_string = tf.timezone_at(lng=self._longitude, lat=self._latitude) - timezone = get_time_zone(timezone_string) - return timezone + return get_time_zone(timezone_string) def _replace_time(self, date, key): - other_date = self._time[key] + other_date = self._manual_time[key] return date.replace( hour=other_date.hour, minute=other_date.minute, @@ -214,10 +212,11 @@ class CircadianLighting: microsecond=other_date.microsecond, ) - def get_sunrise_sunset(self, date=None, as_timestamps=False): - if self._time["sunrise"] is not None and self._time["sunset"] is not None: - if date is None: - date = dt_now(self._timezone) + def get_sunrise_sunset(self, date, as_timestamps=True): + if ( + self._manual_time["sunrise"] is not None + and self._manual_time["sunset"] is not None + ): sunrise = self._replace_time(date, "sunrise") sunset = self._replace_time(date, "sunset") solar_noon = sunrise + (sunset - sunrise) / 2 @@ -229,18 +228,13 @@ class CircadianLighting: location.latitude = self._latitude location.longitude = self._longitude location.elevation = self._elevation - _LOGGER.debug("Astral location: " + str(location)) - if self._time["sunrise"] is not None: - if date is None: - date = dt_now(self._timezone) + if self._manual_time["sunrise"] is not None: sunrise = self._replace_time(date, "sunrise") else: sunrise = location.sunrise(date) - if self._time["sunset"] is not None: - if date is None: - date = dt_now(self._timezone) + if self._manual_time["sunset"] is not None: sunset = self._replace_time(date, "sunset") else: sunset = location.sunset(date) @@ -266,21 +260,13 @@ class CircadianLighting: def calc_percent(self): now = dt_now(self._timezone) - _LOGGER.debug("now: " + str(now)) - - today = self.get_sunrise_sunset(now, as_timestamps=True) - _LOGGER.debug("today: " + str(today)) - - # Convert everything to epoch timestamps for easy calculation now_ts = now.timestamp() + today = self.get_sunrise_sunset(now) if now_ts < today[SUN_EVENT_SUNRISE]: # It's before sunrise (after midnight) # Because it's before sunrise (and after midnight) sunset must have happend yesterday - yesterday = self.get_sunrise_sunset( - now - timedelta(days=1), as_timestamps=True - ) - _LOGGER.debug("yesterday: " + str(yesterday)) + yesterday = self.get_sunrise_sunset(now - timedelta(days=1)) today[SUN_EVENT_SUNSET] = yesterday[SUN_EVENT_SUNSET] if ( today[SUN_EVENT_MIDNIGHT] > today[SUN_EVENT_SUNSET] @@ -291,10 +277,7 @@ class CircadianLighting: elif now_ts > today[SUN_EVENT_SUNSET]: # It's after sunset (before midnight) # Because it's after sunset (and before midnight) sunrise should happen tomorrow - tomorrow = self.get_sunrise_sunset( - now + timedelta(days=1), as_timestamps=True - ) - _LOGGER.debug("tomorrow: " + str(tomorrow)) + tomorrow = self.get_sunrise_sunset(now + timedelta(days=1)) today[SUN_EVENT_SUNRISE] = tomorrow[SUN_EVENT_SUNRISE] if ( today[SUN_EVENT_MIDNIGHT] < today[SUN_EVENT_SUNRISE] @@ -303,14 +286,13 @@ class CircadianLighting: # Solar midnight is before sunrise so use tomorrow's time today[SUN_EVENT_MIDNIGHT] = tomorrow[SUN_EVENT_MIDNIGHT] - _LOGGER.debug(f"now_ts: {now_ts}, {today}") + # Figure out where we are in time so we know which half of the + # parabola to calculate. We're generating a different + # sunset-sunrise parabola for before and after solar midnight. + # because it might not be half way between sunrise and sunset. + # We're also generating a different parabola for sunrise-sunset. - # Figure out where we are in time so we know which half of the parabola to calculate - # We're generating a different sunset-sunrise parabola for before and after solar midnight - # because it might not be half way between sunrise and sunset - # We're also (obviously) generating a different parabola for sunrise-sunset - - # sunrise-sunset parabola + # sunrise -> sunset parabola if today[SUN_EVENT_SUNRISE] < now_ts < today[SUN_EVENT_SUNSET]: h = today[SUN_EVENT_NOON] k = 100 @@ -320,9 +302,8 @@ class CircadianLighting: if now_ts < today[SUN_EVENT_NOON] else today[SUN_EVENT_SUNSET] ) - y = 0 - # sunset_sunrise parabola + # sunset -> sunrise parabola elif today[SUN_EVENT_SUNSET] < now_ts < today[SUN_EVENT_SUNRISE]: h = today[SUN_EVENT_MIDNIGHT] k = -100 @@ -332,15 +313,10 @@ class CircadianLighting: if now_ts < today[SUN_EVENT_MIDNIGHT] else today[SUN_EVENT_SUNRISE] ) - y = 0 + y = 0 a = (y - k) / (h - x) ** 2 percentage = a * (now_ts - h) ** 2 + k - - _LOGGER.debug( - f"h: {h}, k: {k}, x: {x}, y: {y}, a: {a}, percentage: {percentage}" - ) - return percentage def calc_colortemp(self): @@ -360,6 +336,7 @@ class CircadianLighting: def calc_hs(self): return color_xy_to_hs(*self.calc_xy()) + @log() def _update(self): """Update Circadian Values.""" self._percent = self.calc_percent() @@ -368,4 +345,3 @@ class CircadianLighting: self._xy_color = self.calc_xy() self._hs_color = self.calc_hs() dispatcher_send(self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC) - _LOGGER.debug("Circadian Lighting Component Updated") From 29010105c146684c0daffa4e04c061d65073e62f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 23:46:36 +0200 Subject: [PATCH 0079/1077] change elif in if when returning --- .../circadian_lighting/switch.py | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 729046c4..a342b045 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -250,9 +250,9 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def _color_temperature(self): return ( - self._sleep_colortemp - if self.is_sleep() - else self._circadian_lighting._colortemp + self._circadian_lighting._colortemp + if not self.is_sleep() + else self._sleep_colortemp ) def calc_ct(self): @@ -270,14 +270,13 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def calc_brightness(self): if self._disable_brightness_adjust is True: return None - elif self.is_sleep(): + if self.is_sleep(): return self._sleep_brightness - elif self._circadian_lighting._percent > 0: + if self._circadian_lighting._percent > 0: return self._max_brightness - else: - delta_brightness = self._max_brightness - self._min_brightness - procent = (100 + self._circadian_lighting._percent) / 100 - return (delta_brightness * procent) + self._min_brightness + delta_brightness = self._max_brightness - self._min_brightness + procent = (100 + self._circadian_lighting._percent) / 100 + return (delta_brightness * procent) + self._min_brightness @log(logger=_LOGGER) def _update_switch(self, lights=None, transition=None, force=False): @@ -298,10 +297,9 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def _should_adjust(self): if self._state is not True: return False - elif self._is_disabled(): + if self._is_disabled(): return False - else: - return True + return True def _adjust_lights(self, lights, transition=None): if not self._should_adjust(): From 49a0e461d744d1a6b20701765c6f4a46d91e65e8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 23:57:16 +0200 Subject: [PATCH 0080/1077] introduce self._lights_types --- .../circadian_lighting/switch.py | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index a342b045..f390360b 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -179,7 +179,16 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._initial_transition = initial_transition self._once_only = once_only - self._lights = lights_ct + lights_rgb + lights_xy + lights_brightness + self._lights_types = {} + for light in lights_ct: + self._lights_types[light] = "ct" + for light in lights_rgb: + self._lights_types[light] = "rgb" + for light in lights_xy: + self._lights_types[light] = "xy" + for light in lights_brightness: + self._lights_types[light] = "brightness" + self._lights = list(self._lights_types.keys()) # Register callbacks dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self._update_switch) @@ -319,33 +328,18 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): if transition is not None: service_data[ATTR_TRANSITION] = transition - # Set color of array of ct. - if light in self._lights_ct: - which = "CT" + light_type = self._lights_types[light] + if light_type == "ct": service_data[ATTR_COLOR_TEMP] = int(self.calc_ct()) - - # Set color of array of rgb. - elif light in self._lights_rgb: - which = "RGB" + elif light_type == "rgb": service_data[ATTR_RGB_COLOR] = tuple(map(int, self.calc_rgb())) - - # Set color of array of xy. - elif light in self._lights_xy: - which = "XY" + elif light_type == "xy": service_data[ATTR_XY_COLOR] = self.calc_xy() if service_data.get(ATTR_BRIGHTNESS, False): service_data[ATTR_WHITE_VALUE] = service_data[ATTR_BRIGHTNESS] - # Set color of array of brightness. - elif light in self._lights_brightness: - which = "Brightness" - self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - key_value_strings = [ - f"{k}: {v}" for k, v in service_data.items() if k != ATTR_ENTITY_ID - ] - msg = ", ".join(key_value_strings) - _LOGGER.debug(f"{light} {which} Adjusted - {msg}") + _LOGGER.debug(f"{light} {light_type} Adjusted - {service_data}") @log(with_return=True, logger=_LOGGER) def light_state_changed(self, entity_id, from_state, to_state): From 1fd9d4764ce6717bd6e06a69b8ceb5850013e6d5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Aug 2020 23:58:57 +0200 Subject: [PATCH 0081/1077] be consistent with brackets --- custom_components/circadian_lighting/switch.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index f390360b..1999781a 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -55,15 +55,15 @@ CONF_LIGHTS_RGB = "lights_rgb" CONF_LIGHTS_XY = "lights_xy" CONF_LIGHTS_BRIGHT = "lights_brightness" CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust" -CONF_MIN_BRIGHT, DEFAULT_MIN_BRIGHT = ("min_brightness", 1) -CONF_MAX_BRIGHT, DEFAULT_MAX_BRIGHT = ("max_brightness", 100) +CONF_MIN_BRIGHT, DEFAULT_MIN_BRIGHT = "min_brightness", 1 +CONF_MAX_BRIGHT, DEFAULT_MAX_BRIGHT = "max_brightness", 100 CONF_SLEEP_ENTITY = "sleep_entity" CONF_SLEEP_STATE = "sleep_state" -CONF_SLEEP_CT, DEFAULT_SLEEP_CT = ("sleep_colortemp", 1000) -CONF_SLEEP_BRIGHT, DEFAULT_SLEEP_BRIGHT = ("sleep_brightness", 1) +CONF_SLEEP_CT, DEFAULT_SLEEP_CT = "sleep_colortemp", 1000 +CONF_SLEEP_BRIGHT, DEFAULT_SLEEP_BRIGHT = "sleep_brightness", 1 CONF_DISABLE_ENTITY = "disable_entity" CONF_DISABLE_STATE = "disable_state" -CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = ("initial_transition", 1) +CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 CONF_ONCE_ONLY = "once_only" PLATFORM_SCHEMA = vol.Schema( From d865cdbf9fe83c0605ef3f8c468a001ea6177800 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 1 Sep 2020 00:01:34 +0200 Subject: [PATCH 0082/1077] remove unused attributes --- custom_components/circadian_lighting/switch.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 1999781a..9e4e99f7 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -163,10 +163,6 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._icon = ICON self._hs_color = None self._brightness = None - self._lights_ct = lights_ct - self._lights_rgb = lights_rgb - self._lights_xy = lights_xy - self._lights_brightness = lights_brightness self._disable_brightness_adjust = disable_brightness_adjust self._min_brightness = min_brightness self._max_brightness = max_brightness From 9dc04bc7c1d81f7ec42ab8837bdd9645140f1dad Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 1 Sep 2020 00:11:36 +0200 Subject: [PATCH 0083/1077] remove code that wasn't needed --- .../circadian_lighting/__init__.py | 1 - .../circadian_lighting/sensor.py | 26 +++---------------- 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index f5390072..6de46c6f 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -172,7 +172,6 @@ class CircadianLighting: self._latitude = latitude self._longitude = longitude self._elevation = elevation - self._interval = interval self._transition = transition self._timezone = self.get_timezone() self._percent = self.calc_percent() diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py index 082250d3..76971eb5 100755 --- a/custom_components/circadian_lighting/sensor.py +++ b/custom_components/circadian_lighting/sensor.py @@ -7,11 +7,7 @@ import logging from homeassistant.helpers.dispatcher import dispatcher_connect from homeassistant.helpers.entity import Entity -from custom_components.circadian_lighting import ( - CIRCADIAN_LIGHTING_UPDATE_TOPIC, - DOMAIN, - log, -) +from custom_components.circadian_lighting import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -44,16 +40,8 @@ class CircadianSensor(Entity): self._circadian_lighting = circadian_lighting self._name = "Circadian Values" self._entity_id = "sensor.circadian_values" - self._state = self._circadian_lighting._percent self._unit_of_measurement = "%" self._icon = ICON - self._hs_color = self._circadian_lighting._hs_color - self._colortemp = self._circadian_lighting._colortemp - self._rgb_color = self._circadian_lighting._rgb_color - self._xy_color = self._circadian_lighting._xy_color - - # Register callbacks - dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self.update_sensor) @property def entity_id(self): @@ -68,7 +56,7 @@ class CircadianSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return self._state + return self._circadian_lighting._percent @property def unit_of_measurement(self): @@ -82,7 +70,7 @@ class CircadianSensor(Entity): @property def hs_color(self): - return self._hs_color + return self._circadian_lighting._hs_color @property def device_state_attributes(self): @@ -99,11 +87,3 @@ class CircadianSensor(Entity): This is the only method that should fetch new data for Home Assistant. """ self._circadian_lighting.update() - - @log(logger=_LOGGER) - def update_sensor(self): - self._state = self._circadian_lighting._percent - self._hs_color = self._circadian_lighting._hs_color - self._colortemp = self._circadian_lighting._colortemp - self._rgb_color = self._circadian_lighting._rgb_color - self._xy_color = self._circadian_lighting._xy_color From 98efae7e4040bb8798ca1b59da44be6885524bf7 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 1 Sep 2020 00:23:49 +0200 Subject: [PATCH 0084/1077] be explicit in unpacking RGB --- custom_components/circadian_lighting/switch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 9e4e99f7..8519fe9f 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -328,7 +328,8 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): if light_type == "ct": service_data[ATTR_COLOR_TEMP] = int(self.calc_ct()) elif light_type == "rgb": - service_data[ATTR_RGB_COLOR] = tuple(map(int, self.calc_rgb())) + r, g, b = self.calc_rgb() + service_data[ATTR_RGB_COLOR] = (int(r), int(g), int(b)) elif light_type == "xy": service_data[ATTR_XY_COLOR] = self.calc_xy() if service_data.get(ATTR_BRIGHTNESS, False): From 76b544682ef0a9997f571cfbbbbd679c5b023882 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 1 Sep 2020 08:38:19 +0200 Subject: [PATCH 0085/1077] fix typos and use self._brightness --- custom_components/circadian_lighting/switch.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 8519fe9f..312d2c91 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -272,16 +272,16 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def calc_hs(self): return color_xy_to_hs(*self.calc_xy()) - def calc_brightness(self): - if self._disable_brightness_adjust is True: + def calc_brightness(self) -> float: + if self._disable_brightness_adjust: return None if self.is_sleep(): return self._sleep_brightness if self._circadian_lighting._percent > 0: return self._max_brightness delta_brightness = self._max_brightness - self._min_brightness - procent = (100 + self._circadian_lighting._percent) / 100 - return (delta_brightness * procent) + self._min_brightness + percent = (100 + self._circadian_lighting._percent) / 100 + return (delta_brightness * percent) + self._min_brightness @log(logger=_LOGGER) def _update_switch(self, lights=None, transition=None, force=False): @@ -318,9 +318,8 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): continue service_data = {ATTR_ENTITY_ID: light} - brightness = self._brightness - if brightness is not None: - service_data[ATTR_BRIGHTNESS] = int((brightness / 100) * 254) + if self._brightness is not None: + service_data[ATTR_BRIGHTNESS] = int((self._brightness / 100) * 254) if transition is not None: service_data[ATTR_TRANSITION] = transition From 6ffddbda61b5dd78efa3bdcb58ed788e5794e129 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 1 Sep 2020 08:39:19 +0200 Subject: [PATCH 0086/1077] remove unused import and code --- custom_components/circadian_lighting/sensor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py index 76971eb5..4df3cc1e 100755 --- a/custom_components/circadian_lighting/sensor.py +++ b/custom_components/circadian_lighting/sensor.py @@ -4,13 +4,10 @@ Circadian Lighting Sensor for Home-Assistant. import logging -from homeassistant.helpers.dispatcher import dispatcher_connect from homeassistant.helpers.entity import Entity from custom_components.circadian_lighting import DOMAIN -_LOGGER = logging.getLogger(__name__) - ICON = "mdi:theme-light-dark" From b70191d634004f23d6b53dabab3cacd803e5983b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 1 Sep 2020 08:40:19 +0200 Subject: [PATCH 0087/1077] rename once_only -> only_once --- custom_components/circadian_lighting/switch.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 312d2c91..16a38e97 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -64,7 +64,7 @@ CONF_SLEEP_BRIGHT, DEFAULT_SLEEP_BRIGHT = "sleep_brightness", 1 CONF_DISABLE_ENTITY = "disable_entity" CONF_DISABLE_STATE = "disable_state" CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 -CONF_ONCE_ONLY = "once_only" +CONF_ONLY_ONCE = "only_once" PLATFORM_SCHEMA = vol.Schema( { @@ -94,7 +94,7 @@ PLATFORM_SCHEMA = vol.Schema( vol.Optional( CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION ): VALID_TRANSITION, - vol.Optional(CONF_ONCE_ONLY, default=False): cv.boolean, + vol.Optional(CONF_ONLY_ONCE, default=False): cv.boolean, } ) @@ -121,7 +121,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): disable_entity=config.get(CONF_DISABLE_ENTITY), disable_state=config.get(CONF_DISABLE_STATE), initial_transition=config.get(CONF_INITIAL_TRANSITION), - once_only=config.get(CONF_ONCE_ONLY), + only_once=config.get(CONF_ONLY_ONCE), ) add_devices([switch]) @@ -152,7 +152,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): disable_entity, disable_state, initial_transition, - once_only, + only_once, ): """Initialize the Circadian Lighting switch.""" self.hass = hass @@ -173,7 +173,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._disable_entity = disable_entity self._disable_state = disable_state self._initial_transition = initial_transition - self._once_only = once_only + self._only_once = only_once self._lights_types = {} for light in lights_ct: @@ -285,7 +285,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): @log(logger=_LOGGER) def _update_switch(self, lights=None, transition=None, force=False): - if self._once_only and not force: + if self._only_once and not force: return self._hs_color = self.calc_hs() self._brightness = self.calc_brightness() From 4c9d1f66598f4ec0d47565cc7aed84b207afe4da Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 1 Sep 2020 18:39:16 +0200 Subject: [PATCH 0088/1077] use async functions and call trackers/listeners in the correct place --- .../circadian_lighting/__init__.py | 85 ++++++++----------- .../circadian_lighting/sensor.py | 31 ++++--- .../circadian_lighting/switch.py | 74 ++++++++-------- 3 files changed, 93 insertions(+), 97 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 6de46c6f..281d9afa 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -29,11 +29,10 @@ Technical notes: I had to make a lot of assumptions when writing this app import logging from datetime import timedelta -import inspect - -import voluptuous as vol import astral +import voluptuous as vol + import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ATTR_TRANSITION, VALID_TRANSITION from homeassistant.const import ( @@ -44,9 +43,13 @@ from homeassistant.const import ( SUN_EVENT_SUNSET, ) from homeassistant.helpers.discovery import load_platform -from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import track_sunrise, track_sunset, track_time_change -from homeassistant.util import Throttle +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import ( + async_track_sunrise, + async_track_sunset, + async_track_time_change, + async_track_time_interval, +) from homeassistant.util.color import ( color_RGB_to_xy, color_temperature_to_rgb, @@ -89,7 +92,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, vol.Optional(CONF_ELEVATION): float, - vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.positive_int, + vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, vol.Optional( ATTR_TRANSITION, default=DEFAULT_TRANSITION ): VALID_TRANSITION, @@ -100,26 +103,6 @@ CONFIG_SCHEMA = vol.Schema( ) -def log(with_return=False, logger=_LOGGER): - def _log(func): - def wrapper(*args, **kwargs): - func_args = inspect.signature(func).bind(*args, **kwargs).arguments - key_value_pairs = ( - f"{k}={v!r}" for k, v in func_args.items() if k != "self" - ) - func_args_str = ", ".join(key_value_pairs) - out = f"{func.__qualname__}({func_args_str})" - result = func(*args, **kwargs) - if with_return: - out += f" -> {result}" - logger.debug(out) - return result - - return wrapper - - return _log - - def setup(hass, config): """Set up the Circadian Lighting component.""" conf = config[DOMAIN] @@ -180,23 +163,30 @@ class CircadianLighting: self._xy_color = self.calc_xy() self._hs_color = self.calc_hs() - self.update = Throttle(timedelta(seconds=interval))(self._update) + if self._manual_time["sunrise"] is not None: + async_track_time_change( + self.hass, + self.update, + hour=self._manual_time["sunrise"].hour, + minute=self._manual_time["sunrise"].minute, + second=self._manual_time["sunrise"].second, + ) + else: + async_track_sunrise(self.hass, self.update, self._sunrise_offset) - for which in ["sunrise", "sunrise"]: - time = self._manual_time[which] - if time is not None: - track_time_change( - self.hass, - self._update, - hour=time.hour, - minute=time.minute, - second=time.second, - ) + if self._manual_time["sunset"] is not None: + async_track_time_change( + self.hass, + self.update, + hour=self._manual_time["sunset"].hour, + minute=self._manual_time["sunset"].minute, + second=self._manual_time["sunset"].second, + ) + else: + async_track_sunset(self.hass, self.update, self._sunset_offset) - track_sunrise(self.hass, self._update, self._sunrise_offset) - track_sunset(self.hass, self._update, self._sunset_offset) + async_track_time_interval(self.hass, self.update, interval) - @log(with_return=True) def get_timezone(self): tf = TimezoneFinder() timezone_string = tf.timezone_at(lng=self._longitude, lat=self._latitude) @@ -263,8 +253,8 @@ class CircadianLighting: today = self.get_sunrise_sunset(now) if now_ts < today[SUN_EVENT_SUNRISE]: - # It's before sunrise (after midnight) - # Because it's before sunrise (and after midnight) sunset must have happend yesterday + # It's before sunrise (after midnight), because it's before + # sunrise (and after midnight) sunset must have happend yesterday. yesterday = self.get_sunrise_sunset(now - timedelta(days=1)) today[SUN_EVENT_SUNSET] = yesterday[SUN_EVENT_SUNSET] if ( @@ -274,8 +264,8 @@ class CircadianLighting: # Solar midnight is after sunset so use yesterdays's time today[SUN_EVENT_MIDNIGHT] = yesterday[SUN_EVENT_MIDNIGHT] elif now_ts > today[SUN_EVENT_SUNSET]: - # It's after sunset (before midnight) - # Because it's after sunset (and before midnight) sunrise should happen tomorrow + # It's after sunset (before midnight), because it's after sunset + # (and before midnight) sunrise should happen tomorrow. tomorrow = self.get_sunrise_sunset(now + timedelta(days=1)) today[SUN_EVENT_SUNRISE] = tomorrow[SUN_EVENT_SUNRISE] if ( @@ -335,12 +325,11 @@ class CircadianLighting: def calc_hs(self): return color_xy_to_hs(*self.calc_xy()) - @log() - def _update(self): + async def update(self, _=None): """Update Circadian Values.""" self._percent = self.calc_percent() self._colortemp = self.calc_colortemp() self._rgb_color = self.calc_rgb() self._xy_color = self.calc_xy() self._hs_color = self.calc_hs() - dispatcher_send(self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC) + async_dispatcher_send(self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC) diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py index 4df3cc1e..b924fa3f 100755 --- a/custom_components/circadian_lighting/sensor.py +++ b/custom_components/circadian_lighting/sensor.py @@ -2,11 +2,11 @@ Circadian Lighting Sensor for Home-Assistant. """ -import logging - +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from custom_components.circadian_lighting import DOMAIN +from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN ICON = "mdi:theme-light-dark" @@ -16,11 +16,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): circadian_lighting = hass.data.get(DOMAIN) if circadian_lighting is not None: sensor = CircadianSensor(hass, circadian_lighting) - add_devices([sensor]) + add_devices([sensor], True) def update(call=None): """Update component.""" - circadian_lighting._update() + circadian_lighting.update() service_name = "values_update" hass.services.register(DOMAIN, service_name, update) @@ -78,9 +78,20 @@ class CircadianSensor(Entity): "xy_color": self._circadian_lighting._xy_color, } - def update(self): - """Fetch new state data for the sensor. + @property + def should_poll(self) -> bool: + """Disable polling.""" + return False - This is the only method that should fetch new data for Home Assistant. - """ - self._circadian_lighting.update() + async def async_added_to_hass(self) -> None: + """Connect dispatcher to signal from CircadianLighting object.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self._update_callback + ) + ) + + @callback + def _update_callback(self) -> None: + """Triggers update of properties after receiving signal from CircadianLighting.""" + self.async_schedule_update_ha_state(force_refresh=False) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 16a38e97..584d0785 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -2,10 +2,12 @@ Circadian Lighting Switch for Home-Assistant. """ +import functools import logging -import homeassistant.helpers.config_validation as cv import voluptuous as vol + +import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -16,6 +18,7 @@ from homeassistant.components.light import ( ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.light import VALID_TRANSITION, is_on +from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( ATTR_ENTITY_ID, CONF_NAME, @@ -23,8 +26,8 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.helpers.dispatcher import dispatcher_connect -from homeassistant.helpers.event import track_state_change +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util import slugify from homeassistant.util.color import ( @@ -34,17 +37,7 @@ from homeassistant.util.color import ( color_xy_to_hs, ) -from custom_components.circadian_lighting import ( - CIRCADIAN_LIGHTING_UPDATE_TOPIC, - DOMAIN, - log, -) - -try: - from homeassistant.components.switch import SwitchEntity -except ImportError: - from homeassistant.components.switch import SwitchDevice as SwitchEntity - +from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -186,14 +179,6 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._lights_types[light] = "brightness" self._lights = list(self._lights_types.keys()) - # Register callbacks - dispatcher_connect(hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self._update_switch) - track_state_change(hass, self._lights, self.light_state_changed) - if self._sleep_entity is not None: - track_state_change(hass, self._sleep_entity, self.sleep_state_changed) - if self._disable_entity is not None: - track_state_change(hass, self._disable_entity, self.disable_state_changed) - @property def entity_id(self): """Return the entity ID of the switch.""" @@ -211,9 +196,33 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): async def async_added_to_hass(self): """Call when entity about to be added to hass.""" - # If not None, we got an initial value. - await super().async_added_to_hass() - if self._state is not None: + # Add callback + self.async_on_remove( + async_dispatcher_connect( + self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self._update_switch + ) + ) + + # Add listeners + async_track_state_change(self.hass, self._lights, self.light_state_changed) + + if self._sleep_entity is not None: + async_track_state_change( + self.hass, self._sleep_entity, self.sleep_state_changed + ) + + if self._disable_entity is not None: + disable_state_changed = functools.partial( + self._update_switch, transition=self._initial_transition, force=True + ) + async_track_state_change( + self.hass, + self._disable_entity, + disable_state_changed, + from_state=self._disable_state, + ) + + if self._state is not None: # If not None, we got an initial value return state = await self.async_get_last_state() @@ -237,16 +246,13 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): """Turn on circadian lighting.""" self._state = True self._update_switch(transition=self._initial_transition, force=True) - self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn off circadian lighting.""" self._state = False - self.schedule_update_ha_state() self._hs_color = None self._brightness = None - @log(with_return=True, logger=_LOGGER) def is_sleep(self): return ( self._sleep_entity is not None @@ -283,7 +289,6 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): percent = (100 + self._circadian_lighting._percent) / 100 return (delta_brightness * percent) + self._min_brightness - @log(logger=_LOGGER) def _update_switch(self, lights=None, transition=None, force=False): if self._only_once and not force: return @@ -291,14 +296,12 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._brightness = self.calc_brightness() self._adjust_lights(lights or self._lights, transition) - @log(with_return=True, logger=_LOGGER) def _is_disabled(self): return ( self._disable_entity is not None and self.hass.states.get(self._disable_entity).state in self._disable_state ) - @log(with_return=True, logger=_LOGGER) def _should_adjust(self): if self._state is not True: return False @@ -306,7 +309,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): return False return True - def _adjust_lights(self, lights, transition=None): + def _adjust_lights(self, lights, transition): if not self._should_adjust(): return @@ -337,17 +340,10 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) _LOGGER.debug(f"{light} {light_type} Adjusted - {service_data}") - @log(with_return=True, logger=_LOGGER) def light_state_changed(self, entity_id, from_state, to_state): if to_state.state == "on" and from_state.state != "on": self._update_switch([entity_id], self._initial_transition, force=True) - @log(with_return=True, logger=_LOGGER) def sleep_state_changed(self, entity_id, from_state, to_state): if to_state.state in self._sleep_state or from_state.state in self._sleep_state: self._update_switch(transition=self._initial_transition, force=True) - - @log(with_return=True, logger=_LOGGER) - def disable_state_changed(self, entity_id, from_state, to_state): - if from_state.state in self._disable_state: - self._update_switch(transition=self._initial_transition, force=True) From 9e03d5aeebea2694690e6ac5a1101a38737344fd Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 1 Sep 2020 18:40:19 +0200 Subject: [PATCH 0089/1077] remove arg in get_sunrise_sunset and call update in __init__ --- .../circadian_lighting/__init__.py | 30 ++++++++++--------- .../circadian_lighting/sensor.py | 2 +- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 281d9afa..3477a175 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -157,11 +157,13 @@ class CircadianLighting: self._elevation = elevation self._transition = transition self._timezone = self.get_timezone() - self._percent = self.calc_percent() - self._colortemp = self.calc_colortemp() - self._rgb_color = self.calc_rgb() - self._xy_color = self.calc_xy() - self._hs_color = self.calc_hs() + + self._percent = None + self._colortemp = None + self._rgb_color = None + self._xy_color = None + self._hs_color = None + self.update() if self._manual_time["sunrise"] is not None: async_track_time_change( @@ -201,7 +203,7 @@ class CircadianLighting: microsecond=other_date.microsecond, ) - def get_sunrise_sunset(self, date, as_timestamps=True): + def get_sunrise_sunset(self, date): if ( self._manual_time["sunrise"] is not None and self._manual_time["sunset"] is not None @@ -237,15 +239,15 @@ class CircadianLighting: sunset = sunset + self._sunset_offset datetimes = { - SUN_EVENT_SUNRISE: sunrise.astimezone(self._timezone), - SUN_EVENT_SUNSET: sunset.astimezone(self._timezone), - SUN_EVENT_NOON: solar_noon.astimezone(self._timezone), - SUN_EVENT_MIDNIGHT: solar_midnight.astimezone(self._timezone), + SUN_EVENT_SUNRISE: sunrise, + SUN_EVENT_SUNSET: sunset, + SUN_EVENT_NOON: solar_noon, + SUN_EVENT_MIDNIGHT: solar_midnight, + } + + return { + k: dt.astimezone(self._timezone).timestamp() for k, dt in datetimes.items() } - if as_timestamps: - return {k: dt.timestamp() for k, dt in datetimes.items()} - else: - return datetimes def calc_percent(self): now = dt_now(self._timezone) diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py index b924fa3f..35947f92 100755 --- a/custom_components/circadian_lighting/sensor.py +++ b/custom_components/circadian_lighting/sensor.py @@ -93,5 +93,5 @@ class CircadianSensor(Entity): @callback def _update_callback(self) -> None: - """Triggers update of properties after receiving signal from CircadianLighting.""" + """Triggers update of properties.""" self.async_schedule_update_ha_state(force_refresh=False) From ce19976c5ee8b73a4bffb5cbbc0bb6df2ad7c9d3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 1 Sep 2020 18:49:16 +0200 Subject: [PATCH 0090/1077] fixes from previous commit --- .../circadian_lighting/__init__.py | 11 ++++---- .../circadian_lighting/switch.py | 25 ++++++++++++------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 3477a175..ed40ae1c 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -158,12 +158,11 @@ class CircadianLighting: self._transition = transition self._timezone = self.get_timezone() - self._percent = None - self._colortemp = None - self._rgb_color = None - self._xy_color = None - self._hs_color = None - self.update() + self._percent = self.calc_percent() + self._colortemp = self.calc_colortemp() + self._rgb_color = self.calc_rgb() + self._xy_color = self.calc_xy() + self._hs_color = self.calc_hs() if self._manual_time["sunrise"] is not None: async_track_time_change( diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 584d0785..86631047 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -2,7 +2,6 @@ Circadian Lighting Switch for Home-Assistant. """ -import functools import logging import voluptuous as vol @@ -204,7 +203,9 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): ) # Add listeners - async_track_state_change(self.hass, self._lights, self.light_state_changed) + async_track_state_change( + self.hass, self._lights, self.light_state_changed, to_state="on" + ) if self._sleep_entity is not None: async_track_state_change( @@ -212,13 +213,10 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): ) if self._disable_entity is not None: - disable_state_changed = functools.partial( - self._update_switch, transition=self._initial_transition, force=True - ) async_track_state_change( self.hass, self._disable_entity, - disable_state_changed, + self.disable_state_changed, from_state=self._disable_state, ) @@ -245,7 +243,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def turn_on(self, **kwargs): """Turn on circadian lighting.""" self._state = True - self._update_switch(transition=self._initial_transition, force=True) + self._force_update_switch() def turn_off(self, **kwargs): """Turn off circadian lighting.""" @@ -296,6 +294,11 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._brightness = self.calc_brightness() self._adjust_lights(lights or self._lights, transition) + def _force_update_switch(self, lights=None): + return self._update_switch( + lights, transition=self._initial_transition, force=True + ) + def _is_disabled(self): return ( self._disable_entity is not None @@ -342,8 +345,12 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def light_state_changed(self, entity_id, from_state, to_state): if to_state.state == "on" and from_state.state != "on": - self._update_switch([entity_id], self._initial_transition, force=True) + self._force_update_switch(lights=[entity_id]) def sleep_state_changed(self, entity_id, from_state, to_state): if to_state.state in self._sleep_state or from_state.state in self._sleep_state: - self._update_switch(transition=self._initial_transition, force=True) + self._force_update_switch() + + def disable_state_changed(self, entity_id, from_state, to_state): + if from_state.state in self._disable_state: + self._force_update_switch() From f02be53d3fbf3aae32ee7107fee33ab1ae4c7bf2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 1 Sep 2020 18:55:12 +0200 Subject: [PATCH 0091/1077] make methods private --- .../circadian_lighting/switch.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 86631047..b7709ed8 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -204,19 +204,19 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): # Add listeners async_track_state_change( - self.hass, self._lights, self.light_state_changed, to_state="on" + self.hass, self._lights, self._light_state_changed, to_state="on" ) if self._sleep_entity is not None: async_track_state_change( - self.hass, self._sleep_entity, self.sleep_state_changed + self.hass, self._sleep_entity, self._sleep_state_changed ) if self._disable_entity is not None: async_track_state_change( self.hass, self._disable_entity, - self.disable_state_changed, + self._disable_state_changed, from_state=self._disable_state, ) @@ -264,19 +264,19 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): else self._sleep_colortemp ) - def calc_ct(self): + def _calc_ct(self): return color_temperature_kelvin_to_mired(self._color_temperature()) - def calc_rgb(self): + def _calc_rgb(self): return color_temperature_to_rgb(self._color_temperature()) - def calc_xy(self): - return color_RGB_to_xy(*self.calc_rgb()) + def _calc_xy(self): + return color_RGB_to_xy(*self._calc_rgb()) - def calc_hs(self): - return color_xy_to_hs(*self.calc_xy()) + def _calc_hs(self): + return color_xy_to_hs(*self._calc_xy()) - def calc_brightness(self) -> float: + def _calc_brightness(self) -> float: if self._disable_brightness_adjust: return None if self.is_sleep(): @@ -290,8 +290,8 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def _update_switch(self, lights=None, transition=None, force=False): if self._only_once and not force: return - self._hs_color = self.calc_hs() - self._brightness = self.calc_brightness() + self._hs_color = self._calc_hs() + self._brightness = self._calc_brightness() self._adjust_lights(lights or self._lights, transition) def _force_update_switch(self, lights=None): @@ -331,26 +331,26 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): light_type = self._lights_types[light] if light_type == "ct": - service_data[ATTR_COLOR_TEMP] = int(self.calc_ct()) + service_data[ATTR_COLOR_TEMP] = int(self._calc_ct()) elif light_type == "rgb": - r, g, b = self.calc_rgb() + r, g, b = self._calc_rgb() service_data[ATTR_RGB_COLOR] = (int(r), int(g), int(b)) elif light_type == "xy": - service_data[ATTR_XY_COLOR] = self.calc_xy() + service_data[ATTR_XY_COLOR] = self._calc_xy() if service_data.get(ATTR_BRIGHTNESS, False): service_data[ATTR_WHITE_VALUE] = service_data[ATTR_BRIGHTNESS] self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) _LOGGER.debug(f"{light} {light_type} Adjusted - {service_data}") - def light_state_changed(self, entity_id, from_state, to_state): + def _light_state_changed(self, entity_id, from_state, to_state): if to_state.state == "on" and from_state.state != "on": self._force_update_switch(lights=[entity_id]) - def sleep_state_changed(self, entity_id, from_state, to_state): + def _sleep_state_changed(self, entity_id, from_state, to_state): if to_state.state in self._sleep_state or from_state.state in self._sleep_state: self._force_update_switch() - def disable_state_changed(self, entity_id, from_state, to_state): + def _disable_state_changed(self, entity_id, from_state, to_state): if from_state.state in self._disable_state: self._force_update_switch() From 25cbcf635b6479ebe01d8d34633527860fd0e161 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 1 Sep 2020 18:56:10 +0200 Subject: [PATCH 0092/1077] remove ._manual_time dict --- .../circadian_lighting/__init__.py | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index ed40ae1c..ffc91bed 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -148,10 +148,8 @@ class CircadianLighting: self._max_colortemp = max_colortemp self._sunrise_offset = sunrise_offset self._sunset_offset = sunset_offset - self._manual_time = { - "sunrise": sunrise_time, - "sunset": sunset_time, - } + self._manual_sunset = sunset_time + self._manual_sunrise = sunrise_time self._latitude = latitude self._longitude = longitude self._elevation = elevation @@ -164,24 +162,24 @@ class CircadianLighting: self._xy_color = self.calc_xy() self._hs_color = self.calc_hs() - if self._manual_time["sunrise"] is not None: + if self._manual_sunrise is not None: async_track_time_change( self.hass, self.update, - hour=self._manual_time["sunrise"].hour, - minute=self._manual_time["sunrise"].minute, - second=self._manual_time["sunrise"].second, + hour=self._manual_sunrise.hour, + minute=self._manual_sunrise.minute, + second=self._manual_sunrise.second, ) else: async_track_sunrise(self.hass, self.update, self._sunrise_offset) - if self._manual_time["sunset"] is not None: + if self._manual_sunset is not None: async_track_time_change( self.hass, self.update, - hour=self._manual_time["sunset"].hour, - minute=self._manual_time["sunset"].minute, - second=self._manual_time["sunset"].second, + hour=self._manual_sunset.hour, + minute=self._manual_sunset.minute, + second=self._manual_sunset.second, ) else: async_track_sunset(self.hass, self.update, self._sunset_offset) @@ -194,7 +192,7 @@ class CircadianLighting: return get_time_zone(timezone_string) def _replace_time(self, date, key): - other_date = self._manual_time[key] + other_date = self._manual_sunrise if key == "sunrise" else self._manual_sunset return date.replace( hour=other_date.hour, minute=other_date.minute, @@ -203,10 +201,7 @@ class CircadianLighting: ) def get_sunrise_sunset(self, date): - if ( - self._manual_time["sunrise"] is not None - and self._manual_time["sunset"] is not None - ): + if self._manual_sunrise is not None and self._manual_sunset is not None: sunrise = self._replace_time(date, "sunrise") sunset = self._replace_time(date, "sunset") solar_noon = sunrise + (sunset - sunrise) / 2 @@ -219,12 +214,12 @@ class CircadianLighting: location.longitude = self._longitude location.elevation = self._elevation - if self._manual_time["sunrise"] is not None: + if self._manual_sunrise is not None: sunrise = self._replace_time(date, "sunrise") else: sunrise = location.sunrise(date) - if self._manual_time["sunset"] is not None: + if self._manual_sunset is not None: sunset = self._replace_time(date, "sunset") else: sunset = location.sunset(date) From 92d367e3bc9de5d65f00854b3b86db4dc030ad8c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 1 Sep 2020 18:59:40 +0200 Subject: [PATCH 0093/1077] unify _sleep_state_changed and _disable_state_changed into _state_changed --- .../circadian_lighting/switch.py | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index b7709ed8..d15ddc78 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -206,17 +206,16 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): async_track_state_change( self.hass, self._lights, self._light_state_changed, to_state="on" ) - + track_kwargs = dict(hass=self.hass, action=self._state_changed) if self._sleep_entity is not None: - async_track_state_change( - self.hass, self._sleep_entity, self._sleep_state_changed - ) + sleep_kwargs = dict(track_kwargs, entity_ids=self._sleep_entity) + async_track_state_change(**sleep_kwargs, to_state=self._sleep_state) + async_track_state_change(**sleep_kwargs, from_state=self._sleep_state) if self._disable_entity is not None: async_track_state_change( - self.hass, - self._disable_entity, - self._disable_state_changed, + **track_kwargs, + entity_ids=self._disable_entity, from_state=self._disable_state, ) @@ -347,10 +346,5 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): if to_state.state == "on" and from_state.state != "on": self._force_update_switch(lights=[entity_id]) - def _sleep_state_changed(self, entity_id, from_state, to_state): - if to_state.state in self._sleep_state or from_state.state in self._sleep_state: - self._force_update_switch() - - def _disable_state_changed(self, entity_id, from_state, to_state): - if from_state.state in self._disable_state: - self._force_update_switch() + def _state_changed(self, entity_id, from_state, to_state): + self._force_update_switch() From 368bc9fddc9cb4fcc06141001a10c3a5589c8da6 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 1 Sep 2020 20:02:13 +0200 Subject: [PATCH 0094/1077] shorten self._lights_types construction --- custom_components/circadian_lighting/switch.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index d15ddc78..0b0b4b2d 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -3,6 +3,7 @@ Circadian Lighting Switch for Home-Assistant. """ import logging +from itertools import repeat import voluptuous as vol @@ -166,16 +167,10 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._disable_state = disable_state self._initial_transition = initial_transition self._only_once = only_once - - self._lights_types = {} - for light in lights_ct: - self._lights_types[light] = "ct" - for light in lights_rgb: - self._lights_types[light] = "rgb" - for light in lights_xy: - self._lights_types[light] = "xy" - for light in lights_brightness: - self._lights_types[light] = "brightness" + self._lights_types = dict(zip(lights_ct, repeat("ct"))) + self._lights_types.update(zip(lights_rgb, repeat("rgb"))) + self._lights_types.update(zip(lights_xy, repeat("xy"))) + self._lights_types.update(zip(lights_brightness, repeat("brightness"))) self._lights = list(self._lights_types.keys()) @property From becc846287a808cd9d7f093bb1f38035f5060e90 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 1 Sep 2020 20:02:23 +0200 Subject: [PATCH 0095/1077] component -> platform --- custom_components/circadian_lighting/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index ffc91bed..d43b8cb4 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -104,7 +104,7 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): - """Set up the Circadian Lighting component.""" + """Set up the Circadian Lighting platform.""" conf = config[DOMAIN] hass.data[DOMAIN] = CircadianLighting( hass, From ed9389a1e7666724a5b4d3746580a75719dc83b9 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 2 Sep 2020 09:38:21 +0200 Subject: [PATCH 0096/1077] make the switch async to ensure all light adjusting happens at the same time --- .../circadian_lighting/switch.py | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 0b0b4b2d..953374b0 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -2,6 +2,7 @@ Circadian Lighting Switch for Home-Assistant. """ +import asyncio import logging from itertools import repeat @@ -234,10 +235,10 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): """Return the attributes of the switch.""" return {"hs_color": self._hs_color, "brightness": self._brightness} - def turn_on(self, **kwargs): + async def turn_on(self, **kwargs): """Turn on circadian lighting.""" self._state = True - self._force_update_switch() + await self._force_update_switch() def turn_off(self, **kwargs): """Turn off circadian lighting.""" @@ -281,15 +282,15 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): percent = (100 + self._circadian_lighting._percent) / 100 return (delta_brightness * percent) + self._min_brightness - def _update_switch(self, lights=None, transition=None, force=False): + async def _update_switch(self, lights=None, transition=None, force=False): if self._only_once and not force: return self._hs_color = self._calc_hs() self._brightness = self._calc_brightness() - self._adjust_lights(lights or self._lights, transition) + await self._adjust_lights(lights or self._lights, transition) - def _force_update_switch(self, lights=None): - return self._update_switch( + async def _force_update_switch(self, lights=None): + return await self._update_switch( lights, transition=self._initial_transition, force=True ) @@ -306,13 +307,14 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): return False return True - def _adjust_lights(self, lights, transition): + async def _adjust_lights(self, lights, transition): if not self._should_adjust(): return if transition is None: transition = self._circadian_lighting._transition + tasks = [] for light in lights: if not is_on(self.hass, light): continue @@ -334,12 +336,15 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): if service_data.get(ATTR_BRIGHTNESS, False): service_data[ATTR_WHITE_VALUE] = service_data[ATTR_BRIGHTNESS] - self.hass.services.call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) - _LOGGER.debug(f"{light} {light_type} Adjusted - {service_data}") + tasks.append(self.hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data + )) + if tasks: + await asyncio.wait(tasks) - def _light_state_changed(self, entity_id, from_state, to_state): + async def _light_state_changed(self, entity_id, from_state, to_state): if to_state.state == "on" and from_state.state != "on": - self._force_update_switch(lights=[entity_id]) + await self._force_update_switch(lights=[entity_id]) - def _state_changed(self, entity_id, from_state, to_state): - self._force_update_switch() + async def _state_changed(self, entity_id, from_state, to_state): + await self._force_update_switch() From dee9dd10f8acd4a9b9f23ee186a45e2e7886cdf0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 3 Sep 2020 23:56:48 +0200 Subject: [PATCH 0097/1077] run black --- custom_components/circadian_lighting/switch.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 953374b0..879fdf04 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -336,9 +336,11 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): if service_data.get(ATTR_BRIGHTNESS, False): service_data[ATTR_WHITE_VALUE] = service_data[ATTR_BRIGHTNESS] - tasks.append(self.hass.services.async_call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data - )) + tasks.append( + self.hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data + ) + ) if tasks: await asyncio.wait(tasks) From 0264aeaa09af34c05c4e1a918330bb91344c5f82 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 4 Sep 2020 16:18:45 +0200 Subject: [PATCH 0098/1077] add a more useful debug message for state changing --- .../circadian_lighting/switch.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 879fdf04..9c0650f9 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -124,6 +124,39 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False +def _difference_between_states(from_state, to_state): + start = "Lights adjusting because " + if from_state is None and to_state is None: + return start + "Both states None" + if from_state is None: + return start + f"from_state: None, to_state: {to_state}" + if to_state is None: + return start + f"from_state: {from_state}, to_state: None" + + changed_attrs = ", ".join( + [ + f"{key}: {val}" + for key, val in to_state.attributes.items() + if from_state.attributes.get(key) != val + ] + ) + if from_state.state == to_state.state: + return start + ( + f"{from_state.entity_id} is still {to_state.state} but" + f" these attributes changes: {changed_attrs}." + ) + elif changed_attrs != "": + return start + ( + f"{from_state.entity_id} changed from {from_state.state} to" + f" {to_state.state} and these attributes changes: {changed_attrs}." + ) + else: + return start + ( + f"{from_state.entity_id} changed from {from_state.state} to" + f" {to_state.state} and no attributes changed." + ) + + class CircadianSwitch(SwitchEntity, RestoreEntity): """Representation of a Circadian Lighting switch.""" @@ -346,7 +379,9 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): async def _light_state_changed(self, entity_id, from_state, to_state): if to_state.state == "on" and from_state.state != "on": + _LOGGER.debug(_difference_between_states(from_state, to_state)) await self._force_update_switch(lights=[entity_id]) async def _state_changed(self, entity_id, from_state, to_state): + _LOGGER.debug(_difference_between_states(from_state, to_state)) await self._force_update_switch() From 9074b9fe7185ea11621c11f578c390b146ef5594 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 4 Sep 2020 18:34:20 +0200 Subject: [PATCH 0099/1077] add one more _LOGGER line --- custom_components/circadian_lighting/switch.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 9c0650f9..6887219c 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -369,6 +369,10 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): if service_data.get(ATTR_BRIGHTNESS, False): service_data[ATTR_WHITE_VALUE] = service_data[ATTR_BRIGHTNESS] + _LOGGER.debug( + "Scheduling 'light.turn_on' with the following 'service_data': %s", + service_data, + ) tasks.append( self.hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, service_data From a464189ce07b433416f8a5e7fda52bb967dbf05b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 5 Sep 2020 15:02:52 +0200 Subject: [PATCH 0100/1077] Fix case when from_state is None, closes #111 Made this commit on my phone and didn't test it yet. --- custom_components/circadian_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 6887219c..eee3751e 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -382,7 +382,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): await asyncio.wait(tasks) async def _light_state_changed(self, entity_id, from_state, to_state): - if to_state.state == "on" and from_state.state != "on": + if to_state.state == "on" and (from_state is None or from_state.state != "on"): _LOGGER.debug(_difference_between_states(from_state, to_state)) await self._force_update_switch(lights=[entity_id]) From 37ac31c3c633ed3027b80554eae96dae6fb88cf9 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 5 Sep 2020 15:08:26 +0200 Subject: [PATCH 0101/1077] to_state will always be on --- custom_components/circadian_lighting/switch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index eee3751e..75e122fb 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -382,7 +382,8 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): await asyncio.wait(tasks) async def _light_state_changed(self, entity_id, from_state, to_state): - if to_state.state == "on" and (from_state is None or from_state.state != "on"): + assert to_state.state == "on" + if from_state is None or from_state.state != "on": _LOGGER.debug(_difference_between_states(from_state, to_state)) await self._force_update_switch(lights=[entity_id]) From 618234259d658e4165d25681eaf14131808391e2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 5 Sep 2020 17:39:19 +0200 Subject: [PATCH 0102/1077] transition should always exist --- custom_components/circadian_lighting/switch.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 75e122fb..b77e04e4 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -352,11 +352,9 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): if not is_on(self.hass, light): continue - service_data = {ATTR_ENTITY_ID: light} + service_data = {ATTR_ENTITY_ID: light, ATTR_TRANSITION: transition} if self._brightness is not None: service_data[ATTR_BRIGHTNESS] = int((self._brightness / 100) * 254) - if transition is not None: - service_data[ATTR_TRANSITION] = transition light_type = self._lights_types[light] if light_type == "ct": From 46a74cb5a5aa76c5a9f81c36a8d526ef7ced4baf Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 5 Sep 2020 20:09:56 +0200 Subject: [PATCH 0103/1077] prepend async_ to turn_on and turn_off --- custom_components/circadian_lighting/switch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index b77e04e4..54bb5e92 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -268,12 +268,12 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): """Return the attributes of the switch.""" return {"hs_color": self._hs_color, "brightness": self._brightness} - async def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn on circadian lighting.""" self._state = True await self._force_update_switch() - def turn_off(self, **kwargs): + def async_turn_off(self, **kwargs): """Turn off circadian lighting.""" self._state = False self._hs_color = None @@ -306,7 +306,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def _calc_brightness(self) -> float: if self._disable_brightness_adjust: - return None + return if self.is_sleep(): return self._sleep_brightness if self._circadian_lighting._percent > 0: From b381c5a96d873a6bb1cf0f697364f7e4b7114723 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 5 Sep 2020 20:14:51 +0200 Subject: [PATCH 0104/1077] make is_sleep a private method --- custom_components/circadian_lighting/switch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 54bb5e92..43b61a88 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -279,7 +279,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._hs_color = None self._brightness = None - def is_sleep(self): + def _is_sleep(self): return ( self._sleep_entity is not None and self.hass.states.get(self._sleep_entity).state in self._sleep_state @@ -288,7 +288,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def _color_temperature(self): return ( self._circadian_lighting._colortemp - if not self.is_sleep() + if not self._is_sleep() else self._sleep_colortemp ) @@ -307,7 +307,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def _calc_brightness(self) -> float: if self._disable_brightness_adjust: return - if self.is_sleep(): + if self._is_sleep(): return self._sleep_brightness if self._circadian_lighting._percent > 0: return self._max_brightness From 3b868858b006f53a06e4e15dbf517fdd4779c8dc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 5 Sep 2020 20:16:15 +0200 Subject: [PATCH 0105/1077] make async_turn_off actually async --- custom_components/circadian_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 43b61a88..3ded81a2 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -273,7 +273,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._state = True await self._force_update_switch() - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn off circadian lighting.""" self._state = False self._hs_color = None From 7744c2e939376c99448d37d61182ad4e9c48c925 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 5 Sep 2020 20:17:14 +0200 Subject: [PATCH 0106/1077] decapitalize "Both" -> "both" --- custom_components/circadian_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 3ded81a2..7c5b6cc5 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -127,7 +127,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): def _difference_between_states(from_state, to_state): start = "Lights adjusting because " if from_state is None and to_state is None: - return start + "Both states None" + return start + "both states None" if from_state is None: return start + f"from_state: None, to_state: {to_state}" if to_state is None: From 55673b7e61b8b6c0283957f3d6f0bc0640c228ce Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 5 Sep 2020 20:30:10 +0200 Subject: [PATCH 0107/1077] remove timezonefinder dependency --- custom_components/circadian_lighting/__init__.py | 14 +++----------- custom_components/circadian_lighting/manifest.json | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index d43b8cb4..97d90952 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -34,6 +34,7 @@ import astral import voluptuous as vol import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util from homeassistant.components.light import ATTR_TRANSITION, VALID_TRANSITION from homeassistant.const import ( CONF_ELEVATION, @@ -55,9 +56,6 @@ from homeassistant.util.color import ( color_temperature_to_rgb, color_xy_to_hs, ) -from homeassistant.util.dt import get_time_zone -from homeassistant.util.dt import now as dt_now -from timezonefinder import TimezoneFinder _LOGGER = logging.getLogger(__name__) @@ -154,7 +152,6 @@ class CircadianLighting: self._longitude = longitude self._elevation = elevation self._transition = transition - self._timezone = self.get_timezone() self._percent = self.calc_percent() self._colortemp = self.calc_colortemp() @@ -186,11 +183,6 @@ class CircadianLighting: async_track_time_interval(self.hass, self.update, interval) - def get_timezone(self): - tf = TimezoneFinder() - timezone_string = tf.timezone_at(lng=self._longitude, lat=self._latitude) - return get_time_zone(timezone_string) - def _replace_time(self, date, key): other_date = self._manual_sunrise if key == "sunrise" else self._manual_sunset return date.replace( @@ -240,11 +232,11 @@ class CircadianLighting: } return { - k: dt.astimezone(self._timezone).timestamp() for k, dt in datetimes.items() + k: dt.astimezone(dt_util.UTC).timestamp() for k, dt in datetimes.items() } def calc_percent(self): - now = dt_now(self._timezone) + now = dt_util.utcnow() now_ts = now.timestamp() today = self.get_sunrise_sunset(now) diff --git a/custom_components/circadian_lighting/manifest.json b/custom_components/circadian_lighting/manifest.json index 0008ce6b..756dad08 100644 --- a/custom_components/circadian_lighting/manifest.json +++ b/custom_components/circadian_lighting/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://github.com/claytonjn/hass-circadian_lighting", "dependencies": [], "codeowners": ["@claytonjn"], - "requirements": ["timezonefinder==4.2.0", "astral==1.10.1"] + "requirements": ["astral==1.10.1"] } From 3bf3c5f541b8bf8023678db5f2707a0b1f12ff06 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 9 Sep 2020 23:26:30 +0200 Subject: [PATCH 0108/1077] fix the today['sunset'] is not set before used in comparison --- custom_components/circadian_lighting/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 97d90952..24b4fcb1 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -244,24 +244,24 @@ class CircadianLighting: # It's before sunrise (after midnight), because it's before # sunrise (and after midnight) sunset must have happend yesterday. yesterday = self.get_sunrise_sunset(now - timedelta(days=1)) - today[SUN_EVENT_SUNSET] = yesterday[SUN_EVENT_SUNSET] if ( today[SUN_EVENT_MIDNIGHT] > today[SUN_EVENT_SUNSET] and yesterday[SUN_EVENT_MIDNIGHT] > yesterday[SUN_EVENT_SUNSET] ): # Solar midnight is after sunset so use yesterdays's time today[SUN_EVENT_MIDNIGHT] = yesterday[SUN_EVENT_MIDNIGHT] + today[SUN_EVENT_SUNSET] = yesterday[SUN_EVENT_SUNSET] elif now_ts > today[SUN_EVENT_SUNSET]: # It's after sunset (before midnight), because it's after sunset # (and before midnight) sunrise should happen tomorrow. tomorrow = self.get_sunrise_sunset(now + timedelta(days=1)) - today[SUN_EVENT_SUNRISE] = tomorrow[SUN_EVENT_SUNRISE] if ( today[SUN_EVENT_MIDNIGHT] < today[SUN_EVENT_SUNRISE] and tomorrow[SUN_EVENT_MIDNIGHT] < tomorrow[SUN_EVENT_SUNRISE] ): # Solar midnight is before sunrise so use tomorrow's time today[SUN_EVENT_MIDNIGHT] = tomorrow[SUN_EVENT_MIDNIGHT] + today[SUN_EVENT_SUNRISE] = tomorrow[SUN_EVENT_SUNRISE] # Figure out where we are in time so we know which half of the # parabola to calculate. We're generating a different From 7c29e3281aab9b0b11d941ee2841fa8ed10d9a1b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2020 11:27:53 +0200 Subject: [PATCH 0109/1077] start implementing profile settings, as suggested in https://github.com/claytonjn/hass-circadian_lighting/issues/91#issuecomment-647084406 --- .../circadian_lighting/__init__.py | 5 ++ custom_components/circadian_lighting/const.py | 42 +++++++++++ .../circadian_lighting/switch.py | 72 ++++++++----------- 3 files changed, 78 insertions(+), 41 deletions(-) create mode 100644 custom_components/circadian_lighting/const.py diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 24b4fcb1..48cb5f85 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -57,6 +57,8 @@ from homeassistant.util.color import ( color_xy_to_hs, ) +from .const import _PROFILE_SCHEMA, CONF_PROFILE + _LOGGER = logging.getLogger(__name__) DOMAIN = "circadian_lighting" @@ -94,6 +96,9 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional( ATTR_TRANSITION, default=DEFAULT_TRANSITION ): VALID_TRANSITION, + vol.Optional(CONF_PROFILE): vol.Schema( + {cv.string: vol.Schema(_PROFILE_SCHEMA)} + ), } ), }, diff --git a/custom_components/circadian_lighting/const.py b/custom_components/circadian_lighting/const.py new file mode 100644 index 00000000..9dab2828 --- /dev/null +++ b/custom_components/circadian_lighting/const.py @@ -0,0 +1,42 @@ +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.light import VALID_TRANSITION + +# Switch and profile settings +CONF_PROFILE = "profile" +CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust" +CONF_MIN_BRIGHT, DEFAULT_MIN_BRIGHT = "min_brightness", 1 +CONF_MAX_BRIGHT, DEFAULT_MAX_BRIGHT = "max_brightness", 100 +CONF_SLEEP_ENTITY = "sleep_entity" +CONF_SLEEP_STATE = "sleep_state" +CONF_SLEEP_CT, DEFAULT_SLEEP_CT = "sleep_colortemp", 1000 +CONF_SLEEP_BRIGHT, DEFAULT_SLEEP_BRIGHT = "sleep_brightness", 1 +CONF_DISABLE_ENTITY = "disable_entity" +CONF_DISABLE_STATE = "disable_state" +CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 +CONF_ONLY_ONCE = "only_once" + +_PROFILE_SCHEMA = { + vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=False): cv.boolean, + vol.Optional(CONF_MIN_BRIGHT, default=DEFAULT_MIN_BRIGHT): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_MAX_BRIGHT, default=DEFAULT_MAX_BRIGHT): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, + vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_SLEEP_CT, default=DEFAULT_SLEEP_CT): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_SLEEP_BRIGHT, default=DEFAULT_SLEEP_BRIGHT): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, + vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional( + CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION + ): VALID_TRANSITION, + vol.Optional(CONF_ONLY_ONCE, default=False): cv.boolean, +} diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 7c5b6cc5..70b2eb2b 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -18,7 +18,7 @@ from homeassistant.components.light import ( ATTR_XY_COLOR, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.light import VALID_TRANSITION, is_on +from homeassistant.components.light import is_on from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( ATTR_ENTITY_ID, @@ -39,6 +39,21 @@ from homeassistant.util.color import ( ) from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN +from .const import ( + _PROFILE_SCHEMA, + CONF_DISABLE_BRIGHTNESS_ADJUST, + CONF_DISABLE_ENTITY, + CONF_DISABLE_STATE, + CONF_INITIAL_TRANSITION, + CONF_MAX_BRIGHT, + CONF_MIN_BRIGHT, + CONF_ONLY_ONCE, + CONF_PROFILE, + CONF_SLEEP_BRIGHT, + CONF_SLEEP_CT, + CONF_SLEEP_ENTITY, + CONF_SLEEP_STATE, +) _LOGGER = logging.getLogger(__name__) @@ -48,17 +63,6 @@ CONF_LIGHTS_CT = "lights_ct" CONF_LIGHTS_RGB = "lights_rgb" CONF_LIGHTS_XY = "lights_xy" CONF_LIGHTS_BRIGHT = "lights_brightness" -CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust" -CONF_MIN_BRIGHT, DEFAULT_MIN_BRIGHT = "min_brightness", 1 -CONF_MAX_BRIGHT, DEFAULT_MAX_BRIGHT = "max_brightness", 100 -CONF_SLEEP_ENTITY = "sleep_entity" -CONF_SLEEP_STATE = "sleep_state" -CONF_SLEEP_CT, DEFAULT_SLEEP_CT = "sleep_colortemp", 1000 -CONF_SLEEP_BRIGHT, DEFAULT_SLEEP_BRIGHT = "sleep_brightness", 1 -CONF_DISABLE_ENTITY = "disable_entity" -CONF_DISABLE_STATE = "disable_state" -CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 -CONF_ONLY_ONCE = "only_once" PLATFORM_SCHEMA = vol.Schema( { @@ -68,27 +72,7 @@ PLATFORM_SCHEMA = vol.Schema( vol.Optional(CONF_LIGHTS_RGB): cv.entity_ids, vol.Optional(CONF_LIGHTS_XY): cv.entity_ids, vol.Optional(CONF_LIGHTS_BRIGHT): cv.entity_ids, - vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=False): cv.boolean, - vol.Optional(CONF_MIN_BRIGHT, default=DEFAULT_MIN_BRIGHT): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_MAX_BRIGHT, default=DEFAULT_MAX_BRIGHT): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, - vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_SLEEP_CT, default=DEFAULT_SLEEP_CT): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_SLEEP_BRIGHT, default=DEFAULT_SLEEP_BRIGHT): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, - vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional( - CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION - ): VALID_TRANSITION, - vol.Optional(CONF_ONLY_ONCE, default=False): cv.boolean, + **_PROFILE_SCHEMA, } ) @@ -97,14 +81,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Circadian Lighting switches.""" circadian_lighting = hass.data.get(DOMAIN) if circadian_lighting is not None: - switch = CircadianSwitch( - hass, - circadian_lighting, - name=config.get(CONF_NAME), - lights_ct=config.get(CONF_LIGHTS_CT, []), - lights_rgb=config.get(CONF_LIGHTS_RGB, []), - lights_xy=config.get(CONF_LIGHTS_XY, []), - lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []), + switch_settings = dict( disable_brightness_adjust=config.get(CONF_DISABLE_BRIGHTNESS_ADJUST), min_brightness=config.get(CONF_MIN_BRIGHT), max_brightness=config.get(CONF_MAX_BRIGHT), @@ -117,6 +94,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): initial_transition=config.get(CONF_INITIAL_TRANSITION), only_once=config.get(CONF_ONLY_ONCE), ) + profile = config.get(CONF_PROFILE) + profile_settings = circadian_lighting._profiles.get(profile, {}) + settings = dict(switch_settings, **profile_settings) + switch = CircadianSwitch( + hass, + circadian_lighting, + name=config.get(CONF_NAME), + lights_ct=config.get(CONF_LIGHTS_CT, []), + lights_rgb=config.get(CONF_LIGHTS_RGB, []), + lights_xy=config.get(CONF_LIGHTS_XY, []), + lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []), + **settings, + ) add_devices([switch]) return True From ae64db707736b97ce06735ae16dd187db52a7c32 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2020 11:54:32 +0200 Subject: [PATCH 0110/1077] initial implementation of profiles --- custom_components/circadian_lighting/__init__.py | 8 ++++++-- custom_components/circadian_lighting/const.py | 1 + custom_components/circadian_lighting/switch.py | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 48cb5f85..9784868c 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -57,7 +57,7 @@ from homeassistant.util.color import ( color_xy_to_hs, ) -from .const import _PROFILE_SCHEMA, CONF_PROFILE +from .const import _PROFILE_SCHEMA, CONF_PROFILES _LOGGER = logging.getLogger(__name__) @@ -96,7 +96,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional( ATTR_TRANSITION, default=DEFAULT_TRANSITION ): VALID_TRANSITION, - vol.Optional(CONF_PROFILE): vol.Schema( + vol.Optional(CONF_PROFILES): vol.Schema( {cv.string: vol.Schema(_PROFILE_SCHEMA)} ), } @@ -122,6 +122,7 @@ def setup(hass, config): elevation=conf.get(CONF_ELEVATION, hass.config.elevation), interval=conf.get(CONF_INTERVAL), transition=conf.get(ATTR_TRANSITION), + profiles=conf.get(CONF_PROFILES, {}), ) load_platform(hass, "sensor", DOMAIN, {}, config) @@ -145,6 +146,7 @@ class CircadianLighting: elevation, interval, transition, + profiles, ): self.hass = hass self._min_colortemp = min_colortemp @@ -157,6 +159,8 @@ class CircadianLighting: self._longitude = longitude self._elevation = elevation self._transition = transition + self._profiles = profiles + _LOGGER.debug("profiles: %s", self._profiles) self._percent = self.calc_percent() self._colortemp = self.calc_colortemp() diff --git a/custom_components/circadian_lighting/const.py b/custom_components/circadian_lighting/const.py index 9dab2828..19253342 100644 --- a/custom_components/circadian_lighting/const.py +++ b/custom_components/circadian_lighting/const.py @@ -5,6 +5,7 @@ from homeassistant.components.light import VALID_TRANSITION # Switch and profile settings CONF_PROFILE = "profile" +CONF_PROFILES = "profiles" CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust" CONF_MIN_BRIGHT, DEFAULT_MIN_BRIGHT = "min_brightness", 1 CONF_MAX_BRIGHT, DEFAULT_MAX_BRIGHT = "max_brightness", 100 diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 70b2eb2b..f811763e 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -72,6 +72,7 @@ PLATFORM_SCHEMA = vol.Schema( vol.Optional(CONF_LIGHTS_RGB): cv.entity_ids, vol.Optional(CONF_LIGHTS_XY): cv.entity_ids, vol.Optional(CONF_LIGHTS_BRIGHT): cv.entity_ids, + vol.Optional(CONF_PROFILE): cv.string, **_PROFILE_SCHEMA, } ) From 7cbe6a425860da0cd33257e4fdc83eefdb661503 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 7 Sep 2020 15:33:04 +0200 Subject: [PATCH 0111/1077] another attempt --- .../circadian_lighting/__init__.py | 43 ++++----- custom_components/circadian_lighting/const.py | 43 --------- .../circadian_lighting/switch.py | 89 ++++++++++++------- 3 files changed, 79 insertions(+), 96 deletions(-) delete mode 100644 custom_components/circadian_lighting/const.py diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 9784868c..dd650360 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -57,8 +57,6 @@ from homeassistant.util.color import ( color_xy_to_hs, ) -from .const import _PROFILE_SCHEMA, CONF_PROFILES - _LOGGER = logging.getLogger(__name__) DOMAIN = "circadian_lighting" @@ -74,30 +72,35 @@ CONF_SUNSET_OFFSET = "sunset_offset" CONF_SUNRISE_TIME = "sunrise_time" CONF_SUNSET_TIME = "sunset_time" DEFAULT_TRANSITION = 60 +CONF_PROFILES = "profiles" + +_DOMAIN_SCHEMA = { + vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_SUNRISE_OFFSET): cv.time_period_str, + vol.Optional(CONF_SUNSET_OFFSET): cv.time_period_str, + vol.Optional(CONF_SUNRISE_TIME): cv.time, + vol.Optional(CONF_SUNSET_TIME): cv.time, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_ELEVATION): float, + vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, + vol.Optional(ATTR_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION, +} + +_DOMAIN_SCHEMA_NO_DEFAULTS = {type(k)(k): v for k, v in _DOMAIN_SCHEMA.items()} CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { - vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_SUNRISE_OFFSET): cv.time_period_str, - vol.Optional(CONF_SUNSET_OFFSET): cv.time_period_str, - vol.Optional(CONF_SUNRISE_TIME): cv.time, - vol.Optional(CONF_SUNSET_TIME): cv.time, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_ELEVATION): float, - vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, - vol.Optional( - ATTR_TRANSITION, default=DEFAULT_TRANSITION - ): VALID_TRANSITION, + **_DOMAIN_SCHEMA, vol.Optional(CONF_PROFILES): vol.Schema( - {cv.string: vol.Schema(_PROFILE_SCHEMA)} + {cv.string: vol.Schema(_DOMAIN_SCHEMA_NO_DEFAULTS)} ), } ), diff --git a/custom_components/circadian_lighting/const.py b/custom_components/circadian_lighting/const.py deleted file mode 100644 index 19253342..00000000 --- a/custom_components/circadian_lighting/const.py +++ /dev/null @@ -1,43 +0,0 @@ -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.light import VALID_TRANSITION - -# Switch and profile settings -CONF_PROFILE = "profile" -CONF_PROFILES = "profiles" -CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust" -CONF_MIN_BRIGHT, DEFAULT_MIN_BRIGHT = "min_brightness", 1 -CONF_MAX_BRIGHT, DEFAULT_MAX_BRIGHT = "max_brightness", 100 -CONF_SLEEP_ENTITY = "sleep_entity" -CONF_SLEEP_STATE = "sleep_state" -CONF_SLEEP_CT, DEFAULT_SLEEP_CT = "sleep_colortemp", 1000 -CONF_SLEEP_BRIGHT, DEFAULT_SLEEP_BRIGHT = "sleep_brightness", 1 -CONF_DISABLE_ENTITY = "disable_entity" -CONF_DISABLE_STATE = "disable_state" -CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 -CONF_ONLY_ONCE = "only_once" - -_PROFILE_SCHEMA = { - vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=False): cv.boolean, - vol.Optional(CONF_MIN_BRIGHT, default=DEFAULT_MIN_BRIGHT): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_MAX_BRIGHT, default=DEFAULT_MAX_BRIGHT): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, - vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_SLEEP_CT, default=DEFAULT_SLEEP_CT): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_SLEEP_BRIGHT, default=DEFAULT_SLEEP_BRIGHT): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, - vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional( - CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION - ): VALID_TRANSITION, - vol.Optional(CONF_ONLY_ONCE, default=False): cv.boolean, -} diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index f811763e..c1b33f9e 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -18,7 +18,7 @@ from homeassistant.components.light import ( ATTR_XY_COLOR, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.light import is_on +from homeassistant.components.light import VALID_TRANSITION, is_on from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( ATTR_ENTITY_ID, @@ -39,21 +39,6 @@ from homeassistant.util.color import ( ) from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN -from .const import ( - _PROFILE_SCHEMA, - CONF_DISABLE_BRIGHTNESS_ADJUST, - CONF_DISABLE_ENTITY, - CONF_DISABLE_STATE, - CONF_INITIAL_TRANSITION, - CONF_MAX_BRIGHT, - CONF_MIN_BRIGHT, - CONF_ONLY_ONCE, - CONF_PROFILE, - CONF_SLEEP_BRIGHT, - CONF_SLEEP_CT, - CONF_SLEEP_ENTITY, - CONF_SLEEP_STATE, -) _LOGGER = logging.getLogger(__name__) @@ -63,6 +48,18 @@ CONF_LIGHTS_CT = "lights_ct" CONF_LIGHTS_RGB = "lights_rgb" CONF_LIGHTS_XY = "lights_xy" CONF_LIGHTS_BRIGHT = "lights_brightness" +CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust" +CONF_MIN_BRIGHT, DEFAULT_MIN_BRIGHT = "min_brightness", 1 +CONF_MAX_BRIGHT, DEFAULT_MAX_BRIGHT = "max_brightness", 100 +CONF_SLEEP_ENTITY = "sleep_entity" +CONF_SLEEP_STATE = "sleep_state" +CONF_SLEEP_CT, DEFAULT_SLEEP_CT = "sleep_colortemp", 1000 +CONF_SLEEP_BRIGHT, DEFAULT_SLEEP_BRIGHT = "sleep_brightness", 1 +CONF_DISABLE_ENTITY = "disable_entity" +CONF_DISABLE_STATE = "disable_state" +CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 +CONF_ONLY_ONCE = "only_once" +CONF_PROFILE = "profile" PLATFORM_SCHEMA = vol.Schema( { @@ -72,8 +69,28 @@ PLATFORM_SCHEMA = vol.Schema( vol.Optional(CONF_LIGHTS_RGB): cv.entity_ids, vol.Optional(CONF_LIGHTS_XY): cv.entity_ids, vol.Optional(CONF_LIGHTS_BRIGHT): cv.entity_ids, + vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=False): cv.boolean, + vol.Optional(CONF_MIN_BRIGHT, default=DEFAULT_MIN_BRIGHT): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_MAX_BRIGHT, default=DEFAULT_MAX_BRIGHT): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, + vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_SLEEP_CT, default=DEFAULT_SLEEP_CT): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_SLEEP_BRIGHT, default=DEFAULT_SLEEP_BRIGHT): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, + vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional( + CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION + ): VALID_TRANSITION, + vol.Optional(CONF_ONLY_ONCE, default=False): cv.boolean, vol.Optional(CONF_PROFILE): cv.string, - **_PROFILE_SCHEMA, } ) @@ -82,7 +99,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Circadian Lighting switches.""" circadian_lighting = hass.data.get(DOMAIN) if circadian_lighting is not None: - switch_settings = dict( + switch = CircadianSwitch( + hass, + circadian_lighting, + name=config.get(CONF_NAME), + lights_ct=config.get(CONF_LIGHTS_CT, []), + lights_rgb=config.get(CONF_LIGHTS_RGB, []), + lights_xy=config.get(CONF_LIGHTS_XY, []), + lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []), disable_brightness_adjust=config.get(CONF_DISABLE_BRIGHTNESS_ADJUST), min_brightness=config.get(CONF_MIN_BRIGHT), max_brightness=config.get(CONF_MAX_BRIGHT), @@ -94,19 +118,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): disable_state=config.get(CONF_DISABLE_STATE), initial_transition=config.get(CONF_INITIAL_TRANSITION), only_once=config.get(CONF_ONLY_ONCE), - ) - profile = config.get(CONF_PROFILE) - profile_settings = circadian_lighting._profiles.get(profile, {}) - settings = dict(switch_settings, **profile_settings) - switch = CircadianSwitch( - hass, - circadian_lighting, - name=config.get(CONF_NAME), - lights_ct=config.get(CONF_LIGHTS_CT, []), - lights_rgb=config.get(CONF_LIGHTS_RGB, []), - lights_xy=config.get(CONF_LIGHTS_XY, []), - lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []), - **settings, + profile=config.get(CONF_PROFILE), ) add_devices([switch]) @@ -171,6 +183,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): disable_state, initial_transition, only_once, + profile, ): """Initialize the Circadian Lighting switch.""" self.hass = hass @@ -192,12 +205,22 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._disable_state = disable_state self._initial_transition = initial_transition self._only_once = only_once + self._profile = profile self._lights_types = dict(zip(lights_ct, repeat("ct"))) self._lights_types.update(zip(lights_rgb, repeat("rgb"))) self._lights_types.update(zip(lights_xy, repeat("xy"))) self._lights_types.update(zip(lights_brightness, repeat("brightness"))) self._lights = list(self._lights_types.keys()) + def from_profile(key): + default = getattr(self._circadian_lighting, key) + if self._profile is None: + return default + value = self._circadian_lighting._profiles.get(self._profile, {}).get(key) + if value is not None: + return value + return default + @property def entity_id(self): """Return the entity ID of the switch.""" @@ -278,7 +301,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def _color_temperature(self): return ( - self._circadian_lighting._colortemp + self.from_profile("_colortemp") if not self._is_sleep() else self._sleep_colortemp ) @@ -336,7 +359,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): return if transition is None: - transition = self._circadian_lighting._transition + transition = self.from_profile("_transition") tasks = [] for light in lights: From ad57fb0989aa423dae2738f651bc05bad44a9406 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 7 Sep 2020 19:30:58 +0200 Subject: [PATCH 0112/1077] add option to add multiple profiles --- .../circadian_lighting/__init__.py | 98 ++++++++++--------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index dd650360..db29483d 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -27,6 +27,7 @@ Technical notes: I had to make a lot of assumptions when writing this app lights to 2700K (warm white) until your hub goes into Night mode """ +from custom_components.circadian_lighting.switch import CONF_PROFILE import logging from datetime import timedelta @@ -72,61 +73,68 @@ CONF_SUNSET_OFFSET = "sunset_offset" CONF_SUNRISE_TIME = "sunrise_time" CONF_SUNSET_TIME = "sunset_time" DEFAULT_TRANSITION = 60 -CONF_PROFILES = "profiles" +CONF_PROFILE, DEFAULT_PROFILE = "profile", "default" -_DOMAIN_SCHEMA = { - vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_SUNRISE_OFFSET): cv.time_period_str, - vol.Optional(CONF_SUNSET_OFFSET): cv.time_period_str, - vol.Optional(CONF_SUNRISE_TIME): cv.time, - vol.Optional(CONF_SUNSET_TIME): cv.time, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_ELEVATION): float, - vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, - vol.Optional(ATTR_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION, -} +_DOMAIN_SCHEMA = vol.Schema( + { + vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_SUNRISE_OFFSET): cv.time_period_str, + vol.Optional(CONF_SUNSET_OFFSET): cv.time_period_str, + vol.Optional(CONF_SUNRISE_TIME): cv.time, + vol.Optional(CONF_SUNSET_TIME): cv.time, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_ELEVATION): float, + vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, + vol.Optional(ATTR_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION, + vol.Optional(CONF_PROFILE, default=DEFAULT_PROFILE): cv.string, + } +) -_DOMAIN_SCHEMA_NO_DEFAULTS = {type(k)(k): v for k, v in _DOMAIN_SCHEMA.items()} + +def _all_unique_profiles(value): + """Validate that each hub configured has a unique profiles.""" + hosts = [device[CONF_PROFILE] for device in value] + schema = vol.Schema(vol.Unique()) + schema(hosts) + return value + + +# _DOMAIN_SCHEMA_NO_DEFAULTS = {type(k)(k): v for k, v in _DOMAIN_SCHEMA.items()} CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - **_DOMAIN_SCHEMA, - vol.Optional(CONF_PROFILES): vol.Schema( - {cv.string: vol.Schema(_DOMAIN_SCHEMA_NO_DEFAULTS)} - ), - } - ), - }, + {DOMAIN: vol.All(cv.ensure_list, [_DOMAIN_SCHEMA], _all_unique_profiles)}, extra=vol.ALLOW_EXTRA, ) def setup(hass, config): """Set up the Circadian Lighting platform.""" - conf = config[DOMAIN] - hass.data[DOMAIN] = CircadianLighting( - hass, - min_colortemp=conf.get(CONF_MIN_CT), - max_colortemp=conf.get(CONF_MAX_CT), - sunrise_offset=conf.get(CONF_SUNRISE_OFFSET), - sunset_offset=conf.get(CONF_SUNSET_OFFSET), - sunrise_time=conf.get(CONF_SUNRISE_TIME), - sunset_time=conf.get(CONF_SUNSET_TIME), - latitude=conf.get(CONF_LATITUDE, hass.config.latitude), - longitude=conf.get(CONF_LONGITUDE, hass.config.longitude), - elevation=conf.get(CONF_ELEVATION, hass.config.elevation), - interval=conf.get(CONF_INTERVAL), - transition=conf.get(ATTR_TRANSITION), - profiles=conf.get(CONF_PROFILES, {}), - ) + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + configs = config[DOMAIN] + for conf in configs: + profile = conf[CONF_PROFILE] + hass.data[DOMAIN][profile] = CircadianLighting( + hass, + min_colortemp=conf[CONF_MIN_CT], + max_colortemp=conf[CONF_MAX_CT], + sunrise_offset=conf.get(CONF_SUNRISE_OFFSET), + sunset_offset=conf.get(CONF_SUNSET_OFFSET), + sunrise_time=conf.get(CONF_SUNRISE_TIME), + sunset_time=conf.get(CONF_SUNSET_TIME), + latitude=conf.get(CONF_LATITUDE, hass.config.latitude), + longitude=conf.get(CONF_LONGITUDE, hass.config.longitude), + elevation=conf.get(CONF_ELEVATION, hass.config.elevation), + interval=conf[CONF_INTERVAL], + transition=conf[ATTR_TRANSITION], + profile=profile, + ) load_platform(hass, "sensor", DOMAIN, {}, config) return True From cd31b5d9f1aeb08c4df2c1fddb6765bab2621585 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 7 Sep 2020 19:33:32 +0200 Subject: [PATCH 0113/1077] profiles -> profile --- custom_components/circadian_lighting/__init__.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index db29483d..aaa6cac0 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -27,7 +27,6 @@ Technical notes: I had to make a lot of assumptions when writing this app lights to 2700K (warm white) until your hub goes into Night mode """ -from custom_components.circadian_lighting.switch import CONF_PROFILE import logging from datetime import timedelta @@ -36,6 +35,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util +from custom_components.circadian_lighting.switch import CONF_PROFILE from homeassistant.components.light import ATTR_TRANSITION, VALID_TRANSITION from homeassistant.const import ( CONF_ELEVATION, @@ -105,8 +105,6 @@ def _all_unique_profiles(value): return value -# _DOMAIN_SCHEMA_NO_DEFAULTS = {type(k)(k): v for k, v in _DOMAIN_SCHEMA.items()} - CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.All(cv.ensure_list, [_DOMAIN_SCHEMA], _all_unique_profiles)}, extra=vol.ALLOW_EXTRA, @@ -157,7 +155,7 @@ class CircadianLighting: elevation, interval, transition, - profiles, + profile, ): self.hass = hass self._min_colortemp = min_colortemp @@ -170,8 +168,8 @@ class CircadianLighting: self._longitude = longitude self._elevation = elevation self._transition = transition - self._profiles = profiles - _LOGGER.debug("profiles: %s", self._profiles) + self._profile = profile + _LOGGER.debug("profile: %s", self._profile) self._percent = self.calc_percent() self._colortemp = self.calc_colortemp() From 7e41621e4142c4b3d30f9c288a162f78eb0c7e04 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 7 Sep 2020 19:50:42 +0200 Subject: [PATCH 0114/1077] setup sensor for default profile --- custom_components/circadian_lighting/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py index 35947f92..b98c026d 100755 --- a/custom_components/circadian_lighting/sensor.py +++ b/custom_components/circadian_lighting/sensor.py @@ -6,14 +6,14 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN +from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DEFAULT_PROFILE, DOMAIN ICON = "mdi:theme-light-dark" def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Circadian Lighting sensor.""" - circadian_lighting = hass.data.get(DOMAIN) + circadian_lighting = hass.data.get(DOMAIN, {}).get(DEFAULT_PROFILE) if circadian_lighting is not None: sensor = CircadianSensor(hass, circadian_lighting) add_devices([sensor], True) From 18561e5ce68b26f917a7c2a5a15927f580b0389b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 7 Sep 2020 19:51:00 +0200 Subject: [PATCH 0115/1077] fix import --- custom_components/circadian_lighting/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index aaa6cac0..aafbd5ed 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -35,7 +35,6 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -from custom_components.circadian_lighting.switch import CONF_PROFILE from homeassistant.components.light import ATTR_TRANSITION, VALID_TRANSITION from homeassistant.const import ( CONF_ELEVATION, From 24f422ccf616370665e290f6d8d1a14eb90acddc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 7 Sep 2020 19:52:30 +0200 Subject: [PATCH 0116/1077] make switch use profile --- .../circadian_lighting/switch.py | 70 ++++++++----------- 1 file changed, 28 insertions(+), 42 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index c1b33f9e..c5a1755a 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -38,7 +38,7 @@ from homeassistant.util.color import ( color_xy_to_hs, ) -from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN +from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN, CONF_PROFILE, DEFAULT_PROFILE _LOGGER = logging.getLogger(__name__) @@ -59,7 +59,6 @@ CONF_DISABLE_ENTITY = "disable_entity" CONF_DISABLE_STATE = "disable_state" CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 CONF_ONLY_ONCE = "only_once" -CONF_PROFILE = "profile" PLATFORM_SCHEMA = vol.Schema( { @@ -90,41 +89,37 @@ PLATFORM_SCHEMA = vol.Schema( CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION ): VALID_TRANSITION, vol.Optional(CONF_ONLY_ONCE, default=False): cv.boolean, - vol.Optional(CONF_PROFILE): cv.string, + vol.Optional(CONF_PROFILE, default=DEFAULT_PROFILE): cv.string, } ) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Circadian Lighting switches.""" - circadian_lighting = hass.data.get(DOMAIN) - if circadian_lighting is not None: - switch = CircadianSwitch( - hass, - circadian_lighting, - name=config.get(CONF_NAME), - lights_ct=config.get(CONF_LIGHTS_CT, []), - lights_rgb=config.get(CONF_LIGHTS_RGB, []), - lights_xy=config.get(CONF_LIGHTS_XY, []), - lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []), - disable_brightness_adjust=config.get(CONF_DISABLE_BRIGHTNESS_ADJUST), - min_brightness=config.get(CONF_MIN_BRIGHT), - max_brightness=config.get(CONF_MAX_BRIGHT), - sleep_entity=config.get(CONF_SLEEP_ENTITY), - sleep_state=config.get(CONF_SLEEP_STATE), - sleep_colortemp=config.get(CONF_SLEEP_CT), - sleep_brightness=config.get(CONF_SLEEP_BRIGHT), - disable_entity=config.get(CONF_DISABLE_ENTITY), - disable_state=config.get(CONF_DISABLE_STATE), - initial_transition=config.get(CONF_INITIAL_TRANSITION), - only_once=config.get(CONF_ONLY_ONCE), - profile=config.get(CONF_PROFILE), - ) - add_devices([switch]) - - return True - else: - return False + profile = config[CONF_PROFILE] + circadian_lighting = hass.data[DOMAIN][profile] + switch = CircadianSwitch( + hass, + circadian_lighting, + name=config[CONF_NAME], + lights_ct=config.get(CONF_LIGHTS_CT, []), + lights_rgb=config.get(CONF_LIGHTS_RGB, []), + lights_xy=config.get(CONF_LIGHTS_XY, []), + lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []), + disable_brightness_adjust=config[CONF_DISABLE_BRIGHTNESS_ADJUST], + min_brightness=config[CONF_MIN_BRIGHT], + max_brightness=config[CONF_MAX_BRIGHT], + sleep_entity=config.get(CONF_SLEEP_ENTITY), + sleep_state=config.get(CONF_SLEEP_STATE), + sleep_colortemp=config[CONF_SLEEP_CT], + sleep_brightness=config[CONF_SLEEP_BRIGHT], + disable_entity=config.get(CONF_DISABLE_ENTITY), + disable_state=config.get(CONF_DISABLE_STATE), + initial_transition=config[CONF_INITIAL_TRANSITION], + only_once=config[CONF_ONLY_ONCE], + profile=profile, + ) + add_devices([switch]) def _difference_between_states(from_state, to_state): @@ -212,15 +207,6 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._lights_types.update(zip(lights_brightness, repeat("brightness"))) self._lights = list(self._lights_types.keys()) - def from_profile(key): - default = getattr(self._circadian_lighting, key) - if self._profile is None: - return default - value = self._circadian_lighting._profiles.get(self._profile, {}).get(key) - if value is not None: - return value - return default - @property def entity_id(self): """Return the entity ID of the switch.""" @@ -301,7 +287,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def _color_temperature(self): return ( - self.from_profile("_colortemp") + self._circadian_lighting._colortemp if not self._is_sleep() else self._sleep_colortemp ) @@ -359,7 +345,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): return if transition is None: - transition = self.from_profile("_transition") + transition = self._circadian_lighting._transition tasks = [] for light in lights: From c18cb47cbfd0b7189947f053ad9aaa633cdc6baa Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 7 Sep 2020 19:53:59 +0200 Subject: [PATCH 0117/1077] fix doc-string --- custom_components/circadian_lighting/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index aafbd5ed..903bb8e6 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -97,7 +97,7 @@ _DOMAIN_SCHEMA = vol.Schema( def _all_unique_profiles(value): - """Validate that each hub configured has a unique profiles.""" + """Validate that all enties have a unique profile name.""" hosts = [device[CONF_PROFILE] for device in value] schema = vol.Schema(vol.Unique()) schema(hosts) From 6a0f12ab2de03cdbcc283f84f43b8ee86cc171d6 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 7 Sep 2020 20:21:31 +0200 Subject: [PATCH 0118/1077] setup a sensor per profile --- .../circadian_lighting/sensor.py | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py index b98c026d..4461fa6a 100755 --- a/custom_components/circadian_lighting/sensor.py +++ b/custom_components/circadian_lighting/sensor.py @@ -13,20 +13,12 @@ ICON = "mdi:theme-light-dark" def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Circadian Lighting sensor.""" - circadian_lighting = hass.data.get(DOMAIN, {}).get(DEFAULT_PROFILE) - if circadian_lighting is not None: - sensor = CircadianSensor(hass, circadian_lighting) - add_devices([sensor], True) - - def update(call=None): - """Update component.""" - circadian_lighting.update() - - service_name = "values_update" - hass.services.register(DOMAIN, service_name, update) - return True - else: - return False + sensors = [ + CircadianSensor(hass, circadian_lighting) + for circadian_lighting in hass.data[DOMAIN].values() + ] + add_devices(sensors, True) + return True class CircadianSensor(Entity): @@ -37,6 +29,10 @@ class CircadianSensor(Entity): self._circadian_lighting = circadian_lighting self._name = "Circadian Values" self._entity_id = "sensor.circadian_values" + profile = circadian_lighting._profile + if profile != DEFAULT_PROFILE: + self._name += f" {profile}" + self._entity_id += f"_{profile.lower()}" self._unit_of_measurement = "%" self._icon = ICON From b16d69b1c9a3285a33402b20b84a5101cbd89b42 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 7 Sep 2020 20:24:32 +0200 Subject: [PATCH 0119/1077] switch doesn't need to have the profile as attribute --- custom_components/circadian_lighting/switch.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index c5a1755a..62366ad4 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -117,7 +117,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): disable_state=config.get(CONF_DISABLE_STATE), initial_transition=config[CONF_INITIAL_TRANSITION], only_once=config[CONF_ONLY_ONCE], - profile=profile, ) add_devices([switch]) @@ -178,7 +177,6 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): disable_state, initial_transition, only_once, - profile, ): """Initialize the Circadian Lighting switch.""" self.hass = hass @@ -200,7 +198,6 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._disable_state = disable_state self._initial_transition = initial_transition self._only_once = only_once - self._profile = profile self._lights_types = dict(zip(lights_ct, repeat("ct"))) self._lights_types.update(zip(lights_rgb, repeat("rgb"))) self._lights_types.update(zip(lights_xy, repeat("xy"))) From 4aff61d8cb1b007cba5b054ecda92b4e04bd9bd9 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 11 Sep 2020 20:31:51 +0200 Subject: [PATCH 0120/1077] move to single switch --- .../circadian_lighting/__init__.py | 73 +-- .../circadian_lighting/sensor.py | 11 +- .../circadian_lighting/switch.py | 419 ++++++++++++++---- 3 files changed, 339 insertions(+), 164 deletions(-) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 903bb8e6..1a0967c7 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -76,12 +76,8 @@ CONF_PROFILE, DEFAULT_PROFILE = "profile", "default" _DOMAIN_SCHEMA = vol.Schema( { - vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), + vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), + vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), vol.Optional(CONF_SUNRISE_OFFSET): cv.time_period_str, vol.Optional(CONF_SUNSET_OFFSET): cv.time_period_str, vol.Optional(CONF_SUNRISE_TIME): cv.time, @@ -104,10 +100,7 @@ def _all_unique_profiles(value): return value -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.All(cv.ensure_list, [_DOMAIN_SCHEMA], _all_unique_profiles)}, - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.All(cv.ensure_list, [_DOMAIN_SCHEMA], _all_unique_profiles)}, extra=vol.ALLOW_EXTRA,) def setup(hass, config): @@ -141,20 +134,7 @@ class CircadianLighting: """Calculate universal Circadian values.""" def __init__( - self, - hass, - min_colortemp, - max_colortemp, - sunrise_offset, - sunset_offset, - sunrise_time, - sunset_time, - latitude, - longitude, - elevation, - interval, - transition, - profile, + self, hass, min_colortemp, max_colortemp, sunrise_offset, sunset_offset, sunrise_time, sunset_time, latitude, longitude, elevation, interval, transition, profile, ): self.hass = hass self._min_colortemp = min_colortemp @@ -178,22 +158,14 @@ class CircadianLighting: if self._manual_sunrise is not None: async_track_time_change( - self.hass, - self.update, - hour=self._manual_sunrise.hour, - minute=self._manual_sunrise.minute, - second=self._manual_sunrise.second, + self.hass, self.update, hour=self._manual_sunrise.hour, minute=self._manual_sunrise.minute, second=self._manual_sunrise.second, ) else: async_track_sunrise(self.hass, self.update, self._sunrise_offset) if self._manual_sunset is not None: async_track_time_change( - self.hass, - self.update, - hour=self._manual_sunset.hour, - minute=self._manual_sunset.minute, - second=self._manual_sunset.second, + self.hass, self.update, hour=self._manual_sunset.hour, minute=self._manual_sunset.minute, second=self._manual_sunset.second, ) else: async_track_sunset(self.hass, self.update, self._sunset_offset) @@ -202,12 +174,7 @@ class CircadianLighting: def _replace_time(self, date, key): other_date = self._manual_sunrise if key == "sunrise" else self._manual_sunset - return date.replace( - hour=other_date.hour, - minute=other_date.minute, - second=other_date.second, - microsecond=other_date.microsecond, - ) + return date.replace(hour=other_date.hour, minute=other_date.minute, second=other_date.second, microsecond=other_date.microsecond,) def get_sunrise_sunset(self, date): if self._manual_sunrise is not None and self._manual_sunset is not None: @@ -248,9 +215,7 @@ class CircadianLighting: SUN_EVENT_MIDNIGHT: solar_midnight, } - return { - k: dt.astimezone(dt_util.UTC).timestamp() for k, dt in datetimes.items() - } + return {k: dt.astimezone(dt_util.UTC).timestamp() for k, dt in datetimes.items()} def calc_percent(self): now = dt_util.utcnow() @@ -261,10 +226,7 @@ class CircadianLighting: # It's before sunrise (after midnight), because it's before # sunrise (and after midnight) sunset must have happend yesterday. yesterday = self.get_sunrise_sunset(now - timedelta(days=1)) - if ( - today[SUN_EVENT_MIDNIGHT] > today[SUN_EVENT_SUNSET] - and yesterday[SUN_EVENT_MIDNIGHT] > yesterday[SUN_EVENT_SUNSET] - ): + if today[SUN_EVENT_MIDNIGHT] > today[SUN_EVENT_SUNSET] and yesterday[SUN_EVENT_MIDNIGHT] > yesterday[SUN_EVENT_SUNSET]: # Solar midnight is after sunset so use yesterdays's time today[SUN_EVENT_MIDNIGHT] = yesterday[SUN_EVENT_MIDNIGHT] today[SUN_EVENT_SUNSET] = yesterday[SUN_EVENT_SUNSET] @@ -272,10 +234,7 @@ class CircadianLighting: # It's after sunset (before midnight), because it's after sunset # (and before midnight) sunrise should happen tomorrow. tomorrow = self.get_sunrise_sunset(now + timedelta(days=1)) - if ( - today[SUN_EVENT_MIDNIGHT] < today[SUN_EVENT_SUNRISE] - and tomorrow[SUN_EVENT_MIDNIGHT] < tomorrow[SUN_EVENT_SUNRISE] - ): + if today[SUN_EVENT_MIDNIGHT] < today[SUN_EVENT_SUNRISE] and tomorrow[SUN_EVENT_MIDNIGHT] < tomorrow[SUN_EVENT_SUNRISE]: # Solar midnight is before sunrise so use tomorrow's time today[SUN_EVENT_MIDNIGHT] = tomorrow[SUN_EVENT_MIDNIGHT] today[SUN_EVENT_SUNRISE] = tomorrow[SUN_EVENT_SUNRISE] @@ -291,22 +250,14 @@ class CircadianLighting: h = today[SUN_EVENT_NOON] k = 100 # parabola before solar_noon else after solar_noon - x = ( - today[SUN_EVENT_SUNRISE] - if now_ts < today[SUN_EVENT_NOON] - else today[SUN_EVENT_SUNSET] - ) + x = today[SUN_EVENT_SUNRISE] if now_ts < today[SUN_EVENT_NOON] else today[SUN_EVENT_SUNSET] # sunset -> sunrise parabola elif today[SUN_EVENT_SUNSET] < now_ts < today[SUN_EVENT_SUNRISE]: h = today[SUN_EVENT_MIDNIGHT] k = -100 # parabola before solar_midnight else after solar_midnight - x = ( - today[SUN_EVENT_SUNSET] - if now_ts < today[SUN_EVENT_MIDNIGHT] - else today[SUN_EVENT_SUNRISE] - ) + x = today[SUN_EVENT_SUNSET] if now_ts < today[SUN_EVENT_MIDNIGHT] else today[SUN_EVENT_SUNRISE] y = 0 a = (y - k) / (h - x) ** 2 diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py index 4461fa6a..5b5e2855 100755 --- a/custom_components/circadian_lighting/sensor.py +++ b/custom_components/circadian_lighting/sensor.py @@ -13,10 +13,7 @@ ICON = "mdi:theme-light-dark" def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Circadian Lighting sensor.""" - sensors = [ - CircadianSensor(hass, circadian_lighting) - for circadian_lighting in hass.data[DOMAIN].values() - ] + sensors = [CircadianSensor(hass, circadian_lighting) for circadian_lighting in hass.data[DOMAIN].values()] add_devices(sensors, True) return True @@ -81,11 +78,7 @@ class CircadianSensor(Entity): async def async_added_to_hass(self) -> None: """Connect dispatcher to signal from CircadianLighting object.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self._update_callback - ) - ) + self.async_on_remove(async_dispatcher_connect(self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self._update_callback)) @callback def _update_callback(self) -> None: diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 62366ad4..07327757 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -1,14 +1,42 @@ """ -Circadian Lighting Switch for Home-Assistant. +Circadian Lighting Component for Home-Assistant. + +This component calculates color temperature and brightness to synchronize +your color changing lights with perceived color temperature of the sky throughout +the day. This gives your environment a more natural feel, with cooler whites during +the midday and warmer tints near twilight and dawn. + +In addition, the component sets your lights to a nice warm white at 1% in "Sleep" mode, +which is far brighter than starlight but won't reset your circadian rhythm or break down +too much rhodopsin in your eyes. + +Human circadian rhythms are heavily influenced by ambient light levels and +hues. Hormone production, brainwave activity, mood and wakefulness are +just some of the cognitive functions tied to cyclical natural light. +http://en.wikipedia.org/wiki/Zeitgeber + +Here's some further reading: + +http://www.cambridgeincolour.com/tutorials/sunrise-sunset-calculator.htm +http://en.wikipedia.org/wiki/Color_temperature + +Technical notes: I had to make a lot of assumptions when writing this app + * There are no considerations for weather or altitude, but does use your + hub's location to calculate the sun position. + * The component doesn't calculate a true "Blue Hour" -- it just sets the + lights to 2700K (warm white) until your hub goes into Night mode """ import asyncio import logging +from datetime import timedelta from itertools import repeat +import astral import voluptuous as vol import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -22,13 +50,23 @@ from homeassistant.components.light import VALID_TRANSITION, is_on from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_ELEVATION, + CONF_LATITUDE, + CONF_LONGITUDE, CONF_NAME, CONF_PLATFORM, SERVICE_TURN_ON, STATE_ON, + SUN_EVENT_SUNRISE, + SUN_EVENT_SUNSET, +) +from homeassistant.helpers.event import ( + async_track_state_change, + async_track_sunrise, + async_track_sunset, + async_track_time_change, + async_track_time_interval, ) -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util import slugify from homeassistant.util.color import ( @@ -38,85 +76,118 @@ from homeassistant.util.color import ( color_xy_to_hs, ) -from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DOMAIN, CONF_PROFILE, DEFAULT_PROFILE - _LOGGER = logging.getLogger(__name__) ICON = "mdi:theme-light-dark" +DOMAIN = "circadian_lighting" +SUN_EVENT_NOON = "solar_noon" +SUN_EVENT_MIDNIGHT = "solar_midnight" + +CONF_LIGHTS_BRIGHT = "lights_brightness" CONF_LIGHTS_CT = "lights_ct" CONF_LIGHTS_RGB = "lights_rgb" CONF_LIGHTS_XY = "lights_xy" -CONF_LIGHTS_BRIGHT = "lights_brightness" + CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust" -CONF_MIN_BRIGHT, DEFAULT_MIN_BRIGHT = "min_brightness", 1 -CONF_MAX_BRIGHT, DEFAULT_MAX_BRIGHT = "max_brightness", 100 -CONF_SLEEP_ENTITY = "sleep_entity" -CONF_SLEEP_STATE = "sleep_state" -CONF_SLEEP_CT, DEFAULT_SLEEP_CT = "sleep_colortemp", 1000 -CONF_SLEEP_BRIGHT, DEFAULT_SLEEP_BRIGHT = "sleep_brightness", 1 CONF_DISABLE_ENTITY = "disable_entity" CONF_DISABLE_STATE = "disable_state" CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 +CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 300 +CONF_MAX_BRIGHT, DEFAULT_MAX_BRIGHT = "max_brightness", 100 +CONF_MAX_CT, DEFAULT_MAX_CT = "max_colortemp", 5500 +CONF_MIN_BRIGHT, DEFAULT_MIN_BRIGHT = "min_brightness", 1 +CONF_MIN_CT, DEFAULT_MIN_CT = "min_colortemp", 2500 CONF_ONLY_ONCE = "only_once" +CONF_SLEEP_BRIGHT, DEFAULT_SLEEP_BRIGHT = "sleep_brightness", 1 +CONF_SLEEP_CT, DEFAULT_SLEEP_CT = "sleep_colortemp", 1000 +CONF_SLEEP_ENTITY = "sleep_entity" +CONF_SLEEP_STATE = "sleep_state" +CONF_SUNRISE_OFFSET = "sunrise_offset" +CONF_SUNRISE_TIME = "sunrise_time" +CONF_SUNSET_OFFSET = "sunset_offset" +CONF_SUNSET_TIME = "sunset_time" +DEFAULT_TRANSITION = 60 PLATFORM_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): "circadian_lighting", vol.Optional(CONF_NAME, default="Circadian Lighting"): cv.string, + vol.Optional(CONF_LIGHTS_BRIGHT): cv.entity_ids, vol.Optional(CONF_LIGHTS_CT): cv.entity_ids, vol.Optional(CONF_LIGHTS_RGB): cv.entity_ids, vol.Optional(CONF_LIGHTS_XY): cv.entity_ids, - vol.Optional(CONF_LIGHTS_BRIGHT): cv.entity_ids, vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=False): cv.boolean, - vol.Optional(CONF_MIN_BRIGHT, default=DEFAULT_MIN_BRIGHT): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_MAX_BRIGHT, default=DEFAULT_MAX_BRIGHT): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, - vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_SLEEP_CT, default=DEFAULT_SLEEP_CT): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_SLEEP_BRIGHT, default=DEFAULT_SLEEP_BRIGHT): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_ELEVATION): float, vol.Optional( CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION ): VALID_TRANSITION, + vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_MAX_BRIGHT, default=DEFAULT_MAX_BRIGHT): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_MIN_BRIGHT, default=DEFAULT_MIN_BRIGHT): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), vol.Optional(CONF_ONLY_ONCE, default=False): cv.boolean, - vol.Optional(CONF_PROFILE, default=DEFAULT_PROFILE): cv.string, + vol.Optional(CONF_SLEEP_BRIGHT, default=DEFAULT_SLEEP_BRIGHT): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_SLEEP_CT, default=DEFAULT_SLEEP_CT): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, + vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_SUNRISE_OFFSET): cv.time_period_str, + vol.Optional(CONF_SUNRISE_TIME): cv.time, + vol.Optional(CONF_SUNSET_OFFSET): cv.time_period_str, + vol.Optional(CONF_SUNSET_TIME): cv.time, + vol.Optional(ATTR_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION, } ) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Circadian Lighting switches.""" - profile = config[CONF_PROFILE] - circadian_lighting = hass.data[DOMAIN][profile] switch = CircadianSwitch( hass, - circadian_lighting, name=config[CONF_NAME], + lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []), lights_ct=config.get(CONF_LIGHTS_CT, []), lights_rgb=config.get(CONF_LIGHTS_RGB, []), lights_xy=config.get(CONF_LIGHTS_XY, []), - lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []), disable_brightness_adjust=config[CONF_DISABLE_BRIGHTNESS_ADJUST], - min_brightness=config[CONF_MIN_BRIGHT], - max_brightness=config[CONF_MAX_BRIGHT], - sleep_entity=config.get(CONF_SLEEP_ENTITY), - sleep_state=config.get(CONF_SLEEP_STATE), - sleep_colortemp=config[CONF_SLEEP_CT], - sleep_brightness=config[CONF_SLEEP_BRIGHT], disable_entity=config.get(CONF_DISABLE_ENTITY), disable_state=config.get(CONF_DISABLE_STATE), + elevation=config.get(CONF_ELEVATION, hass.config.elevation), initial_transition=config[CONF_INITIAL_TRANSITION], + interval=config[CONF_INTERVAL], + latitude=config.get(CONF_LATITUDE, hass.config.latitude), + longitude=config.get(CONF_LONGITUDE, hass.config.longitude), + max_brightness=config[CONF_MAX_BRIGHT], + max_colortemp=config[CONF_MAX_CT], + min_brightness=config[CONF_MIN_BRIGHT], + min_colortemp=config[CONF_MIN_CT], only_once=config[CONF_ONLY_ONCE], + sleep_brightness=config[CONF_SLEEP_BRIGHT], + sleep_colortemp=config[CONF_SLEEP_CT], + sleep_entity=config.get(CONF_SLEEP_ENTITY), + sleep_state=config.get(CONF_SLEEP_STATE), + sunrise_offset=config.get(CONF_SUNRISE_OFFSET), + sunrise_time=config.get(CONF_SUNRISE_TIME), + sunset_offset=config.get(CONF_SUNSET_OFFSET), + sunset_time=config.get(CONF_SUNSET_TIME), + transition=config[ATTR_TRANSITION], ) add_devices([switch]) @@ -160,50 +231,77 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): def __init__( self, hass, - circadian_lighting, name, + lights_brightness, lights_ct, lights_rgb, lights_xy, - lights_brightness, disable_brightness_adjust, - min_brightness, - max_brightness, - sleep_entity, - sleep_state, - sleep_colortemp, - sleep_brightness, disable_entity, disable_state, + elevation, initial_transition, + interval, + latitude, + longitude, + max_brightness, + max_colortemp, + min_brightness, + min_colortemp, only_once, + sleep_brightness, + sleep_colortemp, + sleep_entity, + sleep_state, + sunrise_offset, + sunrise_time, + sunset_offset, + sunset_time, + transition, ): """Initialize the Circadian Lighting switch.""" self.hass = hass - self._circadian_lighting = circadian_lighting self._name = name self._entity_id = f"switch.circadian_lighting_{slugify(name)}" self._state = None self._icon = ICON - self._hs_color = None - self._brightness = None - self._disable_brightness_adjust = disable_brightness_adjust - self._min_brightness = min_brightness - self._max_brightness = max_brightness - self._sleep_entity = sleep_entity - self._sleep_state = sleep_state - self._sleep_colortemp = sleep_colortemp - self._sleep_brightness = sleep_brightness - self._disable_entity = disable_entity - self._disable_state = disable_state - self._initial_transition = initial_transition - self._only_once = only_once self._lights_types = dict(zip(lights_ct, repeat("ct"))) + self._lights_types.update(zip(lights_brightness, repeat("brightness"))) self._lights_types.update(zip(lights_rgb, repeat("rgb"))) self._lights_types.update(zip(lights_xy, repeat("xy"))) - self._lights_types.update(zip(lights_brightness, repeat("brightness"))) self._lights = list(self._lights_types.keys()) + self._disable_brightness_adjust = disable_brightness_adjust + self._disable_entity = disable_entity + self._disable_state = disable_state + self._elevation = elevation + self._initial_transition = initial_transition + self._interval = interval + self._latitude = latitude + self._longitude = longitude + self._max_brightness = max_brightness + self._max_colortemp = max_colortemp + self._min_brightness = min_brightness + self._min_colortemp = min_colortemp + self._only_once = only_once + self._sleep_brightness = sleep_brightness + self._sleep_colortemp = sleep_colortemp + self._sleep_entity = sleep_entity + self._sleep_state = sleep_state + self._sunrise_offset = sunrise_offset + self._sunrise_time = sunrise_time + self._sunset_offset = sunset_offset + self._sunset_time = sunset_time + self._transition = transition + + self._percent = None + self._brightness = None + self._colortemp_kelvin = None + self._colortemp_mired = None + self._rgb_color = None + self._xy_color = None + self._hs_color = None + @property def entity_id(self): """Return the entity ID of the switch.""" @@ -221,13 +319,6 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): async def async_added_to_hass(self): """Call when entity about to be added to hass.""" - # Add callback - self.async_on_remove( - async_dispatcher_connect( - self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self._update_switch - ) - ) - # Add listeners async_track_state_change( self.hass, self._lights, self._light_state_changed, to_state="on" @@ -245,6 +336,32 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): from_state=self._disable_state, ) + # XXX FROM __INIT__ \/ + if self._manual_sunrise is not None: + async_track_time_change( + self.hass, + self.update, + hour=self._manual_sunrise.hour, + minute=self._manual_sunrise.minute, + second=self._manual_sunrise.second, + ) + else: + async_track_sunrise(self.hass, self.update, self._sunrise_offset) + + if self._manual_sunset is not None: + async_track_time_change( + self.hass, + self.update, + hour=self._manual_sunset.hour, + minute=self._manual_sunset.minute, + second=self._manual_sunset.second, + ) + else: + async_track_sunset(self.hass, self.update, self._sunset_offset) + + async_track_time_interval(self.hass, self.update, self._interval) + # XXX FROM __INIT__ ^ + if self._state is not None: # If not None, we got an initial value return @@ -276,47 +393,161 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._hs_color = None self._brightness = None + def _replace_time(self, date, key): + other_date = self._manual_sunrise if key == "sunrise" else self._manual_sunset + return date.replace( + hour=other_date.hour, + minute=other_date.minute, + second=other_date.second, + microsecond=other_date.microsecond, + ) + + def get_sunrise_sunset(self, date): + if self._manual_sunrise is not None and self._manual_sunset is not None: + sunrise = self._replace_time(date, "sunrise") + sunset = self._replace_time(date, "sunset") + solar_noon = sunrise + (sunset - sunrise) / 2 + solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset) / 2 + else: + location = astral.Location() + location.name = "name" + location.region = "region" + location.latitude = self._latitude + location.longitude = self._longitude + location.elevation = self._elevation + + if self._manual_sunrise is not None: + sunrise = self._replace_time(date, "sunrise") + else: + sunrise = location.sunrise(date) + + if self._manual_sunset is not None: + sunset = self._replace_time(date, "sunset") + else: + sunset = location.sunset(date) + + solar_noon = location.solar_noon(date) + solar_midnight = location.solar_midnight(date) + + if self._sunrise_offset is not None: + sunrise = sunrise + self._sunrise_offset + if self._sunset_offset is not None: + sunset = sunset + self._sunset_offset + + datetimes = { + SUN_EVENT_SUNRISE: sunrise, + SUN_EVENT_SUNSET: sunset, + SUN_EVENT_NOON: solar_noon, + SUN_EVENT_MIDNIGHT: solar_midnight, + } + + return { + k: dt.astimezone(dt_util.UTC).timestamp() for k, dt in datetimes.items() + } + + def _calc_percent(self): + now = dt_util.utcnow() + now_ts = now.timestamp() + + today = self.get_sunrise_sunset(now) + if now_ts < today[SUN_EVENT_SUNRISE]: + # It's before sunrise (after midnight), because it's before + # sunrise (and after midnight) sunset must have happend yesterday. + yesterday = self.get_sunrise_sunset(now - timedelta(days=1)) + if ( + today[SUN_EVENT_MIDNIGHT] > today[SUN_EVENT_SUNSET] + and yesterday[SUN_EVENT_MIDNIGHT] > yesterday[SUN_EVENT_SUNSET] + ): + # Solar midnight is after sunset so use yesterdays's time + today[SUN_EVENT_MIDNIGHT] = yesterday[SUN_EVENT_MIDNIGHT] + today[SUN_EVENT_SUNSET] = yesterday[SUN_EVENT_SUNSET] + elif now_ts > today[SUN_EVENT_SUNSET]: + # It's after sunset (before midnight), because it's after sunset + # (and before midnight) sunrise should happen tomorrow. + tomorrow = self.get_sunrise_sunset(now + timedelta(days=1)) + if ( + today[SUN_EVENT_MIDNIGHT] < today[SUN_EVENT_SUNRISE] + and tomorrow[SUN_EVENT_MIDNIGHT] < tomorrow[SUN_EVENT_SUNRISE] + ): + # Solar midnight is before sunrise so use tomorrow's time + today[SUN_EVENT_MIDNIGHT] = tomorrow[SUN_EVENT_MIDNIGHT] + today[SUN_EVENT_SUNRISE] = tomorrow[SUN_EVENT_SUNRISE] + + # Figure out where we are in time so we know which half of the + # parabola to calculate. We're generating a different + # sunset-sunrise parabola for before and after solar midnight. + # because it might not be half way between sunrise and sunset. + # We're also generating a different parabola for sunrise-sunset. + + # sunrise -> sunset parabola + if today[SUN_EVENT_SUNRISE] < now_ts < today[SUN_EVENT_SUNSET]: + h = today[SUN_EVENT_NOON] + k = 100 + # parabola before solar_noon else after solar_noon + x = ( + today[SUN_EVENT_SUNRISE] + if now_ts < today[SUN_EVENT_NOON] + else today[SUN_EVENT_SUNSET] + ) + + # sunset -> sunrise parabola + elif today[SUN_EVENT_SUNSET] < now_ts < today[SUN_EVENT_SUNRISE]: + h = today[SUN_EVENT_MIDNIGHT] + k = -100 + # parabola before solar_midnight else after solar_midnight + x = ( + today[SUN_EVENT_SUNSET] + if now_ts < today[SUN_EVENT_MIDNIGHT] + else today[SUN_EVENT_SUNRISE] + ) + + y = 0 + a = (y - k) / (h - x) ** 2 + percentage = a * (now_ts - h) ** 2 + k + return percentage + + async def update(self, _=None): # from __init__ + """Update Circadian Values.""" + self._percent = self._calc_percent() + self._brightness = self._calc_brightness() + self._colortemp_kelvin = self._calc_colortemp_kelvin() + self._colortemp_mired = color_temperature_kelvin_to_mired( + self._colortemp_kelvin + ) + self._rgb_color = color_temperature_to_rgb(self._colortemp_kelvin) + self._xy_color = color_RGB_to_xy(*self._rgb_color) + self._hs_color = color_xy_to_hs(*self._xy_color) + def _is_sleep(self): return ( self._sleep_entity is not None and self.hass.states.get(self._sleep_entity).state in self._sleep_state ) - def _color_temperature(self): - return ( - self._circadian_lighting._colortemp - if not self._is_sleep() - else self._sleep_colortemp - ) - - def _calc_ct(self): - return color_temperature_kelvin_to_mired(self._color_temperature()) - - def _calc_rgb(self): - return color_temperature_to_rgb(self._color_temperature()) - - def _calc_xy(self): - return color_RGB_to_xy(*self._calc_rgb()) - - def _calc_hs(self): - return color_xy_to_hs(*self._calc_xy()) + def _calc_colortemp_kelvin(self): + if self._is_sleep(): + return self._sleep_colortemp + if self._percent > 0: + delta = self._max_colortemp - self._min_colortemp + percent = self._percent / 100 + return (delta * percent) + self._min_colortemp + return self._min_colortemp def _calc_brightness(self) -> float: if self._disable_brightness_adjust: return if self._is_sleep(): return self._sleep_brightness - if self._circadian_lighting._percent > 0: + if self._percent > 0: return self._max_brightness delta_brightness = self._max_brightness - self._min_brightness - percent = (100 + self._circadian_lighting._percent) / 100 + percent = (100 + self._percent) / 100 return (delta_brightness * percent) + self._min_brightness async def _update_switch(self, lights=None, transition=None, force=False): if self._only_once and not force: return - self._hs_color = self._calc_hs() - self._brightness = self._calc_brightness() + await self.update() await self._adjust_lights(lights or self._lights, transition) async def _force_update_switch(self, lights=None): @@ -342,7 +573,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): return if transition is None: - transition = self._circadian_lighting._transition + transition = self._transition tasks = [] for light in lights: @@ -355,12 +586,12 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): light_type = self._lights_types[light] if light_type == "ct": - service_data[ATTR_COLOR_TEMP] = int(self._calc_ct()) + service_data[ATTR_COLOR_TEMP] = int(self._colortemp_mired) elif light_type == "rgb": - r, g, b = self._calc_rgb() + r, g, b = self._rgb_color service_data[ATTR_RGB_COLOR] = (int(r), int(g), int(b)) elif light_type == "xy": - service_data[ATTR_XY_COLOR] = self._calc_xy() + service_data[ATTR_XY_COLOR] = self._xy_color if service_data.get(ATTR_BRIGHTNESS, False): service_data[ATTR_WHITE_VALUE] = service_data[ATTR_BRIGHTNESS] From 79944c94688d9d61992f3d4067bfc57064b6284d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 11 Sep 2020 20:58:43 +0200 Subject: [PATCH 0121/1077] use astral location from HA --- .../circadian_lighting/switch.py | 160 ++++++------------ 1 file changed, 52 insertions(+), 108 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 07327757..662c52d7 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -32,7 +32,6 @@ import logging from datetime import timedelta from itertools import repeat -import astral import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -51,8 +50,6 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ELEVATION, - CONF_LATITUDE, - CONF_LONGITUDE, CONF_NAME, CONF_PLATFORM, SERVICE_TURN_ON, @@ -62,11 +59,9 @@ from homeassistant.const import ( ) from homeassistant.helpers.event import ( async_track_state_change, - async_track_sunrise, - async_track_sunset, - async_track_time_change, async_track_time_interval, ) +from homeassistant.helpers.sun import get_astral_location from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util import slugify from homeassistant.util.color import ( @@ -125,8 +120,6 @@ PLATFORM_SCHEMA = vol.Schema( CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION ): VALID_TRANSITION, vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, vol.Optional(CONF_MAX_BRIGHT, default=DEFAULT_MAX_BRIGHT): vol.All( vol.Coerce(int), vol.Range(min=1, max=100) ), @@ -148,9 +141,9 @@ PLATFORM_SCHEMA = vol.Schema( ), vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_SUNRISE_OFFSET): cv.time_period_str, + vol.Optional(CONF_SUNRISE_OFFSET, default=0): cv.time_period_str, vol.Optional(CONF_SUNRISE_TIME): cv.time, - vol.Optional(CONF_SUNSET_OFFSET): cv.time_period_str, + vol.Optional(CONF_SUNSET_OFFSET, default=0): cv.time_period_str, vol.Optional(CONF_SUNSET_TIME): cv.time, vol.Optional(ATTR_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION, } @@ -172,8 +165,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): elevation=config.get(CONF_ELEVATION, hass.config.elevation), initial_transition=config[CONF_INITIAL_TRANSITION], interval=config[CONF_INTERVAL], - latitude=config.get(CONF_LATITUDE, hass.config.latitude), - longitude=config.get(CONF_LONGITUDE, hass.config.longitude), max_brightness=config[CONF_MAX_BRIGHT], max_colortemp=config[CONF_MAX_CT], min_brightness=config[CONF_MIN_BRIGHT], @@ -183,9 +174,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sleep_colortemp=config[CONF_SLEEP_CT], sleep_entity=config.get(CONF_SLEEP_ENTITY), sleep_state=config.get(CONF_SLEEP_STATE), - sunrise_offset=config.get(CONF_SUNRISE_OFFSET), + sunrise_offset=config[CONF_SUNRISE_OFFSET], sunrise_time=config.get(CONF_SUNRISE_TIME), - sunset_offset=config.get(CONF_SUNSET_OFFSET), + sunset_offset=config[CONF_SUNSET_OFFSET], sunset_time=config.get(CONF_SUNSET_TIME), transition=config[ATTR_TRANSITION], ) @@ -242,8 +233,6 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): elevation, initial_transition, interval, - latitude, - longitude, max_brightness, max_colortemp, min_brightness, @@ -277,8 +266,6 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._elevation = elevation self._initial_transition = initial_transition self._interval = interval - self._latitude = latitude - self._longitude = longitude self._max_brightness = max_brightness self._max_colortemp = max_colortemp self._min_brightness = min_brightness @@ -336,31 +323,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): from_state=self._disable_state, ) - # XXX FROM __INIT__ \/ - if self._manual_sunrise is not None: - async_track_time_change( - self.hass, - self.update, - hour=self._manual_sunrise.hour, - minute=self._manual_sunrise.minute, - second=self._manual_sunrise.second, - ) - else: - async_track_sunrise(self.hass, self.update, self._sunrise_offset) - - if self._manual_sunset is not None: - async_track_time_change( - self.hass, - self.update, - hour=self._manual_sunset.hour, - minute=self._manual_sunset.minute, - second=self._manual_sunset.second, - ) - else: - async_track_sunset(self.hass, self.update, self._sunset_offset) - async_track_time_interval(self.hass, self.update, self._interval) - # XXX FROM __INIT__ ^ if self._state is not None: # If not None, we got an initial value return @@ -373,66 +336,70 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): """Icon to use in the frontend, if any.""" return self._icon - @property - def hs_color(self): - return self._hs_color - @property def device_state_attributes(self): """Return the attributes of the switch.""" + if not self._state: + return {"hs_color": None, "brightness": None} return {"hs_color": self._hs_color, "brightness": self._brightness} async def async_turn_on(self, **kwargs): """Turn on circadian lighting.""" self._state = True - await self._force_update_switch() + await self._update_lights() async def async_turn_off(self, **kwargs): """Turn off circadian lighting.""" self._state = False - self._hs_color = None - self._brightness = None - def _replace_time(self, date, key): - other_date = self._manual_sunrise if key == "sunrise" else self._manual_sunset - return date.replace( - hour=other_date.hour, - minute=other_date.minute, - second=other_date.second, - microsecond=other_date.microsecond, + def _update_attrs(self, _=None): + """Update Circadian Values.""" + self._percent = self._calc_percent() + self._brightness = self._calc_brightness() + self._colortemp_kelvin = self._calc_colortemp_kelvin() + self._colortemp_mired = color_temperature_kelvin_to_mired( + self._colortemp_kelvin ) + self._rgb_color = color_temperature_to_rgb(self._colortemp_kelvin) + self._xy_color = color_RGB_to_xy(*self._rgb_color) + self._hs_color = color_xy_to_hs(*self._xy_color) + + async def update(self, now=None): + self._update_lights(force=False) + + async def _update_lights(self, lights=None, transition=None, force=True): + if self._only_once and not force: + return + self._update_attrs() + await self._adjust_lights(lights or self._lights, transition) def get_sunrise_sunset(self, date): - if self._manual_sunrise is not None and self._manual_sunset is not None: - sunrise = self._replace_time(date, "sunrise") - sunset = self._replace_time(date, "sunset") - solar_noon = sunrise + (sunset - sunrise) / 2 - solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset) / 2 - else: - location = astral.Location() - location.name = "name" - location.region = "region" - location.latitude = self._latitude - location.longitude = self._longitude - location.elevation = self._elevation - - if self._manual_sunrise is not None: - sunrise = self._replace_time(date, "sunrise") - else: - sunrise = location.sunrise(date) - - if self._manual_sunset is not None: - sunset = self._replace_time(date, "sunset") - else: - sunset = location.sunset(date) + def _replace_time(date, key): + other_date = getattr(self, f"_manual_{key}") + return date.replace( + hour=other_date.hour, + minute=other_date.minute, + second=other_date.second, + microsecond=other_date.microsecond, + ) + location = get_astral_location(self.hass) + sunrise = ( + location.sunrise(date) + if self._manual_sunrise is None + else _replace_time(date, "sunrise") + ) + self._sunrise_offset + sunset = ( + location.sunset(date) + if self._manual_sunset is None + else _replace_time(date, "sunset") + ) + self._sunset_offset + if self._manual_sunrise is None and self._manual_sunset is None: solar_noon = location.solar_noon(date) solar_midnight = location.solar_midnight(date) - - if self._sunrise_offset is not None: - sunrise = sunrise + self._sunrise_offset - if self._sunset_offset is not None: - sunset = sunset + self._sunset_offset + else: + solar_noon = sunrise + (sunset - sunrise) / 2 + solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset) / 2 datetimes = { SUN_EVENT_SUNRISE: sunrise, @@ -506,18 +473,6 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): percentage = a * (now_ts - h) ** 2 + k return percentage - async def update(self, _=None): # from __init__ - """Update Circadian Values.""" - self._percent = self._calc_percent() - self._brightness = self._calc_brightness() - self._colortemp_kelvin = self._calc_colortemp_kelvin() - self._colortemp_mired = color_temperature_kelvin_to_mired( - self._colortemp_kelvin - ) - self._rgb_color = color_temperature_to_rgb(self._colortemp_kelvin) - self._xy_color = color_RGB_to_xy(*self._rgb_color) - self._hs_color = color_xy_to_hs(*self._xy_color) - def _is_sleep(self): return ( self._sleep_entity is not None @@ -544,17 +499,6 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): percent = (100 + self._percent) / 100 return (delta_brightness * percent) + self._min_brightness - async def _update_switch(self, lights=None, transition=None, force=False): - if self._only_once and not force: - return - await self.update() - await self._adjust_lights(lights or self._lights, transition) - - async def _force_update_switch(self, lights=None): - return await self._update_switch( - lights, transition=self._initial_transition, force=True - ) - def _is_disabled(self): return ( self._disable_entity is not None @@ -611,8 +555,8 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): assert to_state.state == "on" if from_state is None or from_state.state != "on": _LOGGER.debug(_difference_between_states(from_state, to_state)) - await self._force_update_switch(lights=[entity_id]) + await self._update_lights(lights=[entity_id]) async def _state_changed(self, entity_id, from_state, to_state): _LOGGER.debug(_difference_between_states(from_state, to_state)) - await self._force_update_switch() + await self._update_lights() From 8cd6a255cd575a058fb65d1c2772bbfaa73525f5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 11 Sep 2020 21:02:32 +0200 Subject: [PATCH 0122/1077] add transition=self._initial_transition --- custom_components/circadian_lighting/switch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 662c52d7..b86a0bf4 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -346,7 +346,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): async def async_turn_on(self, **kwargs): """Turn on circadian lighting.""" self._state = True - await self._update_lights() + await self._update_lights(transition=self._initial_transition) async def async_turn_off(self, **kwargs): """Turn off circadian lighting.""" @@ -555,8 +555,8 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): assert to_state.state == "on" if from_state is None or from_state.state != "on": _LOGGER.debug(_difference_between_states(from_state, to_state)) - await self._update_lights(lights=[entity_id]) + await self._update_lights(lights=[entity_id], transition=self._initial_transition) async def _state_changed(self, entity_id, from_state, to_state): _LOGGER.debug(_difference_between_states(from_state, to_state)) - await self._update_lights() + await self._update_lights(transition=self._initial_transition) From 99490b4ad5ac03ef4c1a7efd7ed2afa8868438b0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 12 Sep 2020 12:21:40 +0200 Subject: [PATCH 0123/1077] switch to a simpler setup --- .../circadian_lighting/__init__.py | 292 +----------------- .../circadian_lighting/manifest.json | 10 +- .../circadian_lighting/sensor.py | 86 ------ .../circadian_lighting/services.yaml | 2 - .../circadian_lighting/switch.py | 52 ++-- 5 files changed, 32 insertions(+), 410 deletions(-) delete mode 100755 custom_components/circadian_lighting/sensor.py delete mode 100644 custom_components/circadian_lighting/services.yaml diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/circadian_lighting/__init__.py index 1a0967c7..ca7f15ac 100755 --- a/custom_components/circadian_lighting/__init__.py +++ b/custom_components/circadian_lighting/__init__.py @@ -1,291 +1 @@ -""" -Circadian Lighting Component for Home-Assistant. - -This component calculates color temperature and brightness to synchronize -your color changing lights with perceived color temperature of the sky throughout -the day. This gives your environment a more natural feel, with cooler whites during -the midday and warmer tints near twilight and dawn. - -In addition, the component sets your lights to a nice warm white at 1% in "Sleep" mode, -which is far brighter than starlight but won't reset your circadian rhythm or break down -too much rhodopsin in your eyes. - -Human circadian rhythms are heavily influenced by ambient light levels and -hues. Hormone production, brainwave activity, mood and wakefulness are -just some of the cognitive functions tied to cyclical natural light. -http://en.wikipedia.org/wiki/Zeitgeber - -Here's some further reading: - -http://www.cambridgeincolour.com/tutorials/sunrise-sunset-calculator.htm -http://en.wikipedia.org/wiki/Color_temperature - -Technical notes: I had to make a lot of assumptions when writing this app - * There are no considerations for weather or altitude, but does use your - hub's location to calculate the sun position. - * The component doesn't calculate a true "Blue Hour" -- it just sets the - lights to 2700K (warm white) until your hub goes into Night mode -""" - -import logging -from datetime import timedelta - -import astral -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util -from homeassistant.components.light import ATTR_TRANSITION, VALID_TRANSITION -from homeassistant.const import ( - CONF_ELEVATION, - CONF_LATITUDE, - CONF_LONGITUDE, - SUN_EVENT_SUNRISE, - SUN_EVENT_SUNSET, -) -from homeassistant.helpers.discovery import load_platform -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import ( - async_track_sunrise, - async_track_sunset, - async_track_time_change, - async_track_time_interval, -) -from homeassistant.util.color import ( - color_RGB_to_xy, - color_temperature_to_rgb, - color_xy_to_hs, -) - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "circadian_lighting" -CIRCADIAN_LIGHTING_UPDATE_TOPIC = f"{DOMAIN}_update" -SUN_EVENT_NOON = "solar_noon" -SUN_EVENT_MIDNIGHT = "solar_midnight" - -CONF_MIN_CT, DEFAULT_MIN_CT = "min_colortemp", 2500 -CONF_MAX_CT, DEFAULT_MAX_CT = "max_colortemp", 5500 -CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 300 -CONF_SUNRISE_OFFSET = "sunrise_offset" -CONF_SUNSET_OFFSET = "sunset_offset" -CONF_SUNRISE_TIME = "sunrise_time" -CONF_SUNSET_TIME = "sunset_time" -DEFAULT_TRANSITION = 60 -CONF_PROFILE, DEFAULT_PROFILE = "profile", "default" - -_DOMAIN_SCHEMA = vol.Schema( - { - vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_SUNRISE_OFFSET): cv.time_period_str, - vol.Optional(CONF_SUNSET_OFFSET): cv.time_period_str, - vol.Optional(CONF_SUNRISE_TIME): cv.time, - vol.Optional(CONF_SUNSET_TIME): cv.time, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_ELEVATION): float, - vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, - vol.Optional(ATTR_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION, - vol.Optional(CONF_PROFILE, default=DEFAULT_PROFILE): cv.string, - } -) - - -def _all_unique_profiles(value): - """Validate that all enties have a unique profile name.""" - hosts = [device[CONF_PROFILE] for device in value] - schema = vol.Schema(vol.Unique()) - schema(hosts) - return value - - -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.All(cv.ensure_list, [_DOMAIN_SCHEMA], _all_unique_profiles)}, extra=vol.ALLOW_EXTRA,) - - -def setup(hass, config): - """Set up the Circadian Lighting platform.""" - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} - configs = config[DOMAIN] - for conf in configs: - profile = conf[CONF_PROFILE] - hass.data[DOMAIN][profile] = CircadianLighting( - hass, - min_colortemp=conf[CONF_MIN_CT], - max_colortemp=conf[CONF_MAX_CT], - sunrise_offset=conf.get(CONF_SUNRISE_OFFSET), - sunset_offset=conf.get(CONF_SUNSET_OFFSET), - sunrise_time=conf.get(CONF_SUNRISE_TIME), - sunset_time=conf.get(CONF_SUNSET_TIME), - latitude=conf.get(CONF_LATITUDE, hass.config.latitude), - longitude=conf.get(CONF_LONGITUDE, hass.config.longitude), - elevation=conf.get(CONF_ELEVATION, hass.config.elevation), - interval=conf[CONF_INTERVAL], - transition=conf[ATTR_TRANSITION], - profile=profile, - ) - load_platform(hass, "sensor", DOMAIN, {}, config) - - return True - - -class CircadianLighting: - """Calculate universal Circadian values.""" - - def __init__( - self, hass, min_colortemp, max_colortemp, sunrise_offset, sunset_offset, sunrise_time, sunset_time, latitude, longitude, elevation, interval, transition, profile, - ): - self.hass = hass - self._min_colortemp = min_colortemp - self._max_colortemp = max_colortemp - self._sunrise_offset = sunrise_offset - self._sunset_offset = sunset_offset - self._manual_sunset = sunset_time - self._manual_sunrise = sunrise_time - self._latitude = latitude - self._longitude = longitude - self._elevation = elevation - self._transition = transition - self._profile = profile - _LOGGER.debug("profile: %s", self._profile) - - self._percent = self.calc_percent() - self._colortemp = self.calc_colortemp() - self._rgb_color = self.calc_rgb() - self._xy_color = self.calc_xy() - self._hs_color = self.calc_hs() - - if self._manual_sunrise is not None: - async_track_time_change( - self.hass, self.update, hour=self._manual_sunrise.hour, minute=self._manual_sunrise.minute, second=self._manual_sunrise.second, - ) - else: - async_track_sunrise(self.hass, self.update, self._sunrise_offset) - - if self._manual_sunset is not None: - async_track_time_change( - self.hass, self.update, hour=self._manual_sunset.hour, minute=self._manual_sunset.minute, second=self._manual_sunset.second, - ) - else: - async_track_sunset(self.hass, self.update, self._sunset_offset) - - async_track_time_interval(self.hass, self.update, interval) - - def _replace_time(self, date, key): - other_date = self._manual_sunrise if key == "sunrise" else self._manual_sunset - return date.replace(hour=other_date.hour, minute=other_date.minute, second=other_date.second, microsecond=other_date.microsecond,) - - def get_sunrise_sunset(self, date): - if self._manual_sunrise is not None and self._manual_sunset is not None: - sunrise = self._replace_time(date, "sunrise") - sunset = self._replace_time(date, "sunset") - solar_noon = sunrise + (sunset - sunrise) / 2 - solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset) / 2 - else: - location = astral.Location() - location.name = "name" - location.region = "region" - location.latitude = self._latitude - location.longitude = self._longitude - location.elevation = self._elevation - - if self._manual_sunrise is not None: - sunrise = self._replace_time(date, "sunrise") - else: - sunrise = location.sunrise(date) - - if self._manual_sunset is not None: - sunset = self._replace_time(date, "sunset") - else: - sunset = location.sunset(date) - - solar_noon = location.solar_noon(date) - solar_midnight = location.solar_midnight(date) - - if self._sunrise_offset is not None: - sunrise = sunrise + self._sunrise_offset - if self._sunset_offset is not None: - sunset = sunset + self._sunset_offset - - datetimes = { - SUN_EVENT_SUNRISE: sunrise, - SUN_EVENT_SUNSET: sunset, - SUN_EVENT_NOON: solar_noon, - SUN_EVENT_MIDNIGHT: solar_midnight, - } - - return {k: dt.astimezone(dt_util.UTC).timestamp() for k, dt in datetimes.items()} - - def calc_percent(self): - now = dt_util.utcnow() - now_ts = now.timestamp() - - today = self.get_sunrise_sunset(now) - if now_ts < today[SUN_EVENT_SUNRISE]: - # It's before sunrise (after midnight), because it's before - # sunrise (and after midnight) sunset must have happend yesterday. - yesterday = self.get_sunrise_sunset(now - timedelta(days=1)) - if today[SUN_EVENT_MIDNIGHT] > today[SUN_EVENT_SUNSET] and yesterday[SUN_EVENT_MIDNIGHT] > yesterday[SUN_EVENT_SUNSET]: - # Solar midnight is after sunset so use yesterdays's time - today[SUN_EVENT_MIDNIGHT] = yesterday[SUN_EVENT_MIDNIGHT] - today[SUN_EVENT_SUNSET] = yesterday[SUN_EVENT_SUNSET] - elif now_ts > today[SUN_EVENT_SUNSET]: - # It's after sunset (before midnight), because it's after sunset - # (and before midnight) sunrise should happen tomorrow. - tomorrow = self.get_sunrise_sunset(now + timedelta(days=1)) - if today[SUN_EVENT_MIDNIGHT] < today[SUN_EVENT_SUNRISE] and tomorrow[SUN_EVENT_MIDNIGHT] < tomorrow[SUN_EVENT_SUNRISE]: - # Solar midnight is before sunrise so use tomorrow's time - today[SUN_EVENT_MIDNIGHT] = tomorrow[SUN_EVENT_MIDNIGHT] - today[SUN_EVENT_SUNRISE] = tomorrow[SUN_EVENT_SUNRISE] - - # Figure out where we are in time so we know which half of the - # parabola to calculate. We're generating a different - # sunset-sunrise parabola for before and after solar midnight. - # because it might not be half way between sunrise and sunset. - # We're also generating a different parabola for sunrise-sunset. - - # sunrise -> sunset parabola - if today[SUN_EVENT_SUNRISE] < now_ts < today[SUN_EVENT_SUNSET]: - h = today[SUN_EVENT_NOON] - k = 100 - # parabola before solar_noon else after solar_noon - x = today[SUN_EVENT_SUNRISE] if now_ts < today[SUN_EVENT_NOON] else today[SUN_EVENT_SUNSET] - - # sunset -> sunrise parabola - elif today[SUN_EVENT_SUNSET] < now_ts < today[SUN_EVENT_SUNRISE]: - h = today[SUN_EVENT_MIDNIGHT] - k = -100 - # parabola before solar_midnight else after solar_midnight - x = today[SUN_EVENT_SUNSET] if now_ts < today[SUN_EVENT_MIDNIGHT] else today[SUN_EVENT_SUNRISE] - - y = 0 - a = (y - k) / (h - x) ** 2 - percentage = a * (now_ts - h) ** 2 + k - return percentage - - def calc_colortemp(self): - if self._percent > 0: - delta = self._max_colortemp - self._min_colortemp - percent = self._percent / 100 - return (delta * percent) + self._min_colortemp - else: - return self._min_colortemp - - def calc_rgb(self): - return color_temperature_to_rgb(self._colortemp) - - def calc_xy(self): - return color_RGB_to_xy(*self.calc_rgb()) - - def calc_hs(self): - return color_xy_to_hs(*self.calc_xy()) - - async def update(self, _=None): - """Update Circadian Values.""" - self._percent = self.calc_percent() - self._colortemp = self.calc_colortemp() - self._rgb_color = self.calc_rgb() - self._xy_color = self.calc_xy() - self._hs_color = self.calc_hs() - async_dispatcher_send(self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC) +"""Adaptive Lighting Component for Home-Assistant.""" diff --git a/custom_components/circadian_lighting/manifest.json b/custom_components/circadian_lighting/manifest.json index 756dad08..4b103323 100644 --- a/custom_components/circadian_lighting/manifest.json +++ b/custom_components/circadian_lighting/manifest.json @@ -1,8 +1,8 @@ { - "domain": "circadian_lighting", - "name": "Circadian Lighting", - "documentation": "https://github.com/claytonjn/hass-circadian_lighting", + "domain": "adaptive_lighting", + "name": "Adaptive Lighting", + "documentation": "https://github.com/basnijholt/adaptive_lighting", "dependencies": [], - "codeowners": ["@claytonjn"], - "requirements": ["astral==1.10.1"] + "codeowners": ["@claytonjn", "@basnijholt"], + "requirements": [] } diff --git a/custom_components/circadian_lighting/sensor.py b/custom_components/circadian_lighting/sensor.py deleted file mode 100755 index 5b5e2855..00000000 --- a/custom_components/circadian_lighting/sensor.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -Circadian Lighting Sensor for Home-Assistant. -""" - -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity - -from . import CIRCADIAN_LIGHTING_UPDATE_TOPIC, DEFAULT_PROFILE, DOMAIN - -ICON = "mdi:theme-light-dark" - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Circadian Lighting sensor.""" - sensors = [CircadianSensor(hass, circadian_lighting) for circadian_lighting in hass.data[DOMAIN].values()] - add_devices(sensors, True) - return True - - -class CircadianSensor(Entity): - """Representation of a Circadian Lighting sensor.""" - - def __init__(self, hass, circadian_lighting): - """Initialize the Circadian Lighting sensor.""" - self._circadian_lighting = circadian_lighting - self._name = "Circadian Values" - self._entity_id = "sensor.circadian_values" - profile = circadian_lighting._profile - if profile != DEFAULT_PROFILE: - self._name += f" {profile}" - self._entity_id += f"_{profile.lower()}" - self._unit_of_measurement = "%" - self._icon = ICON - - @property - def entity_id(self): - """Return the entity ID of the sensor.""" - return self._entity_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._circadian_lighting._percent - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit_of_measurement - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon - - @property - def hs_color(self): - return self._circadian_lighting._hs_color - - @property - def device_state_attributes(self): - """Return the attributes of the sensor.""" - return { - "colortemp": self._circadian_lighting._colortemp, - "rgb_color": self._circadian_lighting._rgb_color, - "xy_color": self._circadian_lighting._xy_color, - } - - @property - def should_poll(self) -> bool: - """Disable polling.""" - return False - - async def async_added_to_hass(self) -> None: - """Connect dispatcher to signal from CircadianLighting object.""" - self.async_on_remove(async_dispatcher_connect(self.hass, CIRCADIAN_LIGHTING_UPDATE_TOPIC, self._update_callback)) - - @callback - def _update_callback(self) -> None: - """Triggers update of properties.""" - self.async_schedule_update_ha_state(force_refresh=False) diff --git a/custom_components/circadian_lighting/services.yaml b/custom_components/circadian_lighting/services.yaml deleted file mode 100644 index 586f06ea..00000000 --- a/custom_components/circadian_lighting/services.yaml +++ /dev/null @@ -1,2 +0,0 @@ -values_update: - description: Updates values for Circadian Lighting. diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index b86a0bf4..61a9e793 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -1,5 +1,5 @@ """ -Circadian Lighting Component for Home-Assistant. +Adaptive Lighting Component for Home-Assistant. This component calculates color temperature and brightness to synchronize your color changing lights with perceived color temperature of the sky throughout @@ -7,10 +7,10 @@ the day. This gives your environment a more natural feel, with cooler whites dur the midday and warmer tints near twilight and dawn. In addition, the component sets your lights to a nice warm white at 1% in "Sleep" mode, -which is far brighter than starlight but won't reset your circadian rhythm or break down +which is far brighter than starlight but won't reset your adaptive rhythm or break down too much rhodopsin in your eyes. -Human circadian rhythms are heavily influenced by ambient light levels and +Human adaptive rhythms are heavily influenced by ambient light levels and hues. Hormone production, brainwave activity, mood and wakefulness are just some of the cognitive functions tied to cyclical natural light. http://en.wikipedia.org/wiki/Zeitgeber @@ -75,7 +75,7 @@ _LOGGER = logging.getLogger(__name__) ICON = "mdi:theme-light-dark" -DOMAIN = "circadian_lighting" +DOMAIN = "adaptive_lighting" SUN_EVENT_NOON = "solar_noon" SUN_EVENT_MIDNIGHT = "solar_midnight" @@ -106,8 +106,8 @@ DEFAULT_TRANSITION = 60 PLATFORM_SCHEMA = vol.Schema( { - vol.Required(CONF_PLATFORM): "circadian_lighting", - vol.Optional(CONF_NAME, default="Circadian Lighting"): cv.string, + vol.Required(CONF_PLATFORM): "adaptive_lighting", + vol.Optional(CONF_NAME, default="Adaptive Lighting"): cv.string, vol.Optional(CONF_LIGHTS_BRIGHT): cv.entity_ids, vol.Optional(CONF_LIGHTS_CT): cv.entity_ids, vol.Optional(CONF_LIGHTS_RGB): cv.entity_ids, @@ -141,9 +141,9 @@ PLATFORM_SCHEMA = vol.Schema( ), vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_SUNRISE_OFFSET, default=0): cv.time_period_str, + vol.Optional(CONF_SUNRISE_OFFSET, default=0): cv.time_period, vol.Optional(CONF_SUNRISE_TIME): cv.time, - vol.Optional(CONF_SUNSET_OFFSET, default=0): cv.time_period_str, + vol.Optional(CONF_SUNSET_OFFSET, default=0): cv.time_period, vol.Optional(CONF_SUNSET_TIME): cv.time, vol.Optional(ATTR_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION, } @@ -151,8 +151,8 @@ PLATFORM_SCHEMA = vol.Schema( def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Circadian Lighting switches.""" - switch = CircadianSwitch( + """Set up the Adaptive Lighting switches.""" + switch = AdaptiveSwitch( hass, name=config[CONF_NAME], lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []), @@ -216,8 +216,8 @@ def _difference_between_states(from_state, to_state): ) -class CircadianSwitch(SwitchEntity, RestoreEntity): - """Representation of a Circadian Lighting switch.""" +class AdaptiveSwitch(SwitchEntity, RestoreEntity): + """Representation of a Adaptive Lighting switch.""" def __init__( self, @@ -248,10 +248,10 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): sunset_time, transition, ): - """Initialize the Circadian Lighting switch.""" + """Initialize the Adaptive Lighting switch.""" self.hass = hass self._name = name - self._entity_id = f"switch.circadian_lighting_{slugify(name)}" + self._entity_id = f"switch.adaptive_lighting_{slugify(name)}" self._state = None self._icon = ICON self._lights_types = dict(zip(lights_ct, repeat("ct"))) @@ -301,7 +301,7 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): @property def is_on(self): - """Return true if circadian lighting is on.""" + """Return true if adaptive lighting is on.""" return self._state async def async_added_to_hass(self): @@ -344,16 +344,16 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): return {"hs_color": self._hs_color, "brightness": self._brightness} async def async_turn_on(self, **kwargs): - """Turn on circadian lighting.""" + """Turn on adaptive lighting.""" self._state = True - await self._update_lights(transition=self._initial_transition) + await self._update_lights(transition=self._initial_transition, force=True) async def async_turn_off(self, **kwargs): - """Turn off circadian lighting.""" + """Turn off adaptive lighting.""" self._state = False def _update_attrs(self, _=None): - """Update Circadian Values.""" + """Update Adaptive Values.""" self._percent = self._calc_percent() self._brightness = self._calc_brightness() self._colortemp_kelvin = self._calc_colortemp_kelvin() @@ -368,14 +368,14 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): self._update_lights(force=False) async def _update_lights(self, lights=None, transition=None, force=True): + self._update_attrs() if self._only_once and not force: return - self._update_attrs() await self._adjust_lights(lights or self._lights, transition) def get_sunrise_sunset(self, date): def _replace_time(date, key): - other_date = getattr(self, f"_manual_{key}") + other_date = getattr(self, f"_{key}_time") return date.replace( hour=other_date.hour, minute=other_date.minute, @@ -386,15 +386,15 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): location = get_astral_location(self.hass) sunrise = ( location.sunrise(date) - if self._manual_sunrise is None + if self._sunrise_time is None else _replace_time(date, "sunrise") ) + self._sunrise_offset sunset = ( location.sunset(date) - if self._manual_sunset is None + if self._sunset_time is None else _replace_time(date, "sunset") ) + self._sunset_offset - if self._manual_sunrise is None and self._manual_sunset is None: + if self._sunrise_time is None and self._sunset_time is None: solar_noon = location.solar_noon(date) solar_midnight = location.solar_midnight(date) else: @@ -555,8 +555,8 @@ class CircadianSwitch(SwitchEntity, RestoreEntity): assert to_state.state == "on" if from_state is None or from_state.state != "on": _LOGGER.debug(_difference_between_states(from_state, to_state)) - await self._update_lights(lights=[entity_id], transition=self._initial_transition) + await self._update_lights(lights=[entity_id], transition=self._initial_transition, force=True) async def _state_changed(self, entity_id, from_state, to_state): _LOGGER.debug(_difference_between_states(from_state, to_state)) - await self._update_lights(transition=self._initial_transition) + await self._update_lights(transition=self._initial_transition, force=True) From b0e89002cddaa8bdb5cdaa927d597398dc55e0c2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 12 Sep 2020 12:30:26 +0200 Subject: [PATCH 0124/1077] rename ct -> mired --- .../circadian_lighting/switch.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 61a9e793..771edd02 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -80,7 +80,7 @@ SUN_EVENT_NOON = "solar_noon" SUN_EVENT_MIDNIGHT = "solar_midnight" CONF_LIGHTS_BRIGHT = "lights_brightness" -CONF_LIGHTS_CT = "lights_ct" +CONF_LIGHTS_MIRED = "lights_mired" CONF_LIGHTS_RGB = "lights_rgb" CONF_LIGHTS_XY = "lights_xy" @@ -109,7 +109,7 @@ PLATFORM_SCHEMA = vol.Schema( vol.Required(CONF_PLATFORM): "adaptive_lighting", vol.Optional(CONF_NAME, default="Adaptive Lighting"): cv.string, vol.Optional(CONF_LIGHTS_BRIGHT): cv.entity_ids, - vol.Optional(CONF_LIGHTS_CT): cv.entity_ids, + vol.Optional(CONF_LIGHTS_MIRED): cv.entity_ids, vol.Optional(CONF_LIGHTS_RGB): cv.entity_ids, vol.Optional(CONF_LIGHTS_XY): cv.entity_ids, vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=False): cv.boolean, @@ -156,7 +156,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass, name=config[CONF_NAME], lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []), - lights_ct=config.get(CONF_LIGHTS_CT, []), + lights_mired=config.get(CONF_LIGHTS_MIRED, []), lights_rgb=config.get(CONF_LIGHTS_RGB, []), lights_xy=config.get(CONF_LIGHTS_XY, []), disable_brightness_adjust=config[CONF_DISABLE_BRIGHTNESS_ADJUST], @@ -224,7 +224,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): hass, name, lights_brightness, - lights_ct, + lights_mired, lights_rgb, lights_xy, disable_brightness_adjust, @@ -254,7 +254,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._entity_id = f"switch.adaptive_lighting_{slugify(name)}" self._state = None self._icon = ICON - self._lights_types = dict(zip(lights_ct, repeat("ct"))) + self._lights_types = dict(zip(lights_mired, repeat("mired"))) self._lights_types.update(zip(lights_brightness, repeat("brightness"))) self._lights_types.update(zip(lights_rgb, repeat("rgb"))) self._lights_types.update(zip(lights_xy, repeat("xy"))) @@ -339,9 +339,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): @property def device_state_attributes(self): """Return the attributes of the switch.""" + attrs = { + "percent": self._percent, + "brightness": self._brightness, + "colortemp_kelvin": self._colortemp_kelvin, + "colortemp_mired": self._colortemp_mired, + "rgb_color": self._rgb_color, + "xy_color": self._xy_color, + "hs_color": self._hs_color, + } if not self._state: - return {"hs_color": None, "brightness": None} - return {"hs_color": self._hs_color, "brightness": self._brightness} + return {key: None for key in attrs.keys()} + return attrs async def async_turn_on(self, **kwargs): """Turn on adaptive lighting.""" @@ -529,7 +538,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_BRIGHTNESS] = int((self._brightness / 100) * 254) light_type = self._lights_types[light] - if light_type == "ct": + if light_type == "mired": service_data[ATTR_COLOR_TEMP] = int(self._colortemp_mired) elif light_type == "rgb": r, g, b = self._rgb_color From 8dc0b5999365de2ca1349c02c227f6a2be1bdcab Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 12 Sep 2020 12:31:10 +0200 Subject: [PATCH 0125/1077] remove elevation --- custom_components/circadian_lighting/switch.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 771edd02..c81b3eff 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -49,7 +49,6 @@ from homeassistant.components.light import VALID_TRANSITION, is_on from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_ELEVATION, CONF_NAME, CONF_PLATFORM, SERVICE_TURN_ON, @@ -115,7 +114,6 @@ PLATFORM_SCHEMA = vol.Schema( vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=False): cv.boolean, vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_ELEVATION): float, vol.Optional( CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION ): VALID_TRANSITION, @@ -162,7 +160,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): disable_brightness_adjust=config[CONF_DISABLE_BRIGHTNESS_ADJUST], disable_entity=config.get(CONF_DISABLE_ENTITY), disable_state=config.get(CONF_DISABLE_STATE), - elevation=config.get(CONF_ELEVATION, hass.config.elevation), initial_transition=config[CONF_INITIAL_TRANSITION], interval=config[CONF_INTERVAL], max_brightness=config[CONF_MAX_BRIGHT], @@ -230,7 +227,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): disable_brightness_adjust, disable_entity, disable_state, - elevation, initial_transition, interval, max_brightness, @@ -263,7 +259,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._disable_brightness_adjust = disable_brightness_adjust self._disable_entity = disable_entity self._disable_state = disable_state - self._elevation = elevation self._initial_transition = initial_transition self._interval = interval self._max_brightness = max_brightness From 7f99426466da1c131f73221a622096f174772d53 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 12 Sep 2020 12:34:16 +0200 Subject: [PATCH 0126/1077] define CONF_TRANSITION --- custom_components/circadian_lighting/switch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index c81b3eff..9233ea86 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -101,7 +101,7 @@ CONF_SUNRISE_OFFSET = "sunrise_offset" CONF_SUNRISE_TIME = "sunrise_time" CONF_SUNSET_OFFSET = "sunset_offset" CONF_SUNSET_TIME = "sunset_time" -DEFAULT_TRANSITION = 60 +CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 PLATFORM_SCHEMA = vol.Schema( { @@ -143,7 +143,7 @@ PLATFORM_SCHEMA = vol.Schema( vol.Optional(CONF_SUNRISE_TIME): cv.time, vol.Optional(CONF_SUNSET_OFFSET, default=0): cv.time_period, vol.Optional(CONF_SUNSET_TIME): cv.time, - vol.Optional(ATTR_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION, + vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION, } ) @@ -175,7 +175,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sunrise_time=config.get(CONF_SUNRISE_TIME), sunset_offset=config[CONF_SUNSET_OFFSET], sunset_time=config.get(CONF_SUNSET_TIME), - transition=config[ATTR_TRANSITION], + transition=config[CONF_TRANSITION], ) add_devices([switch]) From b7245ec219860d6338d1deaba40f55fb47fcca7b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 12 Sep 2020 12:55:29 +0200 Subject: [PATCH 0127/1077] no need to cast to a different type --- custom_components/circadian_lighting/switch.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 9233ea86..d4647393 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -534,10 +534,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): light_type = self._lights_types[light] if light_type == "mired": - service_data[ATTR_COLOR_TEMP] = int(self._colortemp_mired) + service_data[ATTR_COLOR_TEMP] = self._colortemp_mired elif light_type == "rgb": - r, g, b = self._rgb_color - service_data[ATTR_RGB_COLOR] = (int(r), int(g), int(b)) + service_data[ATTR_RGB_COLOR] = self._rgb_color elif light_type == "xy": service_data[ATTR_XY_COLOR] = self._xy_color if service_data.get(ATTR_BRIGHTNESS, False): From 36f82e8670581e254a4a110c59b89d37c3410589 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 12 Sep 2020 13:03:26 +0200 Subject: [PATCH 0128/1077] comments and style --- .../circadian_lighting/switch.py | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index d4647393..d83183b8 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -10,7 +10,7 @@ In addition, the component sets your lights to a nice warm white at 1% in "Sleep which is far brighter than starlight but won't reset your adaptive rhythm or break down too much rhodopsin in your eyes. -Human adaptive rhythms are heavily influenced by ambient light levels and +Human circadiam rhythms are heavily influenced by ambient light levels and hues. Hormone production, brainwave activity, mood and wakefulness are just some of the cognitive functions tied to cyclical natural light. http://en.wikipedia.org/wiki/Zeitgeber @@ -21,10 +21,10 @@ http://www.cambridgeincolour.com/tutorials/sunrise-sunset-calculator.htm http://en.wikipedia.org/wiki/Color_temperature Technical notes: I had to make a lot of assumptions when writing this app - * There are no considerations for weather or altitude, but does use your - hub's location to calculate the sun position. - * The component doesn't calculate a true "Blue Hour" -- it just sets the - lights to 2700K (warm white) until your hub goes into Night mode +* There are no considerations for weather or altitude, but does use your + hub's location to calculate the sun position. +* The component doesn't calculate a true "Blue Hour" -- it just sets the + lights to 2700K (warm white) until your hub goes into Night mode """ import asyncio @@ -250,12 +250,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._entity_id = f"switch.adaptive_lighting_{slugify(name)}" self._state = None self._icon = ICON - self._lights_types = dict(zip(lights_mired, repeat("mired"))) - self._lights_types.update(zip(lights_brightness, repeat("brightness"))) + + # Create lights dict + self._lights_types = dict(zip(lights_brightness, repeat("brightness"))) + self._lights_types.update(zip(lights_mired, repeat("mired"))) self._lights_types.update(zip(lights_rgb, repeat("rgb"))) self._lights_types.update(zip(lights_xy, repeat("xy"))) self._lights = list(self._lights_types.keys()) + # Set attributes from arguments self._disable_brightness_adjust = disable_brightness_adjust self._disable_entity = disable_entity self._disable_state = disable_state @@ -276,6 +279,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._sunset_time = sunset_time self._transition = transition + # Initialize attributes that will be set in self.update self._percent = None self._brightness = None self._colortemp_kelvin = None @@ -377,7 +381,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return await self._adjust_lights(lights or self._lights, transition) - def get_sunrise_sunset(self, date): + def _get_sunrise_sunset(self, date): def _replace_time(date, key): other_date = getattr(self, f"_{key}_time") return date.replace( @@ -398,6 +402,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self._sunset_time is None else _replace_time(date, "sunset") ) + self._sunset_offset + if self._sunrise_time is None and self._sunset_time is None: solar_noon = location.solar_noon(date) solar_midnight = location.solar_midnight(date) @@ -420,11 +425,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): now = dt_util.utcnow() now_ts = now.timestamp() - today = self.get_sunrise_sunset(now) + today = self._get_sunrise_sunset(now) if now_ts < today[SUN_EVENT_SUNRISE]: # It's before sunrise (after midnight), because it's before # sunrise (and after midnight) sunset must have happend yesterday. - yesterday = self.get_sunrise_sunset(now - timedelta(days=1)) + yesterday = self._get_sunrise_sunset(now - timedelta(days=1)) if ( today[SUN_EVENT_MIDNIGHT] > today[SUN_EVENT_SUNSET] and yesterday[SUN_EVENT_MIDNIGHT] > yesterday[SUN_EVENT_SUNSET] @@ -435,7 +440,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): elif now_ts > today[SUN_EVENT_SUNSET]: # It's after sunset (before midnight), because it's after sunset # (and before midnight) sunrise should happen tomorrow. - tomorrow = self.get_sunrise_sunset(now + timedelta(days=1)) + tomorrow = self._get_sunrise_sunset(now + timedelta(days=1)) if ( today[SUN_EVENT_MIDNIGHT] < today[SUN_EVENT_SUNRISE] and tomorrow[SUN_EVENT_MIDNIGHT] < tomorrow[SUN_EVENT_SUNRISE] @@ -558,7 +563,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): assert to_state.state == "on" if from_state is None or from_state.state != "on": _LOGGER.debug(_difference_between_states(from_state, to_state)) - await self._update_lights(lights=[entity_id], transition=self._initial_transition, force=True) + await self._update_lights( + lights=[entity_id], transition=self._initial_transition, force=True + ) async def _state_changed(self, entity_id, from_state, to_state): _LOGGER.debug(_difference_between_states(from_state, to_state)) From fe6ed9fb2cbe837403b2763d3ce0f6a3595ca780 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 12 Sep 2020 13:18:15 +0200 Subject: [PATCH 0129/1077] get times from astral in UTC --- .../circadian_lighting/switch.py | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index d83183b8..e1cb2b20 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -393,32 +393,28 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): location = get_astral_location(self.hass) sunrise = ( - location.sunrise(date) + location.sunrise(date, local=False) if self._sunrise_time is None else _replace_time(date, "sunrise") ) + self._sunrise_offset sunset = ( - location.sunset(date) + location.sunset(date, local=False) if self._sunset_time is None else _replace_time(date, "sunset") ) + self._sunset_offset if self._sunrise_time is None and self._sunset_time is None: - solar_noon = location.solar_noon(date) - solar_midnight = location.solar_midnight(date) + solar_noon = location.solar_noon(date, local=False) + solar_midnight = location.solar_midnight(date, local=False) else: solar_noon = sunrise + (sunset - sunrise) / 2 solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset) / 2 - datetimes = { - SUN_EVENT_SUNRISE: sunrise, - SUN_EVENT_SUNSET: sunset, - SUN_EVENT_NOON: solar_noon, - SUN_EVENT_MIDNIGHT: solar_midnight, - } - return { - k: dt.astimezone(dt_util.UTC).timestamp() for k, dt in datetimes.items() + SUN_EVENT_SUNRISE: sunrise.timestamp(), + SUN_EVENT_SUNSET: sunset.timestamp(), + SUN_EVENT_NOON: solar_noon.timestamp(), + SUN_EVENT_MIDNIGHT: solar_midnight.timestamp(), } def _calc_percent(self): From 2b891c5521e9704fc152b8c2ed42de9c0a6dbc49 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 12 Sep 2020 13:22:54 +0200 Subject: [PATCH 0130/1077] simplify calculation --- custom_components/circadian_lighting/switch.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index e1cb2b20..cf186672 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -489,8 +489,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return self._sleep_colortemp if self._percent > 0: delta = self._max_colortemp - self._min_colortemp - percent = self._percent / 100 - return (delta * percent) + self._min_colortemp + return (delta * self._percent / 100) + self._min_colortemp return self._min_colortemp def _calc_brightness(self) -> float: @@ -501,7 +500,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self._percent > 0: return self._max_brightness delta_brightness = self._max_brightness - self._min_brightness - percent = (100 + self._percent) / 100 + percent = 1 + self._percent / 100 return (delta_brightness * percent) + self._min_brightness def _is_disabled(self): From 47a4db229fc14c34abcfc9fe7d7df1f75aa5775d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 12 Sep 2020 14:14:05 +0200 Subject: [PATCH 0131/1077] divide percent by 100 and fix updating of attrs --- .../circadian_lighting/switch.py | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index cf186672..df8aa5ee 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -73,6 +73,7 @@ from homeassistant.util.color import ( _LOGGER = logging.getLogger(__name__) ICON = "mdi:theme-light-dark" +SCAN_INTERVAL = timedelta(seconds=10) DOMAIN = "adaptive_lighting" SUN_EVENT_NOON = "solar_noon" @@ -322,7 +323,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): from_state=self._disable_state, ) - async_track_time_interval(self.hass, self.update, self._interval) + async_track_time_interval(self.hass, self._async_update_at_interval, self._interval) if self._state is not None: # If not None, we got an initial value return @@ -360,8 +361,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Turn off adaptive lighting.""" self._state = False - def _update_attrs(self, _=None): + async def _update_attrs(self, _=None): """Update Adaptive Values.""" + # Setting all values because this method takes <0.5ms to execute. self._percent = self._calc_percent() self._brightness = self._calc_brightness() self._colortemp_kelvin = self._calc_colortemp_kelvin() @@ -371,17 +373,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._rgb_color = color_temperature_to_rgb(self._colortemp_kelvin) self._xy_color = color_RGB_to_xy(*self._rgb_color) self._hs_color = color_xy_to_hs(*self._xy_color) + self.async_write_ha_state() + _LOGGER.debug("'_update_attrs' called for %s", self._name) - async def update(self, now=None): - self._update_lights(force=False) + async def _async_update_at_interval(self, now=None): + await self._update_lights(force=False) - async def _update_lights(self, lights=None, transition=None, force=True): - self._update_attrs() + async def _update_lights(self, lights=None, transition=None, force=False): + await self._update_attrs() if self._only_once and not force: return await self._adjust_lights(lights or self._lights, transition) - def _get_sunrise_sunset(self, date): + def _get_sun_events(self, date): def _replace_time(date, key): other_date = getattr(self, f"_{key}_time") return date.replace( @@ -421,11 +425,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): now = dt_util.utcnow() now_ts = now.timestamp() - today = self._get_sunrise_sunset(now) + today = self._get_sun_events(now) if now_ts < today[SUN_EVENT_SUNRISE]: # It's before sunrise (after midnight), because it's before # sunrise (and after midnight) sunset must have happend yesterday. - yesterday = self._get_sunrise_sunset(now - timedelta(days=1)) + yesterday = self._get_sun_events(now - timedelta(days=1)) if ( today[SUN_EVENT_MIDNIGHT] > today[SUN_EVENT_SUNSET] and yesterday[SUN_EVENT_MIDNIGHT] > yesterday[SUN_EVENT_SUNSET] @@ -436,7 +440,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): elif now_ts > today[SUN_EVENT_SUNSET]: # It's after sunset (before midnight), because it's after sunset # (and before midnight) sunrise should happen tomorrow. - tomorrow = self._get_sunrise_sunset(now + timedelta(days=1)) + tomorrow = self._get_sun_events(now + timedelta(days=1)) if ( today[SUN_EVENT_MIDNIGHT] < today[SUN_EVENT_SUNRISE] and tomorrow[SUN_EVENT_MIDNIGHT] < tomorrow[SUN_EVENT_SUNRISE] @@ -454,7 +458,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # sunrise -> sunset parabola if today[SUN_EVENT_SUNRISE] < now_ts < today[SUN_EVENT_SUNSET]: h = today[SUN_EVENT_NOON] - k = 100 + k = 1 # parabola before solar_noon else after solar_noon x = ( today[SUN_EVENT_SUNRISE] @@ -465,7 +469,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # sunset -> sunrise parabola elif today[SUN_EVENT_SUNSET] < now_ts < today[SUN_EVENT_SUNRISE]: h = today[SUN_EVENT_MIDNIGHT] - k = -100 + k = -1 # parabola before solar_midnight else after solar_midnight x = ( today[SUN_EVENT_SUNSET] @@ -489,7 +493,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return self._sleep_colortemp if self._percent > 0: delta = self._max_colortemp - self._min_colortemp - return (delta * self._percent / 100) + self._min_colortemp + return (delta * self._percent) + self._min_colortemp return self._min_colortemp def _calc_brightness(self) -> float: @@ -500,7 +504,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self._percent > 0: return self._max_brightness delta_brightness = self._max_brightness - self._min_brightness - percent = 1 + self._percent / 100 + percent = 1 + self._percent return (delta_brightness * percent) + self._min_brightness def _is_disabled(self): From 4f0b0645c1bd9b05b080e8dc092317f9b688b162 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 12 Sep 2020 14:32:43 +0200 Subject: [PATCH 0132/1077] use self.unsub_tracker instead of _state --- .../circadian_lighting/switch.py | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index df8aa5ee..f7d6e37b 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -249,7 +249,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self.hass = hass self._name = name self._entity_id = f"switch.adaptive_lighting_{slugify(name)}" - self._state = None self._icon = ICON # Create lights dict @@ -289,6 +288,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._xy_color = None self._hs_color = None + # Set and unset tracker in async_turn_on and async_turn_off + self.unsub_tracker = None + @property def entity_id(self): """Return the entity ID of the switch.""" @@ -302,7 +304,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): @property def is_on(self): """Return true if adaptive lighting is on.""" - return self._state + return self.unsub_tracker is not None async def async_added_to_hass(self): """Call when entity about to be added to hass.""" @@ -323,13 +325,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): from_state=self._disable_state, ) - async_track_time_interval(self.hass, self._async_update_at_interval, self._interval) - - if self._state is not None: # If not None, we got an initial value - return - - state = await self.async_get_last_state() - self._state = state and state.state == STATE_ON + last_state = await self.async_get_last_state() + if last_state and last_state.state == STATE_ON: + await self.async_turn_on() @property def icon(self): @@ -348,20 +346,24 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): "xy_color": self._xy_color, "hs_color": self._hs_color, } - if not self._state: + if not self.is_on: return {key: None for key in attrs.keys()} return attrs async def async_turn_on(self, **kwargs): """Turn on adaptive lighting.""" - self._state = True await self._update_lights(transition=self._initial_transition, force=True) + self.unsub_tracker = async_track_time_interval( + self.hass, self._async_update_at_interval, self._interval + ) async def async_turn_off(self, **kwargs): """Turn off adaptive lighting.""" - self._state = False + if self.is_on: + self.unsub_tracker() + self.unsub_tracker = None - async def _update_attrs(self, _=None): + async def _update_attrs(self): """Update Adaptive Values.""" # Setting all values because this method takes <0.5ms to execute. self._percent = self._calc_percent() @@ -514,7 +516,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) def _should_adjust(self): - if self._state is not True: + if not self.is_on: return False if self._is_disabled(): return False From cf414925d8ae7beab3800a293175f826c7ba6168 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 12 Sep 2020 14:33:57 +0200 Subject: [PATCH 0133/1077] change default interval to 90 --- custom_components/circadian_lighting/switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index f7d6e37b..3b475847 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -88,7 +88,7 @@ CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust" CONF_DISABLE_ENTITY = "disable_entity" CONF_DISABLE_STATE = "disable_state" CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 -CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 300 +CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 CONF_MAX_BRIGHT, DEFAULT_MAX_BRIGHT = "max_brightness", 100 CONF_MAX_CT, DEFAULT_MAX_CT = "max_colortemp", 5500 CONF_MIN_BRIGHT, DEFAULT_MIN_BRIGHT = "min_brightness", 1 @@ -279,7 +279,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._sunset_time = sunset_time self._transition = transition - # Initialize attributes that will be set in self.update + # Initialize attributes that will be set in self._update_attrs self._percent = None self._brightness = None self._colortemp_kelvin = None From 667c645ccf23962e53c0bb7a1aac3f53f9bd0e95 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 12 Sep 2020 14:37:38 +0200 Subject: [PATCH 0134/1077] BRIGHT -> BRIGHTNESS --- .../circadian_lighting/switch.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index 3b475847..e9181632 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -79,7 +79,7 @@ DOMAIN = "adaptive_lighting" SUN_EVENT_NOON = "solar_noon" SUN_EVENT_MIDNIGHT = "solar_midnight" -CONF_LIGHTS_BRIGHT = "lights_brightness" +CONF_LIGHTS_BRIGHTNESS = "lights_brightness" CONF_LIGHTS_MIRED = "lights_mired" CONF_LIGHTS_RGB = "lights_rgb" CONF_LIGHTS_XY = "lights_xy" @@ -89,12 +89,12 @@ CONF_DISABLE_ENTITY = "disable_entity" CONF_DISABLE_STATE = "disable_state" CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 -CONF_MAX_BRIGHT, DEFAULT_MAX_BRIGHT = "max_brightness", 100 +CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100 CONF_MAX_CT, DEFAULT_MAX_CT = "max_colortemp", 5500 -CONF_MIN_BRIGHT, DEFAULT_MIN_BRIGHT = "min_brightness", 1 +CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS = "min_brightness", 1 CONF_MIN_CT, DEFAULT_MIN_CT = "min_colortemp", 2500 CONF_ONLY_ONCE = "only_once" -CONF_SLEEP_BRIGHT, DEFAULT_SLEEP_BRIGHT = "sleep_brightness", 1 +CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1 CONF_SLEEP_CT, DEFAULT_SLEEP_CT = "sleep_colortemp", 1000 CONF_SLEEP_ENTITY = "sleep_entity" CONF_SLEEP_STATE = "sleep_state" @@ -108,7 +108,7 @@ PLATFORM_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): "adaptive_lighting", vol.Optional(CONF_NAME, default="Adaptive Lighting"): cv.string, - vol.Optional(CONF_LIGHTS_BRIGHT): cv.entity_ids, + vol.Optional(CONF_LIGHTS_BRIGHTNESS): cv.entity_ids, vol.Optional(CONF_LIGHTS_MIRED): cv.entity_ids, vol.Optional(CONF_LIGHTS_RGB): cv.entity_ids, vol.Optional(CONF_LIGHTS_XY): cv.entity_ids, @@ -119,20 +119,20 @@ PLATFORM_SCHEMA = vol.Schema( CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION ): VALID_TRANSITION, vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, - vol.Optional(CONF_MAX_BRIGHT, default=DEFAULT_MAX_BRIGHT): vol.All( + vol.Optional(CONF_MAX_BRIGHTNESS, default=DEFAULT_MAX_BRIGHTNESS): vol.All( vol.Coerce(int), vol.Range(min=1, max=100) ), vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): vol.All( vol.Coerce(int), vol.Range(min=1000, max=10000) ), - vol.Optional(CONF_MIN_BRIGHT, default=DEFAULT_MIN_BRIGHT): vol.All( + vol.Optional(CONF_MIN_BRIGHTNESS, default=DEFAULT_MIN_BRIGHTNESS): vol.All( vol.Coerce(int), vol.Range(min=1, max=100) ), vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): vol.All( vol.Coerce(int), vol.Range(min=1000, max=10000) ), vol.Optional(CONF_ONLY_ONCE, default=False): cv.boolean, - vol.Optional(CONF_SLEEP_BRIGHT, default=DEFAULT_SLEEP_BRIGHT): vol.All( + vol.Optional(CONF_SLEEP_BRIGHTNESS, default=DEFAULT_SLEEP_BRIGHTNESS): vol.All( vol.Coerce(int), vol.Range(min=1, max=100) ), vol.Optional(CONF_SLEEP_CT, default=DEFAULT_SLEEP_CT): vol.All( @@ -154,7 +154,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): switch = AdaptiveSwitch( hass, name=config[CONF_NAME], - lights_brightness=config.get(CONF_LIGHTS_BRIGHT, []), + lights_brightness=config.get(CONF_LIGHTS_BRIGHTNESS, []), lights_mired=config.get(CONF_LIGHTS_MIRED, []), lights_rgb=config.get(CONF_LIGHTS_RGB, []), lights_xy=config.get(CONF_LIGHTS_XY, []), @@ -163,12 +163,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): disable_state=config.get(CONF_DISABLE_STATE), initial_transition=config[CONF_INITIAL_TRANSITION], interval=config[CONF_INTERVAL], - max_brightness=config[CONF_MAX_BRIGHT], + max_brightness=config[CONF_MAX_BRIGHTNESS], max_colortemp=config[CONF_MAX_CT], - min_brightness=config[CONF_MIN_BRIGHT], + min_brightness=config[CONF_MIN_BRIGHTNESS], min_colortemp=config[CONF_MIN_CT], only_once=config[CONF_ONLY_ONCE], - sleep_brightness=config[CONF_SLEEP_BRIGHT], + sleep_brightness=config[CONF_SLEEP_BRIGHTNESS], sleep_colortemp=config[CONF_SLEEP_CT], sleep_entity=config.get(CONF_SLEEP_ENTITY), sleep_state=config.get(CONF_SLEEP_STATE), From 6964d08df04e1eb5e08209063f3c621c5d03116e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 12 Sep 2020 23:25:36 +0200 Subject: [PATCH 0135/1077] add config flow basics --- .../circadian_lighting/config_flow.py | 165 ++++++++++++++++++ custom_components/circadian_lighting/const.py | 30 ++++ .../circadian_lighting/manifest.json | 1 + .../circadian_lighting/strings.json | 51 ++++++ .../circadian_lighting/switch.py | 70 ++++---- .../circadian_lighting/translations/en.json | 51 ++++++ 6 files changed, 337 insertions(+), 31 deletions(-) create mode 100644 custom_components/circadian_lighting/config_flow.py create mode 100644 custom_components/circadian_lighting/const.py create mode 100644 custom_components/circadian_lighting/strings.json create mode 100644 custom_components/circadian_lighting/translations/en.json diff --git a/custom_components/circadian_lighting/config_flow.py b/custom_components/circadian_lighting/config_flow.py new file mode 100644 index 00000000..2d292cf1 --- /dev/null +++ b/custom_components/circadian_lighting/config_flow.py @@ -0,0 +1,165 @@ +"""Config flow for Coronavirus integration.""" +import logging + +import homeassistant.helpers.config_validation as cv +import voluptuous as vol +from homeassistant import config_entries +from homeassistant.components.light import VALID_TRANSITION +from homeassistant.core import callback + +from .const import ( + CONF_DISABLE_BRIGHTNESS_ADJUST, + CONF_DISABLE_ENTITY, + CONF_DISABLE_STATE, + CONF_INITIAL_TRANSITION, + CONF_INTERVAL, + CONF_LIGHTS_BRIGHTNESS, + CONF_LIGHTS_MIRED, + CONF_LIGHTS_RGB, + CONF_LIGHTS_XY, + CONF_MAX_BRIGHTNESS, + CONF_MAX_CT, + CONF_MIN_BRIGHTNESS, + CONF_MIN_CT, + CONF_ONLY_ONCE, + CONF_SLEEP_BRIGHTNESS, + CONF_SLEEP_CT, + CONF_SLEEP_ENTITY, + CONF_SLEEP_STATE, + CONF_SUNRISE_OFFSET, + CONF_SUNRISE_TIME, + CONF_SUNSET_OFFSET, + CONF_SUNSET_TIME, + CONF_TRANSITION, + DEFAULT_INITIAL_TRANSITION, + DEFAULT_INTERVAL, + DEFAULT_MAX_BRIGHTNESS, + DEFAULT_MAX_CT, + DEFAULT_MIN_BRIGHTNESS, + DEFAULT_MIN_CT, + DEFAULT_SLEEP_BRIGHTNESS, + DEFAULT_SLEEP_CT, + DEFAULT_TRANSITION, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Adaptive Lighting.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if user_input is not None: + await self.async_set_unique_id(user_input["name"]) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=user_input["name"], data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required("name"): str}), + errors=errors, + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for Adaptive Lighting.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = self.config_entry.options + + lights_brightness = options.get(CONF_LIGHTS_BRIGHTNESS, []) + lights_mired = options.get(CONF_LIGHTS_MIRED, []) + lights_rgb = options.get(CONF_LIGHTS_RGB, []) + lights_xy = options.get(CONF_LIGHTS_XY, []) + disable_brightness_adjust = options.get(CONF_DISABLE_BRIGHTNESS_ADJUST, False) + disable_entity = options.get(CONF_DISABLE_ENTITY) + disable_state = options.get(CONF_DISABLE_STATE) + initial_transition = options.get( + CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION + ) + interval = options.get(CONF_INTERVAL, DEFAULT_INTERVAL) + max_brightness = options.get(CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS) + max_colortemp = options.get(CONF_MAX_CT, DEFAULT_MAX_CT) + min_brightness = options.get(CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS) + min_colortemp = options.get(CONF_MIN_CT, DEFAULT_MIN_CT) + only_once = options.get(CONF_ONLY_ONCE, False) + sleep_brightness = options.get(CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS) + sleep_colortemp = options.get(CONF_SLEEP_CT, DEFAULT_SLEEP_CT) + sleep_entity = options.get(CONF_SLEEP_ENTITY) + sleep_state = options.get(CONF_SLEEP_STATE) + sunrise_offset = options.get(CONF_SUNRISE_OFFSET, 0) + sunrise_time = options.get(CONF_SUNRISE_TIME) + sunset_offset = options.get(CONF_SUNSET_OFFSET, 0) + sunset_time = options.get(CONF_SUNSET_TIME) + transition = options.get(CONF_TRANSITION, DEFAULT_TRANSITION) + + all_lights = self.hass.states.async_entity_ids("light") + all_lights = cv.multi_select(all_lights) + + options_schema = vol.Schema( + { + vol.Optional( + CONF_LIGHTS_BRIGHTNESS, default=lights_brightness + ): all_lights, + vol.Optional(CONF_LIGHTS_MIRED, default=lights_mired): all_lights, + vol.Optional(CONF_LIGHTS_RGB, default=lights_rgb): all_lights, + vol.Optional(CONF_LIGHTS_XY, default=lights_xy): all_lights, + vol.Optional( + CONF_DISABLE_BRIGHTNESS_ADJUST, default=disable_brightness_adjust + ): bool, + vol.Optional(CONF_DISABLE_ENTITY, default=disable_entity): str, + vol.Optional(CONF_DISABLE_STATE, default=disable_state): str, + vol.Optional( + CONF_INITIAL_TRANSITION, default=initial_transition + ): cv.positive_int, + vol.Optional(CONF_INTERVAL, default=interval): cv.positive_int, + vol.Optional(CONF_MAX_BRIGHTNESS, default=max_brightness): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_MAX_CT, default=max_colortemp): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_MIN_BRIGHTNESS, default=min_brightness): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_MIN_CT, default=min_colortemp): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_ONLY_ONCE, default=only_once): bool, + vol.Optional(CONF_SLEEP_BRIGHTNESS, default=sleep_brightness): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_SLEEP_CT, default=sleep_colortemp): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_SLEEP_ENTITY, default=sleep_entity): str, + vol.Optional(CONF_SLEEP_STATE, default=sleep_state): str, + vol.Optional(CONF_SUNRISE_OFFSET, default=sunrise_offset): int, + vol.Optional(CONF_SUNRISE_TIME, default=sunrise_time): str, + vol.Optional(CONF_SUNSET_OFFSET, default=sunset_offset): int, + vol.Optional(CONF_SUNSET_TIME, default=sunset_time): str, + vol.Optional(CONF_TRANSITION, default=transition): VALID_TRANSITION, + } + ) + + return self.async_show_form(step_id="init", data_schema=options_schema) diff --git a/custom_components/circadian_lighting/const.py b/custom_components/circadian_lighting/const.py new file mode 100644 index 00000000..1b898362 --- /dev/null +++ b/custom_components/circadian_lighting/const.py @@ -0,0 +1,30 @@ +ICON = "mdi:theme-light-dark" + +DOMAIN = "adaptive_lighting" +SUN_EVENT_NOON = "solar_noon" +SUN_EVENT_MIDNIGHT = "solar_midnight" + +CONF_LIGHTS_BRIGHTNESS = "lights_brightness" +CONF_LIGHTS_MIRED = "lights_mired" +CONF_LIGHTS_RGB = "lights_rgb" +CONF_LIGHTS_XY = "lights_xy" + +CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust" +CONF_DISABLE_ENTITY = "disable_entity" +CONF_DISABLE_STATE = "disable_state" +CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 +CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 +CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100 +CONF_MAX_CT, DEFAULT_MAX_CT = "max_colortemp", 5500 +CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS = "min_brightness", 1 +CONF_MIN_CT, DEFAULT_MIN_CT = "min_colortemp", 2500 +CONF_ONLY_ONCE = "only_once" +CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1 +CONF_SLEEP_CT, DEFAULT_SLEEP_CT = "sleep_colortemp", 1000 +CONF_SLEEP_ENTITY = "sleep_entity" +CONF_SLEEP_STATE = "sleep_state" +CONF_SUNRISE_OFFSET = "sunrise_offset" +CONF_SUNRISE_TIME = "sunrise_time" +CONF_SUNSET_OFFSET = "sunset_offset" +CONF_SUNSET_TIME = "sunset_time" +CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 diff --git a/custom_components/circadian_lighting/manifest.json b/custom_components/circadian_lighting/manifest.json index 4b103323..ceae225d 100644 --- a/custom_components/circadian_lighting/manifest.json +++ b/custom_components/circadian_lighting/manifest.json @@ -2,6 +2,7 @@ "domain": "adaptive_lighting", "name": "Adaptive Lighting", "documentation": "https://github.com/basnijholt/adaptive_lighting", + "config_flow": true, "dependencies": [], "codeowners": ["@claytonjn", "@basnijholt"], "requirements": [] diff --git a/custom_components/circadian_lighting/strings.json b/custom_components/circadian_lighting/strings.json new file mode 100644 index 00000000..d9090b82 --- /dev/null +++ b/custom_components/circadian_lighting/strings.json @@ -0,0 +1,51 @@ +{ + "title": "Adaptive Lighting", + "config": { + "step": { + "user": { + "title": "Choose a name for the Adaptive Lighting", + "description": "Every instance can contain multiple lights!", + "data": { + "name": "Name" + } + } + }, + "abort": { + "already_configured": "This name is already configured." + } + }, + "options": { + "step": { + "init": { + "data": { + "lights_brightness": "lights_brightness", + "lights_mired": "lights_mired", + "lights_rgb": "lights_rgb", + "lights_xy": "lights_xy", + "disable_brightness_adjust": "disable_brightness_adjust", + "disable_entity": "disable_entity", + "disable_state": "disable_state", + "initial_transition": "initial_transition", + "interval": "interval", + "max_brightness": "max_brightness", + "max_colortemp": "max_colortemp", + "min_brightness": "min_brightness", + "min_colortemp": "min_colortemp", + "only_once": "only_once", + "sleep_brightness": "sleep_brightness", + "sleep_colortemp": "sleep_colortemp", + "sleep_entity": "sleep_entity", + "sleep_state": "sleep_state", + "sunrise_offset": "sunrise_offset", + "sunrise_time": "sunrise_time", + "sunset_offset": "sunset_offset", + "sunset_time": "sunset_time", + "transition": "transition" + } + } + }, + "error": { + "retrive_error": "Error retriving servers list" + } + } +} \ No newline at end of file diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index e9181632..a9ec2d4d 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -69,44 +69,52 @@ from homeassistant.util.color import ( color_temperature_to_rgb, color_xy_to_hs, ) +from .const import ( + ICON, + DOMAIN, + SUN_EVENT_NOON, + SUN_EVENT_MIDNIGHT, + CONF_LIGHTS_BRIGHTNESS, + CONF_LIGHTS_MIRED, + CONF_LIGHTS_RGB, + CONF_LIGHTS_XY, + CONF_DISABLE_BRIGHTNESS_ADJUST, + CONF_DISABLE_ENTITY, + CONF_DISABLE_STATE, + CONF_INITIAL_TRANSITION, + DEFAULT_INITIAL_TRANSITION, + CONF_INTERVAL, + DEFAULT_INTERVAL, + CONF_MAX_BRIGHTNESS, + DEFAULT_MAX_BRIGHTNESS, + CONF_MAX_CT, + DEFAULT_MAX_CT, + CONF_MIN_BRIGHTNESS, + DEFAULT_MIN_BRIGHTNESS, + CONF_MIN_CT, + DEFAULT_MIN_CT, + CONF_ONLY_ONCE, + CONF_SLEEP_BRIGHTNESS, + DEFAULT_SLEEP_BRIGHTNESS, + CONF_SLEEP_CT, + DEFAULT_SLEEP_CT, + CONF_SLEEP_ENTITY, + CONF_SLEEP_STATE, + CONF_SUNRISE_OFFSET, + CONF_SUNRISE_TIME, + CONF_SUNSET_OFFSET, + CONF_SUNSET_TIME, + CONF_TRANSITION, + DEFAULT_TRANSITION, +) _LOGGER = logging.getLogger(__name__) -ICON = "mdi:theme-light-dark" SCAN_INTERVAL = timedelta(seconds=10) -DOMAIN = "adaptive_lighting" -SUN_EVENT_NOON = "solar_noon" -SUN_EVENT_MIDNIGHT = "solar_midnight" - -CONF_LIGHTS_BRIGHTNESS = "lights_brightness" -CONF_LIGHTS_MIRED = "lights_mired" -CONF_LIGHTS_RGB = "lights_rgb" -CONF_LIGHTS_XY = "lights_xy" - -CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust" -CONF_DISABLE_ENTITY = "disable_entity" -CONF_DISABLE_STATE = "disable_state" -CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 -CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 -CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100 -CONF_MAX_CT, DEFAULT_MAX_CT = "max_colortemp", 5500 -CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS = "min_brightness", 1 -CONF_MIN_CT, DEFAULT_MIN_CT = "min_colortemp", 2500 -CONF_ONLY_ONCE = "only_once" -CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1 -CONF_SLEEP_CT, DEFAULT_SLEEP_CT = "sleep_colortemp", 1000 -CONF_SLEEP_ENTITY = "sleep_entity" -CONF_SLEEP_STATE = "sleep_state" -CONF_SUNRISE_OFFSET = "sunrise_offset" -CONF_SUNRISE_TIME = "sunrise_time" -CONF_SUNSET_OFFSET = "sunset_offset" -CONF_SUNSET_TIME = "sunset_time" -CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 - PLATFORM_SCHEMA = vol.Schema( { - vol.Required(CONF_PLATFORM): "adaptive_lighting", + vol.Required(CONF_PLATFORM): DOMAIN, vol.Optional(CONF_NAME, default="Adaptive Lighting"): cv.string, vol.Optional(CONF_LIGHTS_BRIGHTNESS): cv.entity_ids, vol.Optional(CONF_LIGHTS_MIRED): cv.entity_ids, diff --git a/custom_components/circadian_lighting/translations/en.json b/custom_components/circadian_lighting/translations/en.json new file mode 100644 index 00000000..973a5d64 --- /dev/null +++ b/custom_components/circadian_lighting/translations/en.json @@ -0,0 +1,51 @@ +{ + "title": "Adaptive Lighting", + "config": { + "step": { + "user": { + "title": "Choose a name for the Adaptive Lighting", + "description": "Every instance can contain multiple lights!", + "data": { + "name": "Name" + } + } + }, + "abort": { + "already_configured": "This name is already configured." + } + }, + "options": { + "step": { + "init": { + "data": { + "lights_brightness": "lights_brightness", + "lights_mired": "lights_mired", + "lights_rgb": "lights_rgb", + "lights_xy": "lights_xy", + "disable_brightness_adjust": "disable_brightness_adjust", + "disable_entity": "disable_entity", + "disable_state": "disable_state", + "initial_transition": "initial_transition", + "interval": "interval", + "max_brightness": "max_brightness", + "max_colortemp": "max_colortemp", + "min_brightness": "min_brightness", + "min_colortemp": "min_colortemp", + "only_once": "only_once", + "sleep_brightness": "sleep_brightness", + "sleep_colortemp": "sleep_colortemp", + "sleep_entity": "sleep_entity", + "sleep_state": "sleep_state", + "sunrise_offset": "sunrise_offset", + "sunrise_time": "sunrise_time", + "sunset_offset": "sunset_offset", + "sunset_time": "sunset_time", + "transition": "transition" + } + } + }, + "error": { + "retrive_error": "Error retriving servers list" + } + } +} From 1331b792db97839abeadaa3e7f043a324db56616 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 13 Sep 2020 10:46:08 +0200 Subject: [PATCH 0136/1077] small changes --- .../circadian_lighting/strings.json | 50 ++++++++++--------- .../circadian_lighting/switch.py | 2 +- .../circadian_lighting/translations/en.json | 48 +++++++++--------- 3 files changed, 52 insertions(+), 48 deletions(-) diff --git a/custom_components/circadian_lighting/strings.json b/custom_components/circadian_lighting/strings.json index d9090b82..a1f4401b 100644 --- a/custom_components/circadian_lighting/strings.json +++ b/custom_components/circadian_lighting/strings.json @@ -17,30 +17,32 @@ "options": { "step": { "init": { + "title": "Adaptive Lighting options", + "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings.", "data": { - "lights_brightness": "lights_brightness", - "lights_mired": "lights_mired", - "lights_rgb": "lights_rgb", - "lights_xy": "lights_xy", - "disable_brightness_adjust": "disable_brightness_adjust", - "disable_entity": "disable_entity", - "disable_state": "disable_state", - "initial_transition": "initial_transition", - "interval": "interval", - "max_brightness": "max_brightness", - "max_colortemp": "max_colortemp", - "min_brightness": "min_brightness", - "min_colortemp": "min_colortemp", - "only_once": "only_once", - "sleep_brightness": "sleep_brightness", - "sleep_colortemp": "sleep_colortemp", - "sleep_entity": "sleep_entity", - "sleep_state": "sleep_state", - "sunrise_offset": "sunrise_offset", - "sunrise_time": "sunrise_time", - "sunset_offset": "sunset_offset", - "sunset_time": "sunset_time", - "transition": "transition" + "lights_brightness": "`lights_brightness`", + "lights_mired": "`lights_mired`", + "lights_rgb": "`lights_rgb`", + "lights_xy": "`lights_xy`", + "disable_brightness_adjust": "`disable_brightness_adjust`", + "disable_entity": "`disable_entity`", + "disable_state": "`disable_state`", + "initial_transition": "`initial_transition`, the transition of the lights when turning them on or when `disable_state` or `sleep_state` change", + "interval": "`interval`", + "max_brightness": "`max_brightness`", + "max_colortemp": "`max_colortemp`", + "min_brightness": "`min_brightness`", + "min_colortemp": "`min_colortemp`", + "only_once": "`only_once`", + "sleep_brightness": "`sleep_brightness`", + "sleep_colortemp": "`sleep_colortemp`", + "sleep_entity": "`sleep_entity`", + "sleep_state": "`sleep_state`", + "sunrise_offset": "`sunrise_offset`", + "sunrise_time": "`sunrise_time`", + "sunset_offset": "`sunset_offset`", + "sunset_time": "`sunset_time`", + "transition": "`transition`" } } }, @@ -48,4 +50,4 @@ "retrive_error": "Error retriving servers list" } } -} \ No newline at end of file +} diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/circadian_lighting/switch.py index a9ec2d4d..4833cd74 100755 --- a/custom_components/circadian_lighting/switch.py +++ b/custom_components/circadian_lighting/switch.py @@ -10,7 +10,7 @@ In addition, the component sets your lights to a nice warm white at 1% in "Sleep which is far brighter than starlight but won't reset your adaptive rhythm or break down too much rhodopsin in your eyes. -Human circadiam rhythms are heavily influenced by ambient light levels and +Human circadian rhythms are heavily influenced by ambient light levels and hues. Hormone production, brainwave activity, mood and wakefulness are just some of the cognitive functions tied to cyclical natural light. http://en.wikipedia.org/wiki/Zeitgeber diff --git a/custom_components/circadian_lighting/translations/en.json b/custom_components/circadian_lighting/translations/en.json index 973a5d64..a1f4401b 100644 --- a/custom_components/circadian_lighting/translations/en.json +++ b/custom_components/circadian_lighting/translations/en.json @@ -17,30 +17,32 @@ "options": { "step": { "init": { + "title": "Adaptive Lighting options", + "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings.", "data": { - "lights_brightness": "lights_brightness", - "lights_mired": "lights_mired", - "lights_rgb": "lights_rgb", - "lights_xy": "lights_xy", - "disable_brightness_adjust": "disable_brightness_adjust", - "disable_entity": "disable_entity", - "disable_state": "disable_state", - "initial_transition": "initial_transition", - "interval": "interval", - "max_brightness": "max_brightness", - "max_colortemp": "max_colortemp", - "min_brightness": "min_brightness", - "min_colortemp": "min_colortemp", - "only_once": "only_once", - "sleep_brightness": "sleep_brightness", - "sleep_colortemp": "sleep_colortemp", - "sleep_entity": "sleep_entity", - "sleep_state": "sleep_state", - "sunrise_offset": "sunrise_offset", - "sunrise_time": "sunrise_time", - "sunset_offset": "sunset_offset", - "sunset_time": "sunset_time", - "transition": "transition" + "lights_brightness": "`lights_brightness`", + "lights_mired": "`lights_mired`", + "lights_rgb": "`lights_rgb`", + "lights_xy": "`lights_xy`", + "disable_brightness_adjust": "`disable_brightness_adjust`", + "disable_entity": "`disable_entity`", + "disable_state": "`disable_state`", + "initial_transition": "`initial_transition`, the transition of the lights when turning them on or when `disable_state` or `sleep_state` change", + "interval": "`interval`", + "max_brightness": "`max_brightness`", + "max_colortemp": "`max_colortemp`", + "min_brightness": "`min_brightness`", + "min_colortemp": "`min_colortemp`", + "only_once": "`only_once`", + "sleep_brightness": "`sleep_brightness`", + "sleep_colortemp": "`sleep_colortemp`", + "sleep_entity": "`sleep_entity`", + "sleep_state": "`sleep_state`", + "sunrise_offset": "`sunrise_offset`", + "sunrise_time": "`sunrise_time`", + "sunset_offset": "`sunset_offset`", + "sunset_time": "`sunset_time`", + "transition": "`transition`" } } }, From f444a8a1428e57c722d0bcba872c9ead224fdca5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 13 Sep 2020 10:46:33 +0200 Subject: [PATCH 0137/1077] need to change the name of folder to get strings.json working --- .../{circadian_lighting => adaptive_lighting}/__init__.py | 0 .../{circadian_lighting => adaptive_lighting}/config_flow.py | 0 .../{circadian_lighting => adaptive_lighting}/const.py | 0 .../{circadian_lighting => adaptive_lighting}/manifest.json | 0 .../{circadian_lighting => adaptive_lighting}/strings.json | 0 .../{circadian_lighting => adaptive_lighting}/switch.py | 0 .../translations/en.json | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename custom_components/{circadian_lighting => adaptive_lighting}/__init__.py (100%) rename custom_components/{circadian_lighting => adaptive_lighting}/config_flow.py (100%) rename custom_components/{circadian_lighting => adaptive_lighting}/const.py (100%) rename custom_components/{circadian_lighting => adaptive_lighting}/manifest.json (100%) rename custom_components/{circadian_lighting => adaptive_lighting}/strings.json (100%) rename custom_components/{circadian_lighting => adaptive_lighting}/switch.py (100%) rename custom_components/{circadian_lighting => adaptive_lighting}/translations/en.json (100%) diff --git a/custom_components/circadian_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py similarity index 100% rename from custom_components/circadian_lighting/__init__.py rename to custom_components/adaptive_lighting/__init__.py diff --git a/custom_components/circadian_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py similarity index 100% rename from custom_components/circadian_lighting/config_flow.py rename to custom_components/adaptive_lighting/config_flow.py diff --git a/custom_components/circadian_lighting/const.py b/custom_components/adaptive_lighting/const.py similarity index 100% rename from custom_components/circadian_lighting/const.py rename to custom_components/adaptive_lighting/const.py diff --git a/custom_components/circadian_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json similarity index 100% rename from custom_components/circadian_lighting/manifest.json rename to custom_components/adaptive_lighting/manifest.json diff --git a/custom_components/circadian_lighting/strings.json b/custom_components/adaptive_lighting/strings.json similarity index 100% rename from custom_components/circadian_lighting/strings.json rename to custom_components/adaptive_lighting/strings.json diff --git a/custom_components/circadian_lighting/switch.py b/custom_components/adaptive_lighting/switch.py similarity index 100% rename from custom_components/circadian_lighting/switch.py rename to custom_components/adaptive_lighting/switch.py diff --git a/custom_components/circadian_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json similarity index 100% rename from custom_components/circadian_lighting/translations/en.json rename to custom_components/adaptive_lighting/translations/en.json From 6e5668ea3bb143401699da39c46bc3098df93b52 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 15 Sep 2020 20:29:59 +0200 Subject: [PATCH 0138/1077] remove lights_brightness, lights_mired, lights_rgb, lights_xy with "lights" --- custom_components/adaptive_lighting/const.py | 5 +- custom_components/adaptive_lighting/switch.py | 107 +++++++++--------- 2 files changed, 55 insertions(+), 57 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 1b898362..12aa2e05 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -4,10 +4,7 @@ DOMAIN = "adaptive_lighting" SUN_EVENT_NOON = "solar_noon" SUN_EVENT_MIDNIGHT = "solar_midnight" -CONF_LIGHTS_BRIGHTNESS = "lights_brightness" -CONF_LIGHTS_MIRED = "lights_mired" -CONF_LIGHTS_RGB = "lights_rgb" -CONF_LIGHTS_XY = "lights_xy" +CONF_LIGHTS = "lights" CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust" CONF_DISABLE_ENTITY = "disable_entity" diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 4833cd74..db8b4c76 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -30,22 +30,26 @@ Technical notes: I had to make a lot of assumptions when writing this app import asyncio import logging from datetime import timedelta -from itertools import repeat import voluptuous as vol import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util from homeassistant.components.light import ( - ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, - ATTR_XY_COLOR, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.light import VALID_TRANSITION, is_on +from homeassistant.components.light import ( + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_TRANSITION, + VALID_TRANSITION, + is_on, +) from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( ATTR_ENTITY_ID, @@ -60,8 +64,8 @@ from homeassistant.helpers.event import ( async_track_state_change, async_track_time_interval, ) -from homeassistant.helpers.sun import get_astral_location from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.sun import get_astral_location from homeassistant.util import slugify from homeassistant.util.color import ( color_RGB_to_xy, @@ -69,35 +73,21 @@ from homeassistant.util.color import ( color_temperature_to_rgb, color_xy_to_hs, ) + from .const import ( - ICON, - DOMAIN, - SUN_EVENT_NOON, - SUN_EVENT_MIDNIGHT, - CONF_LIGHTS_BRIGHTNESS, - CONF_LIGHTS_MIRED, - CONF_LIGHTS_RGB, - CONF_LIGHTS_XY, CONF_DISABLE_BRIGHTNESS_ADJUST, CONF_DISABLE_ENTITY, CONF_DISABLE_STATE, CONF_INITIAL_TRANSITION, - DEFAULT_INITIAL_TRANSITION, CONF_INTERVAL, - DEFAULT_INTERVAL, + CONF_LIGHTS, CONF_MAX_BRIGHTNESS, - DEFAULT_MAX_BRIGHTNESS, CONF_MAX_CT, - DEFAULT_MAX_CT, CONF_MIN_BRIGHTNESS, - DEFAULT_MIN_BRIGHTNESS, CONF_MIN_CT, - DEFAULT_MIN_CT, CONF_ONLY_ONCE, CONF_SLEEP_BRIGHTNESS, - DEFAULT_SLEEP_BRIGHTNESS, CONF_SLEEP_CT, - DEFAULT_SLEEP_CT, CONF_SLEEP_ENTITY, CONF_SLEEP_STATE, CONF_SUNRISE_OFFSET, @@ -105,9 +95,29 @@ from .const import ( CONF_SUNSET_OFFSET, CONF_SUNSET_TIME, CONF_TRANSITION, + DEFAULT_INITIAL_TRANSITION, + DEFAULT_INTERVAL, + DEFAULT_MAX_BRIGHTNESS, + DEFAULT_MAX_CT, + DEFAULT_MIN_BRIGHTNESS, + DEFAULT_MIN_CT, + DEFAULT_SLEEP_BRIGHTNESS, + DEFAULT_SLEEP_CT, DEFAULT_TRANSITION, + DOMAIN, + ICON, + SUN_EVENT_MIDNIGHT, + SUN_EVENT_NOON, ) +_SUPPORT_OPTS = { + "brightness": SUPPORT_BRIGHTNESS, + "color_temp": SUPPORT_COLOR_TEMP, + "color": SUPPORT_COLOR, + "transition": SUPPORT_TRANSITION, +} + + _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=10) @@ -116,10 +126,7 @@ PLATFORM_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): DOMAIN, vol.Optional(CONF_NAME, default="Adaptive Lighting"): cv.string, - vol.Optional(CONF_LIGHTS_BRIGHTNESS): cv.entity_ids, - vol.Optional(CONF_LIGHTS_MIRED): cv.entity_ids, - vol.Optional(CONF_LIGHTS_RGB): cv.entity_ids, - vol.Optional(CONF_LIGHTS_XY): cv.entity_ids, + vol.Optional(CONF_LIGHTS): cv.entity_ids, vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=False): cv.boolean, vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), @@ -162,10 +169,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): switch = AdaptiveSwitch( hass, name=config[CONF_NAME], - lights_brightness=config.get(CONF_LIGHTS_BRIGHTNESS, []), - lights_mired=config.get(CONF_LIGHTS_MIRED, []), - lights_rgb=config.get(CONF_LIGHTS_RGB, []), - lights_xy=config.get(CONF_LIGHTS_XY, []), + lights=config.get(CONF_LIGHTS, []), disable_brightness_adjust=config[CONF_DISABLE_BRIGHTNESS_ADJUST], disable_entity=config.get(CONF_DISABLE_ENTITY), disable_state=config.get(CONF_DISABLE_STATE), @@ -229,10 +233,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self, hass, name, - lights_brightness, - lights_mired, - lights_rgb, - lights_xy, + lights, disable_brightness_adjust, disable_entity, disable_state, @@ -259,14 +260,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._entity_id = f"switch.adaptive_lighting_{slugify(name)}" self._icon = ICON - # Create lights dict - self._lights_types = dict(zip(lights_brightness, repeat("brightness"))) - self._lights_types.update(zip(lights_mired, repeat("mired"))) - self._lights_types.update(zip(lights_rgb, repeat("rgb"))) - self._lights_types.update(zip(lights_xy, repeat("xy"))) - self._lights = list(self._lights_types.keys()) - # Set attributes from arguments + self._lights = lights self._disable_brightness_adjust = disable_brightness_adjust self._disable_entity = disable_entity self._disable_state = disable_state @@ -314,9 +309,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Return true if adaptive lighting is on.""" return self.unsub_tracker is not None + def _supported_features(self, light): + state = self.hass.states.get(light) + supported_features = state.attributes["supported_features"] + return { + key for key, value in _SUPPORT_OPTS.items() if supported_features & value + } + async def async_added_to_hass(self): """Call when entity about to be added to hass.""" - # Add listeners async_track_state_change( self.hass, self._lights, self._light_state_changed, to_state="on" ) @@ -542,19 +543,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if not is_on(self.hass, light): continue - service_data = {ATTR_ENTITY_ID: light, ATTR_TRANSITION: transition} - if self._brightness is not None: - service_data[ATTR_BRIGHTNESS] = int((self._brightness / 100) * 254) + service_data = {ATTR_ENTITY_ID: light} - light_type = self._lights_types[light] - if light_type == "mired": - service_data[ATTR_COLOR_TEMP] = self._colortemp_mired - elif light_type == "rgb": + features = self._supported_features(light) + if "transition" in features: + service_data[ATTR_TRANSITION:transition] + + if self._brightness is not None and "brightness" in features: + service_data[ATTR_BRIGHTNESS_PCT] = round(self._brightness) + + if "color" in features: service_data[ATTR_RGB_COLOR] = self._rgb_color - elif light_type == "xy": - service_data[ATTR_XY_COLOR] = self._xy_color - if service_data.get(ATTR_BRIGHTNESS, False): - service_data[ATTR_WHITE_VALUE] = service_data[ATTR_BRIGHTNESS] + elif "color_temp" in features: + service_data[ATTR_COLOR_TEMP] = self._colortemp_mired _LOGGER.debug( "Scheduling 'light.turn_on' with the following 'service_data': %s", From 4898cbcaff5a4af6fe5725634007632cc222f602 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 15 Sep 2020 20:40:24 +0200 Subject: [PATCH 0139/1077] split _adjust_lights --- custom_components/adaptive_lighting/switch.py | 62 +++++++++---------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index db8b4c76..02e5de1c 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -524,6 +524,31 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): and self.hass.states.get(self._disable_entity).state in self._disable_state ) + async def _adjust_light(self, light, transition): + service_data = {ATTR_ENTITY_ID: light} + features = self._supported_features(light) + + if "transition" in features: + if transition is None: + transition = self._transition + service_data[ATTR_TRANSITION] = transition + + if self._brightness is not None and "brightness" in features: + service_data[ATTR_BRIGHTNESS_PCT] = self._brightness + + if "color" in features: + service_data[ATTR_RGB_COLOR] = self._rgb_color + elif "color_temp" in features: + service_data[ATTR_COLOR_TEMP] = self._colortemp_mired + + _LOGGER.debug( + "Scheduling 'light.turn_on' with the following 'service_data': %s", + service_data, + ) + return self.hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data + ) + def _should_adjust(self): if not self.is_on: return False @@ -534,38 +559,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _adjust_lights(self, lights, transition): if not self._should_adjust(): return - - if transition is None: - transition = self._transition - - tasks = [] - for light in lights: - if not is_on(self.hass, light): - continue - - service_data = {ATTR_ENTITY_ID: light} - - features = self._supported_features(light) - if "transition" in features: - service_data[ATTR_TRANSITION:transition] - - if self._brightness is not None and "brightness" in features: - service_data[ATTR_BRIGHTNESS_PCT] = round(self._brightness) - - if "color" in features: - service_data[ATTR_RGB_COLOR] = self._rgb_color - elif "color_temp" in features: - service_data[ATTR_COLOR_TEMP] = self._colortemp_mired - - _LOGGER.debug( - "Scheduling 'light.turn_on' with the following 'service_data': %s", - service_data, - ) - tasks.append( - self.hass.services.async_call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data - ) - ) + tasks = [ + self._adjust_light(light, transition) + for light in lights + if is_on(self.hass, light) + ] if tasks: await asyncio.wait(tasks) From d4fcaa404d6f1cff0ffc9d3f4dab5f5e30fb4c49 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 15 Sep 2020 21:03:01 +0200 Subject: [PATCH 0140/1077] colortemp -> color_temp --- custom_components/adaptive_lighting/switch.py | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 02e5de1c..052646d6 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -176,12 +176,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): initial_transition=config[CONF_INITIAL_TRANSITION], interval=config[CONF_INTERVAL], max_brightness=config[CONF_MAX_BRIGHTNESS], - max_colortemp=config[CONF_MAX_CT], + max_color_temp=config[CONF_MAX_CT], min_brightness=config[CONF_MIN_BRIGHTNESS], - min_colortemp=config[CONF_MIN_CT], + min_color_temp=config[CONF_MIN_CT], only_once=config[CONF_ONLY_ONCE], sleep_brightness=config[CONF_SLEEP_BRIGHTNESS], - sleep_colortemp=config[CONF_SLEEP_CT], + sleep_color_temp=config[CONF_SLEEP_CT], sleep_entity=config.get(CONF_SLEEP_ENTITY), sleep_state=config.get(CONF_SLEEP_STATE), sunrise_offset=config[CONF_SUNRISE_OFFSET], @@ -240,12 +240,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): initial_transition, interval, max_brightness, - max_colortemp, + max_color_temp, min_brightness, - min_colortemp, + min_color_temp, only_once, sleep_brightness, - sleep_colortemp, + sleep_color_temp, sleep_entity, sleep_state, sunrise_offset, @@ -268,12 +268,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._initial_transition = initial_transition self._interval = interval self._max_brightness = max_brightness - self._max_colortemp = max_colortemp + self._max_color_temp = max_color_temp self._min_brightness = min_brightness - self._min_colortemp = min_colortemp + self._min_color_temp = min_color_temp self._only_once = only_once self._sleep_brightness = sleep_brightness - self._sleep_colortemp = sleep_colortemp + self._sleep_color_temp = sleep_color_temp self._sleep_entity = sleep_entity self._sleep_state = sleep_state self._sunrise_offset = sunrise_offset @@ -285,8 +285,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Initialize attributes that will be set in self._update_attrs self._percent = None self._brightness = None - self._colortemp_kelvin = None - self._colortemp_mired = None + self._color_temp_kelvin = None + self._color_temp_mired = None self._rgb_color = None self._xy_color = None self._hs_color = None @@ -349,8 +349,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): attrs = { "percent": self._percent, "brightness": self._brightness, - "colortemp_kelvin": self._colortemp_kelvin, - "colortemp_mired": self._colortemp_mired, + "color_temp_kelvin": self._color_temp_kelvin, + "color_temp_mired": self._color_temp_mired, "rgb_color": self._rgb_color, "xy_color": self._xy_color, "hs_color": self._hs_color, @@ -377,11 +377,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Setting all values because this method takes <0.5ms to execute. self._percent = self._calc_percent() self._brightness = self._calc_brightness() - self._colortemp_kelvin = self._calc_colortemp_kelvin() - self._colortemp_mired = color_temperature_kelvin_to_mired( - self._colortemp_kelvin + self._color_temp_kelvin = self._calc_color_temp_kelvin() + self._color_temp_mired = color_temperature_kelvin_to_mired( + self._color_temp_kelvin ) - self._rgb_color = color_temperature_to_rgb(self._colortemp_kelvin) + self._rgb_color = color_temperature_to_rgb(self._color_temp_kelvin) self._xy_color = color_RGB_to_xy(*self._rgb_color) self._hs_color = color_xy_to_hs(*self._xy_color) self.async_write_ha_state() @@ -499,13 +499,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): and self.hass.states.get(self._sleep_entity).state in self._sleep_state ) - def _calc_colortemp_kelvin(self): + def _calc_color_temp_kelvin(self): if self._is_sleep(): - return self._sleep_colortemp + return self._sleep_color_temp if self._percent > 0: - delta = self._max_colortemp - self._min_colortemp - return (delta * self._percent) + self._min_colortemp - return self._min_colortemp + delta = self._max_color_temp - self._min_color_temp + return (delta * self._percent) + self._min_color_temp + return self._min_color_temp def _calc_brightness(self) -> float: if self._disable_brightness_adjust: @@ -539,7 +539,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if "color" in features: service_data[ATTR_RGB_COLOR] = self._rgb_color elif "color_temp" in features: - service_data[ATTR_COLOR_TEMP] = self._colortemp_mired + service_data[ATTR_COLOR_TEMP] = self._color_temp_mired _LOGGER.debug( "Scheduling 'light.turn_on' with the following 'service_data': %s", From 276d991c1c55a09612a8ea9840bc02c3e99785f4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 15 Sep 2020 21:05:21 +0200 Subject: [PATCH 0141/1077] unpack light groups --- custom_components/adaptive_lighting/switch.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 052646d6..2a80686e 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -316,10 +316,29 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): key for key, value in _SUPPORT_OPTS.items() if supported_features & value } + def _unpack_light_groups(self, lights): + all_lights = [] + for light in lights: + state = self.hass.states.get(light) + if state is None: + _LOGGER.debug("State of %s is None", light) + # TODO: make sure that the lights are loaded when doing this + all_lights.append(light) + elif "entity_id" in state.attributes: # it's a light group + group = state.attributes["entity_id"] + self.debug("Unpacked %s to %s", group) + all_lights.extend(group) + else: + all_lights.append(light) + return all_lights + async def async_added_to_hass(self): """Call when entity about to be added to hass.""" async_track_state_change( - self.hass, self._lights, self._light_state_changed, to_state="on" + self.hass, + self._unpack_light_groups(self._lights), + self._light_state_changed, + to_state="on", ) track_kwargs = dict(hass=self.hass, action=self._state_changed) if self._sleep_entity is not None: From 0b1be003e9f3cfabb19c577a83f9b33965c0f57e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 15 Sep 2020 21:27:15 +0200 Subject: [PATCH 0142/1077] use events --- custom_components/adaptive_lighting/switch.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 2a80686e..1ffbf34b 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -62,6 +62,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.event import ( async_track_state_change, + async_track_state_change_event, async_track_time_interval, ) from homeassistant.helpers.restore_state import RestoreEntity @@ -334,11 +335,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def async_added_to_hass(self): """Call when entity about to be added to hass.""" - async_track_state_change( + async_track_state_change_event( self.hass, self._unpack_light_groups(self._lights), self._light_state_changed, - to_state="on", ) track_kwargs = dict(hass=self.hass, action=self._state_changed) if self._sleep_entity is not None: @@ -586,10 +586,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if tasks: await asyncio.wait(tasks) - async def _light_state_changed(self, entity_id, from_state, to_state): - assert to_state.state == "on" - if from_state is None or from_state.state != "on": - _LOGGER.debug(_difference_between_states(from_state, to_state)) + async def _light_state_changed(self, event): + _LOGGER.debug("Got event %s", event) + old_state = event.data.get("old_state") + if old_state is not None: + old_state = old_state.state + + new_state = event.data.get("new_state") + if new_state is not None: + new_state = new_state.state + + if new_state == "on" and old_state != "on" and old_state != "unavailable": + entity_id = event.data["entity_id"] await self._update_lights( lights=[entity_id], transition=self._initial_transition, force=True ) From b0b11d06528116a272b8a89bdf6d0f90e9ad40d8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 15 Sep 2020 21:27:33 +0200 Subject: [PATCH 0143/1077] Revert "use events" This reverts commit 0b1be003e9f3cfabb19c577a83f9b33965c0f57e. --- custom_components/adaptive_lighting/switch.py | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 1ffbf34b..2a80686e 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -62,7 +62,6 @@ from homeassistant.const import ( ) from homeassistant.helpers.event import ( async_track_state_change, - async_track_state_change_event, async_track_time_interval, ) from homeassistant.helpers.restore_state import RestoreEntity @@ -335,10 +334,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def async_added_to_hass(self): """Call when entity about to be added to hass.""" - async_track_state_change_event( + async_track_state_change( self.hass, self._unpack_light_groups(self._lights), self._light_state_changed, + to_state="on", ) track_kwargs = dict(hass=self.hass, action=self._state_changed) if self._sleep_entity is not None: @@ -586,18 +586,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if tasks: await asyncio.wait(tasks) - async def _light_state_changed(self, event): - _LOGGER.debug("Got event %s", event) - old_state = event.data.get("old_state") - if old_state is not None: - old_state = old_state.state - - new_state = event.data.get("new_state") - if new_state is not None: - new_state = new_state.state - - if new_state == "on" and old_state != "on" and old_state != "unavailable": - entity_id = event.data["entity_id"] + async def _light_state_changed(self, entity_id, from_state, to_state): + assert to_state.state == "on" + if from_state is None or from_state.state != "on": + _LOGGER.debug(_difference_between_states(from_state, to_state)) await self._update_lights( lights=[entity_id], transition=self._initial_transition, force=True ) From 4870b7c4053ae4d6e027a12c764c03bf45db5be4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 15 Sep 2020 21:48:51 +0200 Subject: [PATCH 0144/1077] add missing await --- custom_components/adaptive_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 2a80686e..9165725c 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -579,7 +579,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if not self._should_adjust(): return tasks = [ - self._adjust_light(light, transition) + await self._adjust_light(light, transition) for light in lights if is_on(self.hass, light) ] From 7b79096abcb034cec61d0d7c650404a5817bdc36 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 15 Sep 2020 22:59:54 +0200 Subject: [PATCH 0145/1077] simplification and don't add trackers when lights is [] --- custom_components/adaptive_lighting/switch.py | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 9165725c..6666f662 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -334,24 +334,26 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def async_added_to_hass(self): """Call when entity about to be added to hass.""" - async_track_state_change( - self.hass, - self._unpack_light_groups(self._lights), - self._light_state_changed, - to_state="on", - ) - track_kwargs = dict(hass=self.hass, action=self._state_changed) - if self._sleep_entity is not None: - sleep_kwargs = dict(track_kwargs, entity_ids=self._sleep_entity) - async_track_state_change(**sleep_kwargs, to_state=self._sleep_state) - async_track_state_change(**sleep_kwargs, from_state=self._sleep_state) - - if self._disable_entity is not None: + if self._lights: async_track_state_change( - **track_kwargs, - entity_ids=self._disable_entity, - from_state=self._disable_state, + self.hass, + self._unpack_light_groups(self._lights), + self._light_state_changed, + to_state="on", + from_state="off", ) + track_kwargs = dict(hass=self.hass, action=self._state_changed) + if self._sleep_entity is not None: + sleep_kwargs = dict(track_kwargs, entity_ids=self._sleep_entity) + async_track_state_change(**sleep_kwargs, to_state=self._sleep_state) + async_track_state_change(**sleep_kwargs, from_state=self._sleep_state) + + if self._disable_entity is not None: + disable_kwargs = dict(track_kwargs, entity_ids=self._disable_entity) + async_track_state_change( + **disable_kwargs, from_state=self._disable_state + ) + async_track_state_change(**disable_kwargs, to_state=self._disable_state) last_state = await self.async_get_last_state() if last_state and last_state.state == STATE_ON: @@ -569,9 +571,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) def _should_adjust(self): - if not self.is_on: - return False - if self._is_disabled(): + if not self._lights or not self.is_on or self._is_disabled(): return False return True @@ -587,12 +587,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): await asyncio.wait(tasks) async def _light_state_changed(self, entity_id, from_state, to_state): - assert to_state.state == "on" - if from_state is None or from_state.state != "on": - _LOGGER.debug(_difference_between_states(from_state, to_state)) - await self._update_lights( - lights=[entity_id], transition=self._initial_transition, force=True - ) + assert to_state.state == "on" and from_state.state == "off" + _LOGGER.debug(_difference_between_states(from_state, to_state)) + await self._update_lights( + lights=[entity_id], transition=self._initial_transition, force=True + ) async def _state_changed(self, entity_id, from_state, to_state): _LOGGER.debug(_difference_between_states(from_state, to_state)) From c4bb689f272d0bf1f40dbcb78e1ae9f2fb157e7f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 15 Sep 2020 23:21:53 +0200 Subject: [PATCH 0146/1077] update config_flow.py --- .../adaptive_lighting/config_flow.py | 17 +++-------------- custom_components/adaptive_lighting/const.py | 1 - 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 2d292cf1..dfc753ca 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -13,10 +13,7 @@ from .const import ( CONF_DISABLE_STATE, CONF_INITIAL_TRANSITION, CONF_INTERVAL, - CONF_LIGHTS_BRIGHTNESS, - CONF_LIGHTS_MIRED, - CONF_LIGHTS_RGB, - CONF_LIGHTS_XY, + CONF_LIGHTS, CONF_MAX_BRIGHTNESS, CONF_MAX_CT, CONF_MIN_BRIGHTNESS, @@ -87,10 +84,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): options = self.config_entry.options - lights_brightness = options.get(CONF_LIGHTS_BRIGHTNESS, []) - lights_mired = options.get(CONF_LIGHTS_MIRED, []) - lights_rgb = options.get(CONF_LIGHTS_RGB, []) - lights_xy = options.get(CONF_LIGHTS_XY, []) + lights = options.get(CONF_LIGHTS, []) disable_brightness_adjust = options.get(CONF_DISABLE_BRIGHTNESS_ADJUST, False) disable_entity = options.get(CONF_DISABLE_ENTITY) disable_state = options.get(CONF_DISABLE_STATE) @@ -118,12 +112,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): options_schema = vol.Schema( { - vol.Optional( - CONF_LIGHTS_BRIGHTNESS, default=lights_brightness - ): all_lights, - vol.Optional(CONF_LIGHTS_MIRED, default=lights_mired): all_lights, - vol.Optional(CONF_LIGHTS_RGB, default=lights_rgb): all_lights, - vol.Optional(CONF_LIGHTS_XY, default=lights_xy): all_lights, + vol.Optional(CONF_LIGHTS, default=lights): all_lights, vol.Optional( CONF_DISABLE_BRIGHTNESS_ADJUST, default=disable_brightness_adjust ): bool, diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 12aa2e05..d18403ba 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -5,7 +5,6 @@ SUN_EVENT_NOON = "solar_noon" SUN_EVENT_MIDNIGHT = "solar_midnight" CONF_LIGHTS = "lights" - CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust" CONF_DISABLE_ENTITY = "disable_entity" CONF_DISABLE_STATE = "disable_state" From 48c16ff63722bae9bc4714d8c8514b5a00038888 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 15 Sep 2020 23:33:19 +0200 Subject: [PATCH 0147/1077] unify configs --- README.md | 2 +- .../adaptive_lighting/config_flow.py | 75 +++----- custom_components/adaptive_lighting/const.py | 19 +- .../adaptive_lighting/strings.json | 6 +- custom_components/adaptive_lighting/switch.py | 176 +++++------------- .../adaptive_lighting/translations/en.json | 6 +- 6 files changed, 96 insertions(+), 188 deletions(-) diff --git a/README.md b/README.md index efb07d56..dbba559b 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ These graphs were generated using the values calculated by the Circadian Lightin ![cl_percent|690x131](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/6/5/657ff98beb65a94598edeb4bdfd939095db1a22c.PNG) ##### Color Temperature: -![cl_colortemp|690x129](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/9/59e84263cbecd8e428cb08777a0413672c48dfcd.PNG) +![cl_color_temp|690x129](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/9/59e84263cbecd8e428cb08777a0413672c48dfcd.PNG) ##### Brightness: ![cl_brightness|690x130](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/8/58ebd994b62a8b1abfb3497a5288d923ff4e2330.PNG) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index dfc753ca..089d625a 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -15,12 +15,12 @@ from .const import ( CONF_INTERVAL, CONF_LIGHTS, CONF_MAX_BRIGHTNESS, - CONF_MAX_CT, + CONF_MAX_COLOR_TEMP, CONF_MIN_BRIGHTNESS, - CONF_MIN_CT, + CONF_MIN_COLOR_TEMP, CONF_ONLY_ONCE, CONF_SLEEP_BRIGHTNESS, - CONF_SLEEP_CT, + CONF_SLEEP_COLOR_TEMP, CONF_SLEEP_ENTITY, CONF_SLEEP_STATE, CONF_SUNRISE_OFFSET, @@ -28,14 +28,19 @@ from .const import ( CONF_SUNSET_OFFSET, CONF_SUNSET_TIME, CONF_TRANSITION, + DEFAULT_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_INITIAL_TRANSITION, DEFAULT_INTERVAL, + DEFAULT_LIGHTS, DEFAULT_MAX_BRIGHTNESS, - DEFAULT_MAX_CT, + DEFAULT_MAX_COLOR_TEMP, DEFAULT_MIN_BRIGHTNESS, - DEFAULT_MIN_CT, + DEFAULT_MIN_COLOR_TEMP, + DEFAULT_ONLY_ONCE, DEFAULT_SLEEP_BRIGHTNESS, - DEFAULT_SLEEP_CT, + DEFAULT_SLEEP_COLOR_TEMP, + DEFAULT_SUNRISE_OFFSET, + DEFAULT_SUNSET_OFFSET, DEFAULT_TRANSITION, DOMAIN, ) @@ -57,11 +62,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry(title=user_input["name"], data=user_input) - return self.async_show_form( - step_id="user", - data_schema=vol.Schema({vol.Required("name"): str}), - errors=errors, - ) + return self.async_show_form(step_id="user", data_schema=vol.Schema({vol.Required("name"): str}), errors=errors,) @staticmethod @callback @@ -84,26 +85,24 @@ class OptionsFlowHandler(config_entries.OptionsFlow): options = self.config_entry.options - lights = options.get(CONF_LIGHTS, []) - disable_brightness_adjust = options.get(CONF_DISABLE_BRIGHTNESS_ADJUST, False) + lights = options.get(CONF_LIGHTS, DEFAULT_LIGHTS) + disable_brightness_adjust = options.get(CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST) disable_entity = options.get(CONF_DISABLE_ENTITY) disable_state = options.get(CONF_DISABLE_STATE) - initial_transition = options.get( - CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION - ) + initial_transition = options.get(CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION) interval = options.get(CONF_INTERVAL, DEFAULT_INTERVAL) max_brightness = options.get(CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS) - max_colortemp = options.get(CONF_MAX_CT, DEFAULT_MAX_CT) + max_color_temp = options.get(CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP) min_brightness = options.get(CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS) - min_colortemp = options.get(CONF_MIN_CT, DEFAULT_MIN_CT) - only_once = options.get(CONF_ONLY_ONCE, False) + min_color_temp = options.get(CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP) + only_once = options.get(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE) sleep_brightness = options.get(CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS) - sleep_colortemp = options.get(CONF_SLEEP_CT, DEFAULT_SLEEP_CT) + sleep_color_temp = options.get(CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP) sleep_entity = options.get(CONF_SLEEP_ENTITY) sleep_state = options.get(CONF_SLEEP_STATE) - sunrise_offset = options.get(CONF_SUNRISE_OFFSET, 0) + sunrise_offset = options.get(CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET) sunrise_time = options.get(CONF_SUNRISE_TIME) - sunset_offset = options.get(CONF_SUNSET_OFFSET, 0) + sunset_offset = options.get(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET) sunset_time = options.get(CONF_SUNSET_TIME) transition = options.get(CONF_TRANSITION, DEFAULT_TRANSITION) @@ -113,34 +112,18 @@ class OptionsFlowHandler(config_entries.OptionsFlow): options_schema = vol.Schema( { vol.Optional(CONF_LIGHTS, default=lights): all_lights, - vol.Optional( - CONF_DISABLE_BRIGHTNESS_ADJUST, default=disable_brightness_adjust - ): bool, + vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=disable_brightness_adjust): bool, vol.Optional(CONF_DISABLE_ENTITY, default=disable_entity): str, vol.Optional(CONF_DISABLE_STATE, default=disable_state): str, - vol.Optional( - CONF_INITIAL_TRANSITION, default=initial_transition - ): cv.positive_int, + vol.Optional(CONF_INITIAL_TRANSITION, default=initial_transition): cv.positive_int, vol.Optional(CONF_INTERVAL, default=interval): cv.positive_int, - vol.Optional(CONF_MAX_BRIGHTNESS, default=max_brightness): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_MAX_CT, default=max_colortemp): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_MIN_BRIGHTNESS, default=min_brightness): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_MIN_CT, default=min_colortemp): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), + vol.Optional(CONF_MAX_BRIGHTNESS, default=max_brightness): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional(CONF_MAX_COLOR_TEMP, default=max_color_temp): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), + vol.Optional(CONF_MIN_BRIGHTNESS, default=min_brightness): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional(CONF_MIN_COLOR_TEMP, default=min_color_temp): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), vol.Optional(CONF_ONLY_ONCE, default=only_once): bool, - vol.Optional(CONF_SLEEP_BRIGHTNESS, default=sleep_brightness): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_SLEEP_CT, default=sleep_colortemp): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), + vol.Optional(CONF_SLEEP_BRIGHTNESS, default=sleep_brightness): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional(CONF_SLEEP_COLOR_TEMP, default=sleep_color_temp): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), vol.Optional(CONF_SLEEP_ENTITY, default=sleep_entity): str, vol.Optional(CONF_SLEEP_STATE, default=sleep_state): str, vol.Optional(CONF_SUNRISE_OFFSET, default=sunrise_offset): int, diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index d18403ba..9075ed06 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -4,23 +4,26 @@ DOMAIN = "adaptive_lighting" SUN_EVENT_NOON = "solar_noon" SUN_EVENT_MIDNIGHT = "solar_midnight" -CONF_LIGHTS = "lights" -CONF_DISABLE_BRIGHTNESS_ADJUST = "disable_brightness_adjust" +CONF_LIGHTS, DEFAULT_LIGHTS = "lights", [] +CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST = ( + "disable_brightness_adjust", + False, +) CONF_DISABLE_ENTITY = "disable_entity" CONF_DISABLE_STATE = "disable_state" CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100 -CONF_MAX_CT, DEFAULT_MAX_CT = "max_colortemp", 5500 +CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP = "max_color_temp", 5500 CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS = "min_brightness", 1 -CONF_MIN_CT, DEFAULT_MIN_CT = "min_colortemp", 2500 -CONF_ONLY_ONCE = "only_once" +CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP = "min_color_temp", 2500 +CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE = "only_once", False CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1 -CONF_SLEEP_CT, DEFAULT_SLEEP_CT = "sleep_colortemp", 1000 +CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP = "sleep_color_temp", 1000 CONF_SLEEP_ENTITY = "sleep_entity" CONF_SLEEP_STATE = "sleep_state" -CONF_SUNRISE_OFFSET = "sunrise_offset" +CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 CONF_SUNRISE_TIME = "sunrise_time" -CONF_SUNSET_OFFSET = "sunset_offset" +CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 CONF_SUNSET_TIME = "sunset_time" CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index a1f4401b..97960184 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -30,12 +30,12 @@ "initial_transition": "`initial_transition`, the transition of the lights when turning them on or when `disable_state` or `sleep_state` change", "interval": "`interval`", "max_brightness": "`max_brightness`", - "max_colortemp": "`max_colortemp`", + "max_color_temp": "`max_color_temp`", "min_brightness": "`min_brightness`", - "min_colortemp": "`min_colortemp`", + "min_color_temp": "`min_color_temp`", "only_once": "`only_once`", "sleep_brightness": "`sleep_brightness`", - "sleep_colortemp": "`sleep_colortemp`", + "sleep_color_temp": "`sleep_color_temp`", "sleep_entity": "`sleep_entity`", "sleep_state": "`sleep_state`", "sunrise_offset": "`sunrise_offset`", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 6666f662..e461c7c6 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -82,12 +82,12 @@ from .const import ( CONF_INTERVAL, CONF_LIGHTS, CONF_MAX_BRIGHTNESS, - CONF_MAX_CT, + CONF_MAX_COLOR_TEMP, CONF_MIN_BRIGHTNESS, - CONF_MIN_CT, + CONF_MIN_COLOR_TEMP, CONF_ONLY_ONCE, CONF_SLEEP_BRIGHTNESS, - CONF_SLEEP_CT, + CONF_SLEEP_COLOR_TEMP, CONF_SLEEP_ENTITY, CONF_SLEEP_STATE, CONF_SUNRISE_OFFSET, @@ -95,14 +95,19 @@ from .const import ( CONF_SUNSET_OFFSET, CONF_SUNSET_TIME, CONF_TRANSITION, + DEFAULT_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_INITIAL_TRANSITION, DEFAULT_INTERVAL, + DEFAULT_LIGHTS, DEFAULT_MAX_BRIGHTNESS, - DEFAULT_MAX_CT, + DEFAULT_MAX_COLOR_TEMP, DEFAULT_MIN_BRIGHTNESS, - DEFAULT_MIN_CT, + DEFAULT_MIN_COLOR_TEMP, + DEFAULT_ONLY_ONCE, DEFAULT_SLEEP_BRIGHTNESS, - DEFAULT_SLEEP_CT, + DEFAULT_SLEEP_COLOR_TEMP, + DEFAULT_SUNRISE_OFFSET, + DEFAULT_SUNSET_OFFSET, DEFAULT_TRANSITION, DOMAIN, ICON, @@ -126,38 +131,24 @@ PLATFORM_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): DOMAIN, vol.Optional(CONF_NAME, default="Adaptive Lighting"): cv.string, - vol.Optional(CONF_LIGHTS): cv.entity_ids, - vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=False): cv.boolean, + vol.Optional(CONF_LIGHTS, default=DEFAULT_LIGHTS): cv.entity_ids, + vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=DEFAULT_DISABLE_BRIGHTNESS_ADJUST): cv.boolean, vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional( - CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION - ): VALID_TRANSITION, + vol.Optional(CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION): VALID_TRANSITION, vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, - vol.Optional(CONF_MAX_BRIGHTNESS, default=DEFAULT_MAX_BRIGHTNESS): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_MAX_CT, default=DEFAULT_MAX_CT): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_MIN_BRIGHTNESS, default=DEFAULT_MIN_BRIGHTNESS): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_MIN_CT, default=DEFAULT_MIN_CT): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_ONLY_ONCE, default=False): cv.boolean, - vol.Optional(CONF_SLEEP_BRIGHTNESS, default=DEFAULT_SLEEP_BRIGHTNESS): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_SLEEP_CT, default=DEFAULT_SLEEP_CT): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), + vol.Optional(CONF_MAX_BRIGHTNESS, default=DEFAULT_MAX_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional(CONF_MAX_COLOR_TEMP, default=DEFAULT_MAX_COLOR_TEMP): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), + vol.Optional(CONF_MIN_BRIGHTNESS, default=DEFAULT_MIN_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional(CONF_MIN_COLOR_TEMP, default=DEFAULT_MIN_COLOR_TEMP): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), + vol.Optional(CONF_ONLY_ONCE, default=DEFAULT_ONLY_ONCE): cv.boolean, + vol.Optional(CONF_SLEEP_BRIGHTNESS, default=DEFAULT_SLEEP_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional(CONF_SLEEP_COLOR_TEMP, default=DEFAULT_SLEEP_COLOR_TEMP): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_SUNRISE_OFFSET, default=0): cv.time_period, + vol.Optional(CONF_SUNRISE_OFFSET, default=DEFAULT_SUNRISE_OFFSET): cv.time_period, vol.Optional(CONF_SUNRISE_TIME): cv.time, - vol.Optional(CONF_SUNSET_OFFSET, default=0): cv.time_period, + vol.Optional(CONF_SUNSET_OFFSET, default=DEFAULT_SUNSET_OFFSET): cv.time_period, vol.Optional(CONF_SUNSET_TIME): cv.time, vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION, } @@ -169,19 +160,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): switch = AdaptiveSwitch( hass, name=config[CONF_NAME], - lights=config.get(CONF_LIGHTS, []), + lights=config[CONF_LIGHTS], disable_brightness_adjust=config[CONF_DISABLE_BRIGHTNESS_ADJUST], disable_entity=config.get(CONF_DISABLE_ENTITY), disable_state=config.get(CONF_DISABLE_STATE), initial_transition=config[CONF_INITIAL_TRANSITION], interval=config[CONF_INTERVAL], max_brightness=config[CONF_MAX_BRIGHTNESS], - max_color_temp=config[CONF_MAX_CT], + max_color_temp=config[CONF_MAX_COLOR_TEMP], min_brightness=config[CONF_MIN_BRIGHTNESS], - min_color_temp=config[CONF_MIN_CT], + min_color_temp=config[CONF_MIN_COLOR_TEMP], only_once=config[CONF_ONLY_ONCE], sleep_brightness=config[CONF_SLEEP_BRIGHTNESS], - sleep_color_temp=config[CONF_SLEEP_CT], + sleep_color_temp=config[CONF_SLEEP_COLOR_TEMP], sleep_entity=config.get(CONF_SLEEP_ENTITY), sleep_state=config.get(CONF_SLEEP_STATE), sunrise_offset=config[CONF_SUNRISE_OFFSET], @@ -202,28 +193,13 @@ def _difference_between_states(from_state, to_state): if to_state is None: return start + f"from_state: {from_state}, to_state: None" - changed_attrs = ", ".join( - [ - f"{key}: {val}" - for key, val in to_state.attributes.items() - if from_state.attributes.get(key) != val - ] - ) + changed_attrs = ", ".join([f"{key}: {val}" for key, val in to_state.attributes.items() if from_state.attributes.get(key) != val]) if from_state.state == to_state.state: - return start + ( - f"{from_state.entity_id} is still {to_state.state} but" - f" these attributes changes: {changed_attrs}." - ) + return start + (f"{from_state.entity_id} is still {to_state.state} but" f" these attributes changes: {changed_attrs}.") elif changed_attrs != "": - return start + ( - f"{from_state.entity_id} changed from {from_state.state} to" - f" {to_state.state} and these attributes changes: {changed_attrs}." - ) + return start + (f"{from_state.entity_id} changed from {from_state.state} to" f" {to_state.state} and these attributes changes: {changed_attrs}.") else: - return start + ( - f"{from_state.entity_id} changed from {from_state.state} to" - f" {to_state.state} and no attributes changed." - ) + return start + (f"{from_state.entity_id} changed from {from_state.state} to" f" {to_state.state} and no attributes changed.") class AdaptiveSwitch(SwitchEntity, RestoreEntity): @@ -312,9 +288,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): def _supported_features(self, light): state = self.hass.states.get(light) supported_features = state.attributes["supported_features"] - return { - key for key, value in _SUPPORT_OPTS.items() if supported_features & value - } + return {key for key, value in _SUPPORT_OPTS.items() if supported_features & value} def _unpack_light_groups(self, lights): all_lights = [] @@ -336,11 +310,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Call when entity about to be added to hass.""" if self._lights: async_track_state_change( - self.hass, - self._unpack_light_groups(self._lights), - self._light_state_changed, - to_state="on", - from_state="off", + self.hass, self._unpack_light_groups(self._lights), self._light_state_changed, to_state="on", from_state="off", ) track_kwargs = dict(hass=self.hass, action=self._state_changed) if self._sleep_entity is not None: @@ -350,9 +320,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self._disable_entity is not None: disable_kwargs = dict(track_kwargs, entity_ids=self._disable_entity) - async_track_state_change( - **disable_kwargs, from_state=self._disable_state - ) + async_track_state_change(**disable_kwargs, from_state=self._disable_state) async_track_state_change(**disable_kwargs, to_state=self._disable_state) last_state = await self.async_get_last_state() @@ -383,9 +351,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def async_turn_on(self, **kwargs): """Turn on adaptive lighting.""" await self._update_lights(transition=self._initial_transition, force=True) - self.unsub_tracker = async_track_time_interval( - self.hass, self._async_update_at_interval, self._interval - ) + self.unsub_tracker = async_track_time_interval(self.hass, self._async_update_at_interval, self._interval) async def async_turn_off(self, **kwargs): """Turn off adaptive lighting.""" @@ -399,9 +365,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._percent = self._calc_percent() self._brightness = self._calc_brightness() self._color_temp_kelvin = self._calc_color_temp_kelvin() - self._color_temp_mired = color_temperature_kelvin_to_mired( - self._color_temp_kelvin - ) + self._color_temp_mired = color_temperature_kelvin_to_mired(self._color_temp_kelvin) self._rgb_color = color_temperature_to_rgb(self._color_temp_kelvin) self._xy_color = color_RGB_to_xy(*self._rgb_color) self._hs_color = color_xy_to_hs(*self._xy_color) @@ -420,24 +384,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): def _get_sun_events(self, date): def _replace_time(date, key): other_date = getattr(self, f"_{key}_time") - return date.replace( - hour=other_date.hour, - minute=other_date.minute, - second=other_date.second, - microsecond=other_date.microsecond, - ) + return date.replace(hour=other_date.hour, minute=other_date.minute, second=other_date.second, microsecond=other_date.microsecond,) location = get_astral_location(self.hass) - sunrise = ( - location.sunrise(date, local=False) - if self._sunrise_time is None - else _replace_time(date, "sunrise") - ) + self._sunrise_offset - sunset = ( - location.sunset(date, local=False) - if self._sunset_time is None - else _replace_time(date, "sunset") - ) + self._sunset_offset + sunrise = (location.sunrise(date, local=False) if self._sunrise_time is None else _replace_time(date, "sunrise")) + self._sunrise_offset + sunset = (location.sunset(date, local=False) if self._sunset_time is None else _replace_time(date, "sunset")) + self._sunset_offset if self._sunrise_time is None and self._sunset_time is None: solar_noon = location.solar_noon(date, local=False) @@ -462,10 +413,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # It's before sunrise (after midnight), because it's before # sunrise (and after midnight) sunset must have happend yesterday. yesterday = self._get_sun_events(now - timedelta(days=1)) - if ( - today[SUN_EVENT_MIDNIGHT] > today[SUN_EVENT_SUNSET] - and yesterday[SUN_EVENT_MIDNIGHT] > yesterday[SUN_EVENT_SUNSET] - ): + if today[SUN_EVENT_MIDNIGHT] > today[SUN_EVENT_SUNSET] and yesterday[SUN_EVENT_MIDNIGHT] > yesterday[SUN_EVENT_SUNSET]: # Solar midnight is after sunset so use yesterdays's time today[SUN_EVENT_MIDNIGHT] = yesterday[SUN_EVENT_MIDNIGHT] today[SUN_EVENT_SUNSET] = yesterday[SUN_EVENT_SUNSET] @@ -473,10 +421,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # It's after sunset (before midnight), because it's after sunset # (and before midnight) sunrise should happen tomorrow. tomorrow = self._get_sun_events(now + timedelta(days=1)) - if ( - today[SUN_EVENT_MIDNIGHT] < today[SUN_EVENT_SUNRISE] - and tomorrow[SUN_EVENT_MIDNIGHT] < tomorrow[SUN_EVENT_SUNRISE] - ): + if today[SUN_EVENT_MIDNIGHT] < today[SUN_EVENT_SUNRISE] and tomorrow[SUN_EVENT_MIDNIGHT] < tomorrow[SUN_EVENT_SUNRISE]: # Solar midnight is before sunrise so use tomorrow's time today[SUN_EVENT_MIDNIGHT] = tomorrow[SUN_EVENT_MIDNIGHT] today[SUN_EVENT_SUNRISE] = tomorrow[SUN_EVENT_SUNRISE] @@ -492,22 +437,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): h = today[SUN_EVENT_NOON] k = 1 # parabola before solar_noon else after solar_noon - x = ( - today[SUN_EVENT_SUNRISE] - if now_ts < today[SUN_EVENT_NOON] - else today[SUN_EVENT_SUNSET] - ) + x = today[SUN_EVENT_SUNRISE] if now_ts < today[SUN_EVENT_NOON] else today[SUN_EVENT_SUNSET] # sunset -> sunrise parabola elif today[SUN_EVENT_SUNSET] < now_ts < today[SUN_EVENT_SUNRISE]: h = today[SUN_EVENT_MIDNIGHT] k = -1 # parabola before solar_midnight else after solar_midnight - x = ( - today[SUN_EVENT_SUNSET] - if now_ts < today[SUN_EVENT_MIDNIGHT] - else today[SUN_EVENT_SUNRISE] - ) + x = today[SUN_EVENT_SUNSET] if now_ts < today[SUN_EVENT_MIDNIGHT] else today[SUN_EVENT_SUNRISE] y = 0 a = (y - k) / (h - x) ** 2 @@ -515,10 +452,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return percentage def _is_sleep(self): - return ( - self._sleep_entity is not None - and self.hass.states.get(self._sleep_entity).state in self._sleep_state - ) + return self._sleep_entity is not None and self.hass.states.get(self._sleep_entity).state in self._sleep_state def _calc_color_temp_kelvin(self): if self._is_sleep(): @@ -540,10 +474,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return (delta_brightness * percent) + self._min_brightness def _is_disabled(self): - return ( - self._disable_entity is not None - and self.hass.states.get(self._disable_entity).state in self._disable_state - ) + return self._disable_entity is not None and self.hass.states.get(self._disable_entity).state in self._disable_state async def _adjust_light(self, light, transition): service_data = {ATTR_ENTITY_ID: light} @@ -563,12 +494,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_COLOR_TEMP] = self._color_temp_mired _LOGGER.debug( - "Scheduling 'light.turn_on' with the following 'service_data': %s", - service_data, - ) - return self.hass.services.async_call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data + "Scheduling 'light.turn_on' with the following 'service_data': %s", service_data, ) + return self.hass.services.async_call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) def _should_adjust(self): if not self._lights or not self.is_on or self._is_disabled(): @@ -578,20 +506,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _adjust_lights(self, lights, transition): if not self._should_adjust(): return - tasks = [ - await self._adjust_light(light, transition) - for light in lights - if is_on(self.hass, light) - ] + tasks = [await self._adjust_light(light, transition) for light in lights if is_on(self.hass, light)] if tasks: await asyncio.wait(tasks) async def _light_state_changed(self, entity_id, from_state, to_state): assert to_state.state == "on" and from_state.state == "off" _LOGGER.debug(_difference_between_states(from_state, to_state)) - await self._update_lights( - lights=[entity_id], transition=self._initial_transition, force=True - ) + await self._update_lights(lights=[entity_id], transition=self._initial_transition, force=True) async def _state_changed(self, entity_id, from_state, to_state): _LOGGER.debug(_difference_between_states(from_state, to_state)) diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index a1f4401b..97960184 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -30,12 +30,12 @@ "initial_transition": "`initial_transition`, the transition of the lights when turning them on or when `disable_state` or `sleep_state` change", "interval": "`interval`", "max_brightness": "`max_brightness`", - "max_colortemp": "`max_colortemp`", + "max_color_temp": "`max_color_temp`", "min_brightness": "`min_brightness`", - "min_colortemp": "`min_colortemp`", + "min_color_temp": "`min_color_temp`", "only_once": "`only_once`", "sleep_brightness": "`sleep_brightness`", - "sleep_colortemp": "`sleep_colortemp`", + "sleep_color_temp": "`sleep_color_temp`", "sleep_entity": "`sleep_entity`", "sleep_state": "`sleep_state`", "sunrise_offset": "`sunrise_offset`", From 954e022860fc484643e2b8ffc06b91d87c5d0ca2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 16 Sep 2020 00:00:54 +0200 Subject: [PATCH 0148/1077] unify config options flow and YAML schema --- .../adaptive_lighting/config_flow.py | 97 ++--------- custom_components/adaptive_lighting/const.py | 66 ++++++++ custom_components/adaptive_lighting/switch.py | 150 +++++++++++------- 3 files changed, 168 insertions(+), 145 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 089d625a..05b81498 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -1,49 +1,13 @@ """Config flow for Coronavirus integration.""" import logging -import homeassistant.helpers.config_validation as cv import voluptuous as vol + +import homeassistant.helpers.config_validation as cv from homeassistant import config_entries -from homeassistant.components.light import VALID_TRANSITION from homeassistant.core import callback -from .const import ( - CONF_DISABLE_BRIGHTNESS_ADJUST, - CONF_DISABLE_ENTITY, - CONF_DISABLE_STATE, - CONF_INITIAL_TRANSITION, - CONF_INTERVAL, - CONF_LIGHTS, - CONF_MAX_BRIGHTNESS, - CONF_MAX_COLOR_TEMP, - CONF_MIN_BRIGHTNESS, - CONF_MIN_COLOR_TEMP, - CONF_ONLY_ONCE, - CONF_SLEEP_BRIGHTNESS, - CONF_SLEEP_COLOR_TEMP, - CONF_SLEEP_ENTITY, - CONF_SLEEP_STATE, - CONF_SUNRISE_OFFSET, - CONF_SUNRISE_TIME, - CONF_SUNSET_OFFSET, - CONF_SUNSET_TIME, - CONF_TRANSITION, - DEFAULT_DISABLE_BRIGHTNESS_ADJUST, - DEFAULT_INITIAL_TRANSITION, - DEFAULT_INTERVAL, - DEFAULT_LIGHTS, - DEFAULT_MAX_BRIGHTNESS, - DEFAULT_MAX_COLOR_TEMP, - DEFAULT_MIN_BRIGHTNESS, - DEFAULT_MIN_COLOR_TEMP, - DEFAULT_ONLY_ONCE, - DEFAULT_SLEEP_BRIGHTNESS, - DEFAULT_SLEEP_COLOR_TEMP, - DEFAULT_SUNRISE_OFFSET, - DEFAULT_SUNSET_OFFSET, - DEFAULT_TRANSITION, - DOMAIN, -) +from .const import DOMAIN, _convert_to_options_schema _LOGGER = logging.getLogger(__name__) @@ -62,7 +26,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry(title=user_input["name"], data=user_input) - return self.async_show_form(step_id="user", data_schema=vol.Schema({vol.Required("name"): str}), errors=errors,) + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required("name"): str}), + errors=errors, + ) @staticmethod @callback @@ -83,55 +51,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: return self.async_create_entry(title="", data=user_input) - options = self.config_entry.options - - lights = options.get(CONF_LIGHTS, DEFAULT_LIGHTS) - disable_brightness_adjust = options.get(CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST) - disable_entity = options.get(CONF_DISABLE_ENTITY) - disable_state = options.get(CONF_DISABLE_STATE) - initial_transition = options.get(CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION) - interval = options.get(CONF_INTERVAL, DEFAULT_INTERVAL) - max_brightness = options.get(CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS) - max_color_temp = options.get(CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP) - min_brightness = options.get(CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS) - min_color_temp = options.get(CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP) - only_once = options.get(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE) - sleep_brightness = options.get(CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS) - sleep_color_temp = options.get(CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP) - sleep_entity = options.get(CONF_SLEEP_ENTITY) - sleep_state = options.get(CONF_SLEEP_STATE) - sunrise_offset = options.get(CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET) - sunrise_time = options.get(CONF_SUNRISE_TIME) - sunset_offset = options.get(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET) - sunset_time = options.get(CONF_SUNSET_TIME) - transition = options.get(CONF_TRANSITION, DEFAULT_TRANSITION) - - all_lights = self.hass.states.async_entity_ids("light") - all_lights = cv.multi_select(all_lights) - options_schema = vol.Schema( - { - vol.Optional(CONF_LIGHTS, default=lights): all_lights, - vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=disable_brightness_adjust): bool, - vol.Optional(CONF_DISABLE_ENTITY, default=disable_entity): str, - vol.Optional(CONF_DISABLE_STATE, default=disable_state): str, - vol.Optional(CONF_INITIAL_TRANSITION, default=initial_transition): cv.positive_int, - vol.Optional(CONF_INTERVAL, default=interval): cv.positive_int, - vol.Optional(CONF_MAX_BRIGHTNESS, default=max_brightness): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional(CONF_MAX_COLOR_TEMP, default=max_color_temp): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_MIN_BRIGHTNESS, default=min_brightness): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional(CONF_MIN_COLOR_TEMP, default=min_color_temp): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_ONLY_ONCE, default=only_once): bool, - vol.Optional(CONF_SLEEP_BRIGHTNESS, default=sleep_brightness): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional(CONF_SLEEP_COLOR_TEMP, default=sleep_color_temp): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_SLEEP_ENTITY, default=sleep_entity): str, - vol.Optional(CONF_SLEEP_STATE, default=sleep_state): str, - vol.Optional(CONF_SUNRISE_OFFSET, default=sunrise_offset): int, - vol.Optional(CONF_SUNRISE_TIME, default=sunrise_time): str, - vol.Optional(CONF_SUNSET_OFFSET, default=sunset_offset): int, - vol.Optional(CONF_SUNSET_TIME, default=sunset_time): str, - vol.Optional(CONF_TRANSITION, default=transition): VALID_TRANSITION, - } + _convert_to_options_schema(self.hass, self.config_entry.options) ) return self.async_show_form(step_id="init", data_schema=options_schema) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 9075ed06..51a97a0d 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,3 +1,8 @@ +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.light import VALID_TRANSITION + ICON = "mdi:theme-light-dark" DOMAIN = "adaptive_lighting" @@ -27,3 +32,64 @@ CONF_SUNRISE_TIME = "sunrise_time" CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 CONF_SUNSET_TIME = "sunset_time" CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 + + +_COMMON_SCHEMA = { + vol.Optional(CONF_LIGHTS, default=DEFAULT_LIGHTS): cv.entity_ids, + vol.Optional( + CONF_DISABLE_BRIGHTNESS_ADJUST, default=DEFAULT_DISABLE_BRIGHTNESS_ADJUST + ): cv.boolean, + vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, + vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional( + CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION + ): VALID_TRANSITION, + vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, + vol.Optional(CONF_MAX_BRIGHTNESS, default=DEFAULT_MAX_BRIGHTNESS): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_MAX_COLOR_TEMP, default=DEFAULT_MAX_COLOR_TEMP): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_MIN_BRIGHTNESS, default=DEFAULT_MIN_BRIGHTNESS): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_MIN_COLOR_TEMP, default=DEFAULT_MIN_COLOR_TEMP): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_ONLY_ONCE, default=DEFAULT_ONLY_ONCE): cv.boolean, + vol.Optional(CONF_SLEEP_BRIGHTNESS, default=DEFAULT_SLEEP_BRIGHTNESS): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_SLEEP_COLOR_TEMP, default=DEFAULT_SLEEP_COLOR_TEMP): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, + vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_SUNRISE_OFFSET, default=DEFAULT_SUNRISE_OFFSET): cv.time_period, + vol.Optional(CONF_SUNRISE_TIME): cv.time, + vol.Optional(CONF_SUNSET_OFFSET, default=DEFAULT_SUNSET_OFFSET): cv.time_period, + vol.Optional(CONF_SUNSET_TIME): cv.time, + vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION, +} + + +def _convert_to_options_schema(hass, options): + schema = {} + for key, value in _COMMON_SCHEMA.items(): + if key.schema == CONF_LIGHTS: + all_lights = hass.states.async_entity_ids("light") + value = cv.multi_select(all_lights) + elif value == cv.boolean: + value = bool + elif ( + isinstance(value, vol.All) and value.validators[0].type == int + ) or value == VALID_TRANSITION: + pass + elif value == cv.time_period: + value = cv.time_period_dict + else: + value = str + default = options.get(key.schema, value.default()) + schema[vol.Optional(key.schema, default=default)] = value + return vol.Schema(schema) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index e461c7c6..d394199c 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -47,7 +47,6 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, - VALID_TRANSITION, is_on, ) from homeassistant.components.switch import SwitchEntity @@ -75,6 +74,7 @@ from homeassistant.util.color import ( ) from .const import ( + _COMMON_SCHEMA, CONF_DISABLE_BRIGHTNESS_ADJUST, CONF_DISABLE_ENTITY, CONF_DISABLE_STATE, @@ -95,20 +95,6 @@ from .const import ( CONF_SUNSET_OFFSET, CONF_SUNSET_TIME, CONF_TRANSITION, - DEFAULT_DISABLE_BRIGHTNESS_ADJUST, - DEFAULT_INITIAL_TRANSITION, - DEFAULT_INTERVAL, - DEFAULT_LIGHTS, - DEFAULT_MAX_BRIGHTNESS, - DEFAULT_MAX_COLOR_TEMP, - DEFAULT_MIN_BRIGHTNESS, - DEFAULT_MIN_COLOR_TEMP, - DEFAULT_ONLY_ONCE, - DEFAULT_SLEEP_BRIGHTNESS, - DEFAULT_SLEEP_COLOR_TEMP, - DEFAULT_SUNRISE_OFFSET, - DEFAULT_SUNSET_OFFSET, - DEFAULT_TRANSITION, DOMAIN, ICON, SUN_EVENT_MIDNIGHT, @@ -131,26 +117,7 @@ PLATFORM_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): DOMAIN, vol.Optional(CONF_NAME, default="Adaptive Lighting"): cv.string, - vol.Optional(CONF_LIGHTS, default=DEFAULT_LIGHTS): cv.entity_ids, - vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=DEFAULT_DISABLE_BRIGHTNESS_ADJUST): cv.boolean, - vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, - vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION): VALID_TRANSITION, - vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, - vol.Optional(CONF_MAX_BRIGHTNESS, default=DEFAULT_MAX_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional(CONF_MAX_COLOR_TEMP, default=DEFAULT_MAX_COLOR_TEMP): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_MIN_BRIGHTNESS, default=DEFAULT_MIN_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional(CONF_MIN_COLOR_TEMP, default=DEFAULT_MIN_COLOR_TEMP): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_ONLY_ONCE, default=DEFAULT_ONLY_ONCE): cv.boolean, - vol.Optional(CONF_SLEEP_BRIGHTNESS, default=DEFAULT_SLEEP_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional(CONF_SLEEP_COLOR_TEMP, default=DEFAULT_SLEEP_COLOR_TEMP): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, - vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_SUNRISE_OFFSET, default=DEFAULT_SUNRISE_OFFSET): cv.time_period, - vol.Optional(CONF_SUNRISE_TIME): cv.time, - vol.Optional(CONF_SUNSET_OFFSET, default=DEFAULT_SUNSET_OFFSET): cv.time_period, - vol.Optional(CONF_SUNSET_TIME): cv.time, - vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION, + **_COMMON_SCHEMA, } ) @@ -193,13 +160,28 @@ def _difference_between_states(from_state, to_state): if to_state is None: return start + f"from_state: {from_state}, to_state: None" - changed_attrs = ", ".join([f"{key}: {val}" for key, val in to_state.attributes.items() if from_state.attributes.get(key) != val]) + changed_attrs = ", ".join( + [ + f"{key}: {val}" + for key, val in to_state.attributes.items() + if from_state.attributes.get(key) != val + ] + ) if from_state.state == to_state.state: - return start + (f"{from_state.entity_id} is still {to_state.state} but" f" these attributes changes: {changed_attrs}.") + return start + ( + f"{from_state.entity_id} is still {to_state.state} but" + f" these attributes changes: {changed_attrs}." + ) elif changed_attrs != "": - return start + (f"{from_state.entity_id} changed from {from_state.state} to" f" {to_state.state} and these attributes changes: {changed_attrs}.") + return start + ( + f"{from_state.entity_id} changed from {from_state.state} to" + f" {to_state.state} and these attributes changes: {changed_attrs}." + ) else: - return start + (f"{from_state.entity_id} changed from {from_state.state} to" f" {to_state.state} and no attributes changed.") + return start + ( + f"{from_state.entity_id} changed from {from_state.state} to" + f" {to_state.state} and no attributes changed." + ) class AdaptiveSwitch(SwitchEntity, RestoreEntity): @@ -288,7 +270,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): def _supported_features(self, light): state = self.hass.states.get(light) supported_features = state.attributes["supported_features"] - return {key for key, value in _SUPPORT_OPTS.items() if supported_features & value} + return { + key for key, value in _SUPPORT_OPTS.items() if supported_features & value + } def _unpack_light_groups(self, lights): all_lights = [] @@ -310,7 +294,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Call when entity about to be added to hass.""" if self._lights: async_track_state_change( - self.hass, self._unpack_light_groups(self._lights), self._light_state_changed, to_state="on", from_state="off", + self.hass, + self._unpack_light_groups(self._lights), + self._light_state_changed, + to_state="on", + from_state="off", ) track_kwargs = dict(hass=self.hass, action=self._state_changed) if self._sleep_entity is not None: @@ -320,7 +308,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self._disable_entity is not None: disable_kwargs = dict(track_kwargs, entity_ids=self._disable_entity) - async_track_state_change(**disable_kwargs, from_state=self._disable_state) + async_track_state_change( + **disable_kwargs, from_state=self._disable_state + ) async_track_state_change(**disable_kwargs, to_state=self._disable_state) last_state = await self.async_get_last_state() @@ -351,7 +341,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def async_turn_on(self, **kwargs): """Turn on adaptive lighting.""" await self._update_lights(transition=self._initial_transition, force=True) - self.unsub_tracker = async_track_time_interval(self.hass, self._async_update_at_interval, self._interval) + self.unsub_tracker = async_track_time_interval( + self.hass, self._async_update_at_interval, self._interval + ) async def async_turn_off(self, **kwargs): """Turn off adaptive lighting.""" @@ -365,7 +357,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._percent = self._calc_percent() self._brightness = self._calc_brightness() self._color_temp_kelvin = self._calc_color_temp_kelvin() - self._color_temp_mired = color_temperature_kelvin_to_mired(self._color_temp_kelvin) + self._color_temp_mired = color_temperature_kelvin_to_mired( + self._color_temp_kelvin + ) self._rgb_color = color_temperature_to_rgb(self._color_temp_kelvin) self._xy_color = color_RGB_to_xy(*self._rgb_color) self._hs_color = color_xy_to_hs(*self._xy_color) @@ -384,11 +378,24 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): def _get_sun_events(self, date): def _replace_time(date, key): other_date = getattr(self, f"_{key}_time") - return date.replace(hour=other_date.hour, minute=other_date.minute, second=other_date.second, microsecond=other_date.microsecond,) + return date.replace( + hour=other_date.hour, + minute=other_date.minute, + second=other_date.second, + microsecond=other_date.microsecond, + ) location = get_astral_location(self.hass) - sunrise = (location.sunrise(date, local=False) if self._sunrise_time is None else _replace_time(date, "sunrise")) + self._sunrise_offset - sunset = (location.sunset(date, local=False) if self._sunset_time is None else _replace_time(date, "sunset")) + self._sunset_offset + sunrise = ( + location.sunrise(date, local=False) + if self._sunrise_time is None + else _replace_time(date, "sunrise") + ) + self._sunrise_offset + sunset = ( + location.sunset(date, local=False) + if self._sunset_time is None + else _replace_time(date, "sunset") + ) + self._sunset_offset if self._sunrise_time is None and self._sunset_time is None: solar_noon = location.solar_noon(date, local=False) @@ -413,7 +420,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # It's before sunrise (after midnight), because it's before # sunrise (and after midnight) sunset must have happend yesterday. yesterday = self._get_sun_events(now - timedelta(days=1)) - if today[SUN_EVENT_MIDNIGHT] > today[SUN_EVENT_SUNSET] and yesterday[SUN_EVENT_MIDNIGHT] > yesterday[SUN_EVENT_SUNSET]: + if ( + today[SUN_EVENT_MIDNIGHT] > today[SUN_EVENT_SUNSET] + and yesterday[SUN_EVENT_MIDNIGHT] > yesterday[SUN_EVENT_SUNSET] + ): # Solar midnight is after sunset so use yesterdays's time today[SUN_EVENT_MIDNIGHT] = yesterday[SUN_EVENT_MIDNIGHT] today[SUN_EVENT_SUNSET] = yesterday[SUN_EVENT_SUNSET] @@ -421,7 +431,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # It's after sunset (before midnight), because it's after sunset # (and before midnight) sunrise should happen tomorrow. tomorrow = self._get_sun_events(now + timedelta(days=1)) - if today[SUN_EVENT_MIDNIGHT] < today[SUN_EVENT_SUNRISE] and tomorrow[SUN_EVENT_MIDNIGHT] < tomorrow[SUN_EVENT_SUNRISE]: + if ( + today[SUN_EVENT_MIDNIGHT] < today[SUN_EVENT_SUNRISE] + and tomorrow[SUN_EVENT_MIDNIGHT] < tomorrow[SUN_EVENT_SUNRISE] + ): # Solar midnight is before sunrise so use tomorrow's time today[SUN_EVENT_MIDNIGHT] = tomorrow[SUN_EVENT_MIDNIGHT] today[SUN_EVENT_SUNRISE] = tomorrow[SUN_EVENT_SUNRISE] @@ -437,14 +450,22 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): h = today[SUN_EVENT_NOON] k = 1 # parabola before solar_noon else after solar_noon - x = today[SUN_EVENT_SUNRISE] if now_ts < today[SUN_EVENT_NOON] else today[SUN_EVENT_SUNSET] + x = ( + today[SUN_EVENT_SUNRISE] + if now_ts < today[SUN_EVENT_NOON] + else today[SUN_EVENT_SUNSET] + ) # sunset -> sunrise parabola elif today[SUN_EVENT_SUNSET] < now_ts < today[SUN_EVENT_SUNRISE]: h = today[SUN_EVENT_MIDNIGHT] k = -1 # parabola before solar_midnight else after solar_midnight - x = today[SUN_EVENT_SUNSET] if now_ts < today[SUN_EVENT_MIDNIGHT] else today[SUN_EVENT_SUNRISE] + x = ( + today[SUN_EVENT_SUNSET] + if now_ts < today[SUN_EVENT_MIDNIGHT] + else today[SUN_EVENT_SUNRISE] + ) y = 0 a = (y - k) / (h - x) ** 2 @@ -452,7 +473,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return percentage def _is_sleep(self): - return self._sleep_entity is not None and self.hass.states.get(self._sleep_entity).state in self._sleep_state + return ( + self._sleep_entity is not None + and self.hass.states.get(self._sleep_entity).state in self._sleep_state + ) def _calc_color_temp_kelvin(self): if self._is_sleep(): @@ -474,7 +498,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return (delta_brightness * percent) + self._min_brightness def _is_disabled(self): - return self._disable_entity is not None and self.hass.states.get(self._disable_entity).state in self._disable_state + return ( + self._disable_entity is not None + and self.hass.states.get(self._disable_entity).state in self._disable_state + ) async def _adjust_light(self, light, transition): service_data = {ATTR_ENTITY_ID: light} @@ -494,9 +521,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_COLOR_TEMP] = self._color_temp_mired _LOGGER.debug( - "Scheduling 'light.turn_on' with the following 'service_data': %s", service_data, + "Scheduling 'light.turn_on' with the following 'service_data': %s", + service_data, + ) + return self.hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data ) - return self.hass.services.async_call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) def _should_adjust(self): if not self._lights or not self.is_on or self._is_disabled(): @@ -506,14 +536,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _adjust_lights(self, lights, transition): if not self._should_adjust(): return - tasks = [await self._adjust_light(light, transition) for light in lights if is_on(self.hass, light)] + tasks = [ + await self._adjust_light(light, transition) + for light in lights + if is_on(self.hass, light) + ] if tasks: await asyncio.wait(tasks) async def _light_state_changed(self, entity_id, from_state, to_state): assert to_state.state == "on" and from_state.state == "off" _LOGGER.debug(_difference_between_states(from_state, to_state)) - await self._update_lights(lights=[entity_id], transition=self._initial_transition, force=True) + await self._update_lights( + lights=[entity_id], transition=self._initial_transition, force=True + ) async def _state_changed(self, entity_id, from_state, to_state): _LOGGER.debug(_difference_between_states(from_state, to_state)) From 909584abbfae6bd473e5b2c7267c198e61be136f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 16 Sep 2020 11:06:15 +0200 Subject: [PATCH 0149/1077] small fixes --- .../adaptive_lighting/config_flow.py | 4 +-- custom_components/adaptive_lighting/const.py | 26 ++++++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 05b81498..af59f121 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -51,8 +51,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: return self.async_create_entry(title="", data=user_input) - options_schema = vol.Schema( - _convert_to_options_schema(self.hass, self.config_entry.options) + options_schema = _convert_to_options_schema( + self.hass, self.config_entry.options ) return self.async_show_form(step_id="init", data_schema=options_schema) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 51a97a0d..5345b777 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -79,17 +79,25 @@ def _convert_to_options_schema(hass, options): for key, value in _COMMON_SCHEMA.items(): if key.schema == CONF_LIGHTS: all_lights = hass.states.async_entity_ids("light") - value = cv.multi_select(all_lights) + to_type = cv.multi_select(all_lights) elif value == cv.boolean: - value = bool + to_type = bool elif ( - isinstance(value, vol.All) and value.validators[0].type == int + isinstance(value, vol.All) + and hasattr(value.validators, "type") + and value.validators[0].type == int ) or value == VALID_TRANSITION: - pass + to_type = value elif value == cv.time_period: - value = cv.time_period_dict + to_type = cv.time_period_dict else: - value = str - default = options.get(key.schema, value.default()) - schema[vol.Optional(key.schema, default=default)] = value - return vol.Schema(schema) + to_type = str + + default = ( + key.default() + if not isinstance(key.default, vol.Undefined) + else vol.UNDEFINED + ) + default = options.get(key.schema, default) + schema[vol.Optional(key.schema, default=default)] = to_type + return vol.Schema(schema) From 9d06be0883799f6961f64a9b4dc6af08742359e6 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 19 Sep 2020 11:10:54 +0200 Subject: [PATCH 0150/1077] WIP --- .../adaptive_lighting/__init__.py | 207 ++++++++++++++- .../adaptive_lighting/config_flow.py | 114 ++++++++- custom_components/adaptive_lighting/const.py | 45 +--- custom_components/adaptive_lighting/switch.py | 239 ++++++++++-------- 4 files changed, 463 insertions(+), 142 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index ca7f15ac..dd3988e9 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -1 +1,206 @@ -"""Adaptive Lighting Component for Home-Assistant.""" +""" +Adaptive Lighting Component for Home-Assistant. + +This component calculates color temperature and brightness to synchronize +your color changing lights with perceived color temperature of the sky throughout +the day. This gives your environment a more natural feel, with cooler whites during +the midday and warmer tints near twilight and dawn. + +In addition, the component sets your lights to a nice warm white at 1% in "Sleep" mode, +which is far brighter than starlight but won't reset your adaptive rhythm or break down +too much rhodopsin in your eyes. + +Human circadian rhythms are heavily influenced by ambient light levels and +hues. Hormone production, brainwave activity, mood and wakefulness are +just some of the cognitive functions tied to cyclical natural light. +http://en.wikipedia.org/wiki/Zeitgeber + +Here's some further reading: + +http://www.cambridgeincolour.com/tutorials/sunrise-sunset-calculator.htm +http://en.wikipedia.org/wiki/Color_temperature + +Technical notes: I had to make a lot of assumptions when writing this app +* There are no considerations for weather or altitude, but does use your + hub's location to calculate the sun position. +* The component doesn't calculate a true "Blue Hour" -- it just sets the + lights to 2700K (warm white) until your hub goes into Night mode +""" +import asyncio +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util +from homeassistant.components.light import ( + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_TEMP, + ATTR_RGB_COLOR, + ATTR_TRANSITION, +) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.light import ( + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_TRANSITION, + VALID_TRANSITION, + is_on, +) +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_NAME, + SERVICE_TURN_ON, + STATE_ON, + SUN_EVENT_SUNRISE, + SUN_EVENT_SUNSET, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import ( + async_track_state_change, + async_track_time_interval, +) +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.sun import get_astral_location +from homeassistant.util import slugify +from homeassistant.util.color import ( + color_RGB_to_xy, + color_temperature_kelvin_to_mired, + color_temperature_to_rgb, + color_xy_to_hs, +) + +from .const import ( + CONF_DISABLE_BRIGHTNESS_ADJUST, + CONF_DISABLE_ENTITY, + CONF_DISABLE_STATE, + CONF_INITIAL_TRANSITION, + CONF_INTERVAL, + CONF_LIGHTS, + CONF_MAX_BRIGHTNESS, + CONF_MAX_COLOR_TEMP, + CONF_MIN_BRIGHTNESS, + CONF_MIN_COLOR_TEMP, + CONF_ONLY_ONCE, + CONF_SLEEP_BRIGHTNESS, + CONF_SLEEP_COLOR_TEMP, + CONF_SLEEP_ENTITY, + CONF_SLEEP_STATE, + CONF_SUNRISE_OFFSET, + CONF_SUNRISE_TIME, + CONF_SUNSET_OFFSET, + CONF_SUNSET_TIME, + CONF_NAME, + DEFAULT_NAME, + CONF_TRANSITION, + DEFAULT_DISABLE_BRIGHTNESS_ADJUST, + DEFAULT_INITIAL_TRANSITION, + DEFAULT_INTERVAL, + DEFAULT_LIGHTS, + DEFAULT_MAX_BRIGHTNESS, + DEFAULT_MAX_COLOR_TEMP, + DEFAULT_MIN_BRIGHTNESS, + DEFAULT_MIN_COLOR_TEMP, + DEFAULT_ONLY_ONCE, + DEFAULT_SLEEP_BRIGHTNESS, + DEFAULT_SLEEP_COLOR_TEMP, + DEFAULT_SUNRISE_OFFSET, + DEFAULT_SUNSET_OFFSET, + DEFAULT_TRANSITION, + DOMAIN, + ICON, + SUN_EVENT_MIDNIGHT, + SUN_EVENT_NOON, +) + + +_SUPPORT_OPTS = { + "brightness": SUPPORT_BRIGHTNESS, + "color_temp": SUPPORT_COLOR_TEMP, + "color": SUPPORT_COLOR, + "transition": SUPPORT_TRANSITION, +} + + +_LOGGER = logging.getLogger(__name__) + + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_LIGHTS, default=DEFAULT_LIGHTS): cv.entity_ids, + vol.Optional( + CONF_DISABLE_BRIGHTNESS_ADJUST, + default=DEFAULT_DISABLE_BRIGHTNESS_ADJUST, + ): cv.boolean, + vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, + vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional( + CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION + ): VALID_TRANSITION, + vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, + vol.Optional( + CONF_MAX_BRIGHTNESS, default=DEFAULT_MAX_BRIGHTNESS + ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional( + CONF_MAX_COLOR_TEMP, default=DEFAULT_MAX_COLOR_TEMP + ): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), + vol.Optional( + CONF_MIN_BRIGHTNESS, default=DEFAULT_MIN_BRIGHTNESS + ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional( + CONF_MIN_COLOR_TEMP, default=DEFAULT_MIN_COLOR_TEMP + ): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), + vol.Optional(CONF_ONLY_ONCE, default=DEFAULT_ONLY_ONCE): cv.boolean, + vol.Optional( + CONF_SLEEP_BRIGHTNESS, default=DEFAULT_SLEEP_BRIGHTNESS + ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional( + CONF_SLEEP_COLOR_TEMP, default=DEFAULT_SLEEP_COLOR_TEMP + ): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), + vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, + vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional( + CONF_SUNRISE_OFFSET, default=DEFAULT_SUNRISE_OFFSET + ): cv.time_period, + vol.Optional(CONF_SUNRISE_TIME): cv.time, + vol.Optional( + CONF_SUNSET_OFFSET, default=DEFAULT_SUNSET_OFFSET + ): cv.time_period, + vol.Optional(CONF_SUNSET_TIME): cv.time, + vol.Optional( + CONF_TRANSITION, default=DEFAULT_TRANSITION + ): VALID_TRANSITION, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Import integration from config.""" + + if DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + ) + ) + return True + + +async def async_setup_entry(hass, config_entry): + """Set up the component.""" + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "switch") + ) + + return True diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index af59f121..5785c0b8 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -5,9 +5,46 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant import config_entries +from homeassistant.components.light import VALID_TRANSITION from homeassistant.core import callback -from .const import DOMAIN, _convert_to_options_schema +from .const import ( # _convert_to_options_schema, + CONF_DISABLE_BRIGHTNESS_ADJUST, + CONF_DISABLE_ENTITY, + CONF_DISABLE_STATE, + CONF_INITIAL_TRANSITION, + CONF_INTERVAL, + CONF_LIGHTS, + CONF_MAX_BRIGHTNESS, + CONF_MAX_COLOR_TEMP, + CONF_MIN_BRIGHTNESS, + CONF_MIN_COLOR_TEMP, + CONF_ONLY_ONCE, + CONF_SLEEP_BRIGHTNESS, + CONF_SLEEP_COLOR_TEMP, + CONF_SLEEP_ENTITY, + CONF_SLEEP_STATE, + CONF_SUNRISE_OFFSET, + CONF_SUNRISE_TIME, + CONF_SUNSET_OFFSET, + CONF_SUNSET_TIME, + CONF_TRANSITION, + DEFAULT_DISABLE_BRIGHTNESS_ADJUST, + DEFAULT_INITIAL_TRANSITION, + DEFAULT_INTERVAL, + DEFAULT_LIGHTS, + DEFAULT_MAX_BRIGHTNESS, + DEFAULT_MAX_COLOR_TEMP, + DEFAULT_MIN_BRIGHTNESS, + DEFAULT_MIN_COLOR_TEMP, + DEFAULT_ONLY_ONCE, + DEFAULT_SLEEP_BRIGHTNESS, + DEFAULT_SLEEP_COLOR_TEMP, + DEFAULT_SUNRISE_OFFSET, + DEFAULT_SUNSET_OFFSET, + DEFAULT_TRANSITION, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) @@ -51,8 +88,79 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: return self.async_create_entry(title="", data=user_input) - options_schema = _convert_to_options_schema( - self.hass, self.config_entry.options + options = self.config_entry.options + + lights = options.get(CONF_LIGHTS, DEFAULT_LIGHTS) + disable_brightness_adjust = options.get( + CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST + ) + disable_entity = options.get(CONF_DISABLE_ENTITY) + disable_state = options.get(CONF_DISABLE_STATE) + initial_transition = options.get( + CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION + ) + interval = options.get(CONF_INTERVAL, DEFAULT_INTERVAL) + max_brightness = options.get(CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS) + max_color_temp = options.get(CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP) + min_brightness = options.get(CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS) + min_color_temp = options.get(CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP) + only_once = options.get(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE) + sleep_brightness = options.get(CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS) + sleep_color_temp = options.get(CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP) + sleep_entity = options.get(CONF_SLEEP_ENTITY) + sleep_state = options.get(CONF_SLEEP_STATE) + sunrise_offset = options.get(CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET) + sunrise_time = options.get(CONF_SUNRISE_TIME) + sunset_offset = options.get(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET) + sunset_time = options.get(CONF_SUNSET_TIME) + transition = options.get(CONF_TRANSITION, DEFAULT_TRANSITION) + + all_lights = self.hass.states.async_entity_ids("light") + all_lights = cv.multi_select(all_lights) + + options_schema = vol.Schema( + { + vol.Optional(CONF_LIGHTS, default=lights): all_lights, + # vol.Optional( + # CONF_DISABLE_BRIGHTNESS_ADJUST, default=disable_brightness_adjust + # ): bool, + # vol.Optional(CONF_DISABLE_ENTITY, default=disable_entity): str, + # vol.Optional(CONF_DISABLE_STATE, default=disable_state): str, + # vol.Optional( + # CONF_INITIAL_TRANSITION, default=initial_transition + # ): cv.positive_int, + # vol.Optional(CONF_INTERVAL, default=interval): cv.positive_int, + # vol.Optional(CONF_MAX_BRIGHTNESS, default=max_brightness): vol.All( + # vol.Coerce(int), vol.Range(min=1, max=100) + # ), + # vol.Optional(CONF_MAX_COLOR_TEMP, default=max_color_temp): vol.All( + # vol.Coerce(int), vol.Range(min=1000, max=10000) + # ), + # vol.Optional(CONF_MIN_BRIGHTNESS, default=min_brightness): vol.All( + # vol.Coerce(int), vol.Range(min=1, max=100) + # ), + # vol.Optional(CONF_MIN_COLOR_TEMP, default=min_color_temp): vol.All( + # vol.Coerce(int), vol.Range(min=1000, max=10000) + # ), + # vol.Optional(CONF_ONLY_ONCE, default=only_once): bool, + # vol.Optional(CONF_SLEEP_BRIGHTNESS, default=sleep_brightness): vol.All( + # vol.Coerce(int), vol.Range(min=1, max=100) + # ), + # vol.Optional(CONF_SLEEP_COLOR_TEMP, default=sleep_color_temp): vol.All( + # vol.Coerce(int), vol.Range(min=1000, max=10000) + # ), + # vol.Optional(CONF_SLEEP_ENTITY, default=sleep_entity): str, + # vol.Optional(CONF_SLEEP_STATE, default=sleep_state): str, + # vol.Optional(CONF_SUNRISE_OFFSET, default=sunrise_offset): int, + # vol.Optional(CONF_SUNRISE_TIME, default=sunrise_time): str, + # vol.Optional(CONF_SUNSET_OFFSET, default=sunset_offset): int, + # vol.Optional(CONF_SUNSET_TIME, default=sunset_time): str, + # vol.Optional(CONF_TRANSITION, default=transition): VALID_TRANSITION, + } ) + # options_schema = _convert_to_options_schema( + # self.hass, self.config_entry.options + # ) + return self.async_show_form(step_id="init", data_schema=options_schema) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 5345b777..cdb3154d 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -9,6 +9,7 @@ DOMAIN = "adaptive_lighting" SUN_EVENT_NOON = "solar_noon" SUN_EVENT_MIDNIGHT = "solar_midnight" +CONF_NAME, DEFAULT_NAME = "name", "default" CONF_LIGHTS, DEFAULT_LIGHTS = "lights", [] CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST = ( "disable_brightness_adjust", @@ -36,34 +37,18 @@ CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 _COMMON_SCHEMA = { vol.Optional(CONF_LIGHTS, default=DEFAULT_LIGHTS): cv.entity_ids, - vol.Optional( - CONF_DISABLE_BRIGHTNESS_ADJUST, default=DEFAULT_DISABLE_BRIGHTNESS_ADJUST - ): cv.boolean, + vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=DEFAULT_DISABLE_BRIGHTNESS_ADJUST): cv.boolean, vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional( - CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION - ): VALID_TRANSITION, + vol.Optional(CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION): VALID_TRANSITION, vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, - vol.Optional(CONF_MAX_BRIGHTNESS, default=DEFAULT_MAX_BRIGHTNESS): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_MAX_COLOR_TEMP, default=DEFAULT_MAX_COLOR_TEMP): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_MIN_BRIGHTNESS, default=DEFAULT_MIN_BRIGHTNESS): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_MIN_COLOR_TEMP, default=DEFAULT_MIN_COLOR_TEMP): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), + vol.Optional(CONF_MAX_BRIGHTNESS, default=DEFAULT_MAX_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional(CONF_MAX_COLOR_TEMP, default=DEFAULT_MAX_COLOR_TEMP): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), + vol.Optional(CONF_MIN_BRIGHTNESS, default=DEFAULT_MIN_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional(CONF_MIN_COLOR_TEMP, default=DEFAULT_MIN_COLOR_TEMP): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), vol.Optional(CONF_ONLY_ONCE, default=DEFAULT_ONLY_ONCE): cv.boolean, - vol.Optional(CONF_SLEEP_BRIGHTNESS, default=DEFAULT_SLEEP_BRIGHTNESS): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_SLEEP_COLOR_TEMP, default=DEFAULT_SLEEP_COLOR_TEMP): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), + vol.Optional(CONF_SLEEP_BRIGHTNESS, default=DEFAULT_SLEEP_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional(CONF_SLEEP_COLOR_TEMP, default=DEFAULT_SLEEP_COLOR_TEMP): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_SUNRISE_OFFSET, default=DEFAULT_SUNRISE_OFFSET): cv.time_period, @@ -82,22 +67,14 @@ def _convert_to_options_schema(hass, options): to_type = cv.multi_select(all_lights) elif value == cv.boolean: to_type = bool - elif ( - isinstance(value, vol.All) - and hasattr(value.validators, "type") - and value.validators[0].type == int - ) or value == VALID_TRANSITION: + elif (isinstance(value, vol.All) and hasattr(value.validators, "type") and value.validators[0].type == int) or value == VALID_TRANSITION: to_type = value elif value == cv.time_period: to_type = cv.time_period_dict else: to_type = str - default = ( - key.default() - if not isinstance(key.default, vol.Undefined) - else vol.UNDEFINED - ) + default = key.default() if not isinstance(key.default, vol.Undefined) else vol.UNDEFINED default = options.get(key.schema, default) schema[vol.Optional(key.schema, default=default)] = to_type return vol.Schema(schema) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index d394199c..31b07e40 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -28,6 +28,7 @@ Technical notes: I had to make a lot of assumptions when writing this app """ import asyncio +import bisect import logging from datetime import timedelta @@ -35,6 +36,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util +from custom_components import adaptive_lighting from homeassistant.components.light import ( ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, @@ -74,7 +76,6 @@ from homeassistant.util.color import ( ) from .const import ( - _COMMON_SCHEMA, CONF_DISABLE_BRIGHTNESS_ADJUST, CONF_DISABLE_ENTITY, CONF_DISABLE_STATE, @@ -95,6 +96,20 @@ from .const import ( CONF_SUNSET_OFFSET, CONF_SUNSET_TIME, CONF_TRANSITION, + DEFAULT_DISABLE_BRIGHTNESS_ADJUST, + DEFAULT_INITIAL_TRANSITION, + DEFAULT_INTERVAL, + DEFAULT_LIGHTS, + DEFAULT_MAX_BRIGHTNESS, + DEFAULT_MAX_COLOR_TEMP, + DEFAULT_MIN_BRIGHTNESS, + DEFAULT_MIN_COLOR_TEMP, + DEFAULT_ONLY_ONCE, + DEFAULT_SLEEP_BRIGHTNESS, + DEFAULT_SLEEP_COLOR_TEMP, + DEFAULT_SUNRISE_OFFSET, + DEFAULT_SUNSET_OFFSET, + DEFAULT_TRANSITION, DOMAIN, ICON, SUN_EVENT_MIDNIGHT, @@ -113,42 +128,15 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=10) -PLATFORM_SCHEMA = vol.Schema( - { - vol.Required(CONF_PLATFORM): DOMAIN, - vol.Optional(CONF_NAME, default="Adaptive Lighting"): cv.string, - **_COMMON_SCHEMA, - } -) - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Adaptive Lighting switches.""" - switch = AdaptiveSwitch( - hass, - name=config[CONF_NAME], - lights=config[CONF_LIGHTS], - disable_brightness_adjust=config[CONF_DISABLE_BRIGHTNESS_ADJUST], - disable_entity=config.get(CONF_DISABLE_ENTITY), - disable_state=config.get(CONF_DISABLE_STATE), - initial_transition=config[CONF_INITIAL_TRANSITION], - interval=config[CONF_INTERVAL], - max_brightness=config[CONF_MAX_BRIGHTNESS], - max_color_temp=config[CONF_MAX_COLOR_TEMP], - min_brightness=config[CONF_MIN_BRIGHTNESS], - min_color_temp=config[CONF_MIN_COLOR_TEMP], - only_once=config[CONF_ONLY_ONCE], - sleep_brightness=config[CONF_SLEEP_BRIGHTNESS], - sleep_color_temp=config[CONF_SLEEP_COLOR_TEMP], - sleep_entity=config.get(CONF_SLEEP_ENTITY), - sleep_state=config.get(CONF_SLEEP_STATE), - sunrise_offset=config[CONF_SUNRISE_OFFSET], - sunrise_time=config.get(CONF_SUNRISE_TIME), - sunset_offset=config[CONF_SUNSET_OFFSET], - sunset_time=config.get(CONF_SUNSET_TIME), - transition=config[CONF_TRANSITION], - ) - add_devices([switch]) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the AdaptiveLighting switch.""" + name = config_entry.data[CONF_NAME] + switch = AdaptiveSwitch(hass, name, config_entry) + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + hass.data[DOMAIN][name] = switch + async_add_entities([switch]) def _difference_between_states(from_state, to_state): @@ -191,54 +179,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self, hass, name, - lights, - disable_brightness_adjust, - disable_entity, - disable_state, - initial_transition, - interval, - max_brightness, - max_color_temp, - min_brightness, - min_color_temp, - only_once, - sleep_brightness, - sleep_color_temp, - sleep_entity, - sleep_state, - sunrise_offset, - sunrise_time, - sunset_offset, - sunset_time, - transition, + config_entry, ): """Initialize the Adaptive Lighting switch.""" self.hass = hass self._name = name - self._entity_id = f"switch.adaptive_lighting_{slugify(name)}" + self._entity_id = f"switch.{DOMAIN}_{slugify(name)}" self._icon = ICON - - # Set attributes from arguments - self._lights = lights - self._disable_brightness_adjust = disable_brightness_adjust - self._disable_entity = disable_entity - self._disable_state = disable_state - self._initial_transition = initial_transition - self._interval = interval - self._max_brightness = max_brightness - self._max_color_temp = max_color_temp - self._min_brightness = min_brightness - self._min_color_temp = min_color_temp - self._only_once = only_once - self._sleep_brightness = sleep_brightness - self._sleep_color_temp = sleep_color_temp - self._sleep_entity = sleep_entity - self._sleep_state = sleep_state - self._sunrise_offset = sunrise_offset - self._sunrise_time = sunrise_time - self._sunset_offset = sunset_offset - self._sunset_time = sunset_time - self._transition = transition + self.config_entry = config_entry # Initialize attributes that will be set in self._update_attrs self._percent = None @@ -252,6 +200,104 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set and unset tracker in async_turn_on and async_turn_off self.unsub_tracker = None + @property + def _lights(self): + return self.config_entry.options.get(CONF_LIGHTS, DEFAULT_LIGHTS) + + @property + def _disable_brightness_adjust(self): + return self.config_entry.options.get( + CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST + ) + + @property + def _disable_entity(self): + return self.config_entry.options.get(CONF_DISABLE_ENTITY) + + @property + def _disable_state(self): + return self.config_entry.options.get(CONF_DISABLE_STATE) + + @property + def _initial_transition(self): + return self.config_entry.options.get( + CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION + ) + + @property + def _interval(self): + return self.config_entry.options.get(CONF_INTERVAL, DEFAULT_INTERVAL) + + @property + def _max_brightness(self): + return self.config_entry.options.get( + CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS + ) + + @property + def _max_color_temp(self): + return self.config_entry.options.get( + CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP + ) + + @property + def _min_brightness(self): + return self.config_entry.options.get( + CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS + ) + + @property + def _min_color_temp(self): + return self.config_entry.options.get( + CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP + ) + + @property + def _only_once(self): + return self.config_entry.options.get(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE) + + @property + def _sleep_brightness(self): + return self.config_entry.options.get( + CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS + ) + + @property + def _sleep_color_temp(self): + return self.config_entry.options.get( + CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP + ) + + @property + def _sleep_entity(self): + return self.config_entry.options.get(CONF_SLEEP_ENTITY) + + @property + def _sleep_state(self): + return self.config_entry.options.get(CONF_SLEEP_STATE) + + @property + def _sunrise_offset(self): + return self.config_entry.options.get( + CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET + ) + + @property + def _sunrise_time(self): + return self.config_entry.options.get(CONF_SUNRISE_TIME) + + @property + def _sunset_offset(self): + return self.config_entry.options.get(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET) + + @property + def _sunset_time(self): + return self.config_entry.options.get(CONF_SUNSET_TIME) + + @property + def _transition(self): + return self.config_entry.options.get(CONF_TRANSITION, DEFAULT_TRANSITION) + @property def entity_id(self): """Return the entity ID of the switch.""" @@ -411,34 +457,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): SUN_EVENT_MIDNIGHT: solar_midnight.timestamp(), } + def _relevant_events(self, now): + events = [] + for days in [-1, 0, 1]: + sun_events = self._get_sun_events(now + timedelta(days=days)) + events.extend(list(sun_events.items())) + events = sorted(events, key=lambda x: x[1]) + i_now = bisect.bisect([ts for _, ts in events], now.timestamp()) + return dict(events[i_now - 2: i_now + 2]) + def _calc_percent(self): now = dt_util.utcnow() now_ts = now.timestamp() - - today = self._get_sun_events(now) - if now_ts < today[SUN_EVENT_SUNRISE]: - # It's before sunrise (after midnight), because it's before - # sunrise (and after midnight) sunset must have happend yesterday. - yesterday = self._get_sun_events(now - timedelta(days=1)) - if ( - today[SUN_EVENT_MIDNIGHT] > today[SUN_EVENT_SUNSET] - and yesterday[SUN_EVENT_MIDNIGHT] > yesterday[SUN_EVENT_SUNSET] - ): - # Solar midnight is after sunset so use yesterdays's time - today[SUN_EVENT_MIDNIGHT] = yesterday[SUN_EVENT_MIDNIGHT] - today[SUN_EVENT_SUNSET] = yesterday[SUN_EVENT_SUNSET] - elif now_ts > today[SUN_EVENT_SUNSET]: - # It's after sunset (before midnight), because it's after sunset - # (and before midnight) sunrise should happen tomorrow. - tomorrow = self._get_sun_events(now + timedelta(days=1)) - if ( - today[SUN_EVENT_MIDNIGHT] < today[SUN_EVENT_SUNRISE] - and tomorrow[SUN_EVENT_MIDNIGHT] < tomorrow[SUN_EVENT_SUNRISE] - ): - # Solar midnight is before sunrise so use tomorrow's time - today[SUN_EVENT_MIDNIGHT] = tomorrow[SUN_EVENT_MIDNIGHT] - today[SUN_EVENT_SUNRISE] = tomorrow[SUN_EVENT_SUNRISE] - + today = self._relevant_events(now) # Figure out where we are in time so we know which half of the # parabola to calculate. We're generating a different # sunset-sunrise parabola for before and after solar midnight. From aea2e83a2090a2c83cf0e487db670c0992264f0a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 19 Sep 2020 11:35:28 +0200 Subject: [PATCH 0151/1077] cleanup unused imports --- .../adaptive_lighting/__init__.py | 62 ++----------------- custom_components/adaptive_lighting/switch.py | 34 +--------- 2 files changed, 6 insertions(+), 90 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index dd3988e9..c4085dd9 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -26,53 +26,14 @@ Technical notes: I had to make a lot of assumptions when writing this app * The component doesn't calculate a true "Blue Hour" -- it just sets the lights to 2700K (warm white) until your hub goes into Night mode """ -import asyncio -import logging -from datetime import timedelta -import voluptuous as vol +import logging import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util -from homeassistant.components.light import ( - ATTR_BRIGHTNESS_PCT, - ATTR_COLOR_TEMP, - ATTR_RGB_COLOR, - ATTR_TRANSITION, -) -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.light import ( - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, - SUPPORT_TRANSITION, - VALID_TRANSITION, - is_on, -) -from homeassistant.components.switch import SwitchEntity +import voluptuous as vol +from homeassistant.components.light import VALID_TRANSITION from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_NAME, - SERVICE_TURN_ON, - STATE_ON, - SUN_EVENT_SUNRISE, - SUN_EVENT_SUNSET, -) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import ( - async_track_state_change, - async_track_time_interval, -) -from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.sun import get_astral_location -from homeassistant.util import slugify -from homeassistant.util.color import ( - color_RGB_to_xy, - color_temperature_kelvin_to_mired, - color_temperature_to_rgb, - color_xy_to_hs, -) +from homeassistant.const import CONF_NAME from .const import ( CONF_DISABLE_BRIGHTNESS_ADJUST, @@ -94,8 +55,6 @@ from .const import ( CONF_SUNRISE_TIME, CONF_SUNSET_OFFSET, CONF_SUNSET_TIME, - CONF_NAME, - DEFAULT_NAME, CONF_TRANSITION, DEFAULT_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_INITIAL_TRANSITION, @@ -105,6 +64,7 @@ from .const import ( DEFAULT_MAX_COLOR_TEMP, DEFAULT_MIN_BRIGHTNESS, DEFAULT_MIN_COLOR_TEMP, + DEFAULT_NAME, DEFAULT_ONLY_ONCE, DEFAULT_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_COLOR_TEMP, @@ -112,20 +72,8 @@ from .const import ( DEFAULT_SUNSET_OFFSET, DEFAULT_TRANSITION, DOMAIN, - ICON, - SUN_EVENT_MIDNIGHT, - SUN_EVENT_NOON, ) - -_SUPPORT_OPTS = { - "brightness": SUPPORT_BRIGHTNESS, - "color_temp": SUPPORT_COLOR_TEMP, - "color": SUPPORT_COLOR, - "transition": SUPPORT_TRANSITION, -} - - _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 31b07e40..aedb91f0 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1,42 +1,11 @@ -""" -Adaptive Lighting Component for Home-Assistant. - -This component calculates color temperature and brightness to synchronize -your color changing lights with perceived color temperature of the sky throughout -the day. This gives your environment a more natural feel, with cooler whites during -the midday and warmer tints near twilight and dawn. - -In addition, the component sets your lights to a nice warm white at 1% in "Sleep" mode, -which is far brighter than starlight but won't reset your adaptive rhythm or break down -too much rhodopsin in your eyes. - -Human circadian rhythms are heavily influenced by ambient light levels and -hues. Hormone production, brainwave activity, mood and wakefulness are -just some of the cognitive functions tied to cyclical natural light. -http://en.wikipedia.org/wiki/Zeitgeber - -Here's some further reading: - -http://www.cambridgeincolour.com/tutorials/sunrise-sunset-calculator.htm -http://en.wikipedia.org/wiki/Color_temperature - -Technical notes: I had to make a lot of assumptions when writing this app -* There are no considerations for weather or altitude, but does use your - hub's location to calculate the sun position. -* The component doesn't calculate a true "Blue Hour" -- it just sets the - lights to 2700K (warm white) until your hub goes into Night mode -""" +"""Adaptive Lighting Component for Home-Assistant.""" import asyncio import bisect import logging from datetime import timedelta -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -from custom_components import adaptive_lighting from homeassistant.components.light import ( ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, @@ -55,7 +24,6 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( ATTR_ENTITY_ID, CONF_NAME, - CONF_PLATFORM, SERVICE_TURN_ON, STATE_ON, SUN_EVENT_SUNRISE, From 9593bca7a81678120c8f77d83eb12ebda32feafa Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 19 Sep 2020 12:18:50 +0200 Subject: [PATCH 0152/1077] simplify _calc_percent --- custom_components/adaptive_lighting/switch.py | 66 ++++++------------- 1 file changed, 20 insertions(+), 46 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index aedb91f0..7fb62544 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -144,10 +144,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" def __init__( - self, - hass, - name, - config_entry, + self, hass, name, config_entry, ): """Initialize the Adaptive Lighting switch.""" self.hass = hass @@ -418,57 +415,34 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): solar_noon = sunrise + (sunset - sunrise) / 2 solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset) / 2 - return { - SUN_EVENT_SUNRISE: sunrise.timestamp(), - SUN_EVENT_SUNSET: sunset.timestamp(), - SUN_EVENT_NOON: solar_noon.timestamp(), - SUN_EVENT_MIDNIGHT: solar_midnight.timestamp(), - } + return [ + (SUN_EVENT_SUNRISE, sunrise.timestamp()), + (SUN_EVENT_SUNSET, sunset.timestamp()), + (SUN_EVENT_NOON, solar_noon.timestamp()), + (SUN_EVENT_MIDNIGHT, solar_midnight.timestamp()), + ] def _relevant_events(self, now): - events = [] - for days in [-1, 0, 1]: - sun_events = self._get_sun_events(now + timedelta(days=days)) - events.extend(list(sun_events.items())) + events = [ + self._get_sun_events(now + timedelta(days=days)) for days in [-1, 0, 1] + ] + events = sum(events, []) # flatten lists events = sorted(events, key=lambda x: x[1]) i_now = bisect.bisect([ts for _, ts in events], now.timestamp()) - return dict(events[i_now - 2: i_now + 2]) + return events[i_now - 1 : i_now + 1] def _calc_percent(self): now = dt_util.utcnow() now_ts = now.timestamp() today = self._relevant_events(now) - # Figure out where we are in time so we know which half of the - # parabola to calculate. We're generating a different - # sunset-sunrise parabola for before and after solar midnight. - # because it might not be half way between sunrise and sunset. - # We're also generating a different parabola for sunrise-sunset. - - # sunrise -> sunset parabola - if today[SUN_EVENT_SUNRISE] < now_ts < today[SUN_EVENT_SUNSET]: - h = today[SUN_EVENT_NOON] - k = 1 - # parabola before solar_noon else after solar_noon - x = ( - today[SUN_EVENT_SUNRISE] - if now_ts < today[SUN_EVENT_NOON] - else today[SUN_EVENT_SUNSET] - ) - - # sunset -> sunrise parabola - elif today[SUN_EVENT_SUNSET] < now_ts < today[SUN_EVENT_SUNRISE]: - h = today[SUN_EVENT_MIDNIGHT] - k = -1 - # parabola before solar_midnight else after solar_midnight - x = ( - today[SUN_EVENT_SUNSET] - if now_ts < today[SUN_EVENT_MIDNIGHT] - else today[SUN_EVENT_SUNRISE] - ) - - y = 0 - a = (y - k) / (h - x) ** 2 - percentage = a * (now_ts - h) ** 2 + k + (prev_event, prev_ts), (next_event, next_ts) = today + h, x = ( + (prev_ts, next_ts) + if next_event in ("solar_sunset", "solar_sunrise") + else (next_ts, prev_ts) + ) + k = 1 if next_event in ("solar_sunset", "solar_noon") else -1 + percentage = (0 - k) * ((now_ts - h) / (h - x)) ** 2 + k return percentage def _is_sleep(self): From edfaedba3c47ee56a07aeac8b6755349484ece28 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 19 Sep 2020 12:24:15 +0200 Subject: [PATCH 0153/1077] use globals --- custom_components/adaptive_lighting/switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 7fb62544..c7dec53a 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -438,10 +438,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): (prev_event, prev_ts), (next_event, next_ts) = today h, x = ( (prev_ts, next_ts) - if next_event in ("solar_sunset", "solar_sunrise") + if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE) else (next_ts, prev_ts) ) - k = 1 if next_event in ("solar_sunset", "solar_noon") else -1 + k = 1 if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_NOON) else -1 percentage = (0 - k) * ((now_ts - h) / (h - x)) ** 2 + k return percentage From 8d8a409a207b4208cf896ce7eb0e36494b2d572e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 19 Sep 2020 12:41:12 +0200 Subject: [PATCH 0154/1077] add _ALLOWED_ORDERS check --- custom_components/adaptive_lighting/switch.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index c7dec53a..e8de92e2 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -91,6 +91,13 @@ _SUPPORT_OPTS = { "transition": SUPPORT_TRANSITION, } +_ALLOWED_ORDERS = { + ("solar_sunrise", "solar_noon", "solar_sunset", "solar_midnight"), + ("solar_sunset", "solar_midnight", "solar_sunrise", "solar_noon"), + ("solar_midnight", "solar_sunrise", "solar_noon", "solar_sunset"), + ("solar_noon", "solar_sunset", "solar_midnight", "solar_sunrise"), +} + _LOGGER = logging.getLogger(__name__) @@ -415,12 +422,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): solar_noon = sunrise + (sunset - sunrise) / 2 solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset) / 2 - return [ + events = [ (SUN_EVENT_SUNRISE, sunrise.timestamp()), (SUN_EVENT_SUNSET, sunset.timestamp()), (SUN_EVENT_NOON, solar_noon.timestamp()), (SUN_EVENT_MIDNIGHT, solar_midnight.timestamp()), ] + # Check whether order is correct + events = sorted(events, key=lambda x: x[1]) + events_names, _ = zip(*events) + assert events_names in _ALLOWED_ORDERS + + return events def _relevant_events(self, now): events = [ From d63abb4d289b50f6e370a3c3e20db63f2e57e994 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 20 Sep 2020 18:33:26 +0200 Subject: [PATCH 0155/1077] commit working version but not with all options --- .../adaptive_lighting/__init__.py | 167 +++++++++++------- .../adaptive_lighting/config_flow.py | 51 +++--- custom_components/adaptive_lighting/const.py | 8 +- custom_components/adaptive_lighting/switch.py | 21 ++- 4 files changed, 147 insertions(+), 100 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index c4085dd9..12d70105 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -26,17 +26,18 @@ Technical notes: I had to make a lot of assumptions when writing this app * The component doesn't calculate a true "Blue Hour" -- it just sets the lights to 2700K (warm white) until your hub goes into Night mode """ - +import asyncio import logging import homeassistant.helpers.config_validation as cv import voluptuous as vol from homeassistant.components.light import VALID_TRANSITION -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import ENTRY_STATE_LOADED, SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME from .const import ( CONF_DISABLE_BRIGHTNESS_ADJUST, + UNDO_UPDATE_LISTENER, CONF_DISABLE_ENTITY, CONF_DISABLE_STATE, CONF_INITIAL_TRANSITION, @@ -77,78 +78,118 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_LIGHTS, default=DEFAULT_LIGHTS): cv.entity_ids, - vol.Optional( - CONF_DISABLE_BRIGHTNESS_ADJUST, - default=DEFAULT_DISABLE_BRIGHTNESS_ADJUST, - ): cv.boolean, - vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, - vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional( - CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION - ): VALID_TRANSITION, - vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, - vol.Optional( - CONF_MAX_BRIGHTNESS, default=DEFAULT_MAX_BRIGHTNESS - ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional( - CONF_MAX_COLOR_TEMP, default=DEFAULT_MAX_COLOR_TEMP - ): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional( - CONF_MIN_BRIGHTNESS, default=DEFAULT_MIN_BRIGHTNESS - ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional( - CONF_MIN_COLOR_TEMP, default=DEFAULT_MIN_COLOR_TEMP - ): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_ONLY_ONCE, default=DEFAULT_ONLY_ONCE): cv.boolean, - vol.Optional( - CONF_SLEEP_BRIGHTNESS, default=DEFAULT_SLEEP_BRIGHTNESS - ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional( - CONF_SLEEP_COLOR_TEMP, default=DEFAULT_SLEEP_COLOR_TEMP - ): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, - vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional( - CONF_SUNRISE_OFFSET, default=DEFAULT_SUNRISE_OFFSET - ): cv.time_period, - vol.Optional(CONF_SUNRISE_TIME): cv.time, - vol.Optional( - CONF_SUNSET_OFFSET, default=DEFAULT_SUNSET_OFFSET - ): cv.time_period, - vol.Optional(CONF_SUNSET_TIME): cv.time, - vol.Optional( - CONF_TRANSITION, default=DEFAULT_TRANSITION - ): VALID_TRANSITION, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) +# CONFIG_SCHEMA = vol.Schema( +# { +# DOMAIN: vol.Schema( +# { +# vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string, +# vol.Optional(CONF_LIGHTS, default=DEFAULT_LIGHTS): cv.entity_ids, +# vol.Optional( +# CONF_DISABLE_BRIGHTNESS_ADJUST, +# default=DEFAULT_DISABLE_BRIGHTNESS_ADJUST, +# ): cv.boolean, +# vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, +# vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), +# vol.Optional( +# CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION +# ): VALID_TRANSITION, +# vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, +# vol.Optional( +# CONF_MAX_BRIGHTNESS, default=DEFAULT_MAX_BRIGHTNESS +# ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), +# vol.Optional( +# CONF_MAX_COLOR_TEMP, default=DEFAULT_MAX_COLOR_TEMP +# ): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), +# vol.Optional( +# CONF_MIN_BRIGHTNESS, default=DEFAULT_MIN_BRIGHTNESS +# ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), +# vol.Optional( +# CONF_MIN_COLOR_TEMP, default=DEFAULT_MIN_COLOR_TEMP +# ): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), +# vol.Optional(CONF_ONLY_ONCE, default=DEFAULT_ONLY_ONCE): cv.boolean, +# vol.Optional( +# CONF_SLEEP_BRIGHTNESS, default=DEFAULT_SLEEP_BRIGHTNESS +# ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), +# vol.Optional( +# CONF_SLEEP_COLOR_TEMP, default=DEFAULT_SLEEP_COLOR_TEMP +# ): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), +# vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, +# vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), +# vol.Optional( +# CONF_SUNRISE_OFFSET, default=DEFAULT_SUNRISE_OFFSET +# ): cv.time_period, +# vol.Optional(CONF_SUNRISE_TIME): cv.time, +# vol.Optional( +# CONF_SUNSET_OFFSET, default=DEFAULT_SUNSET_OFFSET +# ): cv.time_period, +# vol.Optional(CONF_SUNSET_TIME): cv.time, +# vol.Optional( +# CONF_TRANSITION, default=DEFAULT_TRANSITION +# ): VALID_TRANSITION, +# } +# ) +# }, +# extra=vol.ALLOW_EXTRA, +# ) + +PLATFORMS = ["switch"] async def async_setup(hass, config): """Import integration from config.""" if DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + for entry in config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry + ) ) - ) return True -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass, config_entry: ConfigEntry): """Set up the component.""" + hass.data.setdefault(DOMAIN, {}) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "switch") - ) + undo_listener = config_entry.add_update_listener(async_update_options) + hass.data[DOMAIN][config_entry.entry_id] = { + UNDO_UPDATE_LISTENER: undo_listener + } + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, platform) + ) return True + + +async def async_update_options(hass, config_entry: ConfigEntry): + """Update options.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + +async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS + ] + ) + ) + hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() + + # Exclude this config entry because its not unloaded yet + if not any( + entry.state == ENTRY_STATE_LOADED and entry.entry_id != config_entry.entry_id + for entry in hass.config_entries.async_entries(DOMAIN) + ): + hass.data[DOMAIN].pop(config_entry.entry_id) + + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + + return unload_ok \ No newline at end of file diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 5785c0b8..2fb90b4d 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -37,6 +37,7 @@ from .const import ( # _convert_to_options_schema, DEFAULT_MAX_COLOR_TEMP, DEFAULT_MIN_BRIGHTNESS, DEFAULT_MIN_COLOR_TEMP, + UNDO_UPDATE_LISTENER, DEFAULT_ONLY_ONCE, DEFAULT_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_COLOR_TEMP, @@ -121,34 +122,34 @@ class OptionsFlowHandler(config_entries.OptionsFlow): options_schema = vol.Schema( { vol.Optional(CONF_LIGHTS, default=lights): all_lights, - # vol.Optional( - # CONF_DISABLE_BRIGHTNESS_ADJUST, default=disable_brightness_adjust - # ): bool, + vol.Optional( + CONF_DISABLE_BRIGHTNESS_ADJUST, default=disable_brightness_adjust + ): bool, # vol.Optional(CONF_DISABLE_ENTITY, default=disable_entity): str, # vol.Optional(CONF_DISABLE_STATE, default=disable_state): str, - # vol.Optional( - # CONF_INITIAL_TRANSITION, default=initial_transition - # ): cv.positive_int, + vol.Optional( + CONF_INITIAL_TRANSITION, default=initial_transition + ): cv.positive_int, # vol.Optional(CONF_INTERVAL, default=interval): cv.positive_int, - # vol.Optional(CONF_MAX_BRIGHTNESS, default=max_brightness): vol.All( - # vol.Coerce(int), vol.Range(min=1, max=100) - # ), - # vol.Optional(CONF_MAX_COLOR_TEMP, default=max_color_temp): vol.All( - # vol.Coerce(int), vol.Range(min=1000, max=10000) - # ), - # vol.Optional(CONF_MIN_BRIGHTNESS, default=min_brightness): vol.All( - # vol.Coerce(int), vol.Range(min=1, max=100) - # ), - # vol.Optional(CONF_MIN_COLOR_TEMP, default=min_color_temp): vol.All( - # vol.Coerce(int), vol.Range(min=1000, max=10000) - # ), - # vol.Optional(CONF_ONLY_ONCE, default=only_once): bool, - # vol.Optional(CONF_SLEEP_BRIGHTNESS, default=sleep_brightness): vol.All( - # vol.Coerce(int), vol.Range(min=1, max=100) - # ), - # vol.Optional(CONF_SLEEP_COLOR_TEMP, default=sleep_color_temp): vol.All( - # vol.Coerce(int), vol.Range(min=1000, max=10000) - # ), + vol.Optional(CONF_MAX_BRIGHTNESS, default=max_brightness): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_MAX_COLOR_TEMP, default=max_color_temp): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_MIN_BRIGHTNESS, default=min_brightness): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_MIN_COLOR_TEMP, default=min_color_temp): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), + vol.Optional(CONF_ONLY_ONCE, default=only_once): bool, + vol.Optional(CONF_SLEEP_BRIGHTNESS, default=sleep_brightness): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_SLEEP_COLOR_TEMP, default=sleep_color_temp): vol.All( + vol.Coerce(int), vol.Range(min=1000, max=10000) + ), # vol.Optional(CONF_SLEEP_ENTITY, default=sleep_entity): str, # vol.Optional(CONF_SLEEP_STATE, default=sleep_state): str, # vol.Optional(CONF_SUNRISE_OFFSET, default=sunrise_offset): int, diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index cdb3154d..205b689d 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,5 +1,6 @@ import voluptuous as vol +from datetime import timedelta import homeassistant.helpers.config_validation as cv from homeassistant.components.light import VALID_TRANSITION @@ -18,7 +19,7 @@ CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST = ( CONF_DISABLE_ENTITY = "disable_entity" CONF_DISABLE_STATE = "disable_state" CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 -CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 +CONF_INTERVAL, DEFAULT_INTERVAL = "interval", timedelta(seconds=90) CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100 CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP = "max_color_temp", 5500 CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS = "min_brightness", 1 @@ -28,12 +29,13 @@ CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1 CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP = "sleep_color_temp", 1000 CONF_SLEEP_ENTITY = "sleep_entity" CONF_SLEEP_STATE = "sleep_state" -CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 +CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", timedelta(seconds=0) CONF_SUNRISE_TIME = "sunrise_time" -CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 +CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", timedelta(seconds=0) CONF_SUNSET_TIME = "sunset_time" CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 +UNDO_UPDATE_LISTENER = "undo_update_listener" _COMMON_SCHEMA = { vol.Optional(CONF_LIGHTS, default=DEFAULT_LIGHTS): cv.entity_ids, diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index e8de92e2..ad241b0e 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1,3 +1,4 @@ +# CHECK OUT THE VIZIO COMPONENT! """Adaptive Lighting Component for Home-Assistant.""" import asyncio @@ -5,6 +6,8 @@ import bisect import logging from datetime import timedelta +import voluptuous as vol + import homeassistant.util.dt as dt_util from homeassistant.components.light import ( ATTR_BRIGHTNESS_PCT, @@ -46,6 +49,7 @@ from homeassistant.util.color import ( from .const import ( CONF_DISABLE_BRIGHTNESS_ADJUST, CONF_DISABLE_ENTITY, + UNDO_UPDATE_LISTENER, CONF_DISABLE_STATE, CONF_INITIAL_TRANSITION, CONF_INTERVAL, @@ -92,10 +96,10 @@ _SUPPORT_OPTS = { } _ALLOWED_ORDERS = { - ("solar_sunrise", "solar_noon", "solar_sunset", "solar_midnight"), - ("solar_sunset", "solar_midnight", "solar_sunrise", "solar_noon"), - ("solar_midnight", "solar_sunrise", "solar_noon", "solar_sunset"), - ("solar_noon", "solar_sunset", "solar_midnight", "solar_sunrise"), + (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT), + (SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT, SUN_EVENT_SUNRISE, SUN_EVENT_NOON), + (SUN_EVENT_MIDNIGHT, SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET), + (SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT, SUN_EVENT_SUNRISE), } @@ -111,7 +115,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if DOMAIN not in hass.data: hass.data[DOMAIN] = {} hass.data[DOMAIN][name] = switch - async_add_entities([switch]) + async_add_entities([switch], update_before_add=True) def _difference_between_states(from_state, to_state): @@ -150,9 +154,7 @@ def _difference_between_states(from_state, to_state): class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" - def __init__( - self, hass, name, config_entry, - ): + def __init__(self, hass, name, config_entry): """Initialize the Adaptive Lighting switch.""" self.hass = hass self._name = name @@ -171,6 +173,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set and unset tracker in async_turn_on and async_turn_off self.unsub_tracker = None + _LOGGER.error(f"Setting up with {self._lights}: config_entry.data: {config_entry.data}, config_entry.options: {config_entry.options}") @property def _lights(self): @@ -431,7 +434,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Check whether order is correct events = sorted(events, key=lambda x: x[1]) events_names, _ = zip(*events) - assert events_names in _ALLOWED_ORDERS + assert events_names in _ALLOWED_ORDERS, events_names return events From a18a312ea93de6823a143681a0bffe305d078a9b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 20 Sep 2020 18:38:01 +0200 Subject: [PATCH 0156/1077] styling --- .../adaptive_lighting/__init__.py | 116 +----------------- .../adaptive_lighting/config_flow.py | 2 - 2 files changed, 5 insertions(+), 113 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 12d70105..9b003798 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -29,109 +29,12 @@ Technical notes: I had to make a lot of assumptions when writing this app import asyncio import logging -import homeassistant.helpers.config_validation as cv -import voluptuous as vol -from homeassistant.components.light import VALID_TRANSITION -from homeassistant.config_entries import ENTRY_STATE_LOADED, SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from .const import ( - CONF_DISABLE_BRIGHTNESS_ADJUST, - UNDO_UPDATE_LISTENER, - CONF_DISABLE_ENTITY, - CONF_DISABLE_STATE, - CONF_INITIAL_TRANSITION, - CONF_INTERVAL, - CONF_LIGHTS, - CONF_MAX_BRIGHTNESS, - CONF_MAX_COLOR_TEMP, - CONF_MIN_BRIGHTNESS, - CONF_MIN_COLOR_TEMP, - CONF_ONLY_ONCE, - CONF_SLEEP_BRIGHTNESS, - CONF_SLEEP_COLOR_TEMP, - CONF_SLEEP_ENTITY, - CONF_SLEEP_STATE, - CONF_SUNRISE_OFFSET, - CONF_SUNRISE_TIME, - CONF_SUNSET_OFFSET, - CONF_SUNSET_TIME, - CONF_TRANSITION, - DEFAULT_DISABLE_BRIGHTNESS_ADJUST, - DEFAULT_INITIAL_TRANSITION, - DEFAULT_INTERVAL, - DEFAULT_LIGHTS, - DEFAULT_MAX_BRIGHTNESS, - DEFAULT_MAX_COLOR_TEMP, - DEFAULT_MIN_BRIGHTNESS, - DEFAULT_MIN_COLOR_TEMP, - DEFAULT_NAME, - DEFAULT_ONLY_ONCE, - DEFAULT_SLEEP_BRIGHTNESS, - DEFAULT_SLEEP_COLOR_TEMP, - DEFAULT_SUNRISE_OFFSET, - DEFAULT_SUNSET_OFFSET, - DEFAULT_TRANSITION, - DOMAIN, -) +from .const import DOMAIN, UNDO_UPDATE_LISTENER _LOGGER = logging.getLogger(__name__) - -# CONFIG_SCHEMA = vol.Schema( -# { -# DOMAIN: vol.Schema( -# { -# vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string, -# vol.Optional(CONF_LIGHTS, default=DEFAULT_LIGHTS): cv.entity_ids, -# vol.Optional( -# CONF_DISABLE_BRIGHTNESS_ADJUST, -# default=DEFAULT_DISABLE_BRIGHTNESS_ADJUST, -# ): cv.boolean, -# vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, -# vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), -# vol.Optional( -# CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION -# ): VALID_TRANSITION, -# vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, -# vol.Optional( -# CONF_MAX_BRIGHTNESS, default=DEFAULT_MAX_BRIGHTNESS -# ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), -# vol.Optional( -# CONF_MAX_COLOR_TEMP, default=DEFAULT_MAX_COLOR_TEMP -# ): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), -# vol.Optional( -# CONF_MIN_BRIGHTNESS, default=DEFAULT_MIN_BRIGHTNESS -# ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), -# vol.Optional( -# CONF_MIN_COLOR_TEMP, default=DEFAULT_MIN_COLOR_TEMP -# ): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), -# vol.Optional(CONF_ONLY_ONCE, default=DEFAULT_ONLY_ONCE): cv.boolean, -# vol.Optional( -# CONF_SLEEP_BRIGHTNESS, default=DEFAULT_SLEEP_BRIGHTNESS -# ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), -# vol.Optional( -# CONF_SLEEP_COLOR_TEMP, default=DEFAULT_SLEEP_COLOR_TEMP -# ): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), -# vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, -# vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), -# vol.Optional( -# CONF_SUNRISE_OFFSET, default=DEFAULT_SUNRISE_OFFSET -# ): cv.time_period, -# vol.Optional(CONF_SUNRISE_TIME): cv.time, -# vol.Optional( -# CONF_SUNSET_OFFSET, default=DEFAULT_SUNSET_OFFSET -# ): cv.time_period, -# vol.Optional(CONF_SUNSET_TIME): cv.time, -# vol.Optional( -# CONF_TRANSITION, default=DEFAULT_TRANSITION -# ): VALID_TRANSITION, -# } -# ) -# }, -# extra=vol.ALLOW_EXTRA, -# ) - PLATFORMS = ["switch"] @@ -153,9 +56,7 @@ async def async_setup_entry(hass, config_entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) undo_listener = config_entry.add_update_listener(async_update_options) - hass.data[DOMAIN][config_entry.entry_id] = { - UNDO_UPDATE_LISTENER: undo_listener - } + hass.data[DOMAIN][config_entry.entry_id] = {UNDO_UPDATE_LISTENER: undo_listener} for platform in PLATFORMS: hass.async_create_task( @@ -182,14 +83,7 @@ async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool: ) hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() - # Exclude this config entry because its not unloaded yet - if not any( - entry.state == ENTRY_STATE_LOADED and entry.entry_id != config_entry.entry_id - for entry in hass.config_entries.async_entries(DOMAIN) - ): + if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) - if not hass.data[DOMAIN]: - hass.data.pop(DOMAIN) - - return unload_ok \ No newline at end of file + return unload_ok diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 2fb90b4d..f0011315 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -5,7 +5,6 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant import config_entries -from homeassistant.components.light import VALID_TRANSITION from homeassistant.core import callback from .const import ( # _convert_to_options_schema, @@ -37,7 +36,6 @@ from .const import ( # _convert_to_options_schema, DEFAULT_MAX_COLOR_TEMP, DEFAULT_MIN_BRIGHTNESS, DEFAULT_MIN_COLOR_TEMP, - UNDO_UPDATE_LISTENER, DEFAULT_ONLY_ONCE, DEFAULT_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_COLOR_TEMP, From f778f455905a91cd80b5b490d82d1a3921ee667e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 20 Sep 2020 18:38:53 +0200 Subject: [PATCH 0157/1077] remove backticks from strings.json --- .../adaptive_lighting/strings.json | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 97960184..9fc0df8d 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -20,29 +20,29 @@ "title": "Adaptive Lighting options", "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings.", "data": { - "lights_brightness": "`lights_brightness`", - "lights_mired": "`lights_mired`", - "lights_rgb": "`lights_rgb`", - "lights_xy": "`lights_xy`", - "disable_brightness_adjust": "`disable_brightness_adjust`", - "disable_entity": "`disable_entity`", - "disable_state": "`disable_state`", - "initial_transition": "`initial_transition`, the transition of the lights when turning them on or when `disable_state` or `sleep_state` change", - "interval": "`interval`", - "max_brightness": "`max_brightness`", - "max_color_temp": "`max_color_temp`", - "min_brightness": "`min_brightness`", - "min_color_temp": "`min_color_temp`", - "only_once": "`only_once`", - "sleep_brightness": "`sleep_brightness`", - "sleep_color_temp": "`sleep_color_temp`", - "sleep_entity": "`sleep_entity`", - "sleep_state": "`sleep_state`", - "sunrise_offset": "`sunrise_offset`", - "sunrise_time": "`sunrise_time`", - "sunset_offset": "`sunset_offset`", - "sunset_time": "`sunset_time`", - "transition": "`transition`" + "lights_brightness": "lights_brightness", + "lights_mired": "lights_mired", + "lights_rgb": "lights_rgb", + "lights_xy": "lights_xy", + "disable_brightness_adjust": "disable_brightness_adjust", + "disable_entity": "disable_entity", + "disable_state": "disable_state", + "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", + "interval": "interval", + "max_brightness": "max_brightness", + "max_color_temp": "max_color_temp", + "min_brightness": "min_brightness", + "min_color_temp": "min_color_temp", + "only_once": "only_once", + "sleep_brightness": "sleep_brightness", + "sleep_color_temp": "sleep_color_temp", + "sleep_entity": "sleep_entity", + "sleep_state": "sleep_state", + "sunrise_offset": "sunrise_offset", + "sunrise_time": "sunrise_time", + "sunset_offset": "sunset_offset", + "sunset_time": "sunset_time", + "transition": "transition" } } }, From b37e006ba89c69c21bf8e0d01d604923c393f8de Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 20 Sep 2020 19:59:13 +0200 Subject: [PATCH 0158/1077] add semi working version with positive_time_dict --- .../adaptive_lighting/config_flow.py | 19 ++- custom_components/adaptive_lighting/switch.py | 150 ++++++------------ .../adaptive_lighting/translations/en.json | 46 +++--- 3 files changed, 84 insertions(+), 131 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index f0011315..ea7f0171 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -5,9 +5,10 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant import config_entries +from homeassistant.components.light import VALID_TRANSITION from homeassistant.core import callback -from .const import ( # _convert_to_options_schema, +from .const import ( CONF_DISABLE_BRIGHTNESS_ADJUST, CONF_DISABLE_ENTITY, CONF_DISABLE_STATE, @@ -151,15 +152,17 @@ class OptionsFlowHandler(config_entries.OptionsFlow): # vol.Optional(CONF_SLEEP_ENTITY, default=sleep_entity): str, # vol.Optional(CONF_SLEEP_STATE, default=sleep_state): str, # vol.Optional(CONF_SUNRISE_OFFSET, default=sunrise_offset): int, - # vol.Optional(CONF_SUNRISE_TIME, default=sunrise_time): str, + vol.Optional( + CONF_SUNRISE_TIME + # , default=sunrise_time + ): cv.positive_time_period_dict, # vol.Optional(CONF_SUNSET_OFFSET, default=sunset_offset): int, - # vol.Optional(CONF_SUNSET_TIME, default=sunset_time): str, - # vol.Optional(CONF_TRANSITION, default=transition): VALID_TRANSITION, + vol.Optional( + CONF_SUNSET_TIME + # , default=sunset_time + ): cv.positive_time_period_dict, + vol.Optional(CONF_TRANSITION, default=transition): VALID_TRANSITION, } ) - # options_schema = _convert_to_options_schema( - # self.hass, self.config_entry.options - # ) - return self.async_show_form(step_id="init", data_schema=options_schema) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index ad241b0e..d1da49c8 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -8,6 +8,7 @@ from datetime import timedelta import voluptuous as vol +import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util from homeassistant.components.light import ( ATTR_BRIGHTNESS_PCT, @@ -49,7 +50,6 @@ from homeassistant.util.color import ( from .const import ( CONF_DISABLE_BRIGHTNESS_ADJUST, CONF_DISABLE_ENTITY, - UNDO_UPDATE_LISTENER, CONF_DISABLE_STATE, CONF_INITIAL_TRANSITION, CONF_INTERVAL, @@ -86,6 +86,7 @@ from .const import ( ICON, SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, + UNDO_UPDATE_LISTENER, ) _SUPPORT_OPTS = { @@ -160,7 +161,52 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._name = name self._entity_id = f"switch.{DOMAIN}_{slugify(name)}" self._icon = ICON - self.config_entry = config_entry + + opts = config_entry.options + self._lights = opts.get(CONF_LIGHTS, DEFAULT_LIGHTS) + self._disable_brightness_adjust = opts.get( + CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST + ) + self._disable_entity = opts.get(CONF_DISABLE_ENTITY) + self._disable_state = opts.get(CONF_DISABLE_STATE) + self._initial_transition = opts.get( + CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION + ) + self._interval = opts.get(CONF_INTERVAL, DEFAULT_INTERVAL) + self._max_brightness = opts.get(CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS) + self._max_color_temp = opts.get(CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP) + self._min_brightness = opts.get(CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS) + self._min_color_temp = opts.get(CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP) + self._only_once = opts.get(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE) + self._sleep_brightness = opts.get( + CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS + ) + self._sleep_color_temp = opts.get( + CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP + ) + self._sleep_entity = opts.get(CONF_SLEEP_ENTITY) + self._sleep_state = opts.get(CONF_SLEEP_STATE) + self._sunrise_offset = opts.get(CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET) + self._sunrise_time = opts.get(CONF_SUNRISE_TIME) + self._sunset_offset = opts.get(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET) + self._sunset_time = opts.get(CONF_SUNSET_TIME) + self._transition = opts.get(CONF_TRANSITION, DEFAULT_TRANSITION) + + for which in ["_sunrise_time", "_sunset_time"]: + # I use a hack to be able to use cv.positive_time_period_dict in + # the options flow, which is the only serializable time setter, + # however, I need a time, so I convert the timedelta to a datetime. + sun_time = getattr(self, which) + if sun_time is not None: + dt = cv.time( + { + "hours": sun_time.hours, + "minutes": sun_time.minutes, + "seconds": sun_time.seconds, + "milliseconds": sun_time.milliseconds, + } + ) + setattr(self, which, dt) # Initialize attributes that will be set in self._update_attrs self._percent = None @@ -173,106 +219,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set and unset tracker in async_turn_on and async_turn_off self.unsub_tracker = None - _LOGGER.error(f"Setting up with {self._lights}: config_entry.data: {config_entry.data}, config_entry.options: {config_entry.options}") - - @property - def _lights(self): - return self.config_entry.options.get(CONF_LIGHTS, DEFAULT_LIGHTS) - - @property - def _disable_brightness_adjust(self): - return self.config_entry.options.get( - CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST + _LOGGER.error( + f"Setting up with {self._lights}: config_entry.data: {config_entry.data}, config_entry.options: {config_entry.options}" ) - @property - def _disable_entity(self): - return self.config_entry.options.get(CONF_DISABLE_ENTITY) - - @property - def _disable_state(self): - return self.config_entry.options.get(CONF_DISABLE_STATE) - - @property - def _initial_transition(self): - return self.config_entry.options.get( - CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION - ) - - @property - def _interval(self): - return self.config_entry.options.get(CONF_INTERVAL, DEFAULT_INTERVAL) - - @property - def _max_brightness(self): - return self.config_entry.options.get( - CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS - ) - - @property - def _max_color_temp(self): - return self.config_entry.options.get( - CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP - ) - - @property - def _min_brightness(self): - return self.config_entry.options.get( - CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS - ) - - @property - def _min_color_temp(self): - return self.config_entry.options.get( - CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP - ) - - @property - def _only_once(self): - return self.config_entry.options.get(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE) - - @property - def _sleep_brightness(self): - return self.config_entry.options.get( - CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS - ) - - @property - def _sleep_color_temp(self): - return self.config_entry.options.get( - CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP - ) - - @property - def _sleep_entity(self): - return self.config_entry.options.get(CONF_SLEEP_ENTITY) - - @property - def _sleep_state(self): - return self.config_entry.options.get(CONF_SLEEP_STATE) - - @property - def _sunrise_offset(self): - return self.config_entry.options.get( - CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET - ) - - @property - def _sunrise_time(self): - return self.config_entry.options.get(CONF_SUNRISE_TIME) - - @property - def _sunset_offset(self): - return self.config_entry.options.get(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET) - - @property - def _sunset_time(self): - return self.config_entry.options.get(CONF_SUNSET_TIME) - - @property - def _transition(self): - return self.config_entry.options.get(CONF_TRANSITION, DEFAULT_TRANSITION) - @property def entity_id(self): """Return the entity ID of the switch.""" diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 97960184..9fc0df8d 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -20,29 +20,29 @@ "title": "Adaptive Lighting options", "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings.", "data": { - "lights_brightness": "`lights_brightness`", - "lights_mired": "`lights_mired`", - "lights_rgb": "`lights_rgb`", - "lights_xy": "`lights_xy`", - "disable_brightness_adjust": "`disable_brightness_adjust`", - "disable_entity": "`disable_entity`", - "disable_state": "`disable_state`", - "initial_transition": "`initial_transition`, the transition of the lights when turning them on or when `disable_state` or `sleep_state` change", - "interval": "`interval`", - "max_brightness": "`max_brightness`", - "max_color_temp": "`max_color_temp`", - "min_brightness": "`min_brightness`", - "min_color_temp": "`min_color_temp`", - "only_once": "`only_once`", - "sleep_brightness": "`sleep_brightness`", - "sleep_color_temp": "`sleep_color_temp`", - "sleep_entity": "`sleep_entity`", - "sleep_state": "`sleep_state`", - "sunrise_offset": "`sunrise_offset`", - "sunrise_time": "`sunrise_time`", - "sunset_offset": "`sunset_offset`", - "sunset_time": "`sunset_time`", - "transition": "`transition`" + "lights_brightness": "lights_brightness", + "lights_mired": "lights_mired", + "lights_rgb": "lights_rgb", + "lights_xy": "lights_xy", + "disable_brightness_adjust": "disable_brightness_adjust", + "disable_entity": "disable_entity", + "disable_state": "disable_state", + "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", + "interval": "interval", + "max_brightness": "max_brightness", + "max_color_temp": "max_color_temp", + "min_brightness": "min_brightness", + "min_color_temp": "min_color_temp", + "only_once": "only_once", + "sleep_brightness": "sleep_brightness", + "sleep_color_temp": "sleep_color_temp", + "sleep_entity": "sleep_entity", + "sleep_state": "sleep_state", + "sunrise_offset": "sunrise_offset", + "sunrise_time": "sunrise_time", + "sunset_offset": "sunset_offset", + "sunset_time": "sunset_time", + "transition": "transition" } } }, From f8c1a50107032dc619e5d5075c2da61ceabf5ecc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 20 Sep 2020 20:21:17 +0200 Subject: [PATCH 0159/1077] convert strings to timedeltas and datetimes --- .../adaptive_lighting/config_flow.py | 18 ++---- custom_components/adaptive_lighting/const.py | 57 ++++--------------- custom_components/adaptive_lighting/switch.py | 26 ++++----- 3 files changed, 27 insertions(+), 74 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index ea7f0171..e22f7d5d 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -128,8 +128,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): # vol.Optional(CONF_DISABLE_STATE, default=disable_state): str, vol.Optional( CONF_INITIAL_TRANSITION, default=initial_transition - ): cv.positive_int, - # vol.Optional(CONF_INTERVAL, default=interval): cv.positive_int, + ): VALID_TRANSITION, + vol.Optional(CONF_INTERVAL, default=interval): cv.positive_int, vol.Optional(CONF_MAX_BRIGHTNESS, default=max_brightness): vol.All( vol.Coerce(int), vol.Range(min=1, max=100) ), @@ -151,16 +151,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ), # vol.Optional(CONF_SLEEP_ENTITY, default=sleep_entity): str, # vol.Optional(CONF_SLEEP_STATE, default=sleep_state): str, - # vol.Optional(CONF_SUNRISE_OFFSET, default=sunrise_offset): int, - vol.Optional( - CONF_SUNRISE_TIME - # , default=sunrise_time - ): cv.positive_time_period_dict, - # vol.Optional(CONF_SUNSET_OFFSET, default=sunset_offset): int, - vol.Optional( - CONF_SUNSET_TIME - # , default=sunset_time - ): cv.positive_time_period_dict, + vol.Optional(CONF_SUNRISE_OFFSET, default=sunrise_offset): int, + vol.Optional(CONF_SUNRISE_TIME, default=sunrise_time): str, + vol.Optional(CONF_SUNSET_OFFSET, default=sunset_offset): int, + vol.Optional(CONF_SUNSET_TIME, default=sunset_time): str, vol.Optional(CONF_TRANSITION, default=transition): VALID_TRANSITION, } ) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 205b689d..bf83bced 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,6 +1,5 @@ import voluptuous as vol -from datetime import timedelta import homeassistant.helpers.config_validation as cv from homeassistant.components.light import VALID_TRANSITION @@ -19,7 +18,7 @@ CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST = ( CONF_DISABLE_ENTITY = "disable_entity" CONF_DISABLE_STATE = "disable_state" CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 -CONF_INTERVAL, DEFAULT_INTERVAL = "interval", timedelta(seconds=90) +CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100 CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP = "max_color_temp", 5500 CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS = "min_brightness", 1 @@ -29,54 +28,18 @@ CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1 CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP = "sleep_color_temp", 1000 CONF_SLEEP_ENTITY = "sleep_entity" CONF_SLEEP_STATE = "sleep_state" -CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", timedelta(seconds=0) +CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 CONF_SUNRISE_TIME = "sunrise_time" -CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", timedelta(seconds=0) +CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 CONF_SUNSET_TIME = "sunset_time" CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 UNDO_UPDATE_LISTENER = "undo_update_listener" -_COMMON_SCHEMA = { - vol.Optional(CONF_LIGHTS, default=DEFAULT_LIGHTS): cv.entity_ids, - vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST, default=DEFAULT_DISABLE_BRIGHTNESS_ADJUST): cv.boolean, - vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, - vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION): VALID_TRANSITION, - vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, - vol.Optional(CONF_MAX_BRIGHTNESS, default=DEFAULT_MAX_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional(CONF_MAX_COLOR_TEMP, default=DEFAULT_MAX_COLOR_TEMP): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_MIN_BRIGHTNESS, default=DEFAULT_MIN_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional(CONF_MIN_COLOR_TEMP, default=DEFAULT_MIN_COLOR_TEMP): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_ONLY_ONCE, default=DEFAULT_ONLY_ONCE): cv.boolean, - vol.Optional(CONF_SLEEP_BRIGHTNESS, default=DEFAULT_SLEEP_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), - vol.Optional(CONF_SLEEP_COLOR_TEMP, default=DEFAULT_SLEEP_COLOR_TEMP): vol.All(vol.Coerce(int), vol.Range(min=1000, max=10000)), - vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, - vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_SUNRISE_OFFSET, default=DEFAULT_SUNRISE_OFFSET): cv.time_period, - vol.Optional(CONF_SUNRISE_TIME): cv.time, - vol.Optional(CONF_SUNSET_OFFSET, default=DEFAULT_SUNSET_OFFSET): cv.time_period, - vol.Optional(CONF_SUNSET_TIME): cv.time, - vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION, -} - - -def _convert_to_options_schema(hass, options): - schema = {} - for key, value in _COMMON_SCHEMA.items(): - if key.schema == CONF_LIGHTS: - all_lights = hass.states.async_entity_ids("light") - to_type = cv.multi_select(all_lights) - elif value == cv.boolean: - to_type = bool - elif (isinstance(value, vol.All) and hasattr(value.validators, "type") and value.validators[0].type == int) or value == VALID_TRANSITION: - to_type = value - elif value == cv.time_period: - to_type = cv.time_period_dict - else: - to_type = str - - default = key.default() if not isinstance(key.default, vol.Undefined) else vol.UNDEFINED - default = options.get(key.schema, default) - schema[vol.Optional(key.schema, default=default)] = to_type - return vol.Schema(schema) +# _COMMON_SCHEMA = { +# vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, +# vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), +# vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, +# vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, +# vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), +# } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index d1da49c8..c0ccd2b6 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -192,21 +192,17 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._sunset_time = opts.get(CONF_SUNSET_TIME) self._transition = opts.get(CONF_TRANSITION, DEFAULT_TRANSITION) - for which in ["_sunrise_time", "_sunset_time"]: - # I use a hack to be able to use cv.positive_time_period_dict in - # the options flow, which is the only serializable time setter, - # however, I need a time, so I convert the timedelta to a datetime. - sun_time = getattr(self, which) - if sun_time is not None: - dt = cv.time( - { - "hours": sun_time.hours, - "minutes": sun_time.minutes, - "seconds": sun_time.seconds, - "milliseconds": sun_time.milliseconds, - } - ) - setattr(self, which, dt) + for name, validate in [ + ("_sunrise_time", cv.time), + ("_sunset_time", cv.time), + ("_sunrise_offset", cv.time_period), + ("_sunset_offset", cv.time_period), + ("_interval", cv.time_period), + ]: + attr = getattr(self, name) + if attr is not None: + dt = validate(attr) + setattr(self, name, dt) # Initialize attributes that will be set in self._update_attrs self._percent = None From 0ed8ab41c04eda091276637c4b2a2574d63bf0eb Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 20 Sep 2020 20:49:13 +0200 Subject: [PATCH 0160/1077] fix most of the settings by hardcoding 'none' --- .../adaptive_lighting/config_flow.py | 20 +++++++++---------- custom_components/adaptive_lighting/const.py | 1 - custom_components/adaptive_lighting/switch.py | 12 ++++++++--- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index e22f7d5d..fe4867ff 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -94,8 +94,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): disable_brightness_adjust = options.get( CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST ) - disable_entity = options.get(CONF_DISABLE_ENTITY) - disable_state = options.get(CONF_DISABLE_STATE) + disable_entity = options.get(CONF_DISABLE_ENTITY, "none") + disable_state = options.get(CONF_DISABLE_STATE, "none") initial_transition = options.get( CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION ) @@ -107,12 +107,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow): only_once = options.get(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE) sleep_brightness = options.get(CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS) sleep_color_temp = options.get(CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP) - sleep_entity = options.get(CONF_SLEEP_ENTITY) - sleep_state = options.get(CONF_SLEEP_STATE) + sleep_entity = options.get(CONF_SLEEP_ENTITY, "none") + sleep_state = options.get(CONF_SLEEP_STATE, "none") sunrise_offset = options.get(CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET) - sunrise_time = options.get(CONF_SUNRISE_TIME) + sunrise_time = options.get(CONF_SUNRISE_TIME, "none") sunset_offset = options.get(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET) - sunset_time = options.get(CONF_SUNSET_TIME) + sunset_time = options.get(CONF_SUNSET_TIME, "none") transition = options.get(CONF_TRANSITION, DEFAULT_TRANSITION) all_lights = self.hass.states.async_entity_ids("light") @@ -124,8 +124,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): vol.Optional( CONF_DISABLE_BRIGHTNESS_ADJUST, default=disable_brightness_adjust ): bool, - # vol.Optional(CONF_DISABLE_ENTITY, default=disable_entity): str, - # vol.Optional(CONF_DISABLE_STATE, default=disable_state): str, + vol.Optional(CONF_DISABLE_ENTITY, default=disable_entity): str, + vol.Optional(CONF_DISABLE_STATE, default=disable_state): str, vol.Optional( CONF_INITIAL_TRANSITION, default=initial_transition ): VALID_TRANSITION, @@ -149,8 +149,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): vol.Optional(CONF_SLEEP_COLOR_TEMP, default=sleep_color_temp): vol.All( vol.Coerce(int), vol.Range(min=1000, max=10000) ), - # vol.Optional(CONF_SLEEP_ENTITY, default=sleep_entity): str, - # vol.Optional(CONF_SLEEP_STATE, default=sleep_state): str, + vol.Optional(CONF_SLEEP_ENTITY, default=sleep_entity): str, + vol.Optional(CONF_SLEEP_STATE, default=sleep_state): str, vol.Optional(CONF_SUNRISE_OFFSET, default=sunrise_offset): int, vol.Optional(CONF_SUNRISE_TIME, default=sunrise_time): str, vol.Optional(CONF_SUNSET_OFFSET, default=sunset_offset): int, diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index bf83bced..e6cd5989 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -39,7 +39,6 @@ UNDO_UPDATE_LISTENER = "undo_update_listener" # _COMMON_SCHEMA = { # vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, # vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), -# vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, # vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, # vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), # } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index c0ccd2b6..16b64166 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -198,11 +198,17 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ("_sunrise_offset", cv.time_period), ("_sunset_offset", cv.time_period), ("_interval", cv.time_period), + ("_disable_entity", cv.entity_id), + ("_sleep_entity", cv.entity_id), + ("_disable_state", vol.All(cv.ensure_list_csv, [cv.string])), + ("_sleep_state", vol.All(cv.ensure_list_csv, [cv.string])), ]: attr = getattr(self, name) - if attr is not None: - dt = validate(attr) - setattr(self, name, dt) + if attr is not None and attr != "none": + setattr(self, name, validate(attr)) + elif attr == "none": + setattr(self, name, None) + # Initialize attributes that will be set in self._update_attrs self._percent = None From 71cd8f738546448feb19c6b9c7e1c03f048f2137 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 20 Sep 2020 22:44:14 +0200 Subject: [PATCH 0161/1077] style --- custom_components/adaptive_lighting/const.py | 12 ----- custom_components/adaptive_lighting/switch.py | 45 ++++--------------- 2 files changed, 8 insertions(+), 49 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index e6cd5989..1943de1f 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,8 +1,3 @@ -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.light import VALID_TRANSITION - ICON = "mdi:theme-light-dark" DOMAIN = "adaptive_lighting" @@ -35,10 +30,3 @@ CONF_SUNSET_TIME = "sunset_time" CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 UNDO_UPDATE_LISTENER = "undo_update_listener" - -# _COMMON_SCHEMA = { -# vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, -# vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), -# vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, -# vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), -# } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 16b64166..5a25bec8 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -86,7 +86,6 @@ from .const import ( ICON, SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, - UNDO_UPDATE_LISTENER, ) _SUPPORT_OPTS = { @@ -119,39 +118,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities([switch], update_before_add=True) -def _difference_between_states(from_state, to_state): - start = "Lights adjusting because " - if from_state is None and to_state is None: - return start + "both states None" - if from_state is None: - return start + f"from_state: None, to_state: {to_state}" - if to_state is None: - return start + f"from_state: {from_state}, to_state: None" - - changed_attrs = ", ".join( - [ - f"{key}: {val}" - for key, val in to_state.attributes.items() - if from_state.attributes.get(key) != val - ] - ) - if from_state.state == to_state.state: - return start + ( - f"{from_state.entity_id} is still {to_state.state} but" - f" these attributes changes: {changed_attrs}." - ) - elif changed_attrs != "": - return start + ( - f"{from_state.entity_id} changed from {from_state.state} to" - f" {to_state.state} and these attributes changes: {changed_attrs}." - ) - else: - return start + ( - f"{from_state.entity_id} changed from {from_state.state} to" - f" {to_state.state} and no attributes changed." - ) - - class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" @@ -209,7 +175,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): elif attr == "none": setattr(self, name, None) - # Initialize attributes that will be set in self._update_attrs self._percent = None self._brightness = None @@ -487,11 +452,17 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _light_state_changed(self, entity_id, from_state, to_state): assert to_state.state == "on" and from_state.state == "off" - _LOGGER.debug(_difference_between_states(from_state, to_state)) + _LOGGER.debug( + "_light_state_changed, from_state: '%s', to_state: '%s'", + from_state, + to_state, + ) await self._update_lights( lights=[entity_id], transition=self._initial_transition, force=True ) async def _state_changed(self, entity_id, from_state, to_state): - _LOGGER.debug(_difference_between_states(from_state, to_state)) + _LOGGER.debug( + "_state_changed, from_state: '%s', to_state: '%s'", from_state, to_state + ) await self._update_lights(transition=self._initial_transition, force=True) From 355f88bf8f8160206153e25aa1931759a3a17f26 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 20 Sep 2020 22:53:24 +0200 Subject: [PATCH 0162/1077] add comment --- custom_components/adaptive_lighting/switch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 5a25bec8..f5258661 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -173,6 +173,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if attr is not None and attr != "none": setattr(self, name, validate(attr)) elif attr == "none": + # FIX: Can't use `None` in OptionsFlow. For reasons I do + # not understand, I cannot save an option that is empty. setattr(self, name, None) # Initialize attributes that will be set in self._update_attrs From ee607bc60be1bd226f9b1b6c1e93e2f5f2db4f4d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 20 Sep 2020 22:54:45 +0200 Subject: [PATCH 0163/1077] use FAKE_NONE --- custom_components/adaptive_lighting/config_flow.py | 13 +++++++------ custom_components/adaptive_lighting/const.py | 1 + custom_components/adaptive_lighting/switch.py | 5 +++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index fe4867ff..6a340cf9 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -44,6 +44,7 @@ from .const import ( DEFAULT_SUNSET_OFFSET, DEFAULT_TRANSITION, DOMAIN, + FAKE_NONE, ) _LOGGER = logging.getLogger(__name__) @@ -94,8 +95,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): disable_brightness_adjust = options.get( CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST ) - disable_entity = options.get(CONF_DISABLE_ENTITY, "none") - disable_state = options.get(CONF_DISABLE_STATE, "none") + disable_entity = options.get(CONF_DISABLE_ENTITY, FAKE_NONE) + disable_state = options.get(CONF_DISABLE_STATE, FAKE_NONE) initial_transition = options.get( CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION ) @@ -107,12 +108,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow): only_once = options.get(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE) sleep_brightness = options.get(CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS) sleep_color_temp = options.get(CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP) - sleep_entity = options.get(CONF_SLEEP_ENTITY, "none") - sleep_state = options.get(CONF_SLEEP_STATE, "none") + sleep_entity = options.get(CONF_SLEEP_ENTITY, FAKE_NONE) + sleep_state = options.get(CONF_SLEEP_STATE, FAKE_NONE) sunrise_offset = options.get(CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET) - sunrise_time = options.get(CONF_SUNRISE_TIME, "none") + sunrise_time = options.get(CONF_SUNRISE_TIME, FAKE_NONE) sunset_offset = options.get(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET) - sunset_time = options.get(CONF_SUNSET_TIME, "none") + sunset_time = options.get(CONF_SUNSET_TIME, FAKE_NONE) transition = options.get(CONF_TRANSITION, DEFAULT_TRANSITION) all_lights = self.hass.states.async_entity_ids("light") diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 1943de1f..cc6aecd9 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -30,3 +30,4 @@ CONF_SUNSET_TIME = "sunset_time" CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 UNDO_UPDATE_LISTENER = "undo_update_listener" +FAKE_NONE = "none" diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index f5258661..ea6b0081 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -83,6 +83,7 @@ from .const import ( DEFAULT_SUNSET_OFFSET, DEFAULT_TRANSITION, DOMAIN, + FAKE_NONE, ICON, SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, @@ -170,9 +171,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ("_sleep_state", vol.All(cv.ensure_list_csv, [cv.string])), ]: attr = getattr(self, name) - if attr is not None and attr != "none": + if attr is not None and attr != FAKE_NONE: setattr(self, name, validate(attr)) - elif attr == "none": + elif attr == FAKE_NONE: # FIX: Can't use `None` in OptionsFlow. For reasons I do # not understand, I cannot save an option that is empty. setattr(self, name, None) From 575b81cbc53f9c798d54074a8aabeea7060b4ef4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 20 Sep 2020 23:00:15 +0200 Subject: [PATCH 0164/1077] rename FAKE_NONE to None --- custom_components/adaptive_lighting/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index cc6aecd9..9d0185d7 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -30,4 +30,4 @@ CONF_SUNSET_TIME = "sunset_time" CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 UNDO_UPDATE_LISTENER = "undo_update_listener" -FAKE_NONE = "none" +FAKE_NONE = "None" From 2b0eaff884e8d343aa0b0aed7cb939d064d9b573 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 23 Sep 2020 12:17:51 +0200 Subject: [PATCH 0165/1077] validate options --- .../adaptive_lighting/config_flow.py | 28 +++++++++++++++++-- .../adaptive_lighting/strings.json | 2 +- .../adaptive_lighting/translations/en.json | 2 +- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 6a340cf9..f505022f 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -86,8 +86,30 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Handle options flow.""" + errors = {} if user_input is not None: - return self.async_create_entry(title="", data=user_input) + for key, validate in [ + (CONF_SUNRISE_TIME, cv.time), + (CONF_SUNSET_TIME, cv.time), + (CONF_SUNRISE_OFFSET, cv.time_period), + (CONF_SUNSET_OFFSET, cv.time_period), + (CONF_INTERVAL, cv.time_period), + (CONF_DISABLE_ENTITY, cv.entity_id), + (CONF_SLEEP_ENTITY, cv.entity_id), + (CONF_DISABLE_STATE, vol.All(cv.ensure_list_csv, [cv.string])), + (CONF_SLEEP_STATE, vol.All(cv.ensure_list_csv, [cv.string])), + ]: + try: + value = user_input.get(key) + if value == FAKE_NONE: + value = None + if value is not None: + validate(user_input[key]) + except vol.Invalid: + _LOGGER.exception("Configuration option %s=%s is incorrect", key, value) + errors["base"] = "option_error" + if not errors: + return self.async_create_entry(title="", data=user_input) options = self.config_entry.options @@ -160,4 +182,6 @@ class OptionsFlowHandler(config_entries.OptionsFlow): } ) - return self.async_show_form(step_id="init", data_schema=options_schema) + return self.async_show_form( + step_id="init", data_schema=options_schema, errors=errors + ) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 9fc0df8d..0e02634a 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -47,7 +47,7 @@ } }, "error": { - "retrive_error": "Error retriving servers list" + "option_error": "Invalid option" } } } diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 9fc0df8d..0e02634a 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -47,7 +47,7 @@ } }, "error": { - "retrive_error": "Error retriving servers list" + "option_error": "Invalid option" } } } From 5a4da9512a9ae8849785bde1e00e118e3e63c1f3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 23 Sep 2020 12:35:57 +0200 Subject: [PATCH 0166/1077] use VALIDATION in switch.py --- .../adaptive_lighting/__init__.py | 46 ++++++++++++++++++- .../adaptive_lighting/config_flow.py | 17 ++----- custom_components/adaptive_lighting/const.py | 16 +++++++ custom_components/adaptive_lighting/switch.py | 14 ++---- 4 files changed, 69 insertions(+), 24 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 9b003798..3c83bab2 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -28,7 +28,7 @@ Technical notes: I had to make a lot of assumptions when writing this app """ import asyncio import logging - +import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from .const import DOMAIN, UNDO_UPDATE_LISTENER @@ -37,6 +37,50 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["switch"] +# _SCHEMA = { +# vol.Optional(CONF_LIGHTS, default=DEFAULT_LIGHTS): cv.entity_ids, +# vol.Optional( +# CONF_DISABLE_BRIGHTNESS_ADJUST, +# default=DEFAULT_DISABLE_BRIGHTNESS_ADJUST, +# ): cv.boolean, +# vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, +# vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), +# vol.Optional( +# CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION +# ): VALID_TRANSITION, +# vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, +# vol.Optional(CONF_MAX_BRIGHTNESS, default=DEFAULT_MAX_BRIGHTNESS): vol.All( +# vol.Coerce(int), vol.Range(min=1, max=100) +# ), +# vol.Optional(CONF_MAX_COLOR_TEMP, default=DEFAULT_MAX_COLOR_TEMP): vol.All( +# vol.Coerce(int), vol.Range(min=1000, max=10000) +# ), +# vol.Optional(CONF_MIN_BRIGHTNESS, default=DEFAULT_MIN_BRIGHTNESS): vol.All( +# vol.Coerce(int), vol.Range(min=1, max=100) +# ), +# vol.Optional(CONF_MIN_COLOR_TEMP, default=DEFAULT_MIN_COLOR_TEMP): vol.All( +# vol.Coerce(int), vol.Range(min=1000, max=10000) +# ), +# vol.Optional(CONF_ONLY_ONCE, default=DEFAULT_ONLY_ONCE): cv.boolean, +# vol.Optional(CONF_SLEEP_BRIGHTNESS, default=DEFAULT_SLEEP_BRIGHTNESS): vol.All( +# vol.Coerce(int), vol.Range(min=1, max=100) +# ), +# vol.Optional(CONF_SLEEP_COLOR_TEMP, default=DEFAULT_SLEEP_COLOR_TEMP): vol.All( +# vol.Coerce(int), vol.Range(min=1000, max=10000) +# ), +# vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, +# vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), +# vol.Optional(CONF_SUNRISE_OFFSET, default=DEFAULT_SUNRISE_OFFSET): cv.time_period, +# vol.Optional(CONF_SUNRISE_TIME): cv.time, +# vol.Optional(CONF_SUNSET_OFFSET, default=DEFAULT_SUNSET_OFFSET): cv.time_period, +# vol.Optional(CONF_SUNSET_TIME): cv.time, +# vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION, +# } +# CONFIG_SCHEMA = vol.Schema( +# {DOMAIN: vol.All(vol.Schema(_SCHEMA))}, +# extra=vol.ALLOW_EXTRA, +# ) + async def async_setup(hass, config): """Import integration from config.""" diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index f505022f..3136b697 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -45,6 +45,7 @@ from .const import ( DEFAULT_TRANSITION, DOMAIN, FAKE_NONE, + VALIDATION, ) _LOGGER = logging.getLogger(__name__) @@ -88,17 +89,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Handle options flow.""" errors = {} if user_input is not None: - for key, validate in [ - (CONF_SUNRISE_TIME, cv.time), - (CONF_SUNSET_TIME, cv.time), - (CONF_SUNRISE_OFFSET, cv.time_period), - (CONF_SUNSET_OFFSET, cv.time_period), - (CONF_INTERVAL, cv.time_period), - (CONF_DISABLE_ENTITY, cv.entity_id), - (CONF_SLEEP_ENTITY, cv.entity_id), - (CONF_DISABLE_STATE, vol.All(cv.ensure_list_csv, [cv.string])), - (CONF_SLEEP_STATE, vol.All(cv.ensure_list_csv, [cv.string])), - ]: + for key, validate in VALIDATION: try: value = user_input.get(key) if value == FAKE_NONE: @@ -106,7 +97,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if value is not None: validate(user_input[key]) except vol.Invalid: - _LOGGER.exception("Configuration option %s=%s is incorrect", key, value) + _LOGGER.exception( + "Configuration option %s=%s is incorrect", key, value + ) errors["base"] = "option_error" if not errors: return self.async_create_entry(title="", data=user_input) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 9d0185d7..c07a9af6 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,3 +1,7 @@ +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + ICON = "mdi:theme-light-dark" DOMAIN = "adaptive_lighting" @@ -31,3 +35,15 @@ CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 UNDO_UPDATE_LISTENER = "undo_update_listener" FAKE_NONE = "None" + +VALIDATION = [ # these validators cannot be serialized + (CONF_SUNRISE_TIME, cv.time), + (CONF_SUNSET_TIME, cv.time), + (CONF_SUNRISE_OFFSET, cv.time_period), + (CONF_SUNSET_OFFSET, cv.time_period), + (CONF_INTERVAL, cv.time_period), + (CONF_DISABLE_ENTITY, cv.entity_id), + (CONF_SLEEP_ENTITY, cv.entity_id), + (CONF_DISABLE_STATE, vol.All(cv.ensure_list_csv, [cv.string])), + (CONF_SLEEP_STATE, vol.All(cv.ensure_list_csv, [cv.string])), +] diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index ea6b0081..378880d9 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -87,6 +87,7 @@ from .const import ( ICON, SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, + VALIDATION, ) _SUPPORT_OPTS = { @@ -159,17 +160,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._sunset_time = opts.get(CONF_SUNSET_TIME) self._transition = opts.get(CONF_TRANSITION, DEFAULT_TRANSITION) - for name, validate in [ - ("_sunrise_time", cv.time), - ("_sunset_time", cv.time), - ("_sunrise_offset", cv.time_period), - ("_sunset_offset", cv.time_period), - ("_interval", cv.time_period), - ("_disable_entity", cv.entity_id), - ("_sleep_entity", cv.entity_id), - ("_disable_state", vol.All(cv.ensure_list_csv, [cv.string])), - ("_sleep_state", vol.All(cv.ensure_list_csv, [cv.string])), - ]: + for name, validate in VALIDATION: + name = f"_{name}" attr = getattr(self, name) if attr is not None and attr != FAKE_NONE: setattr(self, name, validate(attr)) From d33a96fd048fd15f57174fd75a32e924ae6b1507 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 23 Sep 2020 12:59:58 +0200 Subject: [PATCH 0167/1077] simplify validation --- .../adaptive_lighting/__init__.py | 46 ------------------- .../adaptive_lighting/config_flow.py | 5 +- custom_components/adaptive_lighting/const.py | 16 +++---- custom_components/adaptive_lighting/switch.py | 26 +++++------ 4 files changed, 22 insertions(+), 71 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 3c83bab2..20121f85 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -28,7 +28,6 @@ Technical notes: I had to make a lot of assumptions when writing this app """ import asyncio import logging -import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from .const import DOMAIN, UNDO_UPDATE_LISTENER @@ -37,51 +36,6 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["switch"] -# _SCHEMA = { -# vol.Optional(CONF_LIGHTS, default=DEFAULT_LIGHTS): cv.entity_ids, -# vol.Optional( -# CONF_DISABLE_BRIGHTNESS_ADJUST, -# default=DEFAULT_DISABLE_BRIGHTNESS_ADJUST, -# ): cv.boolean, -# vol.Optional(CONF_DISABLE_ENTITY): cv.entity_id, -# vol.Optional(CONF_DISABLE_STATE): vol.All(cv.ensure_list, [cv.string]), -# vol.Optional( -# CONF_INITIAL_TRANSITION, default=DEFAULT_INITIAL_TRANSITION -# ): VALID_TRANSITION, -# vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): cv.time_period, -# vol.Optional(CONF_MAX_BRIGHTNESS, default=DEFAULT_MAX_BRIGHTNESS): vol.All( -# vol.Coerce(int), vol.Range(min=1, max=100) -# ), -# vol.Optional(CONF_MAX_COLOR_TEMP, default=DEFAULT_MAX_COLOR_TEMP): vol.All( -# vol.Coerce(int), vol.Range(min=1000, max=10000) -# ), -# vol.Optional(CONF_MIN_BRIGHTNESS, default=DEFAULT_MIN_BRIGHTNESS): vol.All( -# vol.Coerce(int), vol.Range(min=1, max=100) -# ), -# vol.Optional(CONF_MIN_COLOR_TEMP, default=DEFAULT_MIN_COLOR_TEMP): vol.All( -# vol.Coerce(int), vol.Range(min=1000, max=10000) -# ), -# vol.Optional(CONF_ONLY_ONCE, default=DEFAULT_ONLY_ONCE): cv.boolean, -# vol.Optional(CONF_SLEEP_BRIGHTNESS, default=DEFAULT_SLEEP_BRIGHTNESS): vol.All( -# vol.Coerce(int), vol.Range(min=1, max=100) -# ), -# vol.Optional(CONF_SLEEP_COLOR_TEMP, default=DEFAULT_SLEEP_COLOR_TEMP): vol.All( -# vol.Coerce(int), vol.Range(min=1000, max=10000) -# ), -# vol.Optional(CONF_SLEEP_ENTITY): cv.entity_id, -# vol.Optional(CONF_SLEEP_STATE): vol.All(cv.ensure_list, [cv.string]), -# vol.Optional(CONF_SUNRISE_OFFSET, default=DEFAULT_SUNRISE_OFFSET): cv.time_period, -# vol.Optional(CONF_SUNRISE_TIME): cv.time, -# vol.Optional(CONF_SUNSET_OFFSET, default=DEFAULT_SUNSET_OFFSET): cv.time_period, -# vol.Optional(CONF_SUNSET_TIME): cv.time, -# vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): VALID_TRANSITION, -# } -# CONFIG_SCHEMA = vol.Schema( -# {DOMAIN: vol.All(vol.Schema(_SCHEMA))}, -# extra=vol.ALLOW_EXTRA, -# ) - - async def async_setup(hass, config): """Import integration from config.""" diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 3136b697..7e417869 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -45,7 +45,7 @@ from .const import ( DEFAULT_TRANSITION, DOMAIN, FAKE_NONE, - VALIDATION, + EXTRA_VALIDATION, ) _LOGGER = logging.getLogger(__name__) @@ -89,7 +89,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Handle options flow.""" errors = {} if user_input is not None: - for key, validate in VALIDATION: + for key, validate in EXTRA_VALIDATION: + # these are unserializable validators try: value = user_input.get(key) if value == FAKE_NONE: diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index c07a9af6..5f03f698 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -34,16 +34,16 @@ CONF_SUNSET_TIME = "sunset_time" CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 UNDO_UPDATE_LISTENER = "undo_update_listener" -FAKE_NONE = "None" +FAKE_NONE = "None" # TODO: use `from homeassistant.const import ENTITY_MATCH_NONE`? -VALIDATION = [ # these validators cannot be serialized - (CONF_SUNRISE_TIME, cv.time), - (CONF_SUNSET_TIME, cv.time), - (CONF_SUNRISE_OFFSET, cv.time_period), - (CONF_SUNSET_OFFSET, cv.time_period), - (CONF_INTERVAL, cv.time_period), +EXTRA_VALIDATION = [ # these validators cannot be serialized (CONF_DISABLE_ENTITY, cv.entity_id), - (CONF_SLEEP_ENTITY, cv.entity_id), (CONF_DISABLE_STATE, vol.All(cv.ensure_list_csv, [cv.string])), + (CONF_INTERVAL, cv.time_period), + (CONF_SLEEP_ENTITY, cv.entity_id), (CONF_SLEEP_STATE, vol.All(cv.ensure_list_csv, [cv.string])), + (CONF_SUNRISE_OFFSET, cv.time_period), + (CONF_SUNRISE_TIME, cv.time), + (CONF_SUNSET_OFFSET, cv.time_period), + (CONF_SUNSET_TIME, cv.time), ] diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 378880d9..40ef889c 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -3,12 +3,10 @@ import asyncio import bisect +from copy import deepcopy import logging from datetime import timedelta -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util from homeassistant.components.light import ( ATTR_BRIGHTNESS_PCT, @@ -87,7 +85,7 @@ from .const import ( ICON, SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, - VALIDATION, + EXTRA_VALIDATION, ) _SUPPORT_OPTS = { @@ -130,7 +128,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._entity_id = f"switch.{DOMAIN}_{slugify(name)}" self._icon = ICON - opts = config_entry.options + opts = { + key: value if value != FAKE_NONE else None + for key, value in config_entry.options.items() + } + for key, validate in EXTRA_VALIDATION: # Fix the types of the inputs + value = opts.get(key) + if value is not None: + opts[key] = validate(value) + self._lights = opts.get(CONF_LIGHTS, DEFAULT_LIGHTS) self._disable_brightness_adjust = opts.get( CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST @@ -160,16 +166,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._sunset_time = opts.get(CONF_SUNSET_TIME) self._transition = opts.get(CONF_TRANSITION, DEFAULT_TRANSITION) - for name, validate in VALIDATION: - name = f"_{name}" - attr = getattr(self, name) - if attr is not None and attr != FAKE_NONE: - setattr(self, name, validate(attr)) - elif attr == FAKE_NONE: - # FIX: Can't use `None` in OptionsFlow. For reasons I do - # not understand, I cannot save an option that is empty. - setattr(self, name, None) - # Initialize attributes that will be set in self._update_attrs self._percent = None self._brightness = None From 5ff1da3c1b3991a57c67b273f07827e2a96ab37b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 23 Sep 2020 13:11:50 +0200 Subject: [PATCH 0168/1077] simplify schema creation --- .../adaptive_lighting/__init__.py | 2 + .../adaptive_lighting/config_flow.py | 96 ++++++------------- custom_components/adaptive_lighting/switch.py | 4 +- 3 files changed, 32 insertions(+), 70 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 20121f85..9b003798 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -28,6 +28,7 @@ Technical notes: I had to make a lot of assumptions when writing this app """ import asyncio import logging + from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from .const import DOMAIN, UNDO_UPDATE_LISTENER @@ -36,6 +37,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["switch"] + async def async_setup(hass, config): """Import integration from config.""" diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 7e417869..ddff3537 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -44,8 +44,8 @@ from .const import ( DEFAULT_SUNSET_OFFSET, DEFAULT_TRANSITION, DOMAIN, - FAKE_NONE, EXTRA_VALIDATION, + FAKE_NONE, ) _LOGGER = logging.getLogger(__name__) @@ -93,9 +93,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): # these are unserializable validators try: value = user_input.get(key) - if value == FAKE_NONE: - value = None - if value is not None: + if value is not None and value != FAKE_NONE: validate(user_input[key]) except vol.Invalid: _LOGGER.exception( @@ -106,73 +104,35 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_create_entry(title="", data=user_input) options = self.config_entry.options - - lights = options.get(CONF_LIGHTS, DEFAULT_LIGHTS) - disable_brightness_adjust = options.get( - CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST - ) - disable_entity = options.get(CONF_DISABLE_ENTITY, FAKE_NONE) - disable_state = options.get(CONF_DISABLE_STATE, FAKE_NONE) - initial_transition = options.get( - CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION - ) - interval = options.get(CONF_INTERVAL, DEFAULT_INTERVAL) - max_brightness = options.get(CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS) - max_color_temp = options.get(CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP) - min_brightness = options.get(CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS) - min_color_temp = options.get(CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP) - only_once = options.get(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE) - sleep_brightness = options.get(CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS) - sleep_color_temp = options.get(CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP) - sleep_entity = options.get(CONF_SLEEP_ENTITY, FAKE_NONE) - sleep_state = options.get(CONF_SLEEP_STATE, FAKE_NONE) - sunrise_offset = options.get(CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET) - sunrise_time = options.get(CONF_SUNRISE_TIME, FAKE_NONE) - sunset_offset = options.get(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET) - sunset_time = options.get(CONF_SUNSET_TIME, FAKE_NONE) - transition = options.get(CONF_TRANSITION, DEFAULT_TRANSITION) - - all_lights = self.hass.states.async_entity_ids("light") - all_lights = cv.multi_select(all_lights) + int_between = lambda a, b: vol.All(vol.Coerce(int), vol.Range(min=a, max=b)) + all_lights = cv.multi_select(self.hass.states.async_entity_ids("light")) + validation_tuples = [ + (CONF_LIGHTS, DEFAULT_LIGHTS, all_lights), + (CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST, bool), + (CONF_DISABLE_ENTITY, FAKE_NONE, str), + (CONF_DISABLE_STATE, FAKE_NONE, str), + (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), + (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), + (CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)), + (CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP, int_between(1000, 10000)), + (CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS, int_between(1, 100)), + (CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP, int_between(1000, 10000)), + (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), + (CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)), + (CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP, int_between(1000, 10000)), + (CONF_SLEEP_ENTITY, FAKE_NONE, str), + (CONF_SLEEP_STATE, FAKE_NONE, str), + (CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int), + (CONF_SUNRISE_TIME, FAKE_NONE, str), + (CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int), + (CONF_SUNSET_TIME, FAKE_NONE, str), + (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), + ] options_schema = vol.Schema( { - vol.Optional(CONF_LIGHTS, default=lights): all_lights, - vol.Optional( - CONF_DISABLE_BRIGHTNESS_ADJUST, default=disable_brightness_adjust - ): bool, - vol.Optional(CONF_DISABLE_ENTITY, default=disable_entity): str, - vol.Optional(CONF_DISABLE_STATE, default=disable_state): str, - vol.Optional( - CONF_INITIAL_TRANSITION, default=initial_transition - ): VALID_TRANSITION, - vol.Optional(CONF_INTERVAL, default=interval): cv.positive_int, - vol.Optional(CONF_MAX_BRIGHTNESS, default=max_brightness): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_MAX_COLOR_TEMP, default=max_color_temp): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_MIN_BRIGHTNESS, default=min_brightness): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_MIN_COLOR_TEMP, default=min_color_temp): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_ONLY_ONCE, default=only_once): bool, - vol.Optional(CONF_SLEEP_BRIGHTNESS, default=sleep_brightness): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_SLEEP_COLOR_TEMP, default=sleep_color_temp): vol.All( - vol.Coerce(int), vol.Range(min=1000, max=10000) - ), - vol.Optional(CONF_SLEEP_ENTITY, default=sleep_entity): str, - vol.Optional(CONF_SLEEP_STATE, default=sleep_state): str, - vol.Optional(CONF_SUNRISE_OFFSET, default=sunrise_offset): int, - vol.Optional(CONF_SUNRISE_TIME, default=sunrise_time): str, - vol.Optional(CONF_SUNSET_OFFSET, default=sunset_offset): int, - vol.Optional(CONF_SUNSET_TIME, default=sunset_time): str, - vol.Optional(CONF_TRANSITION, default=transition): VALID_TRANSITION, + vol.Optional(key, default=options.get(key, default)): validation + for key, default, validation in validation_tuples } ) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 40ef889c..8e8c0b37 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -3,8 +3,8 @@ import asyncio import bisect -from copy import deepcopy import logging +from copy import deepcopy from datetime import timedelta import homeassistant.util.dt as dt_util @@ -81,11 +81,11 @@ from .const import ( DEFAULT_SUNSET_OFFSET, DEFAULT_TRANSITION, DOMAIN, + EXTRA_VALIDATION, FAKE_NONE, ICON, SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, - EXTRA_VALIDATION, ) _SUPPORT_OPTS = { From cadd8279b9b153f079f4e61385c78ca996a64f32 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 23 Sep 2020 13:17:34 +0200 Subject: [PATCH 0169/1077] add comment --- custom_components/adaptive_lighting/switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 8e8c0b37..60b3e34a 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -128,7 +128,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._entity_id = f"switch.{DOMAIN}_{slugify(name)}" self._icon = ICON - opts = { + opts = { # replace "None" -> None key: value if value != FAKE_NONE else None for key, value in config_entry.options.items() } @@ -178,7 +178,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set and unset tracker in async_turn_on and async_turn_off self.unsub_tracker = None _LOGGER.error( - f"Setting up with {self._lights}: config_entry.data: {config_entry.data}, config_entry.options: {config_entry.options}" + f"Setting up with {self._lights}: config_entry.data: {config_entry.data}, config_entry.options: {config_entry.options}, converted to {opts}" ) @property From 3b275e1dd3b8d34bac31c2451bc6ee4d1bf2a73d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 23 Sep 2020 18:20:25 +0200 Subject: [PATCH 0170/1077] start with YAML config validation --- .../adaptive_lighting/__init__.py | 23 +++- .../adaptive_lighting/config_flow.py | 101 +++++------------- custom_components/adaptive_lighting/const.py | 70 +++++++++--- custom_components/adaptive_lighting/switch.py | 8 +- 4 files changed, 112 insertions(+), 90 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 9b003798..0ab50803 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -29,15 +29,36 @@ Technical notes: I had to make a lot of assumptions when writing this app import asyncio import logging +import homeassistant.helpers.config_validation as cv +import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from .const import DOMAIN, UNDO_UPDATE_LISTENER +from .const import CONF_NAME, DOMAIN, UNDO_UPDATE_LISTENER, get_domain_schema _LOGGER = logging.getLogger(__name__) PLATFORMS = ["switch"] +def _all_unique_profiles(value): + """Validate that all enties have a unique profile name.""" + hosts = [device[CONF_NAME] for device in value] + schema = vol.Schema(vol.Unique()) + schema(hosts) + return value + + +_DOMAIN_SCHEMA = get_domain_schema(with_fake_none=False) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, [vol.Schema(_DOMAIN_SCHEMA)], _all_unique_profiles + ) + }, + extra=vol.ALLOW_EXTRA, +) + + async def async_setup(hass, config): """Import integration from config.""" diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index ddff3537..9006841c 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -1,51 +1,18 @@ """Config flow for Coronavirus integration.""" import logging - -import voluptuous as vol +from copy import copy import homeassistant.helpers.config_validation as cv +import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.light import VALID_TRANSITION from homeassistant.core import callback from .const import ( - CONF_DISABLE_BRIGHTNESS_ADJUST, - CONF_DISABLE_ENTITY, - CONF_DISABLE_STATE, - CONF_INITIAL_TRANSITION, - CONF_INTERVAL, - CONF_LIGHTS, - CONF_MAX_BRIGHTNESS, - CONF_MAX_COLOR_TEMP, - CONF_MIN_BRIGHTNESS, - CONF_MIN_COLOR_TEMP, - CONF_ONLY_ONCE, - CONF_SLEEP_BRIGHTNESS, - CONF_SLEEP_COLOR_TEMP, - CONF_SLEEP_ENTITY, - CONF_SLEEP_STATE, - CONF_SUNRISE_OFFSET, - CONF_SUNRISE_TIME, - CONF_SUNSET_OFFSET, - CONF_SUNSET_TIME, - CONF_TRANSITION, - DEFAULT_DISABLE_BRIGHTNESS_ADJUST, - DEFAULT_INITIAL_TRANSITION, - DEFAULT_INTERVAL, - DEFAULT_LIGHTS, - DEFAULT_MAX_BRIGHTNESS, - DEFAULT_MAX_COLOR_TEMP, - DEFAULT_MIN_BRIGHTNESS, - DEFAULT_MIN_COLOR_TEMP, - DEFAULT_ONLY_ONCE, - DEFAULT_SLEEP_BRIGHTNESS, - DEFAULT_SLEEP_COLOR_TEMP, - DEFAULT_SUNRISE_OFFSET, - DEFAULT_SUNSET_OFFSET, - DEFAULT_TRANSITION, DOMAIN, EXTRA_VALIDATION, FAKE_NONE, + VALIDATION_TUPLES, + get_domain_schema, ) _LOGGER = logging.getLogger(__name__) @@ -71,6 +38,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_import(self, user_input=None): + """Handle configuration by yaml file.""" + _DOMAIN_SCHEMA = get_domain_schema(with_fake_none=True) + schema = {k: v for k, v in _DOMAIN_SCHEMA.items() if k in user_input} + vol.Schema(schema)(user_input) + _LOGGER.error(str(user_input) + str(schema)) + return self.async_create_entry(title="", data=user_input) + @staticmethod @callback def async_get_options_flow(config_entry): @@ -78,6 +53,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return OptionsFlowHandler(config_entry) +def validate_options(user_input, errors): + for key, validate in EXTRA_VALIDATION.items(): + # these are unserializable validators + try: + value = user_input.get(key) + if value is not None and value != FAKE_NONE: + validate(user_input[key]) + except vol.Invalid: + _LOGGER.exception("Configuration option %s=%s is incorrect", key, value) + errors["base"] = "option_error" + + class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for Adaptive Lighting.""" @@ -89,46 +76,16 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Handle options flow.""" errors = {} if user_input is not None: - for key, validate in EXTRA_VALIDATION: - # these are unserializable validators - try: - value = user_input.get(key) - if value is not None and value != FAKE_NONE: - validate(user_input[key]) - except vol.Invalid: - _LOGGER.exception( - "Configuration option %s=%s is incorrect", key, value - ) - errors["base"] = "option_error" + validate_options(user_input, errors) if not errors: return self.async_create_entry(title="", data=user_input) - options = self.config_entry.options - int_between = lambda a, b: vol.All(vol.Coerce(int), vol.Range(min=a, max=b)) all_lights = cv.multi_select(self.hass.states.async_entity_ids("light")) - validation_tuples = [ - (CONF_LIGHTS, DEFAULT_LIGHTS, all_lights), - (CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST, bool), - (CONF_DISABLE_ENTITY, FAKE_NONE, str), - (CONF_DISABLE_STATE, FAKE_NONE, str), - (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), - (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), - (CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)), - (CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP, int_between(1000, 10000)), - (CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS, int_between(1, 100)), - (CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP, int_between(1000, 10000)), - (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), - (CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)), - (CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP, int_between(1000, 10000)), - (CONF_SLEEP_ENTITY, FAKE_NONE, str), - (CONF_SLEEP_STATE, FAKE_NONE, str), - (CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int), - (CONF_SUNRISE_TIME, FAKE_NONE, str), - (CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int), - (CONF_SUNSET_TIME, FAKE_NONE, str), - (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), - ] + validation_tuples = copy(VALIDATION_TUPLES) + lights_tuple = (*validation_tuples[0][:-1], all_lights) + validation_tuples[0] = lights_tuple + options = self.config_entry.options options_schema = vol.Schema( { vol.Optional(key, default=options.get(key, default)): validation diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 5f03f698..63b16939 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,6 +1,6 @@ -import voluptuous as vol - import homeassistant.helpers.config_validation as cv +import voluptuous as vol +from homeassistant.components.light import VALID_TRANSITION ICON = "mdi:theme-light-dark" @@ -36,14 +36,60 @@ CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 UNDO_UPDATE_LISTENER = "undo_update_listener" FAKE_NONE = "None" # TODO: use `from homeassistant.const import ENTITY_MATCH_NONE`? -EXTRA_VALIDATION = [ # these validators cannot be serialized - (CONF_DISABLE_ENTITY, cv.entity_id), - (CONF_DISABLE_STATE, vol.All(cv.ensure_list_csv, [cv.string])), - (CONF_INTERVAL, cv.time_period), - (CONF_SLEEP_ENTITY, cv.entity_id), - (CONF_SLEEP_STATE, vol.All(cv.ensure_list_csv, [cv.string])), - (CONF_SUNRISE_OFFSET, cv.time_period), - (CONF_SUNRISE_TIME, cv.time), - (CONF_SUNSET_OFFSET, cv.time_period), - (CONF_SUNSET_TIME, cv.time), + +def int_between(a, b): + return vol.All(vol.Coerce(int), vol.Range(min=a, max=b)) + + +VALIDATION_TUPLES = [ + (CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), + (CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST, bool), + (CONF_DISABLE_ENTITY, FAKE_NONE, str), + (CONF_DISABLE_STATE, FAKE_NONE, str), + (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), + (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), + (CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)), + (CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP, int_between(1000, 10000)), + (CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS, int_between(1, 100)), + (CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP, int_between(1000, 10000)), + (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), + (CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)), + (CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP, int_between(1000, 10000)), + (CONF_SLEEP_ENTITY, FAKE_NONE, str), + (CONF_SLEEP_STATE, FAKE_NONE, str), + (CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int), + (CONF_SUNRISE_TIME, FAKE_NONE, str), + (CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int), + (CONF_SUNSET_TIME, FAKE_NONE, str), + (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), ] + +EXTRA_VALIDATION = { # these validators cannot be serialized + CONF_DISABLE_ENTITY: cv.entity_id, + CONF_DISABLE_STATE: vol.All(cv.ensure_list_csv, [cv.string]), + CONF_INTERVAL: cv.time_period, + CONF_SLEEP_ENTITY: cv.entity_id, + CONF_SLEEP_STATE: vol.All(cv.ensure_list_csv, [cv.string]), + CONF_SUNRISE_OFFSET: cv.time_period, + CONF_SUNRISE_TIME: cv.time, + CONF_SUNSET_OFFSET: cv.time_period, + CONF_SUNSET_TIME: cv.time, +} + + +def get_domain_schema(with_fake_none=False): + validation_tuples = [ + (key, default, EXTRA_VALIDATION.get(key, validation)) + for key, default, validation in VALIDATION_TUPLES + ] + validation_tuples.append((CONF_NAME, DEFAULT_NAME, cv.string)) + + def replace_none(x): + if not with_fake_none and x == FAKE_NONE: + return vol.UNDEFINED + return x + + return { + vol.Optional(key, default=replace_none(default)): validation + for key, default, validation in validation_tuples + } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 60b3e34a..3fc771cc 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1,10 +1,8 @@ -# CHECK OUT THE VIZIO COMPONENT! """Adaptive Lighting Component for Home-Assistant.""" import asyncio import bisect import logging -from copy import deepcopy from datetime import timedelta import homeassistant.util.dt as dt_util @@ -128,11 +126,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._entity_id = f"switch.{DOMAIN}_{slugify(name)}" self._icon = ICON + data = {**config_entry.options, **config_entry.data} opts = { # replace "None" -> None - key: value if value != FAKE_NONE else None - for key, value in config_entry.options.items() + key: value if value != FAKE_NONE else None for key, value in data.items() } - for key, validate in EXTRA_VALIDATION: # Fix the types of the inputs + for key, validate in EXTRA_VALIDATION.items(): # Fix the types of the inputs value = opts.get(key) if value is not None: opts[key] = validate(value) From d4123ee20adef7447a0be6f17debe8da8e5b87d8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 23 Sep 2020 19:09:35 +0200 Subject: [PATCH 0171/1077] use defaults --- .../adaptive_lighting/__init__.py | 18 ++--- .../adaptive_lighting/config_flow.py | 10 +-- custom_components/adaptive_lighting/switch.py | 71 +++++++++---------- 3 files changed, 50 insertions(+), 49 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 0ab50803..a8557561 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -48,15 +48,15 @@ def _all_unique_profiles(value): return value -_DOMAIN_SCHEMA = get_domain_schema(with_fake_none=False) -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, [vol.Schema(_DOMAIN_SCHEMA)], _all_unique_profiles - ) - }, - extra=vol.ALLOW_EXTRA, -) +# _DOMAIN_SCHEMA = get_domain_schema(with_fake_none=False) +# CONFIG_SCHEMA = vol.Schema( +# { +# DOMAIN: vol.All( +# cv.ensure_list, [vol.Schema(_DOMAIN_SCHEMA)], _all_unique_profiles +# ) +# }, +# extra=vol.ALLOW_EXTRA, +# ) async def async_setup(hass, config): diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 9006841c..a15fcb25 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -40,11 +40,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input=None): """Handle configuration by yaml file.""" - _DOMAIN_SCHEMA = get_domain_schema(with_fake_none=True) + _DOMAIN_SCHEMA = get_domain_schema(with_fake_none=False) schema = {k: v for k, v in _DOMAIN_SCHEMA.items() if k in user_input} vol.Schema(schema)(user_input) _LOGGER.error(str(user_input) + str(schema)) - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(title=user_input["name"], data=user_input) @staticmethod @callback @@ -59,7 +59,7 @@ def validate_options(user_input, errors): try: value = user_input.get(key) if value is not None and value != FAKE_NONE: - validate(user_input[key]) + validate(value) except vol.Invalid: _LOGGER.exception("Configuration option %s=%s is incorrect", key, value) errors["base"] = "option_error" @@ -78,7 +78,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: validate_options(user_input, errors) if not errors: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry( + title=user_input["name"], data=user_input + ) all_lights = cv.multi_select(self.hass.states.async_entity_ids("light")) validation_tuples = copy(VALIDATION_TUPLES) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 3fc771cc..bc0857a1 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -84,6 +84,7 @@ from .const import ( ICON, SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, + VALIDATION_TUPLES, ) _SUPPORT_OPTS = { @@ -116,53 +117,51 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities([switch], update_before_add=True) +def replace_none(value): + """Replaces "None" -> None.""" + return value if value != FAKE_NONE else None + + class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" def __init__(self, hass, name, config_entry): """Initialize the Adaptive Lighting switch.""" + _LOGGER.error(f"title={config_entry.title}") self.hass = hass self._name = name self._entity_id = f"switch.{DOMAIN}_{slugify(name)}" self._icon = ICON - data = {**config_entry.options, **config_entry.data} - opts = { # replace "None" -> None - key: value if value != FAKE_NONE else None for key, value in data.items() - } - for key, validate in EXTRA_VALIDATION.items(): # Fix the types of the inputs - value = opts.get(key) + defaults = {key: default for key, default, _ in VALIDATION_TUPLES} + data = dict(defaults, **config_entry.options, **config_entry.data) + data = {key: replace_none(value) for key, value in data.items()} + for key, validate in EXTRA_VALIDATION.items(): + # Fix the types of the inputs + value = data.get(key) if value is not None: - opts[key] = validate(value) + data[key] = validate(value) - self._lights = opts.get(CONF_LIGHTS, DEFAULT_LIGHTS) - self._disable_brightness_adjust = opts.get( - CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST - ) - self._disable_entity = opts.get(CONF_DISABLE_ENTITY) - self._disable_state = opts.get(CONF_DISABLE_STATE) - self._initial_transition = opts.get( - CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION - ) - self._interval = opts.get(CONF_INTERVAL, DEFAULT_INTERVAL) - self._max_brightness = opts.get(CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS) - self._max_color_temp = opts.get(CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP) - self._min_brightness = opts.get(CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS) - self._min_color_temp = opts.get(CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP) - self._only_once = opts.get(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE) - self._sleep_brightness = opts.get( - CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS - ) - self._sleep_color_temp = opts.get( - CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP - ) - self._sleep_entity = opts.get(CONF_SLEEP_ENTITY) - self._sleep_state = opts.get(CONF_SLEEP_STATE) - self._sunrise_offset = opts.get(CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET) - self._sunrise_time = opts.get(CONF_SUNRISE_TIME) - self._sunset_offset = opts.get(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET) - self._sunset_time = opts.get(CONF_SUNSET_TIME) - self._transition = opts.get(CONF_TRANSITION, DEFAULT_TRANSITION) + self._lights = data[CONF_LIGHTS] + self._disable_brightness_adjust = data[CONF_DISABLE_BRIGHTNESS_ADJUST] + self._disable_entity = data[CONF_DISABLE_ENTITY] + self._disable_state = data[CONF_DISABLE_STATE] + self._initial_transition = data[CONF_INITIAL_TRANSITION] + self._interval = data[CONF_INTERVAL] + self._max_brightness = data[CONF_MAX_BRIGHTNESS] + self._max_color_temp = data[CONF_MAX_COLOR_TEMP] + self._min_brightness = data[CONF_MIN_BRIGHTNESS] + self._min_color_temp = data[CONF_MIN_COLOR_TEMP] + self._only_once = data[CONF_ONLY_ONCE] + self._sleep_brightness = data[CONF_SLEEP_BRIGHTNESS] + self._sleep_color_temp = data[CONF_SLEEP_COLOR_TEMP] + self._sleep_entity = data[CONF_SLEEP_ENTITY] + self._sleep_state = data[CONF_SLEEP_STATE] + self._sunrise_offset = data[CONF_SUNRISE_OFFSET] + self._sunrise_time = data[CONF_SUNRISE_TIME] + self._sunset_offset = data[CONF_SUNSET_OFFSET] + self._sunset_time = data[CONF_SUNSET_TIME] + self._transition = data[CONF_TRANSITION] # Initialize attributes that will be set in self._update_attrs self._percent = None @@ -176,7 +175,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set and unset tracker in async_turn_on and async_turn_off self.unsub_tracker = None _LOGGER.error( - f"Setting up with {self._lights}: config_entry.data: {config_entry.data}, config_entry.options: {config_entry.options}, converted to {opts}" + f"Setting up with {self._lights}: config_entry.data: {config_entry.data}, config_entry.options: {config_entry.options}, converted to {data}" ) @property From b67b25da23f069221f8051843cfd3243e7cdce10 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 24 Sep 2020 20:41:24 +0200 Subject: [PATCH 0172/1077] coerce to serializable type --- .../adaptive_lighting/__init__.py | 22 ++++----- .../adaptive_lighting/config_flow.py | 7 +-- custom_components/adaptive_lighting/const.py | 45 +++++++++++++------ custom_components/adaptive_lighting/switch.py | 16 +------ 4 files changed, 49 insertions(+), 41 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index a8557561..e1ad4718 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -29,8 +29,9 @@ Technical notes: I had to make a lot of assumptions when writing this app import asyncio import logging -import homeassistant.helpers.config_validation as cv import voluptuous as vol + +import homeassistant.helpers.config_validation as cv from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from .const import CONF_NAME, DOMAIN, UNDO_UPDATE_LISTENER, get_domain_schema @@ -48,15 +49,16 @@ def _all_unique_profiles(value): return value -# _DOMAIN_SCHEMA = get_domain_schema(with_fake_none=False) -# CONFIG_SCHEMA = vol.Schema( -# { -# DOMAIN: vol.All( -# cv.ensure_list, [vol.Schema(_DOMAIN_SCHEMA)], _all_unique_profiles -# ) -# }, -# extra=vol.ALLOW_EXTRA, -# ) +_DOMAIN_SCHEMA = get_domain_schema(yaml=True) +_LOGGER.error(_DOMAIN_SCHEMA) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, [vol.Schema(_DOMAIN_SCHEMA)], _all_unique_profiles + ) + }, + extra=vol.ALLOW_EXTRA, +) async def async_setup(hass, config): diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index a15fcb25..67b772d5 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -2,8 +2,9 @@ import logging from copy import copy -import homeassistant.helpers.config_validation as cv import voluptuous as vol + +import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.core import callback @@ -40,7 +41,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input=None): """Handle configuration by yaml file.""" - _DOMAIN_SCHEMA = get_domain_schema(with_fake_none=False) + _DOMAIN_SCHEMA = get_domain_schema(yaml=False) schema = {k: v for k, v in _DOMAIN_SCHEMA.items() if k in user_input} vol.Schema(schema)(user_input) _LOGGER.error(str(user_input) + str(schema)) @@ -54,7 +55,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def validate_options(user_input, errors): - for key, validate in EXTRA_VALIDATION.items(): + for key, (validate, coerce) in EXTRA_VALIDATION.items(): # these are unserializable validators try: value = user_input.get(key) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 63b16939..dd3c30e3 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,5 +1,6 @@ -import homeassistant.helpers.config_validation as cv import voluptuous as vol + +import homeassistant.helpers.config_validation as cv from homeassistant.components.light import VALID_TRANSITION ICON = "mdi:theme-light-dark" @@ -64,22 +65,40 @@ VALIDATION_TUPLES = [ (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), ] -EXTRA_VALIDATION = { # these validators cannot be serialized - CONF_DISABLE_ENTITY: cv.entity_id, - CONF_DISABLE_STATE: vol.All(cv.ensure_list_csv, [cv.string]), - CONF_INTERVAL: cv.time_period, - CONF_SLEEP_ENTITY: cv.entity_id, - CONF_SLEEP_STATE: vol.All(cv.ensure_list_csv, [cv.string]), - CONF_SUNRISE_OFFSET: cv.time_period, - CONF_SUNRISE_TIME: cv.time, - CONF_SUNSET_OFFSET: cv.time_period, - CONF_SUNSET_TIME: cv.time, + +def timedelta_as_int(value): + return value.total_seconds() + + +def join_strings(lst): + return ",".join(lst) + + +# these validators cannot be serialized +EXTRA_VALIDATION = { + CONF_DISABLE_ENTITY: (cv.entity_id, str), + CONF_DISABLE_STATE: (vol.All(cv.ensure_list_csv, [cv.string]), join_strings), + CONF_INTERVAL: (cv.time_period, timedelta_as_int), + CONF_SLEEP_ENTITY: (cv.entity_id, str), + CONF_SLEEP_STATE: (vol.All(cv.ensure_list_csv, [cv.string]), join_strings), + CONF_SUNRISE_OFFSET: (cv.time_period, timedelta_as_int), + CONF_SUNRISE_TIME: (cv.time, str), + CONF_SUNSET_OFFSET: (cv.time_period, timedelta_as_int), + CONF_SUNSET_TIME: (cv.time, str), } -def get_domain_schema(with_fake_none=False): +def get_domain_schema(with_fake_none=False, yaml=False): + def get_validation(key, validation): + validation, coerce = EXTRA_VALIDATION.get(key, (validation, None)) + return ( + vol.All(validation, vol.Coerce(coerce)) + if yaml and coerce is not None + else validation + ) + validation_tuples = [ - (key, default, EXTRA_VALIDATION.get(key, validation)) + (key, default, get_validation(key, validation)) for key, default, validation in VALIDATION_TUPLES ] validation_tuples.append((CONF_NAME, DEFAULT_NAME, cv.string)) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index bc0857a1..0f599f41 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -64,20 +64,6 @@ from .const import ( CONF_SUNSET_OFFSET, CONF_SUNSET_TIME, CONF_TRANSITION, - DEFAULT_DISABLE_BRIGHTNESS_ADJUST, - DEFAULT_INITIAL_TRANSITION, - DEFAULT_INTERVAL, - DEFAULT_LIGHTS, - DEFAULT_MAX_BRIGHTNESS, - DEFAULT_MAX_COLOR_TEMP, - DEFAULT_MIN_BRIGHTNESS, - DEFAULT_MIN_COLOR_TEMP, - DEFAULT_ONLY_ONCE, - DEFAULT_SLEEP_BRIGHTNESS, - DEFAULT_SLEEP_COLOR_TEMP, - DEFAULT_SUNRISE_OFFSET, - DEFAULT_SUNSET_OFFSET, - DEFAULT_TRANSITION, DOMAIN, EXTRA_VALIDATION, FAKE_NONE, @@ -136,7 +122,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): defaults = {key: default for key, default, _ in VALIDATION_TUPLES} data = dict(defaults, **config_entry.options, **config_entry.data) data = {key: replace_none(value) for key, value in data.items()} - for key, validate in EXTRA_VALIDATION.items(): + for key, (validate, coerce) in EXTRA_VALIDATION.items(): # Fix the types of the inputs value = data.get(key) if value is not None: From 5168fe91eddbac259ea5c70e328259c7a9fea62c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 24 Sep 2020 20:44:42 +0200 Subject: [PATCH 0173/1077] fix title --- custom_components/adaptive_lighting/config_flow.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 67b772d5..a59d00ec 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -79,9 +79,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: validate_options(user_input, errors) if not errors: - return self.async_create_entry( - title=user_input["name"], data=user_input - ) + return self.async_create_entry(title="", data=user_input) all_lights = cv.multi_select(self.hass.states.async_entity_ids("light")) validation_tuples = copy(VALIDATION_TUPLES) From e9ee147e20d52c51389ba0f82444256429163036 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 24 Sep 2020 20:56:01 +0200 Subject: [PATCH 0174/1077] fix data in switch --- custom_components/adaptive_lighting/__init__.py | 1 - custom_components/adaptive_lighting/config_flow.py | 1 - custom_components/adaptive_lighting/switch.py | 10 +++++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index e1ad4718..c4a91076 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -50,7 +50,6 @@ def _all_unique_profiles(value): _DOMAIN_SCHEMA = get_domain_schema(yaml=True) -_LOGGER.error(_DOMAIN_SCHEMA) CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index a59d00ec..397a3975 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -44,7 +44,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _DOMAIN_SCHEMA = get_domain_schema(yaml=False) schema = {k: v for k, v in _DOMAIN_SCHEMA.items() if k in user_input} vol.Schema(schema)(user_input) - _LOGGER.error(str(user_input) + str(schema)) return self.async_create_entry(title=user_input["name"], data=user_input) @staticmethod diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 0f599f41..7c1c2f3a 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -3,6 +3,7 @@ import asyncio import bisect import logging +from copy import deepcopy from datetime import timedelta import homeassistant.util.dt as dt_util @@ -113,14 +114,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): def __init__(self, hass, name, config_entry): """Initialize the Adaptive Lighting switch.""" - _LOGGER.error(f"title={config_entry.title}") self.hass = hass self._name = name self._entity_id = f"switch.{DOMAIN}_{slugify(name)}" self._icon = ICON defaults = {key: default for key, default, _ in VALIDATION_TUPLES} - data = dict(defaults, **config_entry.options, **config_entry.data) + data = deepcopy(defaults) + data.update(config_entry.options) # come from options flow + data.update(config_entry.data) # all yaml settings come from data data = {key: replace_none(value) for key, value in data.items()} for key, (validate, coerce) in EXTRA_VALIDATION.items(): # Fix the types of the inputs @@ -161,7 +163,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set and unset tracker in async_turn_on and async_turn_off self.unsub_tracker = None _LOGGER.error( - f"Setting up with {self._lights}: config_entry.data: {config_entry.data}, config_entry.options: {config_entry.options}, converted to {data}" + f"Setting up with '{self._lights}'," + f" config_entry.data: '{config_entry.data}'," + f" config_entry.options: '{config_entry.options}', converted to '{data}'." ) @property From 28389ebebe8c647af8192693c0fe02272f2e5ee4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 24 Sep 2020 21:01:03 +0200 Subject: [PATCH 0175/1077] abort if already setup --- custom_components/adaptive_lighting/config_flow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 397a3975..b86fe3ea 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -44,6 +44,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _DOMAIN_SCHEMA = get_domain_schema(yaml=False) schema = {k: v for k, v in _DOMAIN_SCHEMA.items() if k in user_input} vol.Schema(schema)(user_input) + await self.async_set_unique_id(user_input["name"]) + self._abort_if_unique_id_configured() return self.async_create_entry(title=user_input["name"], data=user_input) @staticmethod From 2c0ab9d9294b64f15d21ebef98a136c440a9b2cc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 24 Sep 2020 23:28:03 +0200 Subject: [PATCH 0176/1077] do not show options if managed via YAML --- custom_components/adaptive_lighting/config_flow.py | 6 ++++-- custom_components/adaptive_lighting/strings.json | 2 +- custom_components/adaptive_lighting/translations/en.json | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index b86fe3ea..097ea5be 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -76,6 +76,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Handle options flow.""" + conf = self.config_entry + if conf.source == config_entries.SOURCE_IMPORT: + return self.async_show_form(step_id="init", data_schema={}) errors = {} if user_input is not None: validate_options(user_input, errors) @@ -87,10 +90,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): lights_tuple = (*validation_tuples[0][:-1], all_lights) validation_tuples[0] = lights_tuple - options = self.config_entry.options options_schema = vol.Schema( { - vol.Optional(key, default=options.get(key, default)): validation + vol.Optional(key, default=conf.options.get(key, default)): validation for key, default, validation in validation_tuples } ) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 0e02634a..13851edd 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -18,7 +18,7 @@ "step": { "init": { "title": "Adaptive Lighting options", - "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings.", + "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", "data": { "lights_brightness": "lights_brightness", "lights_mired": "lights_mired", diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 0e02634a..13851edd 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -18,7 +18,7 @@ "step": { "init": { "title": "Adaptive Lighting options", - "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings.", + "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", "data": { "lights_brightness": "lights_brightness", "lights_mired": "lights_mired", From 59f8f743ab3510b1fe9319cc626be4b53ef98adc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 24 Sep 2020 23:42:21 +0200 Subject: [PATCH 0177/1077] simplify setting up DOMAIN_SCHEMA --- .../adaptive_lighting/__init__.py | 9 +---- .../adaptive_lighting/config_flow.py | 11 +----- custom_components/adaptive_lighting/const.py | 37 +++++++++---------- 3 files changed, 21 insertions(+), 36 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index c4a91076..99ed5563 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -34,7 +34,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from .const import CONF_NAME, DOMAIN, UNDO_UPDATE_LISTENER, get_domain_schema +from .const import _DOMAIN_SCHEMA, CONF_NAME, DOMAIN, UNDO_UPDATE_LISTENER _LOGGER = logging.getLogger(__name__) @@ -49,13 +49,8 @@ def _all_unique_profiles(value): return value -_DOMAIN_SCHEMA = get_domain_schema(yaml=True) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, [vol.Schema(_DOMAIN_SCHEMA)], _all_unique_profiles - ) - }, + {DOMAIN: vol.All(cv.ensure_list, [_DOMAIN_SCHEMA], _all_unique_profiles)}, extra=vol.ALLOW_EXTRA, ) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 097ea5be..c9da026d 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -8,13 +8,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.core import callback -from .const import ( - DOMAIN, - EXTRA_VALIDATION, - FAKE_NONE, - VALIDATION_TUPLES, - get_domain_schema, -) +from .const import DOMAIN, EXTRA_VALIDATION, FAKE_NONE, VALIDATION_TUPLES _LOGGER = logging.getLogger(__name__) @@ -41,9 +35,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input=None): """Handle configuration by yaml file.""" - _DOMAIN_SCHEMA = get_domain_schema(yaml=False) - schema = {k: v for k, v in _DOMAIN_SCHEMA.items() if k in user_input} - vol.Schema(schema)(user_input) await self.async_set_unique_id(user_input["name"]) self._abort_if_unique_id_configured() return self.async_create_entry(title=user_input["name"], data=user_input) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index dd3c30e3..86e5b9d8 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -74,7 +74,8 @@ def join_strings(lst): return ",".join(lst) -# these validators cannot be serialized +# conf_option: (validator, coerce) tuples +# these validators cannot be serialized but can be serialized when coerced by coerce. EXTRA_VALIDATION = { CONF_DISABLE_ENTITY: (cv.entity_id, str), CONF_DISABLE_STATE: (vol.All(cv.ensure_list_csv, [cv.string]), join_strings), @@ -88,27 +89,25 @@ EXTRA_VALIDATION = { } -def get_domain_schema(with_fake_none=False, yaml=False): - def get_validation(key, validation): - validation, coerce = EXTRA_VALIDATION.get(key, (validation, None)) - return ( - vol.All(validation, vol.Coerce(coerce)) - if yaml and coerce is not None - else validation - ) +def maybe_coerse(key, validation): + validation, coerce = EXTRA_VALIDATION.get(key, (validation, None)) + if coerce is not None: + return vol.All(validation, vol.Coerce(coerce)) + return validation - validation_tuples = [ - (key, default, get_validation(key, validation)) - for key, default, validation in VALIDATION_TUPLES - ] - validation_tuples.append((CONF_NAME, DEFAULT_NAME, cv.string)) - def replace_none(x): - if not with_fake_none and x == FAKE_NONE: - return vol.UNDEFINED - return x +def replace_none(x): + return x if x != FAKE_NONE else vol.UNDEFINED - return { + +validation_tuples = [ + (key, default, maybe_coerse(key, validation)) + for key, default, validation in VALIDATION_TUPLES +] + [(CONF_NAME, DEFAULT_NAME, cv.string)] + +_DOMAIN_SCHEMA = vol.Schema( + { vol.Optional(key, default=replace_none(default)): validation for key, default, validation in validation_tuples } +) From 8618cd89b5d810f709326fc4a67869c9f6044f23 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 24 Sep 2020 23:47:22 +0200 Subject: [PATCH 0178/1077] create validate function --- custom_components/adaptive_lighting/switch.py | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 7c1c2f3a..594b69c2 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -109,6 +109,20 @@ def replace_none(value): return value if value != FAKE_NONE else None +def validate(config_entry): + """Gets the options and data from the config_entry and adds defaults.""" + defaults = {key: default for key, default, _ in VALIDATION_TUPLES} + data = deepcopy(defaults) + data.update(config_entry.options) # come from options flow + data.update(config_entry.data) # all yaml settings come from data + data = {key: replace_none(value) for key, value in data.items()} + for key, (validate, _) in EXTRA_VALIDATION.items(): + value = data.get(key) + if value is not None: + data[key] = validate(value) # Fix the types of the inputs + return data + + class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" @@ -119,17 +133,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._entity_id = f"switch.{DOMAIN}_{slugify(name)}" self._icon = ICON - defaults = {key: default for key, default, _ in VALIDATION_TUPLES} - data = deepcopy(defaults) - data.update(config_entry.options) # come from options flow - data.update(config_entry.data) # all yaml settings come from data - data = {key: replace_none(value) for key, value in data.items()} - for key, (validate, coerce) in EXTRA_VALIDATION.items(): - # Fix the types of the inputs - value = data.get(key) - if value is not None: - data[key] = validate(value) - + data = validate(config_entry) self._lights = data[CONF_LIGHTS] self._disable_brightness_adjust = data[CONF_DISABLE_BRIGHTNESS_ADJUST] self._disable_entity = data[CONF_DISABLE_ENTITY] From be6dd979204ce374f224447bc346f1529ef6214e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 24 Sep 2020 23:51:41 +0200 Subject: [PATCH 0179/1077] don't pass name into AdaptiveSwitch --- custom_components/adaptive_lighting/switch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 594b69c2..a4f0c4be 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -96,10 +96,10 @@ SCAN_INTERVAL = timedelta(seconds=10) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the AdaptiveLighting switch.""" - name = config_entry.data[CONF_NAME] - switch = AdaptiveSwitch(hass, name, config_entry) + switch = AdaptiveSwitch(hass, config_entry) if DOMAIN not in hass.data: hass.data[DOMAIN] = {} + name = config_entry.data[CONF_NAME] hass.data[DOMAIN][name] = switch async_add_entities([switch], update_before_add=True) @@ -126,14 +126,14 @@ def validate(config_entry): class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" - def __init__(self, hass, name, config_entry): + def __init__(self, hass, config_entry): """Initialize the Adaptive Lighting switch.""" self.hass = hass - self._name = name self._entity_id = f"switch.{DOMAIN}_{slugify(name)}" self._icon = ICON data = validate(config_entry) + self._name = data[CONF_NAME] self._lights = data[CONF_LIGHTS] self._disable_brightness_adjust = data[CONF_DISABLE_BRIGHTNESS_ADJUST] self._disable_entity = data[CONF_DISABLE_ENTITY] From 049fb8b98318b33d4a5603b94353c7346675500e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 24 Sep 2020 23:54:14 +0200 Subject: [PATCH 0180/1077] rename FAKE_NONE -> NONE_STR --- .../adaptive_lighting/config_flow.py | 4 ++-- custom_components/adaptive_lighting/const.py | 16 ++++++++-------- custom_components/adaptive_lighting/switch.py | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index c9da026d..8e2b925a 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -8,7 +8,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.core import callback -from .const import DOMAIN, EXTRA_VALIDATION, FAKE_NONE, VALIDATION_TUPLES +from .const import DOMAIN, EXTRA_VALIDATION, NONE_STR, VALIDATION_TUPLES _LOGGER = logging.getLogger(__name__) @@ -51,7 +51,7 @@ def validate_options(user_input, errors): # these are unserializable validators try: value = user_input.get(key) - if value is not None and value != FAKE_NONE: + if value is not None and value != NONE_STR: validate(value) except vol.Invalid: _LOGGER.exception("Configuration option %s=%s is incorrect", key, value) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 86e5b9d8..17aeb4f6 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -35,7 +35,7 @@ CONF_SUNSET_TIME = "sunset_time" CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 UNDO_UPDATE_LISTENER = "undo_update_listener" -FAKE_NONE = "None" # TODO: use `from homeassistant.const import ENTITY_MATCH_NONE`? +NONE_STR = "None" # TODO: use `from homeassistant.const import ENTITY_MATCH_NONE`? def int_between(a, b): @@ -45,8 +45,8 @@ def int_between(a, b): VALIDATION_TUPLES = [ (CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), (CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST, bool), - (CONF_DISABLE_ENTITY, FAKE_NONE, str), - (CONF_DISABLE_STATE, FAKE_NONE, str), + (CONF_DISABLE_ENTITY, NONE_STR, str), + (CONF_DISABLE_STATE, NONE_STR, str), (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), (CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)), @@ -56,12 +56,12 @@ VALIDATION_TUPLES = [ (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), (CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)), (CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP, int_between(1000, 10000)), - (CONF_SLEEP_ENTITY, FAKE_NONE, str), - (CONF_SLEEP_STATE, FAKE_NONE, str), + (CONF_SLEEP_ENTITY, NONE_STR, str), + (CONF_SLEEP_STATE, NONE_STR, str), (CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int), - (CONF_SUNRISE_TIME, FAKE_NONE, str), + (CONF_SUNRISE_TIME, NONE_STR, str), (CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int), - (CONF_SUNSET_TIME, FAKE_NONE, str), + (CONF_SUNSET_TIME, NONE_STR, str), (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), ] @@ -97,7 +97,7 @@ def maybe_coerse(key, validation): def replace_none(x): - return x if x != FAKE_NONE else vol.UNDEFINED + return x if x != NONE_STR else vol.UNDEFINED validation_tuples = [ diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index a4f0c4be..c5e8f1b8 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -67,8 +67,8 @@ from .const import ( CONF_TRANSITION, DOMAIN, EXTRA_VALIDATION, - FAKE_NONE, ICON, + NONE_STR, SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, VALIDATION_TUPLES, @@ -106,7 +106,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def replace_none(value): """Replaces "None" -> None.""" - return value if value != FAKE_NONE else None + return value if value != NONE_STR else None def validate(config_entry): From 1aa6f8f3455b2a90c77f7eb423552f32062c50a9 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 25 Sep 2020 00:00:35 +0200 Subject: [PATCH 0181/1077] set name correctly --- custom_components/adaptive_lighting/switch.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index c5e8f1b8..36ad2c5b 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -129,8 +129,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): def __init__(self, hass, config_entry): """Initialize the Adaptive Lighting switch.""" self.hass = hass - self._entity_id = f"switch.{DOMAIN}_{slugify(name)}" - self._icon = ICON data = validate(config_entry) self._name = data[CONF_NAME] @@ -155,6 +153,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._sunset_time = data[CONF_SUNSET_TIME] self._transition = data[CONF_TRANSITION] + # Set other attributes + self._icon = ICON + self._entity_id = f"switch.{DOMAIN}_{slugify(self._name)}" + # Initialize attributes that will be set in self._update_attrs self._percent = None self._brightness = None From bd7edb59c1351fb01ca5807dae207a37a12468e0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 25 Sep 2020 00:11:22 +0200 Subject: [PATCH 0182/1077] simplify validate_options --- .../adaptive_lighting/config_flow.py | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 8e2b925a..6874114b 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -8,7 +8,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.core import callback -from .const import DOMAIN, EXTRA_VALIDATION, NONE_STR, VALIDATION_TUPLES +from .const import CONF_LIGHTS, DOMAIN, EXTRA_VALIDATION, NONE_STR, VALIDATION_TUPLES _LOGGER = logging.getLogger(__name__) @@ -47,7 +47,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def validate_options(user_input, errors): - for key, (validate, coerce) in EXTRA_VALIDATION.items(): + for key, (validate, _) in EXTRA_VALIDATION.items(): # these are unserializable validators try: value = user_input.get(key) @@ -77,17 +77,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_create_entry(title="", data=user_input) all_lights = cv.multi_select(self.hass.states.async_entity_ids("light")) - validation_tuples = copy(VALIDATION_TUPLES) - lights_tuple = (*validation_tuples[0][:-1], all_lights) - validation_tuples[0] = lights_tuple - options_schema = vol.Schema( - { - vol.Optional(key, default=conf.options.get(key, default)): validation - for key, default, validation in validation_tuples - } - ) + options_schema = {} + for name, default, validation in VALIDATION_TUPLES: + key = vol.Optional(name, default=conf.options.get(name, default)) + value = validation if name != CONF_LIGHTS else all_lights + options_schema[key] = value return self.async_show_form( - step_id="init", data_schema=options_schema, errors=errors + step_id="init", data_schema=vol.Schema(options_schema), errors=errors ) From 5b11893583c0f07a33c97eeb6afc28677369c258 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 25 Sep 2020 10:27:02 +0200 Subject: [PATCH 0183/1077] add apply service --- custom_components/adaptive_lighting/const.py | 1 + .../adaptive_lighting/services.yaml | 12 +++++++ custom_components/adaptive_lighting/switch.py | 34 +++++++++++++++++-- 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100755 custom_components/adaptive_lighting/services.yaml diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 17aeb4f6..e8494fe6 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -34,6 +34,7 @@ CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 CONF_SUNSET_TIME = "sunset_time" CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 +SERVICE_APPLY = "apply" UNDO_UPDATE_LISTENER = "undo_update_listener" NONE_STR = "None" # TODO: use `from homeassistant.const import ENTITY_MATCH_NONE`? diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml new file mode 100755 index 00000000..b5535234 --- /dev/null +++ b/custom_components/adaptive_lighting/services.yaml @@ -0,0 +1,12 @@ +apply: + description: Applies the current Adaptive Lighting settings to lights. + fields: + entity_id: + description: entity_id of the Adaptive Lighting switch + example: switch.adaptive_lighting_default + lights: + description: entity_id(s) of lights + example: light.bedroom_ceiling + transition: + description: transition of the lights + example: 10 diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 36ad2c5b..b7b8dd4e 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -5,7 +5,11 @@ import bisect import logging from copy import deepcopy from datetime import timedelta +from functools import partial +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util from homeassistant.components.light import ( ATTR_BRIGHTNESS_PCT, @@ -19,6 +23,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, + VALID_TRANSITION, is_on, ) from homeassistant.components.switch import SwitchEntity @@ -30,6 +35,7 @@ from homeassistant.const import ( SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) +from homeassistant.helpers import entity_platform from homeassistant.helpers.event import ( async_track_state_change, async_track_time_interval, @@ -69,6 +75,7 @@ from .const import ( EXTRA_VALIDATION, ICON, NONE_STR, + SERVICE_APPLY, SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, VALIDATION_TUPLES, @@ -94,6 +101,18 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=10) +async def handle_apply(entity, service_call): + """Handle the entity service apply.""" + if not isinstance(entity, AdaptiveSwitch): + raise ValueError("Apply can only be called for a AdaptiveSwitch.") + _LOGGER.error(str(entity) + str(service_call)) + await entity._adjust_lights( + service_call.data[CONF_LIGHTS], + service_call.data.get(CONF_TRANSITION), + force=True, + ) + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the AdaptiveLighting switch.""" switch = AdaptiveSwitch(hass, config_entry) @@ -101,6 +120,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.data[DOMAIN] = {} name = config_entry.data[CONF_NAME] hass.data[DOMAIN][name] = switch + + # Register `apply` service + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + SERVICE_APPLY, + { + vol.Required(CONF_LIGHTS): cv.entity_ids, + vol.Optional(CONF_TRANSITION): VALID_TRANSITION, + }, + handle_apply, + ) async_add_entities([switch], update_before_add=True) @@ -423,8 +453,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return False return True - async def _adjust_lights(self, lights, transition): - if not self._should_adjust(): + async def _adjust_lights(self, lights, transition, force=False): + if not self._should_adjust() or not force: return tasks = [ await self._adjust_light(light, transition) From 21cc22d42c5540f9d6b83ac1b100d6c33262681d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 25 Sep 2020 10:33:49 +0200 Subject: [PATCH 0184/1077] call even if off --- custom_components/adaptive_lighting/const.py | 4 ++-- custom_components/adaptive_lighting/switch.py | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index e8494fe6..a963efae 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -90,7 +90,7 @@ EXTRA_VALIDATION = { } -def maybe_coerse(key, validation): +def maybe_coerce(key, validation): validation, coerce = EXTRA_VALIDATION.get(key, (validation, None)) if coerce is not None: return vol.All(validation, vol.Coerce(coerce)) @@ -102,7 +102,7 @@ def replace_none(x): validation_tuples = [ - (key, default, maybe_coerse(key, validation)) + (key, default, maybe_coerce(key, validation)) for key, default, validation in VALIDATION_TUPLES ] + [(CONF_NAME, DEFAULT_NAME, cv.string)] diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index b7b8dd4e..c47d6fb8 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -105,12 +105,11 @@ async def handle_apply(entity, service_call): """Handle the entity service apply.""" if not isinstance(entity, AdaptiveSwitch): raise ValueError("Apply can only be called for a AdaptiveSwitch.") - _LOGGER.error(str(entity) + str(service_call)) - await entity._adjust_lights( - service_call.data[CONF_LIGHTS], - service_call.data.get(CONF_TRANSITION), - force=True, - ) + lights = service_call.data[CONF_LIGHTS] + transition = service_call.data.get(CONF_TRANSITION, entity._initial_transition) + tasks = [await entity._adjust_light(light, transition) for light in lights] + if tasks: + await asyncio.wait(tasks) async def async_setup_entry(hass, config_entry, async_add_entities): From ab069de4aabb2c972ff062a7f203585ebaef6e16 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 25 Sep 2020 10:49:58 +0200 Subject: [PATCH 0185/1077] simplify --- custom_components/adaptive_lighting/const.py | 7 ++++--- custom_components/adaptive_lighting/switch.py | 17 +++-------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index a963efae..56cb1079 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -97,8 +97,9 @@ def maybe_coerce(key, validation): return validation -def replace_none(x): - return x if x != NONE_STR else vol.UNDEFINED +def replace_none(value, replace_with=None): + """Replaces "None" -> replace_with.""" + return value if value != NONE_STR else replace_with validation_tuples = [ @@ -108,7 +109,7 @@ validation_tuples = [ _DOMAIN_SCHEMA = vol.Schema( { - vol.Optional(key, default=replace_none(default)): validation + vol.Optional(key, default=replace_none(default, vol.UNDEFINED)): validation for key, default, validation in validation_tuples } ) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index c47d6fb8..00b95565 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -5,7 +5,6 @@ import bisect import logging from copy import deepcopy from datetime import timedelta -from functools import partial import voluptuous as vol @@ -74,11 +73,11 @@ from .const import ( DOMAIN, EXTRA_VALIDATION, ICON, - NONE_STR, SERVICE_APPLY, SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, VALIDATION_TUPLES, + replace_none, ) _SUPPORT_OPTS = { @@ -88,13 +87,8 @@ _SUPPORT_OPTS = { "transition": SUPPORT_TRANSITION, } -_ALLOWED_ORDERS = { - (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT), - (SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT, SUN_EVENT_SUNRISE, SUN_EVENT_NOON), - (SUN_EVENT_MIDNIGHT, SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET), - (SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT, SUN_EVENT_SUNRISE), -} - +_ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT) +_ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))} _LOGGER = logging.getLogger(__name__) @@ -133,11 +127,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities([switch], update_before_add=True) -def replace_none(value): - """Replaces "None" -> None.""" - return value if value != NONE_STR else None - - def validate(config_entry): """Gets the options and data from the config_entry and adds defaults.""" defaults = {key: default for key, default, _ in VALIDATION_TUPLES} From ab61cda80f8b9f5026980bc20636c850fa7d753b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 25 Sep 2020 16:45:10 +0200 Subject: [PATCH 0186/1077] add more options to apply service --- custom_components/adaptive_lighting/const.py | 5 ++- .../adaptive_lighting/services.yaml | 12 ++++-- custom_components/adaptive_lighting/switch.py | 37 ++++++++++++++----- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 56cb1079..3c0def2c 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -34,10 +34,13 @@ CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 CONF_SUNSET_TIME = "sunset_time" CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 -SERVICE_APPLY = "apply" UNDO_UPDATE_LISTENER = "undo_update_listener" NONE_STR = "None" # TODO: use `from homeassistant.const import ENTITY_MATCH_NONE`? +SERVICE_APPLY = "apply" +CONF_COLORS_ONLY = "colors_only" +CONF_ON_LIGHTS_ONLY = "on_lights_only" + def int_between(a, b): return vol.All(vol.Coerce(int), vol.Range(min=a, max=b)) diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index b5535234..e5b2aa18 100755 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -2,11 +2,17 @@ apply: description: Applies the current Adaptive Lighting settings to lights. fields: entity_id: - description: entity_id of the Adaptive Lighting switch + description: entity_id of the Adaptive Lighting switch. example: switch.adaptive_lighting_default lights: - description: entity_id(s) of lights + description: entity_id(s) of lights. example: light.bedroom_ceiling transition: - description: transition of the lights + description: Transition of the lights. example: 10 + colors_only: + description: Only change the color of the lights and leave the brightness as is. + example: false + on_lights_only: + description: Only adjust the lights that are already on, otherwise turn the lights on. + example: false diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 00b95565..fad60388 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -50,6 +50,7 @@ from homeassistant.util.color import ( ) from .const import ( + CONF_COLORS_ONLY, CONF_DISABLE_BRIGHTNESS_ADJUST, CONF_DISABLE_ENTITY, CONF_DISABLE_STATE, @@ -60,6 +61,7 @@ from .const import ( CONF_MAX_COLOR_TEMP, CONF_MIN_BRIGHTNESS, CONF_MIN_COLOR_TEMP, + CONF_ON_LIGHTS_ONLY, CONF_ONLY_ONCE, CONF_SLEEP_BRIGHTNESS, CONF_SLEEP_COLOR_TEMP, @@ -95,13 +97,20 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=10) -async def handle_apply(entity, service_call): +async def handle_apply(switch, service_call): """Handle the entity service apply.""" - if not isinstance(entity, AdaptiveSwitch): + if not isinstance(switch, AdaptiveSwitch): raise ValueError("Apply can only be called for a AdaptiveSwitch.") - lights = service_call.data[CONF_LIGHTS] - transition = service_call.data.get(CONF_TRANSITION, entity._initial_transition) - tasks = [await entity._adjust_light(light, transition) for light in lights] + data = service_call.data + tasks = [ + await switch._adjust_light( + light, + data[CONF_TRANSITION], + data[CONF_COLORS_ONLY], + ) + for light in data[CONF_LIGHTS] + if not data[CONF_ON_LIGHTS_ONLY] or is_on(switch.hass, light) + ] if tasks: await asyncio.wait(tasks) @@ -120,7 +129,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): SERVICE_APPLY, { vol.Required(CONF_LIGHTS): cv.entity_ids, - vol.Optional(CONF_TRANSITION): VALID_TRANSITION, + vol.Optional( + CONF_TRANSITION, default=self._initial_transition + ): VALID_TRANSITION, + vol.Optional(CONF_COLORS_ONLY, default=False): cv.boolean, + vol.Optional(CONF_ON_LIGHTS_ONLY, default=False): cv.boolean, }, handle_apply, ) @@ -411,7 +424,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): and self.hass.states.get(self._disable_entity).state in self._disable_state ) - async def _adjust_light(self, light, transition): + async def _adjust_light(self, light, transition, colors_only): service_data = {ATTR_ENTITY_ID: light} features = self._supported_features(light) @@ -420,7 +433,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): transition = self._transition service_data[ATTR_TRANSITION] = transition - if self._brightness is not None and "brightness" in features: + if ( + self._brightness is not None + and "brightness" in features + and not colors_only + ): service_data[ATTR_BRIGHTNESS_PCT] = self._brightness if "color" in features: @@ -441,8 +458,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return False return True - async def _adjust_lights(self, lights, transition, force=False): - if not self._should_adjust() or not force: + async def _adjust_lights(self, lights, transition): + if not self._should_adjust(): return tasks = [ await self._adjust_light(light, transition) From 34acd03f46acb76c16ac3f04d3418dc22168f53c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 26 Sep 2020 13:01:43 +0200 Subject: [PATCH 0187/1077] rename replace_none -> replace_none_str --- custom_components/adaptive_lighting/__init__.py | 6 +++--- custom_components/adaptive_lighting/const.py | 4 ++-- custom_components/adaptive_lighting/switch.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 99ed5563..f76c52b2 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -15,10 +15,10 @@ hues. Hormone production, brainwave activity, mood and wakefulness are just some of the cognitive functions tied to cyclical natural light. http://en.wikipedia.org/wiki/Zeitgeber -Here's some further reading: +Further reading: -http://www.cambridgeincolour.com/tutorials/sunrise-sunset-calculator.htm -http://en.wikipedia.org/wiki/Color_temperature +- http://www.cambridgeincolour.com/tutorials/sunrise-sunset-calculator.htm +- http://en.wikipedia.org/wiki/Color_temperature Technical notes: I had to make a lot of assumptions when writing this app * There are no considerations for weather or altitude, but does use your diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 3c0def2c..7d649ebb 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -100,7 +100,7 @@ def maybe_coerce(key, validation): return validation -def replace_none(value, replace_with=None): +def replace_none_str(value, replace_with=None): """Replaces "None" -> replace_with.""" return value if value != NONE_STR else replace_with @@ -112,7 +112,7 @@ validation_tuples = [ _DOMAIN_SCHEMA = vol.Schema( { - vol.Optional(key, default=replace_none(default, vol.UNDEFINED)): validation + vol.Optional(key, default=replace_none_str(default, vol.UNDEFINED)): validation for key, default, validation in validation_tuples } ) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index fad60388..8b3d8ced 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -79,7 +79,7 @@ from .const import ( SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, VALIDATION_TUPLES, - replace_none, + replace_none_str, ) _SUPPORT_OPTS = { @@ -146,7 +146,7 @@ def validate(config_entry): data = deepcopy(defaults) data.update(config_entry.options) # come from options flow data.update(config_entry.data) # all yaml settings come from data - data = {key: replace_none(value) for key, value in data.items()} + data = {key: replace_none_str(value) for key, value in data.items()} for key, (validate, _) in EXTRA_VALIDATION.items(): value = data.get(key) if value is not None: From 8f4bd6b2abf9d996385afd41e091e43221780e5d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 26 Sep 2020 13:05:27 +0200 Subject: [PATCH 0188/1077] rename variable --- custom_components/adaptive_lighting/const.py | 4 ++-- custom_components/adaptive_lighting/switch.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 7d649ebb..ba526dfc 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -105,7 +105,7 @@ def replace_none_str(value, replace_with=None): return value if value != NONE_STR else replace_with -validation_tuples = [ +_yaml_validation_tuples = [ (key, default, maybe_coerce(key, validation)) for key, default, validation in VALIDATION_TUPLES ] + [(CONF_NAME, DEFAULT_NAME, cv.string)] @@ -113,6 +113,6 @@ validation_tuples = [ _DOMAIN_SCHEMA = vol.Schema( { vol.Optional(key, default=replace_none_str(default, vol.UNDEFINED)): validation - for key, default, validation in validation_tuples + for key, default, validation in _yaml_validation_tuples } ) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 8b3d8ced..0e3546f8 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -104,9 +104,7 @@ async def handle_apply(switch, service_call): data = service_call.data tasks = [ await switch._adjust_light( - light, - data[CONF_TRANSITION], - data[CONF_COLORS_ONLY], + light, data[CONF_TRANSITION], data[CONF_COLORS_ONLY], ) for light in data[CONF_LIGHTS] if not data[CONF_ON_LIGHTS_ONLY] or is_on(switch.hass, light) From 685e4cd39b483866364df723f5dde8da7b4680cf Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 26 Sep 2020 13:12:06 +0200 Subject: [PATCH 0189/1077] rephase some text --- .../adaptive_lighting/__init__.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index f76c52b2..c68c5166 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -1,30 +1,30 @@ """ Adaptive Lighting Component for Home-Assistant. -This component calculates color temperature and brightness to synchronize -your color changing lights with perceived color temperature of the sky throughout -the day. This gives your environment a more natural feel, with cooler whites during -the midday and warmer tints near twilight and dawn. -In addition, the component sets your lights to a nice warm white at 1% in "Sleep" mode, -which is far brighter than starlight but won't reset your adaptive rhythm or break down -too much rhodopsin in your eyes. +This component calculates color temperature and brightness to synchronize +your color-changing lights with the perceived color temperature of the sky +throughout the day. This gives your environment a more natural feel, with +cooler whites during the midday and warmer tints near twilight and dawn. + +Additionally, the component sets your lights to a nice warm white at 1% in +"Sleep mode", which is far brighter than starlight but won't reset your +circadian rhythm or break down too much rhodopsin in your eyes. Human circadian rhythms are heavily influenced by ambient light levels and -hues. Hormone production, brainwave activity, mood and wakefulness are +hues. Hormone production, brainwave activity, mood, and wakefulness are just some of the cognitive functions tied to cyclical natural light. -http://en.wikipedia.org/wiki/Zeitgeber - -Further reading: +Resources: +- http://en.wikipedia.org/wiki/Zeitgeber - http://www.cambridgeincolour.com/tutorials/sunrise-sunset-calculator.htm - http://en.wikipedia.org/wiki/Color_temperature -Technical notes: I had to make a lot of assumptions when writing this app -* There are no considerations for weather or altitude, but does use your - hub's location to calculate the sun position. -* The component doesn't calculate a true "Blue Hour" -- it just sets the - lights to 2700K (warm white) until your hub goes into Night mode +## Notes +* Only your location is taken into account to calculate the the sun's position. +* Weather and altitude are not considered. +* The component does not calculate a true "Blue Hour" -- it just sets the + lights to 2700K (warm white) until your hub goes into "Sleep mode". """ import asyncio import logging From 2109921eee782d46ab0ab911b69f87786e1a5f0f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 26 Sep 2020 13:13:44 +0200 Subject: [PATCH 0190/1077] rename function --- custom_components/adaptive_lighting/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index c68c5166..207ea4e6 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -41,7 +41,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["switch"] -def _all_unique_profiles(value): +def _all_unique_names(value): """Validate that all enties have a unique profile name.""" hosts = [device[CONF_NAME] for device in value] schema = vol.Schema(vol.Unique()) @@ -50,7 +50,7 @@ def _all_unique_profiles(value): CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.All(cv.ensure_list, [_DOMAIN_SCHEMA], _all_unique_profiles)}, + {DOMAIN: vol.All(cv.ensure_list, [_DOMAIN_SCHEMA], _all_unique_names)}, extra=vol.ALLOW_EXTRA, ) From 5be04625b62aff291ca7c3a3d401a6de5e02e821 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 26 Sep 2020 16:38:22 +0200 Subject: [PATCH 0191/1077] fix ha/core pre-commit issues and submit this code as PR --- custom_components/adaptive_lighting/__init__.py | 4 +--- custom_components/adaptive_lighting/config_flow.py | 6 +++++- custom_components/adaptive_lighting/const.py | 10 +++++++++- custom_components/adaptive_lighting/switch.py | 4 ++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 207ea4e6..0a053fb0 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -1,6 +1,4 @@ -""" -Adaptive Lighting Component for Home-Assistant. - +"""Adaptive Lighting Component in Home-Assistant. This component calculates color temperature and brightness to synchronize your color-changing lights with the perceived color temperature of the sky diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 6874114b..ba3e8a04 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -1,6 +1,5 @@ """Config flow for Coronavirus integration.""" import logging -from copy import copy import voluptuous as vol @@ -47,6 +46,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def validate_options(user_input, errors): + """Validate the options in the OptionsFlow. + + This is an extra validation step because the validators + in `EXTRA_VALIDATION` cannot be serialized to json. + """ for key, (validate, _) in EXTRA_VALIDATION.items(): # these are unserializable validators try: diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index ba526dfc..228689c0 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,3 +1,4 @@ +"""Constants for the Adaptive Lighting Component in Home-Assistant.""" import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -43,6 +44,7 @@ CONF_ON_LIGHTS_ONLY = "on_lights_only" def int_between(a, b): + """Return an integer between 'a' and 'b'.""" return vol.All(vol.Coerce(int), vol.Range(min=a, max=b)) @@ -71,10 +73,15 @@ VALIDATION_TUPLES = [ def timedelta_as_int(value): + """Convert a `datetime.timedelta` object to an integer. + + This integer can be serialized to json but a timedelta cannot. + """ return value.total_seconds() def join_strings(lst): + """Join a list to comma-separated values string.""" return ",".join(lst) @@ -94,6 +101,7 @@ EXTRA_VALIDATION = { def maybe_coerce(key, validation): + """Coerce the validation into a json serializable type.""" validation, coerce = EXTRA_VALIDATION.get(key, (validation, None)) if coerce is not None: return vol.All(validation, vol.Coerce(coerce)) @@ -101,7 +109,7 @@ def maybe_coerce(key, validation): def replace_none_str(value, replace_with=None): - """Replaces "None" -> replace_with.""" + """Replace "None" -> replace_with.""" return value if value != NONE_STR else replace_with diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 0e3546f8..74319e14 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -128,7 +128,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): { vol.Required(CONF_LIGHTS): cv.entity_ids, vol.Optional( - CONF_TRANSITION, default=self._initial_transition + CONF_TRANSITION, default=switch._initial_transition ): VALID_TRANSITION, vol.Optional(CONF_COLORS_ONLY, default=False): cv.boolean, vol.Optional(CONF_ON_LIGHTS_ONLY, default=False): cv.boolean, @@ -139,7 +139,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def validate(config_entry): - """Gets the options and data from the config_entry and adds defaults.""" + """Get the options and data from the config_entry and add defaults.""" defaults = {key: default for key, default, _ in VALIDATION_TUPLES} data = deepcopy(defaults) data.update(config_entry.options) # come from options flow From 52fa727c7527c252c7bfb759ec7d6eacc082a92e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 26 Sep 2020 17:39:17 +0200 Subject: [PATCH 0192/1077] sync with HA/core PR --- custom_components/adaptive_lighting/__init__.py | 2 +- custom_components/adaptive_lighting/config_flow.py | 2 +- custom_components/adaptive_lighting/const.py | 2 +- custom_components/adaptive_lighting/switch.py | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 0a053fb0..9e6f5c9c 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -29,8 +29,8 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +import homeassistant.helpers.config_validation as cv from .const import _DOMAIN_SCHEMA, CONF_NAME, DOMAIN, UNDO_UPDATE_LISTENER diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index ba3e8a04..b261a71d 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -3,9 +3,9 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from .const import CONF_LIGHTS, DOMAIN, EXTRA_VALIDATION, NONE_STR, VALIDATION_TUPLES diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 228689c0..0c449a10 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,8 +1,8 @@ """Constants for the Adaptive Lighting Component in Home-Assistant.""" import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import VALID_TRANSITION +import homeassistant.helpers.config_validation as cv ICON = "mdi:theme-light-dark" diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 74319e14..d5f4aacb 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2,22 +2,18 @@ import asyncio import bisect -import logging from copy import deepcopy from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.light import ( ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, -) -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.light import ( + DOMAIN as LIGHT_DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, @@ -35,6 +31,7 @@ from homeassistant.const import ( SUN_EVENT_SUNSET, ) from homeassistant.helpers import entity_platform +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( async_track_state_change, async_track_time_interval, @@ -48,6 +45,7 @@ from homeassistant.util.color import ( color_temperature_to_rgb, color_xy_to_hs, ) +import homeassistant.util.dt as dt_util from .const import ( CONF_COLORS_ONLY, @@ -104,7 +102,9 @@ async def handle_apply(switch, service_call): data = service_call.data tasks = [ await switch._adjust_light( - light, data[CONF_TRANSITION], data[CONF_COLORS_ONLY], + light, + data[CONF_TRANSITION], + data[CONF_COLORS_ONLY], ) for light in data[CONF_LIGHTS] if not data[CONF_ON_LIGHTS_ONLY] or is_on(switch.hass, light) From ef04036bfe70511a15f87c036fd66a41691ff9b8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 26 Sep 2020 18:24:44 +0200 Subject: [PATCH 0193/1077] fix bug and use vol.In for entity selection --- .../adaptive_lighting/config_flow.py | 20 ++++++++++++++++--- custom_components/adaptive_lighting/const.py | 4 ++-- custom_components/adaptive_lighting/switch.py | 2 +- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index b261a71d..a2072531 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -7,7 +7,15 @@ from homeassistant import config_entries from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .const import CONF_LIGHTS, DOMAIN, EXTRA_VALIDATION, NONE_STR, VALIDATION_TUPLES +from .const import ( + CONF_DISABLE_ENTITY, + CONF_LIGHTS, + CONF_SLEEP_ENTITY, + DOMAIN, + EXTRA_VALIDATION, + NONE_STR, + VALIDATION_TUPLES, +) _LOGGER = logging.getLogger(__name__) @@ -80,12 +88,18 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if not errors: return self.async_create_entry(title="", data=user_input) - all_lights = cv.multi_select(self.hass.states.async_entity_ids("light")) + all_lights = sorted(self.hass.states.async_entity_ids("light")) + all_entities = sorted(self.hass.states.async_entity_ids()) + to_replace = { + CONF_LIGHTS: cv.multi_select(all_lights), + CONF_DISABLE_ENTITY: vol.In([NONE_STR] + all_entities), + CONF_SLEEP_ENTITY: vol.In([NONE_STR] + all_entities), + } options_schema = {} for name, default, validation in VALIDATION_TUPLES: key = vol.Optional(name, default=conf.options.get(name, default)) - value = validation if name != CONF_LIGHTS else all_lights + value = to_replace.get(name, validation) options_schema[key] = value return self.async_show_form( diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 0c449a10..ed6c717e 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -51,7 +51,7 @@ def int_between(a, b): VALIDATION_TUPLES = [ (CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), (CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST, bool), - (CONF_DISABLE_ENTITY, NONE_STR, str), + (CONF_DISABLE_ENTITY, NONE_STR, cv.entity_id), (CONF_DISABLE_STATE, NONE_STR, str), (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), @@ -62,7 +62,7 @@ VALIDATION_TUPLES = [ (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), (CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)), (CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP, int_between(1000, 10000)), - (CONF_SLEEP_ENTITY, NONE_STR, str), + (CONF_SLEEP_ENTITY, NONE_STR, cv.entity_id), (CONF_SLEEP_STATE, NONE_STR, str), (CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int), (CONF_SUNRISE_TIME, NONE_STR, str), diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index d5f4aacb..e0c8db9a 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -422,7 +422,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): and self.hass.states.get(self._disable_entity).state in self._disable_state ) - async def _adjust_light(self, light, transition, colors_only): + async def _adjust_light(self, light, transition, colors_only=False): service_data = {ATTR_ENTITY_ID: light} features = self._supported_features(light) From 478200b67e2438f1ed4f63777afe74ee59dbeee5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 26 Sep 2020 18:48:48 +0200 Subject: [PATCH 0194/1077] fix pylint --- custom_components/adaptive_lighting/switch.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index e0c8db9a..48743fd3 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -102,9 +102,7 @@ async def handle_apply(switch, service_call): data = service_call.data tasks = [ await switch._adjust_light( - light, - data[CONF_TRANSITION], - data[CONF_COLORS_ONLY], + light, data[CONF_TRANSITION], data[CONF_COLORS_ONLY], ) for light in data[CONF_LIGHTS] if not data[CONF_ON_LIGHTS_ONLY] or is_on(switch.hass, light) @@ -197,10 +195,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set and unset tracker in async_turn_on and async_turn_off self.unsub_tracker = None - _LOGGER.error( - f"Setting up with '{self._lights}'," - f" config_entry.data: '{config_entry.data}'," - f" config_entry.options: '{config_entry.options}', converted to '{data}'." + _LOGGER.debug( + "Setting up with '%s'," + " config_entry.data: '%s'," + " config_entry.options: '%s', converted to '%s'.", + self._lights, + config_entry.data, + config_entry.options, + data, ) @property From db63c5acefbe239d9c4e0ca178634ce9b082901c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 26 Sep 2020 18:53:00 +0200 Subject: [PATCH 0195/1077] fix strings --- custom_components/adaptive_lighting/strings.json | 5 +---- custom_components/adaptive_lighting/translations/en.json | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 13851edd..1bea6ab5 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -20,10 +20,7 @@ "title": "Adaptive Lighting options", "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", "data": { - "lights_brightness": "lights_brightness", - "lights_mired": "lights_mired", - "lights_rgb": "lights_rgb", - "lights_xy": "lights_xy", + "lights": "lights", "disable_brightness_adjust": "disable_brightness_adjust", "disable_entity": "disable_entity", "disable_state": "disable_state", diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 13851edd..1bea6ab5 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -20,10 +20,7 @@ "title": "Adaptive Lighting options", "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", "data": { - "lights_brightness": "lights_brightness", - "lights_mired": "lights_mired", - "lights_rgb": "lights_rgb", - "lights_xy": "lights_xy", + "lights": "lights", "disable_brightness_adjust": "disable_brightness_adjust", "disable_entity": "disable_entity", "disable_state": "disable_state", From 5ee3575efd6a6183f8faecd15d472a0c8daa7fce Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 27 Sep 2020 00:06:31 +0200 Subject: [PATCH 0196/1077] fix the 'off' -> 'on' -> 'off' switches --- custom_components/adaptive_lighting/const.py | 2 + custom_components/adaptive_lighting/switch.py | 72 ++++++++++++++----- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index ed6c717e..5f670b58 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -42,6 +42,8 @@ SERVICE_APPLY = "apply" CONF_COLORS_ONLY = "colors_only" CONF_ON_LIGHTS_ONLY = "on_lights_only" +TURNING_OFF_DELAY = 5 + def int_between(a, b): """Return an integer between 'a' and 'b'.""" diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 48743fd3..0fc5b983 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -34,6 +34,7 @@ from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( async_track_state_change, + async_track_state_change_event, async_track_time_interval, ) from homeassistant.helpers.restore_state import RestoreEntity @@ -76,6 +77,7 @@ from .const import ( SERVICE_APPLY, SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, + TURNING_OFF_DELAY, VALIDATION_TUPLES, replace_none_str, ) @@ -102,7 +104,9 @@ async def handle_apply(switch, service_call): data = service_call.data tasks = [ await switch._adjust_light( - light, data[CONF_TRANSITION], data[CONF_COLORS_ONLY], + light, + data[CONF_TRANSITION], + data[CONF_COLORS_ONLY], ) for light in data[CONF_LIGHTS] if not data[CONF_ON_LIGHTS_ONLY] or is_on(switch.hass, light) @@ -183,6 +187,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set other attributes self._icon = ICON self._entity_id = f"switch.{DOMAIN}_{slugify(self._name)}" + self._turned_off = {} # Initialize attributes that will be set in self._update_attrs self._percent = None @@ -246,12 +251,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def async_added_to_hass(self): """Call when entity about to be added to hass.""" if self._lights: - async_track_state_change( - self.hass, - self._unpack_light_groups(self._lights), - self._light_state_changed, - to_state="on", - from_state="off", + unpacked_lights = self._unpack_light_groups(self._lights) + async_track_state_change_event( + self.hass, unpacked_lights, self._light_event ) track_kwargs = dict(hass=self.hass, action=self._state_changed) if self._sleep_entity is not None: @@ -469,19 +471,53 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if tasks: await asyncio.wait(tasks) - async def _light_state_changed(self, entity_id, from_state, to_state): - assert to_state.state == "on" and from_state.state == "off" - _LOGGER.debug( - "_light_state_changed, from_state: '%s', to_state: '%s'", - from_state, - to_state, - ) - await self._update_lights( - lights=[entity_id], transition=self._initial_transition, force=True - ) - async def _state_changed(self, entity_id, from_state, to_state): _LOGGER.debug( "_state_changed, from_state: '%s', to_state: '%s'", from_state, to_state ) await self._update_lights(transition=self._initial_transition, force=True) + + async def _light_event(self, event): + old_state = event.data.get("old_state") + new_state = event.data.get("new_state") + + _LOGGER.debug( + "lights event, old_state: '%s', new_state: '%s'", + old_state, + new_state, + ) + entity_id = event.data.get("entity_id") + now = dt_util.now().timestamp() + if ( + old_state is not None + and old_state.state == "off" + and new_state is not None + and new_state.state == "on" + ): + last_turned_off = self._turned_off.get(entity_id, 0) + dt = now - last_turned_off + # TODO: make TURNING_OFF_DELAY depend on the 'transition' time + # passed to 'turn_off' IF transition was passed. + if dt < TURNING_OFF_DELAY: + # Possibly the lights just got a turn_off call, however, the light + # is actually still turning off and HA polls the light before the + # light is 100% off. This might trigger a rapid switch + # 'off' -> 'on' -> 'off'. To prevent this component from interfering + # on the 'on' state, we make sure to wait at least TURNING_OFF_DELAY + # between a 'off' -> 'on' event and then check whether the light is + # still 'on'. Only if it is still 'on' we adjust the lights. + await asyncio.sleep(TURNING_OFF_DELAY - dt) + if not is_on(self.hass, entity_id): + return + await self._update_lights( + lights=[entity_id], + transition=self._initial_transition, + force=True, + ) + if ( + old_state is not None + and old_state.state == "on" + and new_state is not None + and new_state.state == "off" + ): + self._turned_off[entity_id] = now From 199d31b891d0f7a5597de8907b45a003231f10dc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 27 Sep 2020 00:19:08 +0200 Subject: [PATCH 0197/1077] add disable_color option --- custom_components/adaptive_lighting/const.py | 2 ++ custom_components/adaptive_lighting/switch.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 5f670b58..15714e6f 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -12,6 +12,7 @@ SUN_EVENT_MIDNIGHT = "solar_midnight" CONF_NAME, DEFAULT_NAME = "name", "default" CONF_LIGHTS, DEFAULT_LIGHTS = "lights", [] +CONF_DISABLE_COLOR, DEFAULT_DISABLE_COLOR = "disable_color", False CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST = ( "disable_brightness_adjust", False, @@ -52,6 +53,7 @@ def int_between(a, b): VALIDATION_TUPLES = [ (CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), + (CONF_DISABLE_COLOR, DEFAULT_DISABLE_COLOR, bool), (CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST, bool), (CONF_DISABLE_ENTITY, NONE_STR, cv.entity_id), (CONF_DISABLE_STATE, NONE_STR, str), diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 0fc5b983..1e328053 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -51,6 +51,7 @@ import homeassistant.util.dt as dt_util from .const import ( CONF_COLORS_ONLY, CONF_DISABLE_BRIGHTNESS_ADJUST, + CONF_DISABLE_COLOR, CONF_DISABLE_ENTITY, CONF_DISABLE_STATE, CONF_INITIAL_TRANSITION, @@ -165,6 +166,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._name = data[CONF_NAME] self._lights = data[CONF_LIGHTS] self._disable_brightness_adjust = data[CONF_DISABLE_BRIGHTNESS_ADJUST] + self._disable_color = data[CONF_DISABLE_COLOR] self._disable_entity = data[CONF_DISABLE_ENTITY] self._disable_state = data[CONF_DISABLE_STATE] self._initial_transition = data[CONF_INITIAL_TRANSITION] @@ -442,7 +444,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ): service_data[ATTR_BRIGHTNESS_PCT] = self._brightness - if "color" in features: + if "color" in features and not self._disable_color: service_data[ATTR_RGB_COLOR] = self._rgb_color elif "color_temp" in features: service_data[ATTR_COLOR_TEMP] = self._color_temp_mired From 869ff917e0c335ad7ed61053def68ffb99c9358e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 27 Sep 2020 01:18:39 +0200 Subject: [PATCH 0198/1077] rename disable_color -> disable_color_adjust --- custom_components/adaptive_lighting/const.py | 4 ++-- custom_components/adaptive_lighting/strings.json | 1 + custom_components/adaptive_lighting/switch.py | 6 +++--- custom_components/adaptive_lighting/translations/en.json | 1 + 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 15714e6f..faef738b 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -12,11 +12,11 @@ SUN_EVENT_MIDNIGHT = "solar_midnight" CONF_NAME, DEFAULT_NAME = "name", "default" CONF_LIGHTS, DEFAULT_LIGHTS = "lights", [] -CONF_DISABLE_COLOR, DEFAULT_DISABLE_COLOR = "disable_color", False CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST = ( "disable_brightness_adjust", False, ) +CONF_DISABLE_COLOR_ADJUST, DEFAULT_DISABLE_COLOR_ADJUST = "disable_color_adjust", False CONF_DISABLE_ENTITY = "disable_entity" CONF_DISABLE_STATE = "disable_state" CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 @@ -53,8 +53,8 @@ def int_between(a, b): VALIDATION_TUPLES = [ (CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), - (CONF_DISABLE_COLOR, DEFAULT_DISABLE_COLOR, bool), (CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST, bool), + (CONF_DISABLE_COLOR_ADJUST, DEFAULT_DISABLE_COLOR_ADJUST, bool), (CONF_DISABLE_ENTITY, NONE_STR, cv.entity_id), (CONF_DISABLE_STATE, NONE_STR, str), (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 1bea6ab5..b871aac9 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -22,6 +22,7 @@ "data": { "lights": "lights", "disable_brightness_adjust": "disable_brightness_adjust", + "disable_color_adjust": "disable_color_adjust", "disable_entity": "disable_entity", "disable_state": "disable_state", "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 1e328053..6a9a05dd 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -51,7 +51,7 @@ import homeassistant.util.dt as dt_util from .const import ( CONF_COLORS_ONLY, CONF_DISABLE_BRIGHTNESS_ADJUST, - CONF_DISABLE_COLOR, + CONF_DISABLE_COLOR_ADJUST, CONF_DISABLE_ENTITY, CONF_DISABLE_STATE, CONF_INITIAL_TRANSITION, @@ -166,7 +166,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._name = data[CONF_NAME] self._lights = data[CONF_LIGHTS] self._disable_brightness_adjust = data[CONF_DISABLE_BRIGHTNESS_ADJUST] - self._disable_color = data[CONF_DISABLE_COLOR] + self._disable_color_adjust = data[CONF_DISABLE_COLOR_ADJUST] self._disable_entity = data[CONF_DISABLE_ENTITY] self._disable_state = data[CONF_DISABLE_STATE] self._initial_transition = data[CONF_INITIAL_TRANSITION] @@ -444,7 +444,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ): service_data[ATTR_BRIGHTNESS_PCT] = self._brightness - if "color" in features and not self._disable_color: + if "color" in features and not self._disable_color_adjust: service_data[ATTR_RGB_COLOR] = self._rgb_color elif "color_temp" in features: service_data[ATTR_COLOR_TEMP] = self._color_temp_mired diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 1bea6ab5..b871aac9 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -22,6 +22,7 @@ "data": { "lights": "lights", "disable_brightness_adjust": "disable_brightness_adjust", + "disable_color_adjust": "disable_color_adjust", "disable_entity": "disable_entity", "disable_state": "disable_state", "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", From a4b4ef3acd481b82a25324d81c04013c850d6e4b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 27 Sep 2020 16:21:42 +0200 Subject: [PATCH 0199/1077] fix problem with turn_off transition --- .../adaptive_lighting/config_flow.py | 2 +- custom_components/adaptive_lighting/const.py | 6 +- custom_components/adaptive_lighting/switch.py | 197 ++++++++++++++---- 3 files changed, 163 insertions(+), 42 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index a2072531..d022e040 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .const import ( +from .const import ( # pylint: disable=unused-import CONF_DISABLE_ENTITY, CONF_LIGHTS, CONF_SLEEP_ENTITY, diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index faef738b..c939b281 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -46,9 +46,9 @@ CONF_ON_LIGHTS_ONLY = "on_lights_only" TURNING_OFF_DELAY = 5 -def int_between(a, b): - """Return an integer between 'a' and 'b'.""" - return vol.All(vol.Coerce(int), vol.Range(min=a, max=b)) +def int_between(min_int, max_int): + """Return an integer between 'min_int' and 'max_int'.""" + return vol.All(vol.Coerce(int), vol.Range(min=min_int, max=max_int)) VALIDATION_TUPLES = [ diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 6a9a05dd..879881aa 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -5,10 +5,16 @@ import bisect from copy import deepcopy from datetime import timedelta import logging +from typing import Dict, Tuple import voluptuous as vol +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, @@ -23,8 +29,13 @@ from homeassistant.components.light import ( ) from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( + ATTR_DOMAIN, ATTR_ENTITY_ID, + ATTR_SERVICE, + ATTR_SERVICE_DATA, CONF_NAME, + EVENT_CALL_SERVICE, + SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, SUN_EVENT_SUNRISE, @@ -104,7 +115,7 @@ async def handle_apply(switch, service_call): raise ValueError("Apply can only be called for a AdaptiveSwitch.") data = service_call.data tasks = [ - await switch._adjust_light( + await switch._adjust_light( # pylint: disable=protected-access light, data[CONF_TRANSITION], data[CONF_COLORS_ONLY], @@ -131,7 +142,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): { vol.Required(CONF_LIGHTS): cv.entity_ids, vol.Optional( - CONF_TRANSITION, default=switch._initial_transition + CONF_TRANSITION, + default=switch._initial_transition, # pylint: disable=protected-access ): VALID_TRANSITION, vol.Optional(CONF_COLORS_ONLY, default=False): cv.boolean, vol.Optional(CONF_ON_LIGHTS_ONLY, default=False): cv.boolean, @@ -148,10 +160,10 @@ def validate(config_entry): data.update(config_entry.options) # come from options flow data.update(config_entry.data) # all yaml settings come from data data = {key: replace_none_str(value) for key, value in data.items()} - for key, (validate, _) in EXTRA_VALIDATION.items(): + for key, (validate_value, _) in EXTRA_VALIDATION.items(): value = data.get(key) if value is not None: - data[key] = validate(value) # Fix the types of the inputs + data[key] = validate_value(value) # Fix the types of the inputs return data @@ -189,7 +201,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set other attributes self._icon = ICON self._entity_id = f"switch.{DOMAIN}_{slugify(self._name)}" - self._turned_off = {} + + # Tracks 'off' → 'on' state changes + self._on_to_off_event: Dict[str, Tuple[float, str]] = {} + # Tracks 'light.turn_off(..., transition=...)' service calls + self._turn_off_service_event: Dict[str, Tuple[str, float]] = {} # Initialize attributes that will be set in self._update_attrs self._percent = None @@ -203,9 +219,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set and unset tracker in async_turn_on and async_turn_off self.unsub_tracker = None _LOGGER.debug( - "Setting up with '%s'," + "%s: Setting up with '%s'," " config_entry.data: '%s'," " config_entry.options: '%s', converted to '%s'.", + self._name, self._lights, config_entry.data, config_entry.options, @@ -239,12 +256,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): for light in lights: state = self.hass.states.get(light) if state is None: - _LOGGER.debug("State of %s is None", light) + _LOGGER.debug("%s: State of %s is None", self._name, light) # TODO: make sure that the lights are loaded when doing this all_lights.append(light) elif "entity_id" in state.attributes: # it's a light group group = state.attributes["entity_id"] - self.debug("Unpacked %s to %s", group) + _LOGGER.debug("%s: Unpacked %s to %s", self._name, lights, group) all_lights.extend(group) else: all_lights.append(light) @@ -257,6 +274,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async_track_state_change_event( self.hass, unpacked_lights, self._light_event ) + # Tracks 'light.turn_off(..., transition=...)' service calls + self.hass.bus.async_listen( + EVENT_CALL_SERVICE, self._turn_off_event_listener + ) track_kwargs = dict(hass=self.hass, action=self._state_changed) if self._sleep_entity is not None: sleep_kwargs = dict(track_kwargs, entity_ids=self._sleep_entity) @@ -292,7 +313,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): "hs_color": self._hs_color, } if not self.is_on: - return {key: None for key in attrs.keys()} + return {key: None for key in attrs} return attrs async def async_turn_on(self, **kwargs): @@ -321,7 +342,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._xy_color = color_RGB_to_xy(*self._rgb_color) self._hs_color = color_xy_to_hs(*self._xy_color) self.async_write_ha_state() - _LOGGER.debug("'_update_attrs' called for %s", self._name) + _LOGGER.debug("%s: '_update_attrs' called", self._name) async def _async_update_at_interval(self, now=None): await self._update_lights(force=False) @@ -387,8 +408,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): now = dt_util.utcnow() now_ts = now.timestamp() today = self._relevant_events(now) - (prev_event, prev_ts), (next_event, next_ts) = today - h, x = ( + (_, prev_ts), (next_event, next_ts) = today + h, x = ( # pylint: disable=invalid-name (prev_ts, next_ts) if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE) else (next_ts, prev_ts) @@ -450,7 +471,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_COLOR_TEMP] = self._color_temp_mired _LOGGER.debug( - "Scheduling 'light.turn_on' with the following 'service_data': %s", + "%s: Scheduling 'light.turn_on' with the following 'service_data': %s", + self._name, service_data, ) return self.hass.services.async_call( @@ -475,51 +497,150 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _state_changed(self, entity_id, from_state, to_state): _LOGGER.debug( - "_state_changed, from_state: '%s', to_state: '%s'", from_state, to_state + "%s: _state_changed, from_state: '%s', to_state: '%s'", + self._name, + from_state, + to_state, ) await self._update_lights(transition=self._initial_transition, force=True) async def _light_event(self, event): old_state = event.data.get("old_state") new_state = event.data.get("new_state") - - _LOGGER.debug( - "lights event, old_state: '%s', new_state: '%s'", - old_state, - new_state, - ) entity_id = event.data.get("entity_id") - now = dt_util.now().timestamp() + now_ts = dt_util.now().timestamp() if ( old_state is not None and old_state.state == "off" and new_state is not None and new_state.state == "on" ): - last_turned_off = self._turned_off.get(entity_id, 0) - dt = now - last_turned_off - # TODO: make TURNING_OFF_DELAY depend on the 'transition' time - # passed to 'turn_off' IF transition was passed. - if dt < TURNING_OFF_DELAY: - # Possibly the lights just got a turn_off call, however, the light - # is actually still turning off and HA polls the light before the - # light is 100% off. This might trigger a rapid switch - # 'off' -> 'on' -> 'off'. To prevent this component from interfering - # on the 'on' state, we make sure to wait at least TURNING_OFF_DELAY - # between a 'off' -> 'on' event and then check whether the light is - # still 'on'. Only if it is still 'on' we adjust the lights. - await asyncio.sleep(TURNING_OFF_DELAY - dt) - if not is_on(self.hass, entity_id): - return + _LOGGER.debug( + "%s: Detected an 'off' → 'on' event for '%s'", self._name, entity_id + ) + if await self._maybe_cancel(entity_id, now_ts): + # Stop if a rapid 'off' → 'on' → 'off' happens. + _LOGGER.debug( + "%s: Cancelling adjusting lights for %s", self._name, entity_id + ) + return await self._update_lights( lights=[entity_id], transition=self._initial_transition, force=True, ) - if ( + elif ( old_state is not None and old_state.state == "on" and new_state is not None and new_state.state == "off" ): - self._turned_off[entity_id] = now + # Tracks 'off' → 'on' state changes + self._on_to_off_event[entity_id] = (now_ts, event.context.id) + + async def _maybe_cancel(self, entity_id, now_ts) -> bool: + """Cancel the adjusting of a light if it has just been turned off. + + Possibly the lights just got a 'turn_off' call, however, the light + is actually still turning off (e.g., because of a 'transition') and + HA polls the light before the light is 100% off. This might trigger + a rapid switch 'off' → 'on' → 'off'. To prevent this component + from interfering on the 'on' state, we make sure to wait at least + TURNING_OFF_DELAY (or the 'turn_off' transition time) between a + 'off' → 'on' event and then check whether the light is still 'on' or + if the brightness is still decreasing. Only if it is the case we + adjust the lights. + """ + ts_on_to_off, id_on_to_off = self._on_to_off_event.get(entity_id, (0, None)) + id_turn_off, transition = self._turn_off_service_event.get( + entity_id, (None, None) + ) + if ( + id_on_to_off is not None + and id_turn_off is not None + and id_on_to_off == id_turn_off + ): + # State change 'off' → 'on' and 'light.turn_off(..., transition=...)' are + # from the same event, so wait at least the 'turn_off' transition time. + delay = transition + elif ts_on_to_off == 0: + # No state change has been registered before. + return False + else: + # State change 'off' → 'on' happened but **not** because a + # 'light.turn_off' event that is called with 'transition'. + delay = TURNING_OFF_DELAY + + delta_time = now_ts - ts_on_to_off + if delta_time < delay: + delay -= delta_time # already been delta_time since the event + brightness_going_down = True # this might not be the case + _LOGGER.debug( + "%s: Waiting with adjusting '%s' for %s.", self._name, entity_id, delay + ) + current_state = self.hass.states.get(entity_id) + _LOGGER.debug( + "%s: '%s' state before sleep is '%s'", + self._name, + entity_id, + current_state, + ) + for _ in range(3): + # It can happen that the actual transition time is longer than + # the specified time in the 'turn_off' service, so we check + # whether the brightness is still going down, if so, we wait a + # little longer. + await asyncio.sleep(delay) + await self.hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + old_state = current_state + current_state = self.hass.states.get(entity_id) + old_brightness = old_state.attributes.get(ATTR_BRIGHTNESS, 0) + current_brightness = current_state.attributes.get(ATTR_BRIGHTNESS, 0) + brightness_going_down = old_brightness > current_brightness + _LOGGER.debug( + "%s: '%s' state after sleep is '%s'", + self._name, + entity_id, + current_state, + ) + if not brightness_going_down: + break + delay = TURNING_OFF_DELAY + + if transition is not None: + # Always ignore when there's a transition + # TODO: I am doing this because it seems like HA cannot detect + # whether a light is transitioning into 'off'. Because in my + # tests `brightness_going_down == False` even when it is actually + # still going down... Needs some discussion. + return True + + if not is_on(self.hass, entity_id): + return True + return False + + async def _turn_off_event_listener(self, event): + """Track 'light.turn_off(..., transition=...)' service calls.""" + if event.data.get(ATTR_DOMAIN) != LIGHT_DOMAIN: + return + if event.data.get(ATTR_SERVICE) != SERVICE_TURN_OFF: + return + service_data = event.data.get(ATTR_SERVICE_DATA, {}) + transition = service_data.get(ATTR_TRANSITION) + if transition is not None and transition > 0: + entity_id = service_data[ATTR_ENTITY_ID] + _LOGGER.debug( + "%s: Detected an 'light.turn_off('%s', transition=%s)' event", + self._name, + entity_id, + transition, + ) + if isinstance(entity_id, str): + entity_id = [entity_id] + for eid in entity_id: + self._turn_off_service_event[eid] = (event.context.id, transition) From 7575d4d09d826fc57f23706f922aa5f71f68887b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 27 Sep 2020 20:54:38 +0200 Subject: [PATCH 0200/1077] remove indentation level in _maybe_cancel --- custom_components/adaptive_lighting/switch.py | 83 +++++++++---------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 879881aa..52edb6b9 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -572,57 +572,56 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): delay = TURNING_OFF_DELAY delta_time = now_ts - ts_on_to_off - if delta_time < delay: - delay -= delta_time # already been delta_time since the event - brightness_going_down = True # this might not be the case - _LOGGER.debug( - "%s: Waiting with adjusting '%s' for %s.", self._name, entity_id, delay + if delta_time > delay: + return False + delay -= delta_time # delta_time has passed since the 'off' → 'on' event + _LOGGER.debug( + "%s: Waiting with adjusting '%s' for %s.", self._name, entity_id, delay + ) + current_state = self.hass.states.get(entity_id) + _LOGGER.debug( + "%s: '%s' state before sleep is '%s'", + self._name, + entity_id, + current_state, + ) + for _ in range(3): + # It can happen that the actual transition time is longer than the + # specified time in the 'turn_off' service, so we check whether the + # brightness is still going down, if so, we wait a little longer. + await asyncio.sleep(delay) + await self.hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, ) + old_state = current_state current_state = self.hass.states.get(entity_id) + if current_state.state == "off": + return True + old_brightness = old_state.attributes.get(ATTR_BRIGHTNESS, 0) + current_brightness = current_state.attributes.get(ATTR_BRIGHTNESS, 0) + brightness_going_down = old_brightness > current_brightness _LOGGER.debug( - "%s: '%s' state before sleep is '%s'", + "%s: '%s' state after sleep is '%s'", self._name, entity_id, current_state, ) - for _ in range(3): - # It can happen that the actual transition time is longer than - # the specified time in the 'turn_off' service, so we check - # whether the brightness is still going down, if so, we wait a - # little longer. - await asyncio.sleep(delay) - await self.hass.services.async_call( - HA_DOMAIN, - SERVICE_UPDATE_ENTITY, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - old_state = current_state - current_state = self.hass.states.get(entity_id) - old_brightness = old_state.attributes.get(ATTR_BRIGHTNESS, 0) - current_brightness = current_state.attributes.get(ATTR_BRIGHTNESS, 0) - brightness_going_down = old_brightness > current_brightness - _LOGGER.debug( - "%s: '%s' state after sleep is '%s'", - self._name, - entity_id, - current_state, - ) - if not brightness_going_down: - break - delay = TURNING_OFF_DELAY + if not brightness_going_down: + break + delay = TURNING_OFF_DELAY # next time only wait this long - if transition is not None: - # Always ignore when there's a transition - # TODO: I am doing this because it seems like HA cannot detect - # whether a light is transitioning into 'off'. Because in my - # tests `brightness_going_down == False` even when it is actually - # still going down... Needs some discussion. - return True + if transition is not None: + # Always ignore when there's a transition and light is still on. + # TODO: I am doing this because it seems like HA cannot detect + # whether a light is transitioning into 'off'. Because in my + # tests `brightness_going_down == False` even when it is actually + # still going down... Needs some discussion. + return True - if not is_on(self.hass, entity_id): - return True - return False + return current_state.state == "off" async def _turn_off_event_listener(self, event): """Track 'light.turn_off(..., transition=...)' service calls.""" From 2f1531c6924905b416be16d1a76b2e0f6963768c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 28 Sep 2020 00:26:21 +0200 Subject: [PATCH 0201/1077] use light.turn_on --- custom_components/adaptive_lighting/switch.py | 72 +++++++++++-------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 52edb6b9..358647a2 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -205,7 +205,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Tracks 'off' → 'on' state changes self._on_to_off_event: Dict[str, Tuple[float, str]] = {} # Tracks 'light.turn_off(..., transition=...)' service calls - self._turn_off_service_event: Dict[str, Tuple[str, float]] = {} + self._turn_off_event: Dict[str, Tuple[str, float]] = {} + # Tracks 'light.turn_on' service calls + self._turn_on_event: Dict[str, Tuple[str]] = {} # Initialize attributes that will be set in self._update_attrs self._percent = None @@ -518,7 +520,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug( "%s: Detected an 'off' → 'on' event for '%s'", self._name, entity_id ) - if await self._maybe_cancel(entity_id, now_ts): + _LOGGER.error(f"_turn_off_on_event: {event.context}, {entity_id}") + if await self._maybe_cancel_adjusting(entity_id, now_ts, event): # Stop if a rapid 'off' → 'on' → 'off' happens. _LOGGER.debug( "%s: Cancelling adjusting lights for %s", self._name, entity_id @@ -538,7 +541,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Tracks 'off' → 'on' state changes self._on_to_off_event[entity_id] = (now_ts, event.context.id) - async def _maybe_cancel(self, entity_id, now_ts) -> bool: + async def _maybe_cancel_adjusting(self, entity_id, now_ts, off_to_on_event) -> bool: """Cancel the adjusting of a light if it has just been turned off. Possibly the lights just got a 'turn_off' call, however, the light @@ -552,23 +555,23 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): adjust the lights. """ ts_on_to_off, id_on_to_off = self._on_to_off_event.get(entity_id, (0, None)) - id_turn_off, transition = self._turn_off_service_event.get( - entity_id, (None, None) - ) - if ( - id_on_to_off is not None - and id_turn_off is not None - and id_on_to_off == id_turn_off - ): - # State change 'off' → 'on' and 'light.turn_off(..., transition=...)' are - # from the same event, so wait at least the 'turn_off' transition time. - delay = transition + id_turn_off, transition = self._turn_off_event.get(entity_id, (None, None)) + id_turn_on = self._turn_on_event.get(entity_id) + id_off_to_on = off_to_on_event.context.id + + if id_off_to_on == id_turn_on and id_off_to_on is not None: + # State change 'off' → 'on' triggered by 'light.turn_on'. + return False elif ts_on_to_off == 0: # No state change has been registered before. return False + elif id_on_to_off == id_turn_off and id_on_to_off is not None: + # State change 'off' → 'on' and 'light.turn_off(..., transition=...)' come + # from the same event, so wait at least the 'turn_off' transition time. + delay = transition else: - # State change 'off' → 'on' happened but **not** because a - # 'light.turn_off' event that is called with 'transition'. + # State change 'off' → 'on' happened because the light state was set. + # Possibly because of polling. delay = TURNING_OFF_DELAY delta_time = now_ts - ts_on_to_off @@ -618,28 +621,39 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # TODO: I am doing this because it seems like HA cannot detect # whether a light is transitioning into 'off'. Because in my # tests `brightness_going_down == False` even when it is actually - # still going down... Needs some discussion. + # still going down... Maybe needs some discussion. return True return current_state.state == "off" async def _turn_off_event_listener(self, event): - """Track 'light.turn_off(..., transition=...)' service calls.""" - if event.data.get(ATTR_DOMAIN) != LIGHT_DOMAIN: - return - if event.data.get(ATTR_SERVICE) != SERVICE_TURN_OFF: + """Track 'light.turn_off(..., transition=...)' and 'light.turn_on' service calls.""" + domain = event.data.get(ATTR_DOMAIN) + if domain != LIGHT_DOMAIN: return + service = event.data.get(ATTR_SERVICE) service_data = event.data.get(ATTR_SERVICE_DATA, {}) - transition = service_data.get(ATTR_TRANSITION) - if transition is not None and transition > 0: - entity_id = service_data[ATTR_ENTITY_ID] + entity_id = service_data.get(ATTR_ENTITY_ID) + if isinstance(entity_id, str): + entity_id = [entity_id] + + if service == SERVICE_TURN_OFF: + transition = service_data.get(ATTR_TRANSITION) + if transition is not None and transition > 0: + _LOGGER.debug( + "%s: Detected an 'light.turn_off('%s', transition=%s)' event", + self._name, + entity_id, + transition, + ) + for eid in entity_id: + self._turn_off_event[eid] = (event.context.id, transition) + elif service == SERVICE_TURN_ON: _LOGGER.debug( - "%s: Detected an 'light.turn_off('%s', transition=%s)' event", + "%s: Detected an 'light.turn_on('%s')' event", self._name, entity_id, - transition, ) - if isinstance(entity_id, str): - entity_id = [entity_id] for eid in entity_id: - self._turn_off_service_event[eid] = (event.context.id, transition) + self._turn_on_event[eid] = event.context.id + _LOGGER.error(f"_turn_on_event: {event.context}, {entity_id}") From 5f7e8af03d539623a5c15acfa7f0d0497ba35426 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 28 Sep 2020 00:33:15 +0200 Subject: [PATCH 0202/1077] log fixes --- custom_components/adaptive_lighting/switch.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 358647a2..3dc303cc 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -520,7 +520,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug( "%s: Detected an 'off' → 'on' event for '%s'", self._name, entity_id ) - _LOGGER.error(f"_turn_off_on_event: {event.context}, {entity_id}") if await self._maybe_cancel_adjusting(entity_id, now_ts, event): # Stop if a rapid 'off' → 'on' → 'off' happens. _LOGGER.debug( @@ -656,4 +655,3 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) for eid in entity_id: self._turn_on_event[eid] = event.context.id - _LOGGER.error(f"_turn_on_event: {event.context}, {entity_id}") From 6a5ded69819ab1bd4f80402c429a6be44c3779cc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 28 Sep 2020 13:10:41 +0200 Subject: [PATCH 0203/1077] update to latest PR --- .../adaptive_lighting/__init__.py | 11 +- .../adaptive_lighting/config_flow.py | 2 +- custom_components/adaptive_lighting/const.py | 5 +- custom_components/adaptive_lighting/switch.py | 207 ++++++++++-------- 4 files changed, 129 insertions(+), 96 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 9e6f5c9c..1e935df7 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -1,11 +1,11 @@ -"""Adaptive Lighting Component in Home-Assistant. +"""Adaptive Lighting integration in Home-Assistant. -This component calculates color temperature and brightness to synchronize +This integration calculates color temperature and brightness to synchronize your color-changing lights with the perceived color temperature of the sky throughout the day. This gives your environment a more natural feel, with cooler whites during the midday and warmer tints near twilight and dawn. -Additionally, the component sets your lights to a nice warm white at 1% in +Additionally, the integration sets your lights to a nice warm white at 1% in "Sleep mode", which is far brighter than starlight but won't reset your circadian rhythm or break down too much rhodopsin in your eyes. @@ -20,8 +20,8 @@ Resources: ## Notes * Only your location is taken into account to calculate the the sun's position. -* Weather and altitude are not considered. -* The component does not calculate a true "Blue Hour" -- it just sets the +* Weather is not considered. +* The integration does not calculate a true "Blue Hour" -- it just sets the lights to 2700K (warm white) until your hub goes into "Sleep mode". """ import asyncio @@ -72,7 +72,6 @@ async def async_setup_entry(hass, config_entry: ConfigEntry): undo_listener = config_entry.add_update_listener(async_update_options) hass.data[DOMAIN][config_entry.entry_id] = {UNDO_UPDATE_LISTENER: undo_listener} - for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, platform) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index d022e040..7002c7d7 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Coronavirus integration.""" +"""Config flow for Adaptive Lighting integration.""" import logging import voluptuous as vol diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index c939b281..d194efc8 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,4 +1,4 @@ -"""Constants for the Adaptive Lighting Component in Home-Assistant.""" +"""Constants for the Adaptive Lighting integration.""" import voluptuous as vol from homeassistant.components.light import VALID_TRANSITION @@ -36,8 +36,9 @@ CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 CONF_SUNSET_TIME = "sunset_time" CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 +ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener" UNDO_UPDATE_LISTENER = "undo_update_listener" -NONE_STR = "None" # TODO: use `from homeassistant.const import ENTITY_MATCH_NONE`? +NONE_STR = "None" SERVICE_APPLY = "apply" CONF_COLORS_ONLY = "colors_only" diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 3dc303cc..c3f1e955 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1,4 +1,4 @@ -"""Adaptive Lighting Component for Home-Assistant.""" +"""Switch for the Adaptive Lighting integration.""" import asyncio import bisect @@ -35,12 +35,14 @@ from homeassistant.const import ( ATTR_SERVICE_DATA, CONF_NAME, EVENT_CALL_SERVICE, + EVENT_HOMEASSISTANT_START, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) +from homeassistant.core import Event from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( @@ -60,6 +62,7 @@ from homeassistant.util.color import ( import homeassistant.util.dt as dt_util from .const import ( + ATTR_TURN_ON_OFF_LISTENER, CONF_COLORS_ONLY, CONF_DISABLE_BRIGHTNESS_ADJUST, CONF_DISABLE_COLOR_ADJUST, @@ -129,9 +132,14 @@ async def handle_apply(switch, service_call): async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the AdaptiveLighting switch.""" - switch = AdaptiveSwitch(hass, config_entry) if DOMAIN not in hass.data: hass.data[DOMAIN] = {} + + if ATTR_TURN_ON_OFF_LISTENER not in hass.data[DOMAIN]: + hass.data[DOMAIN][ATTR_TURN_ON_OFF_LISTENER] = TurnOnOffListener(hass) + + turn_on_off_listener = hass.data[DOMAIN][ATTR_TURN_ON_OFF_LISTENER] + switch = AdaptiveSwitch(hass, config_entry, turn_on_off_listener) name = config_entry.data[CONF_NAME] hass.data[DOMAIN][name] = switch @@ -170,9 +178,10 @@ def validate(config_entry): class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" - def __init__(self, hass, config_entry): + def __init__(self, hass, config_entry, turn_on_off_listener): """Initialize the Adaptive Lighting switch.""" self.hass = hass + self.turn_on_off_listener = turn_on_off_listener data = validate(config_entry) self._name = data[CONF_NAME] @@ -203,11 +212,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._entity_id = f"switch.{DOMAIN}_{slugify(self._name)}" # Tracks 'off' → 'on' state changes - self._on_to_off_event: Dict[str, Tuple[float, str]] = {} - # Tracks 'light.turn_off(..., transition=...)' service calls - self._turn_off_event: Dict[str, Tuple[str, float]] = {} - # Tracks 'light.turn_on' service calls - self._turn_on_event: Dict[str, Tuple[str]] = {} + self._on_to_off_event: Dict[str, Event] = {} + # Locks that prevent light adjusting when waiting for a light to 'turn_off' + self._locks: Dict[str, asyncio.Lock] = {} # Initialize attributes that will be set in self._update_attrs self._percent = None @@ -253,50 +260,51 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): key for key, value in _SUPPORT_OPTS.items() if supported_features & value } - def _unpack_light_groups(self, lights): - all_lights = [] - for light in lights: - state = self.hass.states.get(light) - if state is None: - _LOGGER.debug("%s: State of %s is None", self._name, light) - # TODO: make sure that the lights are loaded when doing this - all_lights.append(light) - elif "entity_id" in state.attributes: # it's a light group - group = state.attributes["entity_id"] - _LOGGER.debug("%s: Unpacked %s to %s", self._name, lights, group) - all_lights.extend(group) - else: - all_lights.append(light) - return all_lights - async def async_added_to_hass(self): """Call when entity about to be added to hass.""" if self._lights: - unpacked_lights = self._unpack_light_groups(self._lights) - async_track_state_change_event( - self.hass, unpacked_lights, self._light_event - ) - # Tracks 'light.turn_off(..., transition=...)' service calls - self.hass.bus.async_listen( - EVENT_CALL_SERVICE, self._turn_off_event_listener - ) - track_kwargs = dict(hass=self.hass, action=self._state_changed) - if self._sleep_entity is not None: - sleep_kwargs = dict(track_kwargs, entity_ids=self._sleep_entity) - async_track_state_change(**sleep_kwargs, to_state=self._sleep_state) - async_track_state_change(**sleep_kwargs, from_state=self._sleep_state) - - if self._disable_entity is not None: - disable_kwargs = dict(track_kwargs, entity_ids=self._disable_entity) - async_track_state_change( - **disable_kwargs, from_state=self._disable_state + if self.hass.is_running: + await self._setup_listeners() + else: + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, self._setup_listeners ) - async_track_state_change(**disable_kwargs, to_state=self._disable_state) - last_state = await self.async_get_last_state() if last_state and last_state.state == STATE_ON: await self.async_turn_on() + def _unpack_light_groups(self) -> None: + all_lights = [] + for light in self._lights: + state = self.hass.states.get(light) + if state is None: + _LOGGER.debug("%s: State of %s is None", self._name, light) + all_lights.append(light) + elif "entity_id" in state.attributes: # it's a light group + group = state.attributes["entity_id"] + all_lights.extend(group) + _LOGGER.debug("%s: Unpacked %s to %s", self._name, light, group) + else: + _LOGGER.debug("%s: Did not unpack %s to %s", self._name, light) + all_lights.append(light) + self._lights = all_lights + + async def _setup_listeners(self, _=None): + self._unpack_light_groups() + for light in self._lights: + self.turn_on_off_listener.lights.add(light) + async_track_state_change_event(self.hass, self._lights, self._light_event) + track_kwargs = dict(hass=self.hass, action=self._state_changed) + if self._sleep_entity is not None: + sleep_kwargs = dict(track_kwargs, entity_ids=self._sleep_entity) + async_track_state_change(**sleep_kwargs, to_state=self._sleep_state) + async_track_state_change(**sleep_kwargs, from_state=self._sleep_state) + + if self._disable_entity is not None: + disable_kwargs = dict(track_kwargs, entity_ids=self._disable_entity) + async_track_state_change(**disable_kwargs, from_state=self._disable_state) + async_track_state_change(**disable_kwargs, to_state=self._disable_state) + @property def icon(self): """Icon to use in the frontend, if any.""" @@ -504,13 +512,16 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): from_state, to_state, ) + lock = self._locks.get(entity_id) + if lock is not None and lock.locked: + return await self._update_lights(transition=self._initial_transition, force=True) async def _light_event(self, event): + old_state = event.data.get("old_state") new_state = event.data.get("new_state") entity_id = event.data.get("entity_id") - now_ts = dt_util.now().timestamp() if ( old_state is not None and old_state.state == "off" @@ -520,12 +531,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug( "%s: Detected an 'off' → 'on' event for '%s'", self._name, entity_id ) - if await self._maybe_cancel_adjusting(entity_id, now_ts, event): - # Stop if a rapid 'off' → 'on' → 'off' happens. - _LOGGER.debug( - "%s: Cancelling adjusting lights for %s", self._name, entity_id - ) - return + on_to_off_event = self._on_to_off_event.get(entity_id) + lock = self._locks.setdefault(entity_id, asyncio.Lock()) + async with lock: + if await self.turn_on_off_listener.maybe_cancel_adjusting( + entity_id, + off_to_on_event=event, + on_to_off_event=on_to_off_event, + ): + # Stop if a rapid 'off' → 'on' → 'off' happens. + _LOGGER.debug( + "%s: Cancelling adjusting lights for %s", self._name, entity_id + ) + return await self._update_lights( lights=[entity_id], transition=self._initial_transition, @@ -538,9 +556,27 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): and new_state.state == "off" ): # Tracks 'off' → 'on' state changes - self._on_to_off_event[entity_id] = (now_ts, event.context.id) + self._on_to_off_event[entity_id] = event - async def _maybe_cancel_adjusting(self, entity_id, now_ts, off_to_on_event) -> bool: + +class TurnOnOffListener: + """Track 'light.turn_off(..., transition=...)' and 'light.turn_on' service calls.""" + + def __init__(self, hass): + """Initialize the TurnOnOffListener that is shared among all switches.""" + self.hass = hass + self.lights = set() + + # Tracks 'light.turn_off(..., transition=...)' service calls + self.turn_off_event: Dict[str, Tuple[str, float]] = {} + # Tracks 'light.turn_on' service calls + self.turn_on_event: Dict[str, Tuple[str]] = {} + + self.hass.bus.async_listen(EVENT_CALL_SERVICE, self.turn_on_off_event_listener) + + async def maybe_cancel_adjusting( + self, entity_id, off_to_on_event, on_to_off_event + ) -> bool: """Cancel the adjusting of a light if it has just been turned off. Possibly the lights just got a 'turn_off' call, however, the light @@ -553,18 +589,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if the brightness is still decreasing. Only if it is the case we adjust the lights. """ - ts_on_to_off, id_on_to_off = self._on_to_off_event.get(entity_id, (0, None)) - id_turn_off, transition = self._turn_off_event.get(entity_id, (None, None)) - id_turn_on = self._turn_on_event.get(entity_id) + if on_to_off_event is None: + # No state change has been registered before. + return False + + id_on_to_off = on_to_off_event.context.id + id_turn_off, transition = self.turn_off_event.get(entity_id, (None, None)) + id_turn_on = self.turn_on_event.get(entity_id) id_off_to_on = off_to_on_event.context.id if id_off_to_on == id_turn_on and id_off_to_on is not None: # State change 'off' → 'on' triggered by 'light.turn_on'. return False - elif ts_on_to_off == 0: - # No state change has been registered before. - return False - elif id_on_to_off == id_turn_off and id_on_to_off is not None: + + if id_on_to_off == id_turn_off and id_on_to_off is not None: # State change 'off' → 'on' and 'light.turn_off(..., transition=...)' come # from the same event, so wait at least the 'turn_off' transition time. delay = transition @@ -573,20 +611,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Possibly because of polling. delay = TURNING_OFF_DELAY - delta_time = now_ts - ts_on_to_off + delta_time = (dt_util.utcnow() - on_to_off_event.time_fired).total_seconds() if delta_time > delay: return False + + # Here we could just `return True` but because we want to prevent any updates + # from happening to this light (through async_track_time_interval or + # sleep_state or disable_state) for some time, we wait below until the light + # is 'off' or the time has passed. + delay -= delta_time # delta_time has passed since the 'off' → 'on' event - _LOGGER.debug( - "%s: Waiting with adjusting '%s' for %s.", self._name, entity_id, delay - ) + _LOGGER.debug("Waiting with adjusting '%s' for %s.", entity_id, delay) current_state = self.hass.states.get(entity_id) - _LOGGER.debug( - "%s: '%s' state before sleep is '%s'", - self._name, - entity_id, - current_state, - ) + _LOGGER.debug("'%s' state before sleep is '%s'", entity_id, current_state) for _ in range(3): # It can happen that the actual transition time is longer than the # specified time in the 'turn_off' service, so we check whether the @@ -605,12 +642,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): old_brightness = old_state.attributes.get(ATTR_BRIGHTNESS, 0) current_brightness = current_state.attributes.get(ATTR_BRIGHTNESS, 0) brightness_going_down = old_brightness > current_brightness - _LOGGER.debug( - "%s: '%s' state after sleep is '%s'", - self._name, - entity_id, - current_state, - ) + _LOGGER.debug("'%s' state after sleep is '%s'", entity_id, current_state) if not brightness_going_down: break delay = TURNING_OFF_DELAY # next time only wait this long @@ -620,38 +652,39 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # TODO: I am doing this because it seems like HA cannot detect # whether a light is transitioning into 'off'. Because in my # tests `brightness_going_down == False` even when it is actually - # still going down... Maybe needs some discussion. + # still going down... Maybe needs some discussion/input? return True return current_state.state == "off" - async def _turn_off_event_listener(self, event): + async def turn_on_off_event_listener(self, event): """Track 'light.turn_off(..., transition=...)' and 'light.turn_on' service calls.""" domain = event.data.get(ATTR_DOMAIN) if domain != LIGHT_DOMAIN: return + service = event.data.get(ATTR_SERVICE) service_data = event.data.get(ATTR_SERVICE_DATA, {}) + entity_id = service_data.get(ATTR_ENTITY_ID) if isinstance(entity_id, str): entity_id = [entity_id] + if not any(eid in self.lights for eid in entity_id): + return + if service == SERVICE_TURN_OFF: transition = service_data.get(ATTR_TRANSITION) if transition is not None and transition > 0: _LOGGER.debug( - "%s: Detected an 'light.turn_off('%s', transition=%s)' event", - self._name, + "Detected an 'light.turn_off('%s', transition=%s)' event", entity_id, transition, ) for eid in entity_id: - self._turn_off_event[eid] = (event.context.id, transition) + self.turn_off_event[eid] = (event.context.id, transition) + elif service == SERVICE_TURN_ON: - _LOGGER.debug( - "%s: Detected an 'light.turn_on('%s')' event", - self._name, - entity_id, - ) + _LOGGER.debug("Detected an 'light.turn_on('%s')' event", entity_id) for eid in entity_id: - self._turn_on_event[eid] = event.context.id + self.turn_on_event[eid] = event.context.id From 555d4053991cb001be3ad09572edb4a82a4c521f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 28 Sep 2020 14:35:32 +0200 Subject: [PATCH 0204/1077] simplify turn on and off listening --- custom_components/adaptive_lighting/switch.py | 74 +++++++------------ 1 file changed, 26 insertions(+), 48 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index c3f1e955..9f28be3c 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -9,12 +9,7 @@ from typing import Dict, Tuple import voluptuous as vol -from homeassistant.components.homeassistant import ( - DOMAIN as HA_DOMAIN, - SERVICE_UPDATE_ENTITY, -) from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, @@ -285,7 +280,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): all_lights.extend(group) _LOGGER.debug("%s: Unpacked %s to %s", self._name, light, group) else: - _LOGGER.debug("%s: Did not unpack %s to %s", self._name, light) all_lights.append(light) self._lights = all_lights @@ -531,13 +525,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug( "%s: Detected an 'off' → 'on' event for '%s'", self._name, entity_id ) - on_to_off_event = self._on_to_off_event.get(entity_id) lock = self._locks.setdefault(entity_id, asyncio.Lock()) async with lock: if await self.turn_on_off_listener.maybe_cancel_adjusting( entity_id, off_to_on_event=event, - on_to_off_event=on_to_off_event, + on_to_off_event=self._on_to_off_event.get(entity_id), ): # Stop if a rapid 'off' → 'on' → 'off' happens. _LOGGER.debug( @@ -560,14 +553,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): class TurnOnOffListener: - """Track 'light.turn_off(..., transition=...)' and 'light.turn_on' service calls.""" + """Track 'light.turn_off' and 'light.turn_on' service calls.""" def __init__(self, hass): """Initialize the TurnOnOffListener that is shared among all switches.""" self.hass = hass self.lights = set() - # Tracks 'light.turn_off(..., transition=...)' service calls + # Tracks 'light.turn_off' service calls self.turn_off_event: Dict[str, Tuple[str, float]] = {} # Tracks 'light.turn_on' service calls self.turn_on_event: Dict[str, Tuple[str]] = {} @@ -602,10 +595,14 @@ class TurnOnOffListener: # State change 'off' → 'on' triggered by 'light.turn_on'. return False - if id_on_to_off == id_turn_off and id_on_to_off is not None: - # State change 'off' → 'on' and 'light.turn_off(..., transition=...)' come + if ( + id_on_to_off == id_turn_off + and id_on_to_off is not None + and transition is not None # 'turn_off' is called with transition=... + ): + # State change 'on' → 'off' and 'light.turn_off(..., transition=...)' come # from the same event, so wait at least the 'turn_off' transition time. - delay = transition + delay = max(transition, TURNING_OFF_DELAY) else: # State change 'off' → 'on' happened because the light state was set. # Possibly because of polling. @@ -621,44 +618,26 @@ class TurnOnOffListener: # is 'off' or the time has passed. delay -= delta_time # delta_time has passed since the 'off' → 'on' event - _LOGGER.debug("Waiting with adjusting '%s' for %s.", entity_id, delay) - current_state = self.hass.states.get(entity_id) - _LOGGER.debug("'%s' state before sleep is '%s'", entity_id, current_state) + _LOGGER.debug("Waiting with adjusting '%s' for %s", entity_id, delay) + for _ in range(3): # It can happen that the actual transition time is longer than the - # specified time in the 'turn_off' service, so we check whether the - # brightness is still going down, if so, we wait a little longer. + # specified time in the 'turn_off' service. await asyncio.sleep(delay) - await self.hass.services.async_call( - HA_DOMAIN, - SERVICE_UPDATE_ENTITY, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - old_state = current_state - current_state = self.hass.states.get(entity_id) - if current_state.state == "off": + if not is_on(self.hass, entity_id): return True - old_brightness = old_state.attributes.get(ATTR_BRIGHTNESS, 0) - current_brightness = current_state.attributes.get(ATTR_BRIGHTNESS, 0) - brightness_going_down = old_brightness > current_brightness - _LOGGER.debug("'%s' state after sleep is '%s'", entity_id, current_state) - if not brightness_going_down: - break delay = TURNING_OFF_DELAY # next time only wait this long if transition is not None: - # Always ignore when there's a transition and light is still on. - # TODO: I am doing this because it seems like HA cannot detect - # whether a light is transitioning into 'off'. Because in my - # tests `brightness_going_down == False` even when it is actually - # still going down... Maybe needs some discussion/input? + # Always ignore when there's a 'turn_off' transition. + # Because it seems like HA cannot detect whether a light is + # transitioning into 'off'. Maybe needs some discussion/input? return True - return current_state.state == "off" + return False async def turn_on_off_event_listener(self, event): - """Track 'light.turn_off(..., transition=...)' and 'light.turn_on' service calls.""" + """Track 'light.turn_off' and 'light.turn_on' service calls.""" domain = event.data.get(ATTR_DOMAIN) if domain != LIGHT_DOMAIN: return @@ -675,14 +654,13 @@ class TurnOnOffListener: if service == SERVICE_TURN_OFF: transition = service_data.get(ATTR_TRANSITION) - if transition is not None and transition > 0: - _LOGGER.debug( - "Detected an 'light.turn_off('%s', transition=%s)' event", - entity_id, - transition, - ) - for eid in entity_id: - self.turn_off_event[eid] = (event.context.id, transition) + _LOGGER.debug( + "Detected an 'light.turn_off('%s', transition=%s)' event", + entity_id, + transition, + ) + for eid in entity_id: + self.turn_off_event[eid] = (event.context.id, transition) elif service == SERVICE_TURN_ON: _LOGGER.debug("Detected an 'light.turn_on('%s')' event", entity_id) From d198528029115a2084f3cb0662945248eca85398 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 28 Sep 2020 14:48:09 +0200 Subject: [PATCH 0205/1077] add hacs.json --- custom_updater.json | 16 ---------------- hacs.json | 5 +++++ 2 files changed, 5 insertions(+), 16 deletions(-) delete mode 100644 custom_updater.json create mode 100644 hacs.json diff --git a/custom_updater.json b/custom_updater.json deleted file mode 100644 index 73a04bb6..00000000 --- a/custom_updater.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "circadian_lighting": { - "updated_at": "2020-06-11", - "version": "1.0.13", - "local_location": "/custom_components/circadian_lighting/__init__.py", - "remote_location": "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/__init__.py", - "visit_repo": "https://github.com/claytonjn/hass-circadian_lighting", - "changelog": "https://github.com/claytonjn/hass-circadian_lighting/releases", - "resources": [ - "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/manifest.json", - "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/sensor.py", - "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/services.yaml", - "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/switch.py" - ] - } -} diff --git a/hacs.json b/hacs.json new file mode 100644 index 00000000..1de0dd51 --- /dev/null +++ b/hacs.json @@ -0,0 +1,5 @@ +{ + "name": "adaptive_lighting", + "render_readme": true, + "domains": ["switch"] +} From 6bb5e4229dcb5c628b1038bac53e7ae41e9a1700 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 28 Sep 2020 16:10:05 +0200 Subject: [PATCH 0206/1077] update yaml settings --- custom_components/adaptive_lighting/config_flow.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 7002c7d7..543363a2 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -43,7 +43,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input=None): """Handle configuration by yaml file.""" await self.async_set_unique_id(user_input["name"]) - self._abort_if_unique_id_configured() + for entry in self._async_current_entries(): + if entry.unique_id == self.unique_id: + self.hass.config_entries.async_update_entry( + entry, data=dict(entry.data, **user_input) + ) + return return self.async_create_entry(title=user_input["name"], data=user_input) @staticmethod From e6e51dac7668f2b44bc065c41932640097d2c26d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 28 Sep 2020 16:31:41 +0200 Subject: [PATCH 0207/1077] always use color_temp over rgb --- custom_components/adaptive_lighting/const.py | 7 +++++-- custom_components/adaptive_lighting/switch.py | 17 +++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index d194efc8..f49188ab 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -16,7 +16,10 @@ CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST = ( "disable_brightness_adjust", False, ) -CONF_DISABLE_COLOR_ADJUST, DEFAULT_DISABLE_COLOR_ADJUST = "disable_color_adjust", False +CONF_DISABLE_COLOR_TEMP_ADJUST, DEFAULT_DISABLE_COLOR_TEMP_ADJUST = ( + "disable_color_temp_adjust", + False, +) CONF_DISABLE_ENTITY = "disable_entity" CONF_DISABLE_STATE = "disable_state" CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 @@ -55,7 +58,7 @@ def int_between(min_int, max_int): VALIDATION_TUPLES = [ (CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), (CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST, bool), - (CONF_DISABLE_COLOR_ADJUST, DEFAULT_DISABLE_COLOR_ADJUST, bool), + (CONF_DISABLE_COLOR_TEMP_ADJUST, DEFAULT_DISABLE_COLOR_TEMP_ADJUST, bool), (CONF_DISABLE_ENTITY, NONE_STR, cv.entity_id), (CONF_DISABLE_STATE, NONE_STR, str), (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 9f28be3c..93d7955a 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -60,7 +60,7 @@ from .const import ( ATTR_TURN_ON_OFF_LISTENER, CONF_COLORS_ONLY, CONF_DISABLE_BRIGHTNESS_ADJUST, - CONF_DISABLE_COLOR_ADJUST, + CONF_DISABLE_COLOR_TEMP_ADJUST, CONF_DISABLE_ENTITY, CONF_DISABLE_STATE, CONF_INITIAL_TRANSITION, @@ -182,7 +182,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._name = data[CONF_NAME] self._lights = data[CONF_LIGHTS] self._disable_brightness_adjust = data[CONF_DISABLE_BRIGHTNESS_ADJUST] - self._disable_color_adjust = data[CONF_DISABLE_COLOR_ADJUST] + self._disable_color_temp_adjust = data[CONF_DISABLE_COLOR_TEMP_ADJUST] self._disable_entity = data[CONF_DISABLE_ENTITY] self._disable_state = data[CONF_DISABLE_STATE] self._initial_transition = data[CONF_INITIAL_TRANSITION] @@ -469,10 +469,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ): service_data[ATTR_BRIGHTNESS_PCT] = self._brightness - if "color" in features and not self._disable_color_adjust: + if "color_temp" in features and not self._disable_color_temp_adjust: + attributes = self.hass.states.get(light).attributes + min_mireds, max_mireds = attributes["min_mireds"], attributes["max_mireds"] + color_temp_mired = max(min(self._color_temp_mired, max_mireds), min_mireds) + service_data[ATTR_COLOR_TEMP] = color_temp_mired + elif "color" in features: service_data[ATTR_RGB_COLOR] = self._rgb_color - elif "color_temp" in features: - service_data[ATTR_COLOR_TEMP] = self._color_temp_mired _LOGGER.debug( "%s: Scheduling 'light.turn_on' with the following 'service_data': %s", @@ -623,7 +626,9 @@ class TurnOnOffListener: for _ in range(3): # It can happen that the actual transition time is longer than the # specified time in the 'turn_off' service. - await asyncio.sleep(delay) + await asyncio.sleep( + delay + ) # TODO: cancel this somehow when 'turn_on' event happens if not is_on(self.hass, entity_id): return True delay = TURNING_OFF_DELAY # next time only wait this long From 0b1ff117d42c31cb86b7b7c482f041269f1a3648 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 28 Sep 2020 16:50:15 +0200 Subject: [PATCH 0208/1077] fixup --- custom_components/adaptive_lighting/strings.json | 2 +- custom_components/adaptive_lighting/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index b871aac9..d63dae61 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -22,7 +22,7 @@ "data": { "lights": "lights", "disable_brightness_adjust": "disable_brightness_adjust", - "disable_color_adjust": "disable_color_adjust", + "disable_color_temp_adjust": "disable_color_temp_adjust", "disable_entity": "disable_entity", "disable_state": "disable_state", "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index b871aac9..d63dae61 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -22,7 +22,7 @@ "data": { "lights": "lights", "disable_brightness_adjust": "disable_brightness_adjust", - "disable_color_adjust": "disable_color_adjust", + "disable_color_temp_adjust": "disable_color_temp_adjust", "disable_entity": "disable_entity", "disable_state": "disable_state", "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", From 0c76334124abc62ddb623113fae9f573965d3d6b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 28 Sep 2020 20:48:09 +0200 Subject: [PATCH 0209/1077] update to latest PR https://github.com/home-assistant/core/pull/40626 --- custom_components/adaptive_lighting/const.py | 7 ++ custom_components/adaptive_lighting/switch.py | 72 +++++++++++++------ 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index f49188ab..cdf20d1a 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -20,6 +20,10 @@ CONF_DISABLE_COLOR_TEMP_ADJUST, DEFAULT_DISABLE_COLOR_TEMP_ADJUST = ( "disable_color_temp_adjust", False, ) +CONF_DISABLE_RGB_COLOR_ADJUST, DEFAULT_DISABLE_RGB_COLOR_ADJUST = ( + "disable_rgb_color_adjust", + False, +) CONF_DISABLE_ENTITY = "disable_entity" CONF_DISABLE_STATE = "disable_state" CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 @@ -29,6 +33,7 @@ CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP = "max_color_temp", 5500 CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS = "min_brightness", 1 CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP = "min_color_temp", 2500 CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE = "only_once", False +CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1 CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP = "sleep_color_temp", 1000 CONF_SLEEP_ENTITY = "sleep_entity" @@ -60,6 +65,7 @@ VALIDATION_TUPLES = [ (CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST, bool), (CONF_DISABLE_COLOR_TEMP_ADJUST, DEFAULT_DISABLE_COLOR_TEMP_ADJUST, bool), (CONF_DISABLE_ENTITY, NONE_STR, cv.entity_id), + (CONF_DISABLE_RGB_COLOR_ADJUST, DEFAULT_DISABLE_RGB_COLOR_ADJUST, bool), (CONF_DISABLE_STATE, NONE_STR, str), (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), @@ -68,6 +74,7 @@ VALIDATION_TUPLES = [ (CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS, int_between(1, 100)), (CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP, int_between(1000, 10000)), (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), + (CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool), (CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)), (CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP, int_between(1000, 10000)), (CONF_SLEEP_ENTITY, NONE_STR, cv.entity_id), diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 93d7955a..e1854275 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -3,6 +3,7 @@ import asyncio import bisect from copy import deepcopy +import datetime from datetime import timedelta import logging from typing import Dict, Tuple @@ -62,6 +63,7 @@ from .const import ( CONF_DISABLE_BRIGHTNESS_ADJUST, CONF_DISABLE_COLOR_TEMP_ADJUST, CONF_DISABLE_ENTITY, + CONF_DISABLE_RGB_COLOR_ADJUST, CONF_DISABLE_STATE, CONF_INITIAL_TRANSITION, CONF_INTERVAL, @@ -72,6 +74,7 @@ from .const import ( CONF_MIN_COLOR_TEMP, CONF_ON_LIGHTS_ONLY, CONF_ONLY_ONCE, + CONF_PREFER_RGB_COLOR, CONF_SLEEP_BRIGHTNESS, CONF_SLEEP_COLOR_TEMP, CONF_SLEEP_ENTITY, @@ -182,6 +185,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._name = data[CONF_NAME] self._lights = data[CONF_LIGHTS] self._disable_brightness_adjust = data[CONF_DISABLE_BRIGHTNESS_ADJUST] + self._disable_rgb_color_adjust = data[CONF_DISABLE_RGB_COLOR_ADJUST] self._disable_color_temp_adjust = data[CONF_DISABLE_COLOR_TEMP_ADJUST] self._disable_entity = data[CONF_DISABLE_ENTITY] self._disable_state = data[CONF_DISABLE_STATE] @@ -192,6 +196,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._min_brightness = data[CONF_MIN_BRIGHTNESS] self._min_color_temp = data[CONF_MIN_COLOR_TEMP] self._only_once = data[CONF_ONLY_ONCE] + self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR] self._sleep_brightness = data[CONF_SLEEP_BRIGHTNESS] self._sleep_color_temp = data[CONF_SLEEP_COLOR_TEMP] self._sleep_entity = data[CONF_SLEEP_ENTITY] @@ -359,12 +364,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): def _get_sun_events(self, date): def _replace_time(date, key): - other_date = getattr(self, f"_{key}_time") + time = getattr(self, f"_{key}_time") + dt = datetime.datetime.combine(datetime.date.today(), time) + tz = self.hass.config.time_zone + utc_time = tz.localize(dt).astimezone(dt_util.UTC) return date.replace( - hour=other_date.hour, - minute=other_date.minute, - second=other_date.second, - microsecond=other_date.microsecond, + hour=utc_time.hour, + minute=utc_time.minute, + second=utc_time.second, + microsecond=utc_time.microsecond, ) location = get_astral_location(self.hass) @@ -437,8 +445,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return self._min_color_temp def _calc_brightness(self) -> float: - if self._disable_brightness_adjust: - return if self._is_sleep(): return self._sleep_brightness if self._percent > 0: @@ -463,18 +469,23 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_TRANSITION] = transition if ( - self._brightness is not None - and "brightness" in features + "brightness" in features + and not self._disable_brightness_adjust and not colors_only ): service_data[ATTR_BRIGHTNESS_PCT] = self._brightness - if "color_temp" in features and not self._disable_color_temp_adjust: + prefer_rgb_color = self._prefer_rgb_color + if ( + "color_temp" in features + and not self._disable_color_temp_adjust + and not (prefer_rgb_color and "color" in features) + ): attributes = self.hass.states.get(light).attributes min_mireds, max_mireds = attributes["min_mireds"], attributes["max_mireds"] color_temp_mired = max(min(self._color_temp_mired, max_mireds), min_mireds) service_data[ATTR_COLOR_TEMP] = color_temp_mired - elif "color" in features: + elif "color" in features and not self._disable_rgb_color_adjust: service_data[ATTR_RGB_COLOR] = self._rgb_color _LOGGER.debug( @@ -494,6 +505,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _adjust_lights(self, lights, transition): if not self._should_adjust(): return + _LOGGER.debug( + "%s: '_adjust_lights(%s, %s)' called", self.name, lights, transition + ) tasks = [ await self._adjust_light(light, transition) for light in lights @@ -515,7 +529,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): await self._update_lights(transition=self._initial_transition, force=True) async def _light_event(self, event): - old_state = event.data.get("old_state") new_state = event.data.get("new_state") entity_id = event.data.get("entity_id") @@ -568,6 +581,8 @@ class TurnOnOffListener: # Tracks 'light.turn_on' service calls self.turn_on_event: Dict[str, Tuple[str]] = {} + self.sleep_tasks: Dict[str, asyncio.Task] = {} + self.hass.bus.async_listen(EVENT_CALL_SERVICE, self.turn_on_off_event_listener) async def maybe_cancel_adjusting( @@ -626,9 +641,17 @@ class TurnOnOffListener: for _ in range(3): # It can happen that the actual transition time is longer than the # specified time in the 'turn_off' service. - await asyncio.sleep( - delay - ) # TODO: cancel this somehow when 'turn_on' event happens + coro = asyncio.sleep(delay) + task = self.sleep_tasks[entity_id] = asyncio.ensure_future(coro) + try: + await task + except asyncio.CancelledError: # 'light.turn_on' has been called + _LOGGER.debug( + "Sleep task is cancelled due to 'light.turn_on('%s')' call", + entity_id, + ) + return False + if not is_on(self.hass, entity_id): return True delay = TURNING_OFF_DELAY # next time only wait this long @@ -650,24 +673,27 @@ class TurnOnOffListener: service = event.data.get(ATTR_SERVICE) service_data = event.data.get(ATTR_SERVICE_DATA, {}) - entity_id = service_data.get(ATTR_ENTITY_ID) - if isinstance(entity_id, str): - entity_id = [entity_id] + entity_ids = service_data.get(ATTR_ENTITY_ID) + if isinstance(entity_ids, str): + entity_ids = [entity_ids] - if not any(eid in self.lights for eid in entity_id): + if not any(eid in self.lights for eid in entity_ids): return if service == SERVICE_TURN_OFF: transition = service_data.get(ATTR_TRANSITION) _LOGGER.debug( "Detected an 'light.turn_off('%s', transition=%s)' event", - entity_id, + entity_ids, transition, ) - for eid in entity_id: + for eid in entity_ids: self.turn_off_event[eid] = (event.context.id, transition) elif service == SERVICE_TURN_ON: - _LOGGER.debug("Detected an 'light.turn_on('%s')' event", entity_id) - for eid in entity_id: + _LOGGER.debug("Detected an 'light.turn_on('%s')' event", entity_ids) + for eid in entity_ids: + task = self.sleep_tasks.get(eid) + if task is not None: + task.cancel() self.turn_on_event[eid] = event.context.id From 5167dc656581126abbf2a0306f2ee51b66259658 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 28 Sep 2020 21:08:12 +0200 Subject: [PATCH 0210/1077] fix toggle bug --- custom_components/adaptive_lighting/strings.json | 2 ++ custom_components/adaptive_lighting/switch.py | 7 ++++--- custom_components/adaptive_lighting/translations/en.json | 2 ++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index d63dae61..45a0396a 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -24,6 +24,7 @@ "disable_brightness_adjust": "disable_brightness_adjust", "disable_color_temp_adjust": "disable_color_temp_adjust", "disable_entity": "disable_entity", + "disable_rgb_color_adjust": "disable_rgb_color_adjust", "disable_state": "disable_state", "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", "interval": "interval", @@ -32,6 +33,7 @@ "min_brightness": "min_brightness", "min_color_temp": "min_color_temp", "only_once": "only_once", + "prefer_rgb_color": "prefer_rgb_color", "sleep_brightness": "sleep_brightness", "sleep_color_temp": "sleep_color_temp", "sleep_entity": "sleep_entity", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index e1854275..143911ce 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -271,7 +271,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) last_state = await self.async_get_last_state() if last_state and last_state.state == STATE_ON: - await self.async_turn_on() + await self.async_turn_on(adjust_lights=False) def _unpack_light_groups(self) -> None: all_lights = [] @@ -325,12 +325,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return {key: None for key in attrs} return attrs - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, adjust_lights=True): """Turn on adaptive lighting.""" - await self._update_lights(transition=self._initial_transition, force=True) self.unsub_tracker = async_track_time_interval( self.hass, self._async_update_at_interval, self._interval ) + if adjust_lights: + await self._update_lights(transition=self._initial_transition, force=True) async def async_turn_off(self, **kwargs): """Turn off adaptive lighting.""" diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index d63dae61..45a0396a 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -24,6 +24,7 @@ "disable_brightness_adjust": "disable_brightness_adjust", "disable_color_temp_adjust": "disable_color_temp_adjust", "disable_entity": "disable_entity", + "disable_rgb_color_adjust": "disable_rgb_color_adjust", "disable_state": "disable_state", "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", "interval": "interval", @@ -32,6 +33,7 @@ "min_brightness": "min_brightness", "min_color_temp": "min_color_temp", "only_once": "only_once", + "prefer_rgb_color": "prefer_rgb_color", "sleep_brightness": "sleep_brightness", "sleep_color_temp": "sleep_color_temp", "sleep_entity": "sleep_entity", From 02923a3c9e8f8d08dd62703a65fe92a88807a500 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 28 Sep 2020 21:13:23 +0200 Subject: [PATCH 0211/1077] do not raise but call self._abort_if_unique_id_configured --- custom_components/adaptive_lighting/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 543363a2..2b2de991 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -48,7 +48,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_update_entry( entry, data=dict(entry.data, **user_input) ) - return + self._abort_if_unique_id_configured() return self.async_create_entry(title=user_input["name"], data=user_input) @staticmethod From 95823576ea3ded481599b2075c17948bb4f609ad Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 28 Sep 2020 21:16:10 +0200 Subject: [PATCH 0212/1077] pylint fix --- custom_components/adaptive_lighting/switch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 143911ce..efd19bf3 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -366,9 +366,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): def _get_sun_events(self, date): def _replace_time(date, key): time = getattr(self, f"_{key}_time") - dt = datetime.datetime.combine(datetime.date.today(), time) - tz = self.hass.config.time_zone - utc_time = tz.localize(dt).astimezone(dt_util.UTC) + date_time = datetime.datetime.combine(datetime.date.today(), time) + time_zone = self.hass.config.time_zone + utc_time = time_zone.localize(date_time).astimezone(dt_util.UTC) return date.replace( hour=utc_time.hour, minute=utc_time.minute, From d18b2a9da6c64aeef950afbcc6398339b05d27d0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 28 Sep 2020 23:39:55 +0200 Subject: [PATCH 0213/1077] fix unsub sub --- .../adaptive_lighting/__init__.py | 14 ++-- custom_components/adaptive_lighting/switch.py | 67 ++++++++++++------- 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 1e935df7..5c0aa080 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -29,6 +29,7 @@ import logging import voluptuous as vol +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry import homeassistant.helpers.config_validation as cv @@ -68,10 +69,10 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry: ConfigEntry): """Set up the component.""" - hass.data.setdefault(DOMAIN, {}) + data = hass.data.setdefault(DOMAIN, {}) undo_listener = config_entry.add_update_listener(async_update_options) - hass.data[DOMAIN][config_entry.entry_id] = {UNDO_UPDATE_LISTENER: undo_listener} + data[config_entry.entry_id] = {UNDO_UPDATE_LISTENER: undo_listener} for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, platform) @@ -95,9 +96,14 @@ async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool: ] ) ) - hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() + data = hass.data[DOMAIN] + data[config_entry.entry_id][UNDO_UPDATE_LISTENER]() + switch = data[config_entry.entry_id][SWITCH_DOMAIN] + while switch.unsub_trackers: + unsub = switch.unsub_trackers.pop() + unsub() if unload_ok: - hass.data[DOMAIN].pop(config_entry.entry_id) + data.pop(config_entry.entry_id) return unload_ok diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index efd19bf3..482780be 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -23,7 +23,7 @@ from homeassistant.components.light import ( VALID_TRANSITION, is_on, ) -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity from homeassistant.const import ( ATTR_DOMAIN, ATTR_ENTITY_ID, @@ -130,16 +130,14 @@ async def handle_apply(switch, service_call): async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the AdaptiveLighting switch.""" - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} + data = hass.data[DOMAIN] - if ATTR_TURN_ON_OFF_LISTENER not in hass.data[DOMAIN]: - hass.data[DOMAIN][ATTR_TURN_ON_OFF_LISTENER] = TurnOnOffListener(hass) + if ATTR_TURN_ON_OFF_LISTENER not in data: + data[ATTR_TURN_ON_OFF_LISTENER] = TurnOnOffListener(hass) + turn_on_off_listener = data[ATTR_TURN_ON_OFF_LISTENER] - turn_on_off_listener = hass.data[DOMAIN][ATTR_TURN_ON_OFF_LISTENER] switch = AdaptiveSwitch(hass, config_entry, turn_on_off_listener) - name = config_entry.data[CONF_NAME] - hass.data[DOMAIN][name] = switch + data[config_entry.entry_id][SWITCH_DOMAIN] = switch # Register `apply` service platform = entity_platform.current_platform.get() @@ -185,9 +183,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._name = data[CONF_NAME] self._lights = data[CONF_LIGHTS] self._disable_brightness_adjust = data[CONF_DISABLE_BRIGHTNESS_ADJUST] - self._disable_rgb_color_adjust = data[CONF_DISABLE_RGB_COLOR_ADJUST] self._disable_color_temp_adjust = data[CONF_DISABLE_COLOR_TEMP_ADJUST] self._disable_entity = data[CONF_DISABLE_ENTITY] + self._disable_rgb_color_adjust = data[CONF_DISABLE_RGB_COLOR_ADJUST] self._disable_state = data[CONF_DISABLE_STATE] self._initial_transition = data[CONF_INITIAL_TRANSITION] self._interval = data[CONF_INTERVAL] @@ -226,7 +224,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._hs_color = None # Set and unset tracker in async_turn_on and async_turn_off - self.unsub_tracker = None + self.unsub_trackers = [] _LOGGER.debug( "%s: Setting up with '%s'," " config_entry.data: '%s'," @@ -251,7 +249,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): @property def is_on(self): """Return true if adaptive lighting is on.""" - return self.unsub_tracker is not None + return bool(self.unsub_trackers) def _supported_features(self, light): state = self.hass.states.get(light) @@ -271,7 +269,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) last_state = await self.async_get_last_state() if last_state and last_state.state == STATE_ON: - await self.async_turn_on(adjust_lights=False) + await self.async_turn_on(adjust_lights=False, setup_listeners=False) def _unpack_light_groups(self) -> None: all_lights = [] @@ -292,17 +290,29 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._unpack_light_groups() for light in self._lights: self.turn_on_off_listener.lights.add(light) - async_track_state_change_event(self.hass, self._lights, self._light_event) + self.unsub_trackers.append( + async_track_state_change_event(self.hass, self._lights, self._light_event) + ) track_kwargs = dict(hass=self.hass, action=self._state_changed) if self._sleep_entity is not None: sleep_kwargs = dict(track_kwargs, entity_ids=self._sleep_entity) - async_track_state_change(**sleep_kwargs, to_state=self._sleep_state) - async_track_state_change(**sleep_kwargs, from_state=self._sleep_state) + self.unsub_trackers.append( + async_track_state_change(**sleep_kwargs, to_state=self._sleep_state) + ) + self.unsub_trackers.append( + async_track_state_change(**sleep_kwargs, from_state=self._sleep_state) + ) if self._disable_entity is not None: disable_kwargs = dict(track_kwargs, entity_ids=self._disable_entity) - async_track_state_change(**disable_kwargs, from_state=self._disable_state) - async_track_state_change(**disable_kwargs, to_state=self._disable_state) + self.unsub_trackers.append( + async_track_state_change( + **disable_kwargs, from_state=self._disable_state + ) + ) + self.unsub_trackers.append( + async_track_state_change(**disable_kwargs, to_state=self._disable_state) + ) @property def icon(self): @@ -325,19 +335,27 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return {key: None for key in attrs} return attrs - async def async_turn_on(self, adjust_lights=True): + async def async_turn_on(self, adjust_lights=True, setup_listeners=True): """Turn on adaptive lighting.""" - self.unsub_tracker = async_track_time_interval( - self.hass, self._async_update_at_interval, self._interval + if self.is_on: + return + self.unsub_trackers.append( + async_track_time_interval( + self.hass, self._async_update_at_interval, self._interval + ) ) + if setup_listeners: + self._setup_listeners() if adjust_lights: await self._update_lights(transition=self._initial_transition, force=True) async def async_turn_off(self, **kwargs): """Turn off adaptive lighting.""" - if self.is_on: - self.unsub_tracker() - self.unsub_tracker = None + if not self.is_on: + return + while self.unsub_trackers: + unsub = self.unsub_trackers.pop() + unsub() async def _update_attrs(self): """Update Adaptive Values.""" @@ -476,11 +494,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ): service_data[ATTR_BRIGHTNESS_PCT] = self._brightness - prefer_rgb_color = self._prefer_rgb_color if ( "color_temp" in features and not self._disable_color_temp_adjust - and not (prefer_rgb_color and "color" in features) + and not (self._prefer_rgb_color and "color" in features) ): attributes = self.hass.states.get(light).attributes min_mireds, max_mireds = attributes["min_mireds"], attributes["max_mireds"] From ee7174264643167bb0cc932ec6cc9f4dd986b3bc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 29 Sep 2020 10:28:01 +0200 Subject: [PATCH 0214/1077] fix turning on and off, delay setup listeners until HA start --- custom_components/adaptive_lighting/switch.py | 82 +++++++++++-------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 482780be..e07c559a 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -38,7 +38,7 @@ from homeassistant.const import ( SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) -from homeassistant.core import Event +from homeassistant.core import Context, Event from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( @@ -208,6 +208,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set other attributes self._icon = ICON self._entity_id = f"switch.{DOMAIN}_{slugify(self._name)}" + self._state = None # Tracks 'off' → 'on' state changes self._on_to_off_event: Dict[str, Event] = {} @@ -249,7 +250,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): @property def is_on(self): """Return true if adaptive lighting is on.""" - return bool(self.unsub_trackers) + return self._state def _supported_features(self, light): state = self.hass.states.get(light) @@ -269,50 +270,56 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) last_state = await self.async_get_last_state() if last_state and last_state.state == STATE_ON: - await self.async_turn_on(adjust_lights=False, setup_listeners=False) + self._state = True + await self.async_turn_on( + adjust_lights=not self._only_once, + setup_listeners=False, + ) + else: + self._state = False def _unpack_light_groups(self) -> None: - all_lights = [] + all_lights = set() for light in self._lights: state = self.hass.states.get(light) if state is None: _LOGGER.debug("%s: State of %s is None", self._name, light) - all_lights.append(light) + all_lights.add(light) elif "entity_id" in state.attributes: # it's a light group group = state.attributes["entity_id"] - all_lights.extend(group) + all_lights.update(group) _LOGGER.debug("%s: Unpacked %s to %s", self._name, light, group) else: - all_lights.append(light) - self._lights = all_lights + all_lights.add(light) + self.turn_on_off_listener.lights.update(all_lights) + self._lights = list(all_lights) async def _setup_listeners(self, _=None): + if self.unsub_trackers: + _LOGGER.error( + "%s: Calling '_setup_listeners' when they are already set up", self.name + ) + return + self._unpack_light_groups() - for light in self._lights: - self.turn_on_off_listener.lights.add(light) - self.unsub_trackers.append( - async_track_state_change_event(self.hass, self._lights, self._light_event) + rm_interval = async_track_time_interval( + self.hass, self._async_update_at_interval, self._interval ) + rm_state = async_track_state_change_event( + self.hass, self._lights, self._light_event + ) + self.unsub_trackers.extend([rm_interval, rm_state]) track_kwargs = dict(hass=self.hass, action=self._state_changed) if self._sleep_entity is not None: - sleep_kwargs = dict(track_kwargs, entity_ids=self._sleep_entity) - self.unsub_trackers.append( - async_track_state_change(**sleep_kwargs, to_state=self._sleep_state) - ) - self.unsub_trackers.append( - async_track_state_change(**sleep_kwargs, from_state=self._sleep_state) - ) - + kwgs = dict(track_kwargs, entity_ids=self._sleep_entity) + rm_from = async_track_state_change(**kwgs, from_state=self._sleep_state) + rm_to = async_track_state_change(**kwgs, to_state=self._sleep_state) + self.unsub_trackers.extend([rm_from, rm_to]) if self._disable_entity is not None: - disable_kwargs = dict(track_kwargs, entity_ids=self._disable_entity) - self.unsub_trackers.append( - async_track_state_change( - **disable_kwargs, from_state=self._disable_state - ) - ) - self.unsub_trackers.append( - async_track_state_change(**disable_kwargs, to_state=self._disable_state) - ) + kwgs = dict(track_kwargs, entity_ids=self._disable_entity) + rm_from = async_track_state_change(**kwgs, from_state=self._disable_state) + rm_to = async_track_state_change(**kwgs, to_state=self._disable_state) + self.unsub_trackers.extend([rm_from, rm_to]) @property def icon(self): @@ -339,11 +346,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Turn on adaptive lighting.""" if self.is_on: return - self.unsub_trackers.append( - async_track_time_interval( - self.hass, self._async_update_at_interval, self._interval - ) - ) + self._state = True if setup_listeners: self._setup_listeners() if adjust_lights: @@ -353,6 +356,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Turn off adaptive lighting.""" if not self.is_on: return + self._state = False while self.unsub_trackers: unsub = self.unsub_trackers.pop() unsub() @@ -511,8 +515,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._name, service_data, ) + return self.hass.services.async_call( - LIGHT_DOMAIN, SERVICE_TURN_ON, service_data + LIGHT_DOMAIN, + SERVICE_TURN_ON, + service_data, + context=Context(), ) def _should_adjust(self): @@ -559,7 +567,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug( "%s: Detected an 'off' → 'on' event for '%s'", self._name, entity_id ) - lock = self._locks.setdefault(entity_id, asyncio.Lock()) + lock = self._locks.get(entity_id) + if lock is None: + lock = asyncio.Lock() async with lock: if await self.turn_on_off_listener.maybe_cancel_adjusting( entity_id, From 58b4c639dd5b6449a2f5274926534d4bfee43a66 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 29 Sep 2020 10:48:35 +0200 Subject: [PATCH 0215/1077] define and use switch._unsub_trackers --- .../adaptive_lighting/__init__.py | 4 +--- custom_components/adaptive_lighting/switch.py | 23 ++++++++++--------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 5c0aa080..e8257a42 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -99,9 +99,7 @@ async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool: data = hass.data[DOMAIN] data[config_entry.entry_id][UNDO_UPDATE_LISTENER]() switch = data[config_entry.entry_id][SWITCH_DOMAIN] - while switch.unsub_trackers: - unsub = switch.unsub_trackers.pop() - unsub() + switch._unsub_trackers() # pylint: disable=protected-access if unload_ok: data.pop(config_entry.entry_id) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index e07c559a..20785d48 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -263,10 +263,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Call when entity about to be added to hass.""" if self._lights: if self.hass.is_running: - await self._setup_listeners() + await self._setup_trackers() else: self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, self._setup_listeners + EVENT_HOMEASSISTANT_START, self._setup_trackers ) last_state = await self.async_get_last_state() if last_state and last_state.state == STATE_ON: @@ -294,13 +294,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self.turn_on_off_listener.lights.update(all_lights) self._lights = list(all_lights) - async def _setup_listeners(self, _=None): - if self.unsub_trackers: - _LOGGER.error( - "%s: Calling '_setup_listeners' when they are already set up", self.name - ) - return - + async def _setup_trackers(self, _=None): + assert not self.unsub_trackers self._unpack_light_groups() rm_interval = async_track_time_interval( self.hass, self._async_update_at_interval, self._interval @@ -342,13 +337,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return {key: None for key in attrs} return attrs - async def async_turn_on(self, adjust_lights=True, setup_listeners=True): + async def async_turn_on( + self, adjust_lights=True, setup_listeners=True + ): # pylint: disable=arguments-differ """Turn on adaptive lighting.""" if self.is_on: return self._state = True if setup_listeners: - self._setup_listeners() + self._setup_trackers() if adjust_lights: await self._update_lights(transition=self._initial_transition, force=True) @@ -357,6 +354,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if not self.is_on: return self._state = False + self._unsub_trackers() + + def _unsub_trackers(self): + assert self.unsub_trackers while self.unsub_trackers: unsub = self.unsub_trackers.pop() unsub() From d06d4e706a42a0799b5cc6bb487751beb912fda1 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 29 Sep 2020 10:52:22 +0200 Subject: [PATCH 0216/1077] fix bug --- custom_components/adaptive_lighting/switch.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 20785d48..56614c2d 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -316,6 +316,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): rm_to = async_track_state_change(**kwgs, to_state=self._disable_state) self.unsub_trackers.extend([rm_from, rm_to]) + def _unsub_trackers(self): + assert self.unsub_trackers + while self.unsub_trackers: + unsub = self.unsub_trackers.pop() + unsub() + @property def icon(self): """Icon to use in the frontend, if any.""" @@ -345,7 +351,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return self._state = True if setup_listeners: - self._setup_trackers() + await self._setup_trackers() if adjust_lights: await self._update_lights(transition=self._initial_transition, force=True) @@ -356,12 +362,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._state = False self._unsub_trackers() - def _unsub_trackers(self): - assert self.unsub_trackers - while self.unsub_trackers: - unsub = self.unsub_trackers.pop() - unsub() - async def _update_attrs(self): """Update Adaptive Values.""" # Setting all values because this method takes <0.5ms to execute. From 297539adda78c19402acba57978ff9a75cd17b1f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 29 Sep 2020 13:13:08 +0200 Subject: [PATCH 0217/1077] fixes --- custom_components/adaptive_lighting/__init__.py | 10 ++-------- custom_components/adaptive_lighting/config_flow.py | 4 +--- custom_components/adaptive_lighting/switch.py | 1 - 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index e8257a42..6b5f6c7d 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -24,7 +24,6 @@ Resources: * The integration does not calculate a true "Blue Hour" -- it just sets the lights to 2700K (warm white) until your hub goes into "Sleep mode". """ -import asyncio import logging import voluptuous as vol @@ -88,13 +87,8 @@ async def async_update_options(hass, config_entry: ConfigEntry): async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_forward_entry_unload( + config_entry, "switch" ) data = hass.data[DOMAIN] data[config_entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 2b2de991..a0c746a0 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -45,9 +45,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(user_input["name"]) for entry in self._async_current_entries(): if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry( - entry, data=dict(entry.data, **user_input) - ) + self.hass.config_entries.async_update_entry(entry, data=user_input) self._abort_if_unique_id_configured() return self.async_create_entry(title=user_input["name"], data=user_input) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 56614c2d..47d892ff 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -317,7 +317,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self.unsub_trackers.extend([rm_from, rm_to]) def _unsub_trackers(self): - assert self.unsub_trackers while self.unsub_trackers: unsub = self.unsub_trackers.pop() unsub() From 37f110f5d166266303e5345def7e62def250b0a6 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 29 Sep 2020 14:21:39 +0200 Subject: [PATCH 0218/1077] use locks --- custom_components/adaptive_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 47d892ff..ac592efb 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -569,7 +569,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) lock = self._locks.get(entity_id) if lock is None: - lock = asyncio.Lock() + lock = self._locks[entity_id] = asyncio.Lock() async with lock: if await self.turn_on_off_listener.maybe_cancel_adjusting( entity_id, From 9bb16f1487c960bb6cc5d51f277bfb9fc1238619 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 29 Sep 2020 14:31:01 +0200 Subject: [PATCH 0219/1077] sync with PR --- custom_components/adaptive_lighting/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index ceae225d..dc730449 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -1,9 +1,9 @@ { "domain": "adaptive_lighting", "name": "Adaptive Lighting", - "documentation": "https://github.com/basnijholt/adaptive_lighting", + "documentation": "https://www.home-assistant.io/integrations/adaptive_lighting", "config_flow": true, "dependencies": [], - "codeowners": ["@claytonjn", "@basnijholt"], + "codeowners": ["@basnijholt", "@claytonjn"], "requirements": [] } From 53bf68748a31d910523aae16326c591a643eb491 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 29 Sep 2020 23:08:09 +0200 Subject: [PATCH 0220/1077] rename to adapt_brightness, adapt_color_temp, and adapt_rgb_color --- custom_components/adaptive_lighting/const.py | 21 +++++------------- .../adaptive_lighting/strings.json | 6 ++--- custom_components/adaptive_lighting/switch.py | 22 ++++++++----------- .../adaptive_lighting/translations/en.json | 6 ++--- 4 files changed, 21 insertions(+), 34 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index cdf20d1a..64c38429 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -12,18 +12,9 @@ SUN_EVENT_MIDNIGHT = "solar_midnight" CONF_NAME, DEFAULT_NAME = "name", "default" CONF_LIGHTS, DEFAULT_LIGHTS = "lights", [] -CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST = ( - "disable_brightness_adjust", - False, -) -CONF_DISABLE_COLOR_TEMP_ADJUST, DEFAULT_DISABLE_COLOR_TEMP_ADJUST = ( - "disable_color_temp_adjust", - False, -) -CONF_DISABLE_RGB_COLOR_ADJUST, DEFAULT_DISABLE_RGB_COLOR_ADJUST = ( - "disable_rgb_color_adjust", - False, -) +CONF_ADJUST_BRIGHTNESS, DEFAULT_ADJUST_BRIGHTNESS = "adjust_brightness", True +CONF_ADJUST_COLOR_TEMP, DEFAULT_ADJUST_COLOR_TEMP = "adjust_color_temp", True +CONF_ADJUST_RGB_COLOR, DEFAULT_ADJUST_RGB_COLOR = "adjust_rgb_color", True CONF_DISABLE_ENTITY = "disable_entity" CONF_DISABLE_STATE = "disable_state" CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 @@ -62,10 +53,10 @@ def int_between(min_int, max_int): VALIDATION_TUPLES = [ (CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), - (CONF_DISABLE_BRIGHTNESS_ADJUST, DEFAULT_DISABLE_BRIGHTNESS_ADJUST, bool), - (CONF_DISABLE_COLOR_TEMP_ADJUST, DEFAULT_DISABLE_COLOR_TEMP_ADJUST, bool), + (CONF_ADJUST_BRIGHTNESS, DEFAULT_ADJUST_BRIGHTNESS, bool), + (CONF_ADJUST_COLOR_TEMP, DEFAULT_ADJUST_COLOR_TEMP, bool), + (CONF_ADJUST_RGB_COLOR, DEFAULT_ADJUST_RGB_COLOR, bool), (CONF_DISABLE_ENTITY, NONE_STR, cv.entity_id), - (CONF_DISABLE_RGB_COLOR_ADJUST, DEFAULT_DISABLE_RGB_COLOR_ADJUST, bool), (CONF_DISABLE_STATE, NONE_STR, str), (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 45a0396a..f2ba323e 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -21,10 +21,10 @@ "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", "data": { "lights": "lights", - "disable_brightness_adjust": "disable_brightness_adjust", - "disable_color_temp_adjust": "disable_color_temp_adjust", + "adjust_brightness": "adjust_brightness", + "adjust_color_temp": "adjust_color_temp", + "adjust_rgb_color": "adjust_rgb_color", "disable_entity": "disable_entity", - "disable_rgb_color_adjust": "disable_rgb_color_adjust", "disable_state": "disable_state", "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", "interval": "interval", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index ac592efb..ec5f3b1b 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -59,11 +59,11 @@ import homeassistant.util.dt as dt_util from .const import ( ATTR_TURN_ON_OFF_LISTENER, + CONF_ADJUST_BRIGHTNESS, + CONF_ADJUST_COLOR_TEMP, + CONF_ADJUST_RGB_COLOR, CONF_COLORS_ONLY, - CONF_DISABLE_BRIGHTNESS_ADJUST, - CONF_DISABLE_COLOR_TEMP_ADJUST, CONF_DISABLE_ENTITY, - CONF_DISABLE_RGB_COLOR_ADJUST, CONF_DISABLE_STATE, CONF_INITIAL_TRANSITION, CONF_INTERVAL, @@ -182,10 +182,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): data = validate(config_entry) self._name = data[CONF_NAME] self._lights = data[CONF_LIGHTS] - self._disable_brightness_adjust = data[CONF_DISABLE_BRIGHTNESS_ADJUST] - self._disable_color_temp_adjust = data[CONF_DISABLE_COLOR_TEMP_ADJUST] + self._adjust_brightness = data[CONF_ADJUST_BRIGHTNESS] + self._adjust_color_temp = data[CONF_ADJUST_COLOR_TEMP] + self._adjust_rgb_color = data[CONF_ADJUST_RGB_COLOR] self._disable_entity = data[CONF_DISABLE_ENTITY] - self._disable_rgb_color_adjust = data[CONF_DISABLE_RGB_COLOR_ADJUST] self._disable_state = data[CONF_DISABLE_STATE] self._initial_transition = data[CONF_INITIAL_TRANSITION] self._interval = data[CONF_INTERVAL] @@ -491,23 +491,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): transition = self._transition service_data[ATTR_TRANSITION] = transition - if ( - "brightness" in features - and not self._disable_brightness_adjust - and not colors_only - ): + if "brightness" in features and self._adjust_brightness and not colors_only: service_data[ATTR_BRIGHTNESS_PCT] = self._brightness if ( "color_temp" in features - and not self._disable_color_temp_adjust + and self._adjust_color_temp and not (self._prefer_rgb_color and "color" in features) ): attributes = self.hass.states.get(light).attributes min_mireds, max_mireds = attributes["min_mireds"], attributes["max_mireds"] color_temp_mired = max(min(self._color_temp_mired, max_mireds), min_mireds) service_data[ATTR_COLOR_TEMP] = color_temp_mired - elif "color" in features and not self._disable_rgb_color_adjust: + elif "color" in features and self._adjust_rgb_color: service_data[ATTR_RGB_COLOR] = self._rgb_color _LOGGER.debug( diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 45a0396a..f2ba323e 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -21,10 +21,10 @@ "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", "data": { "lights": "lights", - "disable_brightness_adjust": "disable_brightness_adjust", - "disable_color_temp_adjust": "disable_color_temp_adjust", + "adjust_brightness": "adjust_brightness", + "adjust_color_temp": "adjust_color_temp", + "adjust_rgb_color": "adjust_rgb_color", "disable_entity": "disable_entity", - "disable_rgb_color_adjust": "disable_rgb_color_adjust", "disable_state": "disable_state", "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", "interval": "interval", From eda8c971f318a449c129bfea05bb3de3a8a0b67b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 29 Sep 2020 23:11:52 +0200 Subject: [PATCH 0221/1077] rename again --- custom_components/adaptive_lighting/const.py | 12 +++---- .../adaptive_lighting/strings.json | 6 ++-- custom_components/adaptive_lighting/switch.py | 36 +++++++++---------- .../adaptive_lighting/translations/en.json | 6 ++-- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 64c38429..7683752d 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -12,9 +12,9 @@ SUN_EVENT_MIDNIGHT = "solar_midnight" CONF_NAME, DEFAULT_NAME = "name", "default" CONF_LIGHTS, DEFAULT_LIGHTS = "lights", [] -CONF_ADJUST_BRIGHTNESS, DEFAULT_ADJUST_BRIGHTNESS = "adjust_brightness", True -CONF_ADJUST_COLOR_TEMP, DEFAULT_ADJUST_COLOR_TEMP = "adjust_color_temp", True -CONF_ADJUST_RGB_COLOR, DEFAULT_ADJUST_RGB_COLOR = "adjust_rgb_color", True +CONF_ADAPT_BRIGHTNESS, DEFAULT_ADAPT_BRIGHTNESS = "adapt_brightness", True +CONF_ADAPT_COLOR_TEMP, DEFAULT_ADAPT_COLOR_TEMP = "adapt_color_temp", True +CONF_ADAPT_RGB_COLOR, DEFAULT_ADAPT_RGB_COLOR = "adapt_rgb_color", True CONF_DISABLE_ENTITY = "disable_entity" CONF_DISABLE_STATE = "disable_state" CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 @@ -53,9 +53,9 @@ def int_between(min_int, max_int): VALIDATION_TUPLES = [ (CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), - (CONF_ADJUST_BRIGHTNESS, DEFAULT_ADJUST_BRIGHTNESS, bool), - (CONF_ADJUST_COLOR_TEMP, DEFAULT_ADJUST_COLOR_TEMP, bool), - (CONF_ADJUST_RGB_COLOR, DEFAULT_ADJUST_RGB_COLOR, bool), + (CONF_ADAPT_BRIGHTNESS, DEFAULT_ADAPT_BRIGHTNESS, bool), + (CONF_ADAPT_COLOR_TEMP, DEFAULT_ADAPT_COLOR_TEMP, bool), + (CONF_ADAPT_RGB_COLOR, DEFAULT_ADAPT_RGB_COLOR, bool), (CONF_DISABLE_ENTITY, NONE_STR, cv.entity_id), (CONF_DISABLE_STATE, NONE_STR, str), (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index f2ba323e..5317c2e5 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -21,9 +21,9 @@ "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", "data": { "lights": "lights", - "adjust_brightness": "adjust_brightness", - "adjust_color_temp": "adjust_color_temp", - "adjust_rgb_color": "adjust_rgb_color", + "adapt_brightness": "adapt_brightness", + "adapt_color_temp": "adapt_color_temp", + "adapt_rgb_color": "adapt_rgb_color", "disable_entity": "disable_entity", "disable_state": "disable_state", "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index ec5f3b1b..6ff79f28 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -59,9 +59,9 @@ import homeassistant.util.dt as dt_util from .const import ( ATTR_TURN_ON_OFF_LISTENER, - CONF_ADJUST_BRIGHTNESS, - CONF_ADJUST_COLOR_TEMP, - CONF_ADJUST_RGB_COLOR, + CONF_ADAPT_BRIGHTNESS, + CONF_ADAPT_COLOR_TEMP, + CONF_ADAPT_RGB_COLOR, CONF_COLORS_ONLY, CONF_DISABLE_ENTITY, CONF_DISABLE_STATE, @@ -116,7 +116,7 @@ async def handle_apply(switch, service_call): raise ValueError("Apply can only be called for a AdaptiveSwitch.") data = service_call.data tasks = [ - await switch._adjust_light( # pylint: disable=protected-access + await switch._adapt_light( # pylint: disable=protected-access light, data[CONF_TRANSITION], data[CONF_COLORS_ONLY], @@ -182,9 +182,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): data = validate(config_entry) self._name = data[CONF_NAME] self._lights = data[CONF_LIGHTS] - self._adjust_brightness = data[CONF_ADJUST_BRIGHTNESS] - self._adjust_color_temp = data[CONF_ADJUST_COLOR_TEMP] - self._adjust_rgb_color = data[CONF_ADJUST_RGB_COLOR] + self._adapt_brightness = data[CONF_ADAPT_BRIGHTNESS] + self._adapt_color_temp = data[CONF_ADAPT_COLOR_TEMP] + self._adapt_rgb_color = data[CONF_ADAPT_RGB_COLOR] self._disable_entity = data[CONF_DISABLE_ENTITY] self._disable_state = data[CONF_DISABLE_STATE] self._initial_transition = data[CONF_INITIAL_TRANSITION] @@ -272,7 +272,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if last_state and last_state.state == STATE_ON: self._state = True await self.async_turn_on( - adjust_lights=not self._only_once, + adapt_lights=not self._only_once, setup_listeners=False, ) else: @@ -343,7 +343,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return attrs async def async_turn_on( - self, adjust_lights=True, setup_listeners=True + self, adapt_lights=True, setup_listeners=True ): # pylint: disable=arguments-differ """Turn on adaptive lighting.""" if self.is_on: @@ -351,7 +351,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._state = True if setup_listeners: await self._setup_trackers() - if adjust_lights: + if adapt_lights: await self._update_lights(transition=self._initial_transition, force=True) async def async_turn_off(self, **kwargs): @@ -383,7 +383,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): await self._update_attrs() if self._only_once and not force: return - await self._adjust_lights(lights or self._lights, transition) + await self._adapt_lights(lights or self._lights, transition) def _get_sun_events(self, date): def _replace_time(date, key): @@ -482,7 +482,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): and self.hass.states.get(self._disable_entity).state in self._disable_state ) - async def _adjust_light(self, light, transition, colors_only=False): + async def _adapt_light(self, light, transition, colors_only=False): service_data = {ATTR_ENTITY_ID: light} features = self._supported_features(light) @@ -491,19 +491,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): transition = self._transition service_data[ATTR_TRANSITION] = transition - if "brightness" in features and self._adjust_brightness and not colors_only: + if "brightness" in features and self._adapt_brightness and not colors_only: service_data[ATTR_BRIGHTNESS_PCT] = self._brightness if ( "color_temp" in features - and self._adjust_color_temp + and self._adapt_color_temp and not (self._prefer_rgb_color and "color" in features) ): attributes = self.hass.states.get(light).attributes min_mireds, max_mireds = attributes["min_mireds"], attributes["max_mireds"] color_temp_mired = max(min(self._color_temp_mired, max_mireds), min_mireds) service_data[ATTR_COLOR_TEMP] = color_temp_mired - elif "color" in features and self._adjust_rgb_color: + elif "color" in features and self._adapt_rgb_color: service_data[ATTR_RGB_COLOR] = self._rgb_color _LOGGER.debug( @@ -524,14 +524,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return False return True - async def _adjust_lights(self, lights, transition): + async def _adapt_lights(self, lights, transition): if not self._should_adjust(): return _LOGGER.debug( - "%s: '_adjust_lights(%s, %s)' called", self.name, lights, transition + "%s: '_adapt_lights(%s, %s)' called", self.name, lights, transition ) tasks = [ - await self._adjust_light(light, transition) + await self._adapt_light(light, transition) for light in lights if is_on(self.hass, light) ] diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index f2ba323e..5317c2e5 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -21,9 +21,9 @@ "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", "data": { "lights": "lights", - "adjust_brightness": "adjust_brightness", - "adjust_color_temp": "adjust_color_temp", - "adjust_rgb_color": "adjust_rgb_color", + "adapt_brightness": "adapt_brightness", + "adapt_color_temp": "adapt_color_temp", + "adapt_rgb_color": "adapt_rgb_color", "disable_entity": "disable_entity", "disable_state": "disable_state", "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", From 1c88ecc57223ec86caf164a7cd18e9512adbda56 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 29 Sep 2020 23:39:46 +0200 Subject: [PATCH 0222/1077] renames --- custom_components/adaptive_lighting/const.py | 3 +- .../adaptive_lighting/services.yaml | 16 +++++--- custom_components/adaptive_lighting/switch.py | 41 +++++++++++++------ 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 7683752d..9d8000c3 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -40,8 +40,7 @@ UNDO_UPDATE_LISTENER = "undo_update_listener" NONE_STR = "None" SERVICE_APPLY = "apply" -CONF_COLORS_ONLY = "colors_only" -CONF_ON_LIGHTS_ONLY = "on_lights_only" +CONF_TURN_ON_LIGHTS = "turn_on_lights" TURNING_OFF_DELAY = 5 diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index e5b2aa18..dcc5bbfd 100755 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -10,9 +10,15 @@ apply: transition: description: Transition of the lights. example: 10 - colors_only: - description: Only change the color of the lights and leave the brightness as is. - example: false - on_lights_only: - description: Only adjust the lights that are already on, otherwise turn the lights on. + adapt_brightness: + description: "Adapt the 'brightness', default: true" + example: true + adapt_color_temp: + description: "Adapt the 'color_temp', default: true" + example: true + adapt_rgb_color: + description: "Adapt the 'rgb_color', default: true" + example: true + turn_on_lights: + description: "Turn on the lights that are off, default: false" example: false diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 6ff79f28..5eb0b270 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -62,7 +62,6 @@ from .const import ( CONF_ADAPT_BRIGHTNESS, CONF_ADAPT_COLOR_TEMP, CONF_ADAPT_RGB_COLOR, - CONF_COLORS_ONLY, CONF_DISABLE_ENTITY, CONF_DISABLE_STATE, CONF_INITIAL_TRANSITION, @@ -72,7 +71,6 @@ from .const import ( CONF_MAX_COLOR_TEMP, CONF_MIN_BRIGHTNESS, CONF_MIN_COLOR_TEMP, - CONF_ON_LIGHTS_ONLY, CONF_ONLY_ONCE, CONF_PREFER_RGB_COLOR, CONF_SLEEP_BRIGHTNESS, @@ -84,6 +82,7 @@ from .const import ( CONF_SUNSET_OFFSET, CONF_SUNSET_TIME, CONF_TRANSITION, + CONF_TURN_ON_LIGHTS, DOMAIN, EXTRA_VALIDATION, ICON, @@ -119,10 +118,12 @@ async def handle_apply(switch, service_call): await switch._adapt_light( # pylint: disable=protected-access light, data[CONF_TRANSITION], - data[CONF_COLORS_ONLY], + data[CONF_ADAPT_BRIGHTNESS], + data[CONF_ADAPT_COLOR_TEMP], + data[CONF_ADAPT_RGB_COLOR], ) for light in data[CONF_LIGHTS] - if not data[CONF_ON_LIGHTS_ONLY] or is_on(switch.hass, light) + if data[CONF_TURN_ON_LIGHTS] or is_on(switch.hass, light) ] if tasks: await asyncio.wait(tasks) @@ -149,8 +150,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): CONF_TRANSITION, default=switch._initial_transition, # pylint: disable=protected-access ): VALID_TRANSITION, - vol.Optional(CONF_COLORS_ONLY, default=False): cv.boolean, - vol.Optional(CONF_ON_LIGHTS_ONLY, default=False): cv.boolean, + vol.Optional(CONF_ADAPT_BRIGHTNESS, default=True): cv.boolean, + vol.Optional(CONF_ADAPT_COLOR_TEMP, default=True): cv.boolean, + vol.Optional(CONF_ADAPT_RGB_COLOR, default=True): cv.boolean, + vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean, }, handle_apply, ) @@ -482,28 +485,42 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): and self.hass.states.get(self._disable_entity).state in self._disable_state ) - async def _adapt_light(self, light, transition, colors_only=False): + async def _adapt_light( + self, + light, + transition=None, + adapt_brightness=None, + adapt_color_temp=None, + adapt_rgb_color=None, + ): service_data = {ATTR_ENTITY_ID: light} features = self._supported_features(light) + if transition is None: + transition = self._transition + if adapt_brightness is None: + adapt_brightness = self._adapt_brightness + if adapt_color_temp is None: + adapt_color_temp = self._adapt_color_temp + if adapt_rgb_color is None: + adapt_rgb_color = self._adapt_rgb_color + if "transition" in features: - if transition is None: - transition = self._transition service_data[ATTR_TRANSITION] = transition - if "brightness" in features and self._adapt_brightness and not colors_only: + if "brightness" in features and adapt_brightness: service_data[ATTR_BRIGHTNESS_PCT] = self._brightness if ( "color_temp" in features - and self._adapt_color_temp + and adapt_color_temp and not (self._prefer_rgb_color and "color" in features) ): attributes = self.hass.states.get(light).attributes min_mireds, max_mireds = attributes["min_mireds"], attributes["max_mireds"] color_temp_mired = max(min(self._color_temp_mired, max_mireds), min_mireds) service_data[ATTR_COLOR_TEMP] = color_temp_mired - elif "color" in features and self._adapt_rgb_color: + elif "color" in features and adapt_rgb_color: service_data[ATTR_RGB_COLOR] = self._rgb_color _LOGGER.debug( From f07d1281f41becc1d9518416ede4cb24e4e34e91 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 30 Sep 2020 00:10:49 +0200 Subject: [PATCH 0223/1077] expand --- custom_components/adaptive_lighting/switch.py | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 5eb0b270..9c81c786 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -6,7 +6,7 @@ from copy import deepcopy import datetime from datetime import timedelta import logging -from typing import Dict, Tuple +from typing import Dict, List, Tuple import voluptuous as vol @@ -113,7 +113,9 @@ async def handle_apply(switch, service_call): """Handle the entity service apply.""" if not isinstance(switch, AdaptiveSwitch): raise ValueError("Apply can only be called for a AdaptiveSwitch.") + hass = switch.hass data = service_call.data + all_lights = _expand_light_groups(hass, data[CONF_LIGHTS]) tasks = [ await switch._adapt_light( # pylint: disable=protected-access light, @@ -122,8 +124,8 @@ async def handle_apply(switch, service_call): data[CONF_ADAPT_COLOR_TEMP], data[CONF_ADAPT_RGB_COLOR], ) - for light in data[CONF_LIGHTS] - if data[CONF_TURN_ON_LIGHTS] or is_on(switch.hass, light) + for light in all_lights + if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light) ] if tasks: await asyncio.wait(tasks) @@ -174,6 +176,22 @@ def validate(config_entry): return data +def _expand_light_groups(hass, lights) -> List[str]: + all_lights = set() + for light in lights: + state = hass.states.get(light) + if state is None: + _LOGGER.debug("State of %s is None", light) + all_lights.add(light) + elif "entity_id" in state.attributes: # it's a light group + group = state.attributes["entity_id"] + all_lights.update(group) + _LOGGER.debug("Expanded %s to %s", light, group) + else: + all_lights.add(light) + return list(all_lights) + + class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" @@ -281,25 +299,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): else: self._state = False - def _unpack_light_groups(self) -> None: - all_lights = set() - for light in self._lights: - state = self.hass.states.get(light) - if state is None: - _LOGGER.debug("%s: State of %s is None", self._name, light) - all_lights.add(light) - elif "entity_id" in state.attributes: # it's a light group - group = state.attributes["entity_id"] - all_lights.update(group) - _LOGGER.debug("%s: Unpacked %s to %s", self._name, light, group) - else: - all_lights.add(light) + def _expand_light_groups(self) -> None: + all_lights = _expand_light_groups(self.hass, self._lights) self.turn_on_off_listener.lights.update(all_lights) self._lights = list(all_lights) async def _setup_trackers(self, _=None): assert not self.unsub_trackers - self._unpack_light_groups() + self._expand_light_groups() rm_interval = async_track_time_interval( self.hass, self._async_update_at_interval, self._interval ) @@ -368,7 +375,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Update Adaptive Values.""" # Setting all values because this method takes <0.5ms to execute. self._percent = self._calc_percent() - self._brightness = self._calc_brightness() + self._brightness = self._calc_brightness() # TODO: rename to brightness_pct self._color_temp_kelvin = self._calc_color_temp_kelvin() self._color_temp_mired = color_temperature_kelvin_to_mired( self._color_temp_kelvin From 09b835c72cb90c3036d53fdc7a587df2f7241d40 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 30 Sep 2020 00:42:27 +0200 Subject: [PATCH 0224/1077] fix unsubs --- .../adaptive_lighting/__init__.py | 12 ++++- .../adaptive_lighting/config_flow.py | 1 + custom_components/adaptive_lighting/switch.py | 48 +++++++++---------- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 6b5f6c7d..88f81dc8 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -32,7 +32,13 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry import homeassistant.helpers.config_validation as cv -from .const import _DOMAIN_SCHEMA, CONF_NAME, DOMAIN, UNDO_UPDATE_LISTENER +from .const import ( + _DOMAIN_SCHEMA, + ATTR_TURN_ON_OFF_LISTENER, + CONF_NAME, + DOMAIN, + UNDO_UPDATE_LISTENER, +) _LOGGER = logging.getLogger(__name__) @@ -93,7 +99,9 @@ async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool: data = hass.data[DOMAIN] data[config_entry.entry_id][UNDO_UPDATE_LISTENER]() switch = data[config_entry.entry_id][SWITCH_DOMAIN] - switch._unsub_trackers() # pylint: disable=protected-access + switch._remove_listeners() # pylint: disable=protected-access + if len(data) == 1: # no more config_entries + data.pop(ATTR_TURN_ON_OFF_LISTENER).remove_listener() if unload_ok: data.pop(config_entry.entry_id) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index a0c746a0..8b4e8936 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -92,6 +92,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_create_entry(title="", data=user_input) all_lights = sorted(self.hass.states.async_entity_ids("light")) + # TODO: only use statefull entities all_entities = sorted(self.hass.states.async_entity_ids()) to_replace = { CONF_LIGHTS: cv.multi_select(all_lights), diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 9c81c786..1cc5da93 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -246,7 +246,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._hs_color = None # Set and unset tracker in async_turn_on and async_turn_off - self.unsub_trackers = [] + self.remove_listeners = [] _LOGGER.debug( "%s: Setting up with '%s'," " config_entry.data: '%s'," @@ -284,18 +284,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Call when entity about to be added to hass.""" if self._lights: if self.hass.is_running: - await self._setup_trackers() + await self._setup_listeners() else: self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, self._setup_trackers + EVENT_HOMEASSISTANT_START, self._setup_listeners ) last_state = await self.async_get_last_state() if last_state and last_state.state == STATE_ON: self._state = True - await self.async_turn_on( - adapt_lights=not self._only_once, - setup_listeners=False, - ) + await self.async_turn_on(adapt_lights=not self._only_once) else: self._state = False @@ -304,8 +301,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self.turn_on_off_listener.lights.update(all_lights) self._lights = list(all_lights) - async def _setup_trackers(self, _=None): - assert not self.unsub_trackers + async def _setup_listeners(self, _=None): + if not self.is_on: + return + assert not self.remove_listeners self._expand_light_groups() rm_interval = async_track_time_interval( self.hass, self._async_update_at_interval, self._interval @@ -313,23 +312,23 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): rm_state = async_track_state_change_event( self.hass, self._lights, self._light_event ) - self.unsub_trackers.extend([rm_interval, rm_state]) + self.remove_listeners.extend([rm_interval, rm_state]) track_kwargs = dict(hass=self.hass, action=self._state_changed) if self._sleep_entity is not None: kwgs = dict(track_kwargs, entity_ids=self._sleep_entity) rm_from = async_track_state_change(**kwgs, from_state=self._sleep_state) rm_to = async_track_state_change(**kwgs, to_state=self._sleep_state) - self.unsub_trackers.extend([rm_from, rm_to]) + self.remove_listeners.extend([rm_from, rm_to]) if self._disable_entity is not None: kwgs = dict(track_kwargs, entity_ids=self._disable_entity) rm_from = async_track_state_change(**kwgs, from_state=self._disable_state) rm_to = async_track_state_change(**kwgs, to_state=self._disable_state) - self.unsub_trackers.extend([rm_from, rm_to]) + self.remove_listeners.extend([rm_from, rm_to]) - def _unsub_trackers(self): - while self.unsub_trackers: - unsub = self.unsub_trackers.pop() - unsub() + def _remove_listeners(self): + while self.remove_listeners: + remove_listener = self.remove_listeners.pop() + remove_listener() @property def icon(self): @@ -353,14 +352,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return attrs async def async_turn_on( - self, adapt_lights=True, setup_listeners=True + self, adapt_lights=True ): # pylint: disable=arguments-differ """Turn on adaptive lighting.""" if self.is_on: return self._state = True - if setup_listeners: - await self._setup_trackers() + await self._setup_listeners() if adapt_lights: await self._update_lights(transition=self._initial_transition, force=True) @@ -369,13 +367,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if not self.is_on: return self._state = False - self._unsub_trackers() + self._remove_listeners() async def _update_attrs(self): """Update Adaptive Values.""" # Setting all values because this method takes <0.5ms to execute. self._percent = self._calc_percent() - self._brightness = self._calc_brightness() # TODO: rename to brightness_pct + self._brightness = self._calc_brightness() self._color_temp_kelvin = self._calc_color_temp_kelvin() self._color_temp_mired = color_temperature_kelvin_to_mired( self._color_temp_kelvin @@ -543,13 +541,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context=Context(), ) - def _should_adjust(self): + def _should_adapt(self): if not self._lights or not self.is_on or self._is_disabled(): return False return True async def _adapt_lights(self, lights, transition): - if not self._should_adjust(): + if not self._should_adapt(): return _LOGGER.debug( "%s: '_adapt_lights(%s, %s)' called", self.name, lights, transition @@ -631,7 +629,9 @@ class TurnOnOffListener: self.sleep_tasks: Dict[str, asyncio.Task] = {} - self.hass.bus.async_listen(EVENT_CALL_SERVICE, self.turn_on_off_event_listener) + self.remove_listener = self.hass.bus.async_listen( + EVENT_CALL_SERVICE, self.turn_on_off_event_listener + ) async def maybe_cancel_adjusting( self, entity_id, off_to_on_event, on_to_off_event From f2fcd1aab96cdb14f943a9aa272147a5bbe94f63 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 30 Sep 2020 00:52:26 +0200 Subject: [PATCH 0225/1077] better strings --- .../adaptive_lighting/strings.json | 28 +++++++++---------- .../adaptive_lighting/translations/en.json | 28 +++++++++---------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 5317c2e5..df2149be 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -27,22 +27,22 @@ "disable_entity": "disable_entity", "disable_state": "disable_state", "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", - "interval": "interval", - "max_brightness": "max_brightness", - "max_color_temp": "max_color_temp", - "min_brightness": "min_brightness", - "min_color_temp": "min_color_temp", - "only_once": "only_once", - "prefer_rgb_color": "prefer_rgb_color", - "sleep_brightness": "sleep_brightness", - "sleep_color_temp": "sleep_color_temp", + "interval": "interval, time between switch updates in seconds", + "max_brightness": "max_brightness, in %", + "max_color_temp": "max_color_temp, in Kelvin", + "min_brightness": "min_brightness, in %", + "min_color_temp": "min_color_temp, in Kelvin", + "only_once": "only_once, only adapt the lights when turning them on", + "prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible", + "sleep_brightness": "sleep_brightness, in %", + "sleep_color_temp": "sleep_color_temp, in Kelvin", "sleep_entity": "sleep_entity", "sleep_state": "sleep_state", - "sunrise_offset": "sunrise_offset", - "sunrise_time": "sunrise_time", - "sunset_offset": "sunset_offset", - "sunset_time": "sunset_time", - "transition": "transition" + "sunrise_offset": "sunrise_offset, in +/- seconds", + "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", + "sunset_offset": "sunset_offset, in +/- seconds", + "sunset_time": "sunset_time, in 'HH:MM:SS' format", + "transition": "transition, in seconds" } } }, diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 5317c2e5..df2149be 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -27,22 +27,22 @@ "disable_entity": "disable_entity", "disable_state": "disable_state", "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", - "interval": "interval", - "max_brightness": "max_brightness", - "max_color_temp": "max_color_temp", - "min_brightness": "min_brightness", - "min_color_temp": "min_color_temp", - "only_once": "only_once", - "prefer_rgb_color": "prefer_rgb_color", - "sleep_brightness": "sleep_brightness", - "sleep_color_temp": "sleep_color_temp", + "interval": "interval, time between switch updates in seconds", + "max_brightness": "max_brightness, in %", + "max_color_temp": "max_color_temp, in Kelvin", + "min_brightness": "min_brightness, in %", + "min_color_temp": "min_color_temp, in Kelvin", + "only_once": "only_once, only adapt the lights when turning them on", + "prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible", + "sleep_brightness": "sleep_brightness, in %", + "sleep_color_temp": "sleep_color_temp, in Kelvin", "sleep_entity": "sleep_entity", "sleep_state": "sleep_state", - "sunrise_offset": "sunrise_offset", - "sunrise_time": "sunrise_time", - "sunset_offset": "sunset_offset", - "sunset_time": "sunset_time", - "transition": "transition" + "sunrise_offset": "sunrise_offset, in +/- seconds", + "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", + "sunset_offset": "sunset_offset, in +/- seconds", + "sunset_time": "sunset_time, in 'HH:MM:SS' format", + "transition": "transition, in seconds" } } }, From d304b49d53f656ccadb531776c502c4978c8752a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 30 Sep 2020 10:05:36 +0200 Subject: [PATCH 0226/1077] fix setting state before turn_on --- custom_components/adaptive_lighting/switch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 1cc5da93..e76a92e8 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -291,7 +291,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) last_state = await self.async_get_last_state() if last_state and last_state.state == STATE_ON: - self._state = True await self.async_turn_on(adapt_lights=not self._only_once) else: self._state = False From efcc2681864723584251a42b68e91f98b0315b8b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 30 Sep 2020 10:29:47 +0200 Subject: [PATCH 0227/1077] comments and strings --- custom_components/adaptive_lighting/config_flow.py | 1 - custom_components/adaptive_lighting/strings.json | 12 ++++++------ custom_components/adaptive_lighting/switch.py | 7 ++++++- .../adaptive_lighting/translations/en.json | 12 ++++++------ 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 8b4e8936..a0c746a0 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -92,7 +92,6 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_create_entry(title="", data=user_input) all_lights = sorted(self.hass.states.async_entity_ids("light")) - # TODO: only use statefull entities all_entities = sorted(self.hass.states.async_entity_ids()) to_replace = { CONF_LIGHTS: cv.multi_select(all_lights), diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index df2149be..0c08519e 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -22,10 +22,10 @@ "data": { "lights": "lights", "adapt_brightness": "adapt_brightness", - "adapt_color_temp": "adapt_color_temp", - "adapt_rgb_color": "adapt_rgb_color", - "disable_entity": "disable_entity", - "disable_state": "disable_state", + "adapt_color_temp": "adapt_color_temp, adapt color temperature using 'color_temp' if supported", + "adapt_rgb_color": "adapt_rgb_color, adapt color temperature using RGB/XY if supported", + "disable_entity": "disable_entity, entity_id that stops the switch from adapting lights", + "disable_state": "disable_state, state(s) of 'disable_entity', e.g., 'off' or 'total,half'", "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", "interval": "interval, time between switch updates in seconds", "max_brightness": "max_brightness, in %", @@ -36,8 +36,8 @@ "prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible", "sleep_brightness": "sleep_brightness, in %", "sleep_color_temp": "sleep_color_temp, in Kelvin", - "sleep_entity": "sleep_entity", - "sleep_state": "sleep_state", + "sleep_entity": "sleep_entity, entity_id turns the switch into sleep mode", + "sleep_state": "sleep_state, state(s) of 'sleep_entity', e.g., 'off' or 'total,half'", "sunrise_offset": "sunrise_offset, in +/- seconds", "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", "sunset_offset": "sunset_offset, in +/- seconds", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index e76a92e8..640809cc 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -625,7 +625,7 @@ class TurnOnOffListener: self.turn_off_event: Dict[str, Tuple[str, float]] = {} # Tracks 'light.turn_on' service calls self.turn_on_event: Dict[str, Tuple[str]] = {} - + # Keeps 'asyncio.sleep` tasks that can be cancelled by 'light.turn_on' events self.sleep_tasks: Dict[str, asyncio.Task] = {} self.remove_listener = self.hass.bus.async_listen( @@ -709,6 +709,11 @@ class TurnOnOffListener: # transitioning into 'off'. Maybe needs some discussion/input? return True + # Now we assume that the lights are still on and they were intended + # to be on. In case this still gives problems for some, we might + # choose to **only** adapt on 'light.turn_on' events and ignore + # other 'off' → 'on' state switches resulting from polling. That + # would mean we 'return True' here. return False async def turn_on_off_event_listener(self, event): diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index df2149be..0c08519e 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -22,10 +22,10 @@ "data": { "lights": "lights", "adapt_brightness": "adapt_brightness", - "adapt_color_temp": "adapt_color_temp", - "adapt_rgb_color": "adapt_rgb_color", - "disable_entity": "disable_entity", - "disable_state": "disable_state", + "adapt_color_temp": "adapt_color_temp, adapt color temperature using 'color_temp' if supported", + "adapt_rgb_color": "adapt_rgb_color, adapt color temperature using RGB/XY if supported", + "disable_entity": "disable_entity, entity_id that stops the switch from adapting lights", + "disable_state": "disable_state, state(s) of 'disable_entity', e.g., 'off' or 'total,half'", "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", "interval": "interval, time between switch updates in seconds", "max_brightness": "max_brightness, in %", @@ -36,8 +36,8 @@ "prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible", "sleep_brightness": "sleep_brightness, in %", "sleep_color_temp": "sleep_color_temp, in Kelvin", - "sleep_entity": "sleep_entity", - "sleep_state": "sleep_state", + "sleep_entity": "sleep_entity, entity_id turns the switch into sleep mode", + "sleep_state": "sleep_state, state(s) of 'sleep_entity', e.g., 'off' or 'total,half'", "sunrise_offset": "sunrise_offset, in +/- seconds", "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", "sunset_offset": "sunset_offset, in +/- seconds", From d57ef3f2e44128f26733d4ef31979fd59128e99f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 30 Sep 2020 12:54:02 +0200 Subject: [PATCH 0228/1077] sync PR --- custom_components/adaptive_lighting/strings.json | 4 ++-- custom_components/adaptive_lighting/switch.py | 3 ++- custom_components/adaptive_lighting/translations/en.json | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 0c08519e..97fb913b 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -26,7 +26,7 @@ "adapt_rgb_color": "adapt_rgb_color, adapt color temperature using RGB/XY if supported", "disable_entity": "disable_entity, entity_id that stops the switch from adapting lights", "disable_state": "disable_state, state(s) of 'disable_entity', e.g., 'off' or 'total,half'", - "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", + "initial_transition": "initial_transition, when lights go 'off' → 'on' or when 'disable_state'/'sleep_state' changes", "interval": "interval, time between switch updates in seconds", "max_brightness": "max_brightness, in %", "max_color_temp": "max_color_temp, in Kelvin", @@ -36,7 +36,7 @@ "prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible", "sleep_brightness": "sleep_brightness, in %", "sleep_color_temp": "sleep_color_temp, in Kelvin", - "sleep_entity": "sleep_entity, entity_id turns the switch into sleep mode", + "sleep_entity": "sleep_entity, 'entity_id' that manages the switch's sleep mode", "sleep_state": "sleep_state, state(s) of 'sleep_entity', e.g., 'off' or 'total,half'", "sunrise_offset": "sunrise_offset, in +/- seconds", "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 640809cc..c3e6e358 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -290,7 +290,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): EVENT_HOMEASSISTANT_START, self._setup_listeners ) last_state = await self.async_get_last_state() - if last_state and last_state.state == STATE_ON: + is_new_entry = last_state is None # newly added to HA + if is_new_entry or last_state.state == STATE_ON: await self.async_turn_on(adapt_lights=not self._only_once) else: self._state = False diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 0c08519e..97fb913b 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -26,7 +26,7 @@ "adapt_rgb_color": "adapt_rgb_color, adapt color temperature using RGB/XY if supported", "disable_entity": "disable_entity, entity_id that stops the switch from adapting lights", "disable_state": "disable_state, state(s) of 'disable_entity', e.g., 'off' or 'total,half'", - "initial_transition": "initial_transition, the transition of the lights when turning them on or when 'disable_state' or 'sleep_state' change", + "initial_transition": "initial_transition, when lights go 'off' → 'on' or when 'disable_state'/'sleep_state' changes", "interval": "interval, time between switch updates in seconds", "max_brightness": "max_brightness, in %", "max_color_temp": "max_color_temp, in Kelvin", @@ -36,7 +36,7 @@ "prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible", "sleep_brightness": "sleep_brightness, in %", "sleep_color_temp": "sleep_color_temp, in Kelvin", - "sleep_entity": "sleep_entity, entity_id turns the switch into sleep mode", + "sleep_entity": "sleep_entity, 'entity_id' that manages the switch's sleep mode", "sleep_state": "sleep_state, state(s) of 'sleep_entity', e.g., 'off' or 'total,half'", "sunrise_offset": "sunrise_offset, in +/- seconds", "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", From 8d4e0d12c1995452765ed96e04b5ed7e9e1b72dc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 30 Sep 2020 21:14:23 +0200 Subject: [PATCH 0229/1077] synx --- .../adaptive_lighting/strings.json | 2 +- custom_components/adaptive_lighting/switch.py | 202 +++++++++++------- .../adaptive_lighting/translations/en.json | 2 +- 3 files changed, 123 insertions(+), 83 deletions(-) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 97fb913b..a64e8a88 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -26,7 +26,7 @@ "adapt_rgb_color": "adapt_rgb_color, adapt color temperature using RGB/XY if supported", "disable_entity": "disable_entity, entity_id that stops the switch from adapting lights", "disable_state": "disable_state, state(s) of 'disable_entity', e.g., 'off' or 'total,half'", - "initial_transition": "initial_transition, when lights go 'off' → 'on' or when 'disable_state'/'sleep_state' changes", + "initial_transition": "initial_transition, when lights go 'off' to 'on' or when 'disable_state'/'sleep_state' changes", "interval": "interval, time between switch updates in seconds", "max_brightness": "max_brightness, in %", "max_color_temp": "max_color_temp, in Kelvin", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index c3e6e358..1a8a3eed 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1,4 +1,5 @@ """Switch for the Adaptive Lighting integration.""" +from __future__ import annotations import asyncio import bisect @@ -6,7 +7,7 @@ from copy import deepcopy import datetime from datetime import timedelta import logging -from typing import Dict, List, Tuple +from typing import Dict, List, Optional, Tuple import voluptuous as vol @@ -24,6 +25,7 @@ from homeassistant.components.light import ( is_on, ) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DOMAIN, ATTR_ENTITY_ID, @@ -38,11 +40,10 @@ from homeassistant.const import ( SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) -from homeassistant.core import Context, Event +from homeassistant.core import Context, Event, ServiceCall from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( - async_track_state_change, async_track_state_change_event, async_track_time_interval, ) @@ -109,29 +110,28 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=10) -async def handle_apply(switch, service_call): +async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): """Handle the entity service apply.""" if not isinstance(switch, AdaptiveSwitch): raise ValueError("Apply can only be called for a AdaptiveSwitch.") hass = switch.hass data = service_call.data all_lights = _expand_light_groups(hass, data[CONF_LIGHTS]) - tasks = [ - await switch._adapt_light( # pylint: disable=protected-access - light, - data[CONF_TRANSITION], - data[CONF_ADAPT_BRIGHTNESS], - data[CONF_ADAPT_COLOR_TEMP], - data[CONF_ADAPT_RGB_COLOR], - ) - for light in all_lights - if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light) - ] - if tasks: - await asyncio.wait(tasks) + switch.turn_on_off_listener.lights.update(all_lights) + + for light in all_lights: + if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light): + await switch._adapt_light( # pylint: disable=protected-access + light, + data[CONF_TRANSITION], + data[CONF_ADAPT_BRIGHTNESS], + data[CONF_ADAPT_COLOR_TEMP], + data[CONF_ADAPT_RGB_COLOR], + service_call.context, + ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass, config_entry: ConfigEntry, async_add_entities: bool): """Set up the AdaptiveLighting switch.""" data = hass.data[DOMAIN] @@ -176,7 +176,19 @@ def validate(config_entry): return data -def _expand_light_groups(hass, lights) -> List[str]: +def match_state_event(event: Event, from_or_to_state: List[str]): + """Match state event when either 'from_state' or 'to_state' matches.""" + old_state = event.data.get("old_state") + from_state_match = old_state is not None and old_state.state in from_or_to_state + + new_state = event.data.get("new_state") + to_state_match = new_state is not None and new_state.state in from_or_to_state + + match = from_state_match or to_state_match + return match + + +def _expand_light_groups(hass, lights: List[str]) -> List[str]: all_lights = set() for light in lights: state = hass.states.get(light) @@ -195,7 +207,7 @@ def _expand_light_groups(hass, lights) -> List[str]: class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" - def __init__(self, hass, config_entry, turn_on_off_listener): + def __init__(self, hass, config_entry, turn_on_off_listener: TurnOnOffListener): """Initialize the Adaptive Lighting switch.""" self.hass = hass self.turn_on_off_listener = turn_on_off_listener @@ -269,11 +281,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return self._name @property - def is_on(self): + def is_on(self) -> Optional[bool]: """Return true if adaptive lighting is on.""" return self._state - def _supported_features(self, light): + def _supported_features(self, light: str): state = self.hass.states.get(light) supported_features = state.attributes["supported_features"] return { @@ -282,19 +294,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def async_added_to_hass(self): """Call when entity about to be added to hass.""" - if self._lights: - if self.hass.is_running: - await self._setup_listeners() - else: - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, self._setup_listeners - ) + if self.hass.is_running: + await self._setup_listeners() + else: + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, self._setup_listeners + ) last_state = await self.async_get_last_state() is_new_entry = last_state is None # newly added to HA if is_new_entry or last_state.state == STATE_ON: await self.async_turn_on(adapt_lights=not self._only_once) else: self._state = False + assert not self.remove_listeners def _expand_light_groups(self) -> None: all_lights = _expand_light_groups(self.hass, self._lights) @@ -302,28 +314,35 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._lights = list(all_lights) async def _setup_listeners(self, _=None): - if not self.is_on: + _LOGGER.debug("%s: Called '_setup_listeners'", self._name) + if not self.is_on or not self.hass.is_running: + _LOGGER.debug("%s: Cancelled '_setup_listeners'", self._name) return assert not self.remove_listeners - self._expand_light_groups() - rm_interval = async_track_time_interval( + remove_interval = async_track_time_interval( self.hass, self._async_update_at_interval, self._interval ) - rm_state = async_track_state_change_event( - self.hass, self._lights, self._light_event - ) - self.remove_listeners.extend([rm_interval, rm_state]) - track_kwargs = dict(hass=self.hass, action=self._state_changed) + self.remove_listeners.append(remove_interval) + if self._lights: + self._expand_light_groups() + remove_state = async_track_state_change_event( + self.hass, self._lights, self._light_event + ) + self.remove_listeners.append(remove_state) if self._sleep_entity is not None: - kwgs = dict(track_kwargs, entity_ids=self._sleep_entity) - rm_from = async_track_state_change(**kwgs, from_state=self._sleep_state) - rm_to = async_track_state_change(**kwgs, to_state=self._sleep_state) - self.remove_listeners.extend([rm_from, rm_to]) + remove_sleep = async_track_state_change_event( + self.hass, + self._sleep_entity, + self._sleep_state_event, + ) + self.remove_listeners.append(remove_sleep) if self._disable_entity is not None: - kwgs = dict(track_kwargs, entity_ids=self._disable_entity) - rm_from = async_track_state_change(**kwgs, from_state=self._disable_state) - rm_to = async_track_state_change(**kwgs, to_state=self._disable_state) - self.remove_listeners.extend([rm_from, rm_to]) + remove_disable = async_track_state_change_event( + self.hass, + self._disable_entity, + self._disable_state_event, + ) + self.remove_listeners.append(remove_disable) def _remove_listeners(self): while self.remove_listeners: @@ -352,15 +371,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return attrs async def async_turn_on( - self, adapt_lights=True + self, adapt_lights: bool = True ): # pylint: disable=arguments-differ """Turn on adaptive lighting.""" + _LOGGER.debug( + "%s: Called 'async_turn_on', current state is '%s'", self._name, self._state + ) if self.is_on: return self._state = True await self._setup_listeners() if adapt_lights: - await self._update_lights(transition=self._initial_transition, force=True) + await self._maybe_adapt_lights( + transition=self._initial_transition, force=True + ) async def async_turn_off(self, **kwargs): """Turn off adaptive lighting.""" @@ -385,15 +409,21 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug("%s: '_update_attrs' called", self._name) async def _async_update_at_interval(self, now=None): - await self._update_lights(force=False) + await self._maybe_adapt_lights(force=False) - async def _update_lights(self, lights=None, transition=None, force=False): + async def _maybe_adapt_lights( + self, + lights: Optional[List[str]] = None, + transition: Optional[int] = None, + force: bool = False, + context: Optional[Context] = None, + ): await self._update_attrs() if self._only_once and not force: return - await self._adapt_lights(lights or self._lights, transition) + await self._adapt_lights(lights or self._lights, transition, context) - def _get_sun_events(self, date): + def _get_sun_events(self, date: datetime.datetime): def _replace_time(date, key): time = getattr(self, f"_{key}_time") date_time = datetime.datetime.combine(datetime.date.today(), time) @@ -438,7 +468,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return events - def _relevant_events(self, now): + def _relevant_events(self, now: datetime.datetime): events = [ self._get_sun_events(now + timedelta(days=days)) for days in [-1, 0, 1] ] @@ -492,12 +522,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _adapt_light( self, - light, - transition=None, - adapt_brightness=None, - adapt_color_temp=None, - adapt_rgb_color=None, + light: str, + transition: Optional[int] = None, + adapt_brightness: Optional[bool] = None, + adapt_color_temp: Optional[bool] = None, + adapt_rgb_color: Optional[bool] = None, + context: Optional[Context] = None, ): + lock = self._locks.get(light) + if lock is not None and lock.locked(): + _LOGGER.debug("%s: '%s' is locked", self._name, light) + return + service_data = {ATTR_ENTITY_ID: light} features = self._supported_features(light) @@ -534,11 +570,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): service_data, ) - return self.hass.services.async_call( + await self.hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, service_data, - context=Context(), + context=context, ) def _should_adapt(self): @@ -546,33 +582,35 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return False return True - async def _adapt_lights(self, lights, transition): + async def _adapt_lights( + self, lights: List[str], transition: Optional[int], context=Optional[Context] + ): if not self._should_adapt(): return _LOGGER.debug( "%s: '_adapt_lights(%s, %s)' called", self.name, lights, transition ) - tasks = [ - await self._adapt_light(light, transition) - for light in lights - if is_on(self.hass, light) - ] - if tasks: - await asyncio.wait(tasks) + for light in lights: + if is_on(self.hass, light): + await self._adapt_light(light, transition, context=context) - async def _state_changed(self, entity_id, from_state, to_state): - _LOGGER.debug( - "%s: _state_changed, from_state: '%s', to_state: '%s'", - self._name, - from_state, - to_state, - ) - lock = self._locks.get(entity_id) - if lock is not None and lock.locked: + async def _disable_state_event(self, event: Event): + if not match_state_event(event, self._disable_state): return - await self._update_lights(transition=self._initial_transition, force=True) + _LOGGER.debug("%s: _disable_state_event, event: '%s'", self._name, event) + await self._maybe_adapt_lights( + transition=self._initial_transition, force=True, context=event.context + ) - async def _light_event(self, event): + async def _sleep_state_event(self, event: Event): + if not match_state_event(event, self._sleep_state): + return + _LOGGER.debug("%s: _sleep_state_event, event: '%s'", self._name, event) + await self._maybe_adapt_lights( + transition=self._initial_transition, force=True, context=event.context + ) + + async def _light_event(self, event: Event): old_state = event.data.get("old_state") new_state = event.data.get("new_state") entity_id = event.data.get("entity_id") @@ -599,10 +637,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): "%s: Cancelling adjusting lights for %s", self._name, entity_id ) return - await self._update_lights( + + await self._maybe_adapt_lights( lights=[entity_id], transition=self._initial_transition, force=True, + context=event.context, ) elif ( old_state is not None @@ -634,7 +674,7 @@ class TurnOnOffListener: ) async def maybe_cancel_adjusting( - self, entity_id, off_to_on_event, on_to_off_event + self, entity_id: str, off_to_on_event: Event, on_to_off_event: Optional[Event] ) -> bool: """Cancel the adjusting of a light if it has just been turned off. @@ -717,7 +757,7 @@ class TurnOnOffListener: # would mean we 'return True' here. return False - async def turn_on_off_event_listener(self, event): + async def turn_on_off_event_listener(self, event: Event): """Track 'light.turn_off' and 'light.turn_on' service calls.""" domain = event.data.get(ATTR_DOMAIN) if domain != LIGHT_DOMAIN: diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 97fb913b..a64e8a88 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -26,7 +26,7 @@ "adapt_rgb_color": "adapt_rgb_color, adapt color temperature using RGB/XY if supported", "disable_entity": "disable_entity, entity_id that stops the switch from adapting lights", "disable_state": "disable_state, state(s) of 'disable_entity', e.g., 'off' or 'total,half'", - "initial_transition": "initial_transition, when lights go 'off' → 'on' or when 'disable_state'/'sleep_state' changes", + "initial_transition": "initial_transition, when lights go 'off' to 'on' or when 'disable_state'/'sleep_state' changes", "interval": "interval, time between switch updates in seconds", "max_brightness": "max_brightness, in %", "max_color_temp": "max_color_temp, in Kelvin", From 8ec62305f010f68ae84353fd7c7a85c16d5c6e21 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 1 Oct 2020 10:09:06 +0200 Subject: [PATCH 0230/1077] exception --- custom_components/adaptive_lighting/switch.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 1a8a3eed..c6f9fe37 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -40,7 +40,7 @@ from homeassistant.const import ( SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) -from homeassistant.core import Context, Event, ServiceCall +from homeassistant.core import Context, Event, ServiceCall, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( @@ -393,7 +393,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._state = False self._remove_listeners() - async def _update_attrs(self): + @callback + def _update_attrs(self): """Update Adaptive Values.""" # Setting all values because this method takes <0.5ms to execute. self._percent = self._calc_percent() @@ -418,7 +419,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): force: bool = False, context: Optional[Context] = None, ): - await self._update_attrs() + self._update_attrs() if self._only_once and not force: return await self._adapt_lights(lights or self._lights, transition, context) @@ -464,7 +465,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Check whether order is correct events = sorted(events, key=lambda x: x[1]) events_names, _ = zip(*events) - assert events_names in _ALLOWED_ORDERS, events_names + if events_names not in _ALLOWED_ORDERS: + msg = ( + f"{self._name}: The sun events {events_names} are not in the expected" + " order. The Adaptive Lighting integration will not work!" + " This might happen if your sunrise/sunset offset is too large or" + " your manually set sunrise/sunset time is past/before noon/midnight." + ) + _LOGGER.error(msg) + raise ValueError(msg) return events From 303e2fe27a1c29967fdc6c64f364cbe67fdd962b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 2 Oct 2020 19:47:06 +0200 Subject: [PATCH 0231/1077] add sleep_mode switch and deprecate disable_state/entity --- .../adaptive_lighting/config_flow.py | 7 +- custom_components/adaptive_lighting/const.py | 14 +- .../adaptive_lighting/strings.json | 7 +- custom_components/adaptive_lighting/switch.py | 696 ++++++++++-------- .../adaptive_lighting/translations/en.json | 5 +- 5 files changed, 394 insertions(+), 335 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index a0c746a0..bb75cc07 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -8,9 +8,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from .const import ( # pylint: disable=unused-import - CONF_DISABLE_ENTITY, CONF_LIGHTS, - CONF_SLEEP_ENTITY, DOMAIN, EXTRA_VALIDATION, NONE_STR, @@ -64,8 +62,8 @@ def validate_options(user_input, errors): """ for key, (validate, _) in EXTRA_VALIDATION.items(): # these are unserializable validators + value = user_input.get(key) try: - value = user_input.get(key) if value is not None and value != NONE_STR: validate(value) except vol.Invalid: @@ -92,11 +90,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_create_entry(title="", data=user_input) all_lights = sorted(self.hass.states.async_entity_ids("light")) - all_entities = sorted(self.hass.states.async_entity_ids()) to_replace = { CONF_LIGHTS: cv.multi_select(all_lights), - CONF_DISABLE_ENTITY: vol.In([NONE_STR] + all_entities), - CONF_SLEEP_ENTITY: vol.In([NONE_STR] + all_entities), } options_schema = {} diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 9d8000c3..6cf2ec92 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -15,8 +15,6 @@ CONF_LIGHTS, DEFAULT_LIGHTS = "lights", [] CONF_ADAPT_BRIGHTNESS, DEFAULT_ADAPT_BRIGHTNESS = "adapt_brightness", True CONF_ADAPT_COLOR_TEMP, DEFAULT_ADAPT_COLOR_TEMP = "adapt_color_temp", True CONF_ADAPT_RGB_COLOR, DEFAULT_ADAPT_RGB_COLOR = "adapt_rgb_color", True -CONF_DISABLE_ENTITY = "disable_entity" -CONF_DISABLE_STATE = "disable_state" CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100 @@ -27,12 +25,11 @@ CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE = "only_once", False CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1 CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP = "sleep_color_temp", 1000 -CONF_SLEEP_ENTITY = "sleep_entity" -CONF_SLEEP_STATE = "sleep_state" CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 CONF_SUNRISE_TIME = "sunrise_time" CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 CONF_SUNSET_TIME = "sunset_time" +CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener" @@ -55,8 +52,6 @@ VALIDATION_TUPLES = [ (CONF_ADAPT_BRIGHTNESS, DEFAULT_ADAPT_BRIGHTNESS, bool), (CONF_ADAPT_COLOR_TEMP, DEFAULT_ADAPT_COLOR_TEMP, bool), (CONF_ADAPT_RGB_COLOR, DEFAULT_ADAPT_RGB_COLOR, bool), - (CONF_DISABLE_ENTITY, NONE_STR, cv.entity_id), - (CONF_DISABLE_STATE, NONE_STR, str), (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), (CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)), @@ -67,12 +62,11 @@ VALIDATION_TUPLES = [ (CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool), (CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)), (CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP, int_between(1000, 10000)), - (CONF_SLEEP_ENTITY, NONE_STR, cv.entity_id), - (CONF_SLEEP_STATE, NONE_STR, str), (CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int), (CONF_SUNRISE_TIME, NONE_STR, str), (CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int), (CONF_SUNSET_TIME, NONE_STR, str), + (CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool), (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), ] @@ -93,11 +87,7 @@ def join_strings(lst): # conf_option: (validator, coerce) tuples # these validators cannot be serialized but can be serialized when coerced by coerce. EXTRA_VALIDATION = { - CONF_DISABLE_ENTITY: (cv.entity_id, str), - CONF_DISABLE_STATE: (vol.All(cv.ensure_list_csv, [cv.string]), join_strings), CONF_INTERVAL: (cv.time_period, timedelta_as_int), - CONF_SLEEP_ENTITY: (cv.entity_id, str), - CONF_SLEEP_STATE: (vol.All(cv.ensure_list_csv, [cv.string]), join_strings), CONF_SUNRISE_OFFSET: (cv.time_period, timedelta_as_int), CONF_SUNRISE_TIME: (cv.time, str), CONF_SUNSET_OFFSET: (cv.time_period, timedelta_as_int), diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index a64e8a88..e89e0b5a 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -24,9 +24,7 @@ "adapt_brightness": "adapt_brightness", "adapt_color_temp": "adapt_color_temp, adapt color temperature using 'color_temp' if supported", "adapt_rgb_color": "adapt_rgb_color, adapt color temperature using RGB/XY if supported", - "disable_entity": "disable_entity, entity_id that stops the switch from adapting lights", - "disable_state": "disable_state, state(s) of 'disable_entity', e.g., 'off' or 'total,half'", - "initial_transition": "initial_transition, when lights go 'off' to 'on' or when 'disable_state'/'sleep_state' changes", + "initial_transition": "initial_transition, when lights go 'off' to 'on' or when 'sleep_state' changes", "interval": "interval, time between switch updates in seconds", "max_brightness": "max_brightness, in %", "max_color_temp": "max_color_temp, in Kelvin", @@ -36,12 +34,11 @@ "prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible", "sleep_brightness": "sleep_brightness, in %", "sleep_color_temp": "sleep_color_temp, in Kelvin", - "sleep_entity": "sleep_entity, 'entity_id' that manages the switch's sleep mode", - "sleep_state": "sleep_state, state(s) of 'sleep_entity', e.g., 'off' or 'total,half'", "sunrise_offset": "sunrise_offset, in +/- seconds", "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", "sunset_offset": "sunset_offset, in +/- seconds", "sunset_time": "sunset_time, in 'HH:MM:SS' format", + "take_over_control": "take_over_control, (NOT YET IMPLEMENTED!) if manually adjusting the lights when they are already on", "transition": "transition, in seconds" } } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index c6f9fe37..0e873570 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -4,11 +4,13 @@ from __future__ import annotations import asyncio import bisect from copy import deepcopy +from dataclasses import dataclass import datetime from datetime import timedelta import logging -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union +import astral import voluptuous as vol from homeassistant.components.light import ( @@ -36,11 +38,12 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, STATE_ON, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) -from homeassistant.core import Context, Event, ServiceCall, callback +from homeassistant.core import Context, Event, ServiceCall from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( @@ -63,8 +66,6 @@ from .const import ( CONF_ADAPT_BRIGHTNESS, CONF_ADAPT_COLOR_TEMP, CONF_ADAPT_RGB_COLOR, - CONF_DISABLE_ENTITY, - CONF_DISABLE_STATE, CONF_INITIAL_TRANSITION, CONF_INTERVAL, CONF_LIGHTS, @@ -76,12 +77,11 @@ from .const import ( CONF_PREFER_RGB_COLOR, CONF_SLEEP_BRIGHTNESS, CONF_SLEEP_COLOR_TEMP, - CONF_SLEEP_ENTITY, - CONF_SLEEP_STATE, CONF_SUNRISE_OFFSET, CONF_SUNRISE_TIME, CONF_SUNSET_OFFSET, CONF_SUNSET_TIME, + CONF_TAKE_OVER_CONTROL, CONF_TRANSITION, CONF_TURN_ON_LIGHTS, DOMAIN, @@ -139,7 +139,10 @@ async def async_setup_entry(hass, config_entry: ConfigEntry, async_add_entities: data[ATTR_TURN_ON_OFF_LISTENER] = TurnOnOffListener(hass) turn_on_off_listener = data[ATTR_TURN_ON_OFF_LISTENER] - switch = AdaptiveSwitch(hass, config_entry, turn_on_off_listener) + sleep_mode_switch = AdaptiveSleepModeSwitch(hass, config_entry) + switch = AdaptiveSwitch(hass, config_entry, turn_on_off_listener, sleep_mode_switch) + + data[config_entry.entry_id]["sleep_mode_switch"] = sleep_mode_switch data[config_entry.entry_id][SWITCH_DOMAIN] = switch # Register `apply` service @@ -159,7 +162,7 @@ async def async_setup_entry(hass, config_entry: ConfigEntry, async_add_entities: }, handle_apply, ) - async_add_entities([switch], update_before_add=True) + async_add_entities([switch, sleep_mode_switch], update_before_add=True) def validate(config_entry): @@ -204,40 +207,57 @@ def _expand_light_groups(hass, lights: List[str]) -> List[str]: return list(all_lights) +def _supported_features(hass, light: str): + state = hass.states.get(light) + supported_features = state.attributes["supported_features"] + return {key for key, value in _SUPPORT_OPTS.items() if supported_features & value} + + class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" - def __init__(self, hass, config_entry, turn_on_off_listener: TurnOnOffListener): + def __init__( + self, + hass, + config_entry: ConfigEntry, + turn_on_off_listener: TurnOnOffListener, + sleep_mode_switch: AdaptiveSleepModeSwitch, + ): """Initialize the Adaptive Lighting switch.""" self.hass = hass self.turn_on_off_listener = turn_on_off_listener + self.sleep_mode_switch = sleep_mode_switch data = validate(config_entry) self._name = data[CONF_NAME] self._lights = data[CONF_LIGHTS] + self._adapt_brightness = data[CONF_ADAPT_BRIGHTNESS] self._adapt_color_temp = data[CONF_ADAPT_COLOR_TEMP] self._adapt_rgb_color = data[CONF_ADAPT_RGB_COLOR] - self._disable_entity = data[CONF_DISABLE_ENTITY] - self._disable_state = data[CONF_DISABLE_STATE] self._initial_transition = data[CONF_INITIAL_TRANSITION] self._interval = data[CONF_INTERVAL] - self._max_brightness = data[CONF_MAX_BRIGHTNESS] - self._max_color_temp = data[CONF_MAX_COLOR_TEMP] - self._min_brightness = data[CONF_MIN_BRIGHTNESS] - self._min_color_temp = data[CONF_MIN_COLOR_TEMP] self._only_once = data[CONF_ONLY_ONCE] self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR] - self._sleep_brightness = data[CONF_SLEEP_BRIGHTNESS] - self._sleep_color_temp = data[CONF_SLEEP_COLOR_TEMP] - self._sleep_entity = data[CONF_SLEEP_ENTITY] - self._sleep_state = data[CONF_SLEEP_STATE] - self._sunrise_offset = data[CONF_SUNRISE_OFFSET] - self._sunrise_time = data[CONF_SUNRISE_TIME] - self._sunset_offset = data[CONF_SUNSET_OFFSET] - self._sunset_time = data[CONF_SUNSET_TIME] + self._take_over_control = data[CONF_TAKE_OVER_CONTROL] self._transition = data[CONF_TRANSITION] + self._sun_light_settings = SunLightSettings( + name=self._name, + astral_location=get_astral_location(self.hass), + max_brightness=data[CONF_MAX_BRIGHTNESS], + max_color_temp=data[CONF_MAX_COLOR_TEMP], + min_brightness=data[CONF_MIN_BRIGHTNESS], + min_color_temp=data[CONF_MIN_COLOR_TEMP], + sleep_brightness=data[CONF_SLEEP_BRIGHTNESS], + sleep_color_temp=data[CONF_SLEEP_COLOR_TEMP], + sunrise_offset=data[CONF_SUNRISE_OFFSET], + sunrise_time=data[CONF_SUNRISE_TIME], + sunset_offset=data[CONF_SUNSET_OFFSET], + sunset_time=data[CONF_SUNSET_TIME], + time_zone=self.hass.config.time_zone, + ) + # Set other attributes self._icon = ICON self._entity_id = f"switch.{DOMAIN}_{slugify(self._name)}" @@ -245,17 +265,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Tracks 'off' → 'on' state changes self._on_to_off_event: Dict[str, Event] = {} + # Tracks 'on' → 'off' state changes + self._off_to_on_event: Dict[str, Event] = {} # Locks that prevent light adjusting when waiting for a light to 'turn_off' self._locks: Dict[str, asyncio.Lock] = {} - # Initialize attributes that will be set in self._update_attrs - self._percent = None - self._brightness = None - self._color_temp_kelvin = None - self._color_temp_mired = None - self._rgb_color = None - self._xy_color = None - self._hs_color = None + # Set in self._update_attrs_and_maybe_adapt_lights + self._light_settings = {} # Set and unset tracker in async_turn_on and async_turn_off self.remove_listeners = [] @@ -278,21 +294,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): @property def name(self): """Return the name of the device if any.""" - return self._name + return f"Adaptive Lighting: {self._name}" @property def is_on(self) -> Optional[bool]: """Return true if adaptive lighting is on.""" return self._state - def _supported_features(self, light: str): - state = self.hass.states.get(light) - supported_features = state.attributes["supported_features"] - return { - key for key, value in _SUPPORT_OPTS.items() if supported_features & value - } - - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity about to be added to hass.""" if self.hass.is_running: await self._setup_listeners() @@ -313,66 +322,51 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self.turn_on_off_listener.lights.update(all_lights) self._lights = list(all_lights) - async def _setup_listeners(self, _=None): + async def _setup_listeners(self, _=None) -> None: _LOGGER.debug("%s: Called '_setup_listeners'", self._name) if not self.is_on or not self.hass.is_running: _LOGGER.debug("%s: Cancelled '_setup_listeners'", self._name) return + assert not self.remove_listeners + remove_interval = async_track_time_interval( self.hass, self._async_update_at_interval, self._interval ) - self.remove_listeners.append(remove_interval) + remove_sleep = async_track_state_change_event( + self.hass, + self.sleep_mode_switch.entity_id, + self._sleep_state_event, + ) + self.remove_listeners.extend([remove_interval, remove_sleep]) + if self._lights: self._expand_light_groups() remove_state = async_track_state_change_event( self.hass, self._lights, self._light_event ) self.remove_listeners.append(remove_state) - if self._sleep_entity is not None: - remove_sleep = async_track_state_change_event( - self.hass, - self._sleep_entity, - self._sleep_state_event, - ) - self.remove_listeners.append(remove_sleep) - if self._disable_entity is not None: - remove_disable = async_track_state_change_event( - self.hass, - self._disable_entity, - self._disable_state_event, - ) - self.remove_listeners.append(remove_disable) - def _remove_listeners(self): + def _remove_listeners(self) -> None: while self.remove_listeners: remove_listener = self.remove_listeners.pop() remove_listener() @property - def icon(self): + def icon(self) -> str: """Icon to use in the frontend, if any.""" return self._icon @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the attributes of the switch.""" - attrs = { - "percent": self._percent, - "brightness": self._brightness, - "color_temp_kelvin": self._color_temp_kelvin, - "color_temp_mired": self._color_temp_mired, - "rgb_color": self._rgb_color, - "xy_color": self._xy_color, - "hs_color": self._hs_color, - } if not self.is_on: - return {key: None for key in attrs} - return attrs + return {key: None for key in self._light_settings} + return self._light_settings async def async_turn_on( self, adapt_lights: bool = True - ): # pylint: disable=arguments-differ + ) -> None: # pylint: disable=arguments-differ """Turn on adaptive lighting.""" _LOGGER.debug( "%s: Called 'async_turn_on', current state is '%s'", self._name, self._state @@ -382,54 +376,243 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._state = True await self._setup_listeners() if adapt_lights: - await self._maybe_adapt_lights( + await self._update_attrs_and_maybe_adapt_lights( transition=self._initial_transition, force=True ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn off adaptive lighting.""" if not self.is_on: return self._state = False self._remove_listeners() - @callback - def _update_attrs(self): - """Update Adaptive Values.""" - # Setting all values because this method takes <0.5ms to execute. - self._percent = self._calc_percent() - self._brightness = self._calc_brightness() - self._color_temp_kelvin = self._calc_color_temp_kelvin() - self._color_temp_mired = color_temperature_kelvin_to_mired( - self._color_temp_kelvin + async def _async_update_at_interval(self, now=None) -> None: + await self._update_attrs_and_maybe_adapt_lights(force=False) + + def _is_sleep(self) -> bool: + return self.sleep_mode_switch.is_on + + async def _adapt_light( + self, + light: str, + transition: Optional[int] = None, + adapt_brightness: Optional[bool] = None, + adapt_color_temp: Optional[bool] = None, + adapt_rgb_color: Optional[bool] = None, + context: Optional[Context] = None, + ) -> None: + lock = self._locks.get(light) + if lock is not None and lock.locked(): + _LOGGER.debug("%s: '%s' is locked", self._name, light) + return + + service_data = {ATTR_ENTITY_ID: light} + features = _supported_features(self.hass, light) + + if transition is None: + transition = self._transition + if adapt_brightness is None: + adapt_brightness = self._adapt_brightness + if adapt_color_temp is None: + adapt_color_temp = self._adapt_color_temp + if adapt_rgb_color is None: + adapt_rgb_color = self._adapt_rgb_color + + if "transition" in features: + service_data[ATTR_TRANSITION] = transition + + if "brightness" in features and adapt_brightness: + service_data[ATTR_BRIGHTNESS_PCT] = self._light_settings["brightness_pct"] + + if ( + "color_temp" in features + and adapt_color_temp + and not (self._prefer_rgb_color and "color" in features) + ): + attributes = self.hass.states.get(light).attributes + min_mireds, max_mireds = attributes["min_mireds"], attributes["max_mireds"] + color_temp_mired = self._light_settings["color_temp_mired"] + color_temp_mired = max(min(color_temp_mired, max_mireds), min_mireds) + service_data[ATTR_COLOR_TEMP] = color_temp_mired + elif "color" in features and adapt_rgb_color: + service_data[ATTR_RGB_COLOR] = self._light_settings["rgb_color"] + + _LOGGER.debug( + "%s: Scheduling 'light.turn_on' with the following 'service_data': %s", + self._name, + service_data, ) - self._rgb_color = color_temperature_to_rgb(self._color_temp_kelvin) - self._xy_color = color_RGB_to_xy(*self._rgb_color) - self._hs_color = color_xy_to_hs(*self._xy_color) - self.async_write_ha_state() - _LOGGER.debug("%s: '_update_attrs' called", self._name) - async def _async_update_at_interval(self, now=None): - await self._maybe_adapt_lights(force=False) + await self.hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + service_data, + context=context, + ) - async def _maybe_adapt_lights( + async def _update_attrs_and_maybe_adapt_lights( self, lights: Optional[List[str]] = None, transition: Optional[int] = None, force: bool = False, context: Optional[Context] = None, ): - self._update_attrs() - if self._only_once and not force: + _LOGGER.debug("%s: '_update_attrs_and_maybe_adapt_lights' called", self._name) + assert self.is_on + self._light_settings = self._sun_light_settings.get_settings(self._is_sleep()) + self.async_write_ha_state() + if lights is None: + lights = self._lights + if (self._only_once and not force) or not lights: return - await self._adapt_lights(lights or self._lights, transition, context) + await self._adapt_lights(lights, transition, context) - def _get_sun_events(self, date: datetime.datetime): - def _replace_time(date, key): - time = getattr(self, f"_{key}_time") + async def _adapt_lights( + self, lights: List[str], transition: Optional[int], context=Optional[Context] + ): + _LOGGER.debug( + "%s: '_adapt_lights(%s, %s)' called", self.name, lights, transition + ) + for light in lights: + if not is_on(self.hass, light): + continue + if self._take_over_control: + if await self.turn_on_off_listener.is_manually_adjusted( + light, + off_to_on_event=self._off_to_on_event.get(light), + ): + continue + await self._adapt_light(light, transition, context=context) + + async def _sleep_state_event(self, event: Event): + if not match_state_event(event, ("on", "off")): + return + _LOGGER.debug("%s: _sleep_state_event, event: '%s'", self._name, event) + await self._update_attrs_and_maybe_adapt_lights( + transition=self._initial_transition, force=True, context=event.context + ) + + async def _light_event(self, event: Event): + old_state = event.data.get("old_state") + new_state = event.data.get("new_state") + entity_id = event.data.get("entity_id") + if ( + old_state is not None + and old_state.state == "off" + and new_state is not None + and new_state.state == "on" + ): + _LOGGER.debug( + "%s: Detected an 'off' → 'on' event for '%s'", self._name, entity_id + ) + # Tracks 'off' → 'on' state changes + self._off_to_on_event[entity_id] = event + lock = self._locks.get(entity_id) + if lock is None: + lock = self._locks[entity_id] = asyncio.Lock() + async with lock: + if await self.turn_on_off_listener.maybe_cancel_adjusting( + entity_id, + off_to_on_event=event, + on_to_off_event=self._on_to_off_event.get(entity_id), + ): + # Stop if a rapid 'off' → 'on' → 'off' happens. + _LOGGER.debug( + "%s: Cancelling adjusting lights for %s", self._name, entity_id + ) + return + + await self._update_attrs_and_maybe_adapt_lights( + lights=[entity_id], + transition=self._initial_transition, + force=True, + context=event.context, + ) + elif ( + old_state is not None + and old_state.state == "on" + and new_state is not None + and new_state.state == "off" + ): + # Tracks 'off' → 'on' state changes + self._on_to_off_event[entity_id] = event + + +class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity): + """Representation of a Adaptive Lighting switch.""" + + def __init__(self, hass, config_entry): + """Initialize the Adaptive Lighting switch.""" + self.hass = hass + data = validate(config_entry) + self._name = data[CONF_NAME] + self._icon = ICON + self._entity_id = f"switch.{DOMAIN}_sleep_mode_{slugify(self._name)}" + self._state = None + + @property + def entity_id(self): + """Return the entity ID of the switch.""" + return self._entity_id + + @property + def name(self): + """Return the name of the device if any.""" + return f"Adaptive Lighting Sleep Mode: {self._name}" + + @property + def icon(self) -> str: + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def is_on(self) -> Optional[bool]: + """Return true if adaptive lighting is on.""" + return self._state + + async def async_added_to_hass(self) -> None: + """Call when entity about to be added to hass.""" + last_state = await self.async_get_last_state() + if last_state is None or STATE_OFF: # newly added to HA + await self.async_turn_off() + else: + await self.async_turn_on() + + async def async_turn_on(self) -> None: + """Turn on adaptive lighting sleep mode.""" + self._state = True + + async def async_turn_off(self) -> None: + """Turn off adaptive lighting sleep mode.""" + self._state = False + + +@dataclass(frozen=True) +class SunLightSettings: + """Track the state of the sun and associated light settings.""" + + name: str + astral_location: astral.Location + max_brightness: int + max_color_temp: int + min_brightness: int + min_color_temp: int + sleep_brightness: int + sleep_color_temp: int + sunrise_offset: Optional[datetime.timedelta] + sunrise_time: Optional[datetime.time] + sunset_offset: Optional[datetime.timedelta] + sunset_time: Optional[datetime.time] + time_zone: datetime.tzinfo + + def get_sun_events(self, date: datetime.datetime) -> Dict[str, float]: + """Get the four sun event's timestamps at 'date'.""" + + def _replace_time(date: datetime.datetime, key: str) -> datetime.datetime: + time = getattr(self, f"{key}_time") date_time = datetime.datetime.combine(datetime.date.today(), time) - time_zone = self.hass.config.time_zone - utc_time = time_zone.localize(date_time).astimezone(dt_util.UTC) + utc_time = self.time_zone.localize(date_time).astimezone(dt_util.UTC) return date.replace( hour=utc_time.hour, minute=utc_time.minute, @@ -437,19 +620,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): microsecond=utc_time.microsecond, ) - location = get_astral_location(self.hass) + location = self.astral_location sunrise = ( location.sunrise(date, local=False) - if self._sunrise_time is None + if self.sunrise_time is None else _replace_time(date, "sunrise") - ) + self._sunrise_offset + ) + self.sunrise_offset sunset = ( location.sunset(date, local=False) - if self._sunset_time is None + if self.sunset_time is None else _replace_time(date, "sunset") - ) + self._sunset_offset + ) + self.sunset_offset - if self._sunrise_time is None and self._sunset_time is None: + if self.sunrise_time is None and self.sunset_time is None: solar_noon = location.solar_noon(date, local=False) solar_midnight = location.solar_midnight(date, local=False) else: @@ -467,7 +650,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): events_names, _ = zip(*events) if events_names not in _ALLOWED_ORDERS: msg = ( - f"{self._name}: The sun events {events_names} are not in the expected" + f"{self.name}: The sun events {events_names} are not in the expected" " order. The Adaptive Lighting integration will not work!" " This might happen if your sunrise/sunset offset is too large or" " your manually set sunrise/sunset time is past/before noon/midnight." @@ -477,19 +660,21 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return events - def _relevant_events(self, now: datetime.datetime): + def relevant_events(self, now: datetime.datetime) -> List[Tuple[str, float]]: + """Get the previous and next sun event.""" events = [ - self._get_sun_events(now + timedelta(days=days)) for days in [-1, 0, 1] + self.get_sun_events(now + timedelta(days=days)) for days in [-1, 0, 1] ] events = sum(events, []) # flatten lists events = sorted(events, key=lambda x: x[1]) i_now = bisect.bisect([ts for _, ts in events], now.timestamp()) return events[i_now - 1 : i_now + 1] - def _calc_percent(self): + def calc_percent(self) -> float: + """Calculate the position of the sun in %.""" now = dt_util.utcnow() now_ts = now.timestamp() - today = self._relevant_events(now) + today = self.relevant_events(now) (_, prev_ts), (next_event, next_ts) = today h, x = ( # pylint: disable=invalid-name (prev_ts, next_ts) @@ -500,167 +685,49 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): percentage = (0 - k) * ((now_ts - h) / (h - x)) ** 2 + k return percentage - def _is_sleep(self): - return ( - self._sleep_entity is not None - and self.hass.states.get(self._sleep_entity).state in self._sleep_state + def calc_brightness_pct(self, percent: float, is_sleep: bool) -> float: + """Calculate the brightness in %.""" + if is_sleep: + return self.sleep_brightness + if percent > 0: + return self.max_brightness + delta_brightness = self.max_brightness - self.min_brightness + percent = 1 + percent + return (delta_brightness * percent) + self.min_brightness + + def calc_color_temp_kelvin(self, percent: float, is_sleep: bool) -> float: + """Calculate the color temperature in Kelvin.""" + if is_sleep: + return self.sleep_color_temp + if percent > 0: + delta = self.max_color_temp - self.min_color_temp + return (delta * percent) + self.min_color_temp + return self.min_color_temp + + def get_settings( + self, is_sleep + ) -> Dict[str, Union[float, Tuple[float, float], Tuple[float, float, float]]]: + """Get all light settings. + + Calculating all values takes <0.5ms. + """ + percent = self.calc_percent() + brightness_pct = self.calc_brightness_pct(percent, is_sleep) + color_temp_kelvin = self.calc_color_temp_kelvin(percent, is_sleep) + color_temp_mired: float = color_temperature_kelvin_to_mired(color_temp_kelvin) + rgb_color: Tuple[float, float, float] = color_temperature_to_rgb( + color_temp_kelvin ) - - def _calc_color_temp_kelvin(self): - if self._is_sleep(): - return self._sleep_color_temp - if self._percent > 0: - delta = self._max_color_temp - self._min_color_temp - return (delta * self._percent) + self._min_color_temp - return self._min_color_temp - - def _calc_brightness(self) -> float: - if self._is_sleep(): - return self._sleep_brightness - if self._percent > 0: - return self._max_brightness - delta_brightness = self._max_brightness - self._min_brightness - percent = 1 + self._percent - return (delta_brightness * percent) + self._min_brightness - - def _is_disabled(self): - return ( - self._disable_entity is not None - and self.hass.states.get(self._disable_entity).state in self._disable_state - ) - - async def _adapt_light( - self, - light: str, - transition: Optional[int] = None, - adapt_brightness: Optional[bool] = None, - adapt_color_temp: Optional[bool] = None, - adapt_rgb_color: Optional[bool] = None, - context: Optional[Context] = None, - ): - lock = self._locks.get(light) - if lock is not None and lock.locked(): - _LOGGER.debug("%s: '%s' is locked", self._name, light) - return - - service_data = {ATTR_ENTITY_ID: light} - features = self._supported_features(light) - - if transition is None: - transition = self._transition - if adapt_brightness is None: - adapt_brightness = self._adapt_brightness - if adapt_color_temp is None: - adapt_color_temp = self._adapt_color_temp - if adapt_rgb_color is None: - adapt_rgb_color = self._adapt_rgb_color - - if "transition" in features: - service_data[ATTR_TRANSITION] = transition - - if "brightness" in features and adapt_brightness: - service_data[ATTR_BRIGHTNESS_PCT] = self._brightness - - if ( - "color_temp" in features - and adapt_color_temp - and not (self._prefer_rgb_color and "color" in features) - ): - attributes = self.hass.states.get(light).attributes - min_mireds, max_mireds = attributes["min_mireds"], attributes["max_mireds"] - color_temp_mired = max(min(self._color_temp_mired, max_mireds), min_mireds) - service_data[ATTR_COLOR_TEMP] = color_temp_mired - elif "color" in features and adapt_rgb_color: - service_data[ATTR_RGB_COLOR] = self._rgb_color - - _LOGGER.debug( - "%s: Scheduling 'light.turn_on' with the following 'service_data': %s", - self._name, - service_data, - ) - - await self.hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - service_data, - context=context, - ) - - def _should_adapt(self): - if not self._lights or not self.is_on or self._is_disabled(): - return False - return True - - async def _adapt_lights( - self, lights: List[str], transition: Optional[int], context=Optional[Context] - ): - if not self._should_adapt(): - return - _LOGGER.debug( - "%s: '_adapt_lights(%s, %s)' called", self.name, lights, transition - ) - for light in lights: - if is_on(self.hass, light): - await self._adapt_light(light, transition, context=context) - - async def _disable_state_event(self, event: Event): - if not match_state_event(event, self._disable_state): - return - _LOGGER.debug("%s: _disable_state_event, event: '%s'", self._name, event) - await self._maybe_adapt_lights( - transition=self._initial_transition, force=True, context=event.context - ) - - async def _sleep_state_event(self, event: Event): - if not match_state_event(event, self._sleep_state): - return - _LOGGER.debug("%s: _sleep_state_event, event: '%s'", self._name, event) - await self._maybe_adapt_lights( - transition=self._initial_transition, force=True, context=event.context - ) - - async def _light_event(self, event: Event): - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") - entity_id = event.data.get("entity_id") - if ( - old_state is not None - and old_state.state == "off" - and new_state is not None - and new_state.state == "on" - ): - _LOGGER.debug( - "%s: Detected an 'off' → 'on' event for '%s'", self._name, entity_id - ) - lock = self._locks.get(entity_id) - if lock is None: - lock = self._locks[entity_id] = asyncio.Lock() - async with lock: - if await self.turn_on_off_listener.maybe_cancel_adjusting( - entity_id, - off_to_on_event=event, - on_to_off_event=self._on_to_off_event.get(entity_id), - ): - # Stop if a rapid 'off' → 'on' → 'off' happens. - _LOGGER.debug( - "%s: Cancelling adjusting lights for %s", self._name, entity_id - ) - return - - await self._maybe_adapt_lights( - lights=[entity_id], - transition=self._initial_transition, - force=True, - context=event.context, - ) - elif ( - old_state is not None - and old_state.state == "on" - and new_state is not None - and new_state.state == "off" - ): - # Tracks 'off' → 'on' state changes - self._on_to_off_event[entity_id] = event + xy_color: Tuple[float, float] = color_RGB_to_xy(*rgb_color) + hs_color: Tuple[float, float] = color_xy_to_hs(*xy_color) + return { + "brightness_pct": brightness_pct, + "color_temp_kelvin": color_temp_kelvin, + "color_temp_mired": color_temp_mired, + "rgb_color": rgb_color, + "xy_color": xy_color, + "hs_color": hs_color, + } class TurnOnOffListener: @@ -672,9 +739,9 @@ class TurnOnOffListener: self.lights = set() # Tracks 'light.turn_off' service calls - self.turn_off_event: Dict[str, Tuple[str, float]] = {} + self.turn_off_event: Dict[str, Event] = {} # Tracks 'light.turn_on' service calls - self.turn_on_event: Dict[str, Tuple[str]] = {} + self.turn_on_event: Dict[str, Event] = {} # Keeps 'asyncio.sleep` tasks that can be cancelled by 'light.turn_on' events self.sleep_tasks: Dict[str, asyncio.Task] = {} @@ -682,6 +749,47 @@ class TurnOnOffListener: EVENT_CALL_SERVICE, self.turn_on_off_event_listener ) + async def turn_on_off_event_listener(self, event: Event): + """Track 'light.turn_off' and 'light.turn_on' service calls.""" + domain = event.data.get(ATTR_DOMAIN) + if domain != LIGHT_DOMAIN: + return + + service = event.data.get(ATTR_SERVICE) + service_data = event.data.get(ATTR_SERVICE_DATA, {}) + + entity_ids = service_data.get(ATTR_ENTITY_ID) + if isinstance(entity_ids, str): + entity_ids = [entity_ids] + + if not any(eid in self.lights for eid in entity_ids): + return + + if service == SERVICE_TURN_OFF: + transition = service_data.get(ATTR_TRANSITION) + _LOGGER.debug( + "Detected an 'light.turn_off('%s', transition=%s)' event", + entity_ids, + transition, + ) + for eid in entity_ids: + self.turn_off_event[eid] = event + + elif service == SERVICE_TURN_ON: + _LOGGER.debug("Detected an 'light.turn_on('%s')' event", entity_ids) + for eid in entity_ids: + task = self.sleep_tasks.get(eid) + if task is not None: + task.cancel() + self.turn_on_event[eid] = event + + async def is_manually_adjusted(self, light: str, off_to_on_event: Optional[Event]): + """Check if the light has been 'on' and is now manually being adjusted.""" + if off_to_on_event is None: + # No state change has been registered before, so we can't tell. + return False + return False + async def maybe_cancel_adjusting( self, entity_id: str, off_to_on_event: Event, on_to_off_event: Optional[Event] ) -> bool: @@ -702,8 +810,14 @@ class TurnOnOffListener: return False id_on_to_off = on_to_off_event.context.id - id_turn_off, transition = self.turn_off_event.get(entity_id, (None, None)) - id_turn_on = self.turn_on_event.get(entity_id) + + turn_off_event = self.turn_off_event.get(entity_id) + id_turn_off = turn_off_event.context.id + transition = turn_off_event.data[ATTR_SERVICE_DATA].get(ATTR_TRANSITION) + + turn_on_event = self.turn_on_event.get(entity_id) + id_turn_on = turn_on_event.context.id + id_off_to_on = off_to_on_event.context.id if id_off_to_on == id_turn_on and id_off_to_on is not None: @@ -729,7 +843,7 @@ class TurnOnOffListener: # Here we could just `return True` but because we want to prevent any updates # from happening to this light (through async_track_time_interval or - # sleep_state or disable_state) for some time, we wait below until the light + # sleep_state) for some time, we wait below until the light # is 'off' or the time has passed. delay -= delta_time # delta_time has passed since the 'off' → 'on' event @@ -765,37 +879,3 @@ class TurnOnOffListener: # other 'off' → 'on' state switches resulting from polling. That # would mean we 'return True' here. return False - - async def turn_on_off_event_listener(self, event: Event): - """Track 'light.turn_off' and 'light.turn_on' service calls.""" - domain = event.data.get(ATTR_DOMAIN) - if domain != LIGHT_DOMAIN: - return - - service = event.data.get(ATTR_SERVICE) - service_data = event.data.get(ATTR_SERVICE_DATA, {}) - - entity_ids = service_data.get(ATTR_ENTITY_ID) - if isinstance(entity_ids, str): - entity_ids = [entity_ids] - - if not any(eid in self.lights for eid in entity_ids): - return - - if service == SERVICE_TURN_OFF: - transition = service_data.get(ATTR_TRANSITION) - _LOGGER.debug( - "Detected an 'light.turn_off('%s', transition=%s)' event", - entity_ids, - transition, - ) - for eid in entity_ids: - self.turn_off_event[eid] = (event.context.id, transition) - - elif service == SERVICE_TURN_ON: - _LOGGER.debug("Detected an 'light.turn_on('%s')' event", entity_ids) - for eid in entity_ids: - task = self.sleep_tasks.get(eid) - if task is not None: - task.cancel() - self.turn_on_event[eid] = event.context.id diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index a64e8a88..fcf38d3d 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -24,8 +24,6 @@ "adapt_brightness": "adapt_brightness", "adapt_color_temp": "adapt_color_temp, adapt color temperature using 'color_temp' if supported", "adapt_rgb_color": "adapt_rgb_color, adapt color temperature using RGB/XY if supported", - "disable_entity": "disable_entity, entity_id that stops the switch from adapting lights", - "disable_state": "disable_state, state(s) of 'disable_entity', e.g., 'off' or 'total,half'", "initial_transition": "initial_transition, when lights go 'off' to 'on' or when 'disable_state'/'sleep_state' changes", "interval": "interval, time between switch updates in seconds", "max_brightness": "max_brightness, in %", @@ -36,12 +34,11 @@ "prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible", "sleep_brightness": "sleep_brightness, in %", "sleep_color_temp": "sleep_color_temp, in Kelvin", - "sleep_entity": "sleep_entity, 'entity_id' that manages the switch's sleep mode", - "sleep_state": "sleep_state, state(s) of 'sleep_entity', e.g., 'off' or 'total,half'", "sunrise_offset": "sunrise_offset, in +/- seconds", "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", "sunset_offset": "sunset_offset, in +/- seconds", "sunset_time": "sunset_time, in 'HH:MM:SS' format", + "take_over_control": "take_over_control, (NOT YET IMPLEMENTED!) if manually adjusting the lights when they are already on", "transition": "transition, in seconds" } } From d29649bdfdc56cee7f208375465d2334bab2a049 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 3 Oct 2020 01:00:26 +0200 Subject: [PATCH 0232/1077] fix temp issue --- custom_components/adaptive_lighting/switch.py | 44 +++++++++++-------- .../adaptive_lighting/translations/en.json | 2 +- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 0e873570..1e5587c5 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -127,7 +127,6 @@ async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): data[CONF_ADAPT_BRIGHTNESS], data[CONF_ADAPT_COLOR_TEMP], data[CONF_ADAPT_RGB_COLOR], - service_call.context, ) @@ -269,6 +268,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._off_to_on_event: Dict[str, Event] = {} # Locks that prevent light adjusting when waiting for a light to 'turn_off' self._locks: Dict[str, asyncio.Lock] = {} + # To identify that this integration made a change + self._context = Context() # Set in self._update_attrs_and_maybe_adapt_lights self._light_settings = {} @@ -296,6 +297,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Return the name of the device if any.""" return f"Adaptive Lighting: {self._name}" + # @property + # def unique_id(self): + # """Return the unique ID of entity.""" + # return self._name + @property def is_on(self) -> Optional[bool]: """Return true if adaptive lighting is on.""" @@ -364,9 +370,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return {key: None for key in self._light_settings} return self._light_settings - async def async_turn_on( + async def async_turn_on( # pylint: disable=arguments-differ self, adapt_lights: bool = True - ) -> None: # pylint: disable=arguments-differ + ) -> None: """Turn on adaptive lighting.""" _LOGGER.debug( "%s: Called 'async_turn_on', current state is '%s'", self._name, self._state @@ -390,9 +396,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _async_update_at_interval(self, now=None) -> None: await self._update_attrs_and_maybe_adapt_lights(force=False) - def _is_sleep(self) -> bool: - return self.sleep_mode_switch.is_on - async def _adapt_light( self, light: str, @@ -400,7 +403,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): adapt_brightness: Optional[bool] = None, adapt_color_temp: Optional[bool] = None, adapt_rgb_color: Optional[bool] = None, - context: Optional[Context] = None, ) -> None: lock = self._locks.get(light) if lock is not None and lock.locked(): @@ -448,7 +450,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): LIGHT_DOMAIN, SERVICE_TURN_ON, service_data, - context=context, + context=self._context, ) async def _update_attrs_and_maybe_adapt_lights( @@ -456,20 +458,21 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): lights: Optional[List[str]] = None, transition: Optional[int] = None, force: bool = False, - context: Optional[Context] = None, ): _LOGGER.debug("%s: '_update_attrs_and_maybe_adapt_lights' called", self._name) assert self.is_on - self._light_settings = self._sun_light_settings.get_settings(self._is_sleep()) + self._light_settings = self._sun_light_settings.get_settings( + self.sleep_mode_switch.is_on + ) self.async_write_ha_state() if lights is None: lights = self._lights if (self._only_once and not force) or not lights: return - await self._adapt_lights(lights, transition, context) + await self._adapt_lights(lights, transition) async def _adapt_lights( - self, lights: List[str], transition: Optional[int], context=Optional[Context] + self, lights: List[str], transition: Optional[int] ): _LOGGER.debug( "%s: '_adapt_lights(%s, %s)' called", self.name, lights, transition @@ -481,16 +484,17 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if await self.turn_on_off_listener.is_manually_adjusted( light, off_to_on_event=self._off_to_on_event.get(light), + adaptive_lighting_context=self._context, ): continue - await self._adapt_light(light, transition, context=context) + await self._adapt_light(light, transition) async def _sleep_state_event(self, event: Event): if not match_state_event(event, ("on", "off")): return _LOGGER.debug("%s: _sleep_state_event, event: '%s'", self._name, event) await self._update_attrs_and_maybe_adapt_lights( - transition=self._initial_transition, force=True, context=event.context + transition=self._initial_transition, force=True ) async def _light_event(self, event: Event): @@ -527,7 +531,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): lights=[entity_id], transition=self._initial_transition, force=True, - context=event.context, ) elif ( old_state is not None @@ -561,6 +564,11 @@ class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity): """Return the name of the device if any.""" return f"Adaptive Lighting Sleep Mode: {self._name}" + # @property + # def unique_id(self): + # """Return the unique ID of entity.""" + # return f"{self._name}_sleep_mode" + @property def icon(self) -> str: """Icon to use in the frontend, if any.""" @@ -579,11 +587,11 @@ class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity): else: await self.async_turn_on() - async def async_turn_on(self) -> None: + async def async_turn_on(self, **kwargs) -> None: """Turn on adaptive lighting sleep mode.""" self._state = True - async def async_turn_off(self) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn off adaptive lighting sleep mode.""" self._state = False @@ -783,7 +791,7 @@ class TurnOnOffListener: task.cancel() self.turn_on_event[eid] = event - async def is_manually_adjusted(self, light: str, off_to_on_event: Optional[Event]): + async def is_manually_adjusted(self, light: str, off_to_on_event: Optional[Event], adaptive_lighting_context: Context): """Check if the light has been 'on' and is now manually being adjusted.""" if off_to_on_event is None: # No state change has been registered before, so we can't tell. diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index fcf38d3d..e89e0b5a 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -24,7 +24,7 @@ "adapt_brightness": "adapt_brightness", "adapt_color_temp": "adapt_color_temp, adapt color temperature using 'color_temp' if supported", "adapt_rgb_color": "adapt_rgb_color, adapt color temperature using RGB/XY if supported", - "initial_transition": "initial_transition, when lights go 'off' to 'on' or when 'disable_state'/'sleep_state' changes", + "initial_transition": "initial_transition, when lights go 'off' to 'on' or when 'sleep_state' changes", "interval": "interval, time between switch updates in seconds", "max_brightness": "max_brightness, in %", "max_color_temp": "max_color_temp, in Kelvin", From 3eb593ec6d5f92638c5148b31ca95bc7ec8798f5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 3 Oct 2020 14:55:02 +0200 Subject: [PATCH 0233/1077] add take_over_control feat --- custom_components/adaptive_lighting/const.py | 2 +- .../adaptive_lighting/strings.json | 2 +- custom_components/adaptive_lighting/switch.py | 69 +++++++++++++++---- .../adaptive_lighting/translations/en.json | 2 +- 4 files changed, 59 insertions(+), 16 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 6cf2ec92..5b11e768 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -29,7 +29,7 @@ CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 CONF_SUNRISE_TIME = "sunrise_time" CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 CONF_SUNSET_TIME = "sunset_time" -CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True +CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", False CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener" diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index e89e0b5a..f9a82c98 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -38,7 +38,7 @@ "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", "sunset_offset": "sunset_offset, in +/- seconds", "sunset_time": "sunset_time, in 'HH:MM:SS' format", - "take_over_control": "take_over_control, (NOT YET IMPLEMENTED!) if manually adjusting the lights when they are already on", + "take_over_control": "take_over_control, (beta feature) if manually adjusting the lights when they are already on", "transition": "transition, in seconds" } } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 1e5587c5..358bb777 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -269,7 +269,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Locks that prevent light adjusting when waiting for a light to 'turn_off' self._locks: Dict[str, asyncio.Lock] = {} # To identify that this integration made a change - self._context = Context() + self.__context = Context() # self._context will be overwritten # Set in self._update_attrs_and_maybe_adapt_lights self._light_settings = {} @@ -370,6 +370,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return {key: None for key in self._light_settings} return self._light_settings + def _reset_take_over_control(self): + for light in self._lights: + self.turn_on_off_listener.manually_controlled[light] = False + async def async_turn_on( # pylint: disable=arguments-differ self, adapt_lights: bool = True ) -> None: @@ -380,6 +384,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self.is_on: return self._state = True + self._reset_take_over_control() await self._setup_listeners() if adapt_lights: await self._update_attrs_and_maybe_adapt_lights( @@ -392,6 +397,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return self._state = False self._remove_listeners() + self._reset_take_over_control() async def _async_update_at_interval(self, now=None) -> None: await self._update_attrs_and_maybe_adapt_lights(force=False) @@ -450,7 +456,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): LIGHT_DOMAIN, SERVICE_TURN_ON, service_data, - context=self._context, + context=self.__context, ) async def _update_attrs_and_maybe_adapt_lights( @@ -469,23 +475,32 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): lights = self._lights if (self._only_once and not force) or not lights: return - await self._adapt_lights(lights, transition) + await self._adapt_lights(lights, transition, force) async def _adapt_lights( - self, lights: List[str], transition: Optional[int] + self, lights: List[str], transition: Optional[int], force: bool ): _LOGGER.debug( - "%s: '_adapt_lights(%s, %s)' called", self.name, lights, transition + "%s: '_adapt_lights(%s, %s, %s)' called", + self.name, + lights, + transition, + force, ) for light in lights: if not is_on(self.hass, light): continue if self._take_over_control: - if await self.turn_on_off_listener.is_manually_adjusted( + if self.turn_on_off_listener.is_manually_controlled( light, - off_to_on_event=self._off_to_on_event.get(light), - adaptive_lighting_context=self._context, + force, + adaptive_lighting_context=self.__context, ): + _LOGGER.debug( + "%s: '%s' is being manually controlled, stop adapting.", + self._name, + light, + ) continue await self._adapt_light(light, transition) @@ -493,6 +508,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if not match_state_event(event, ("on", "off")): return _LOGGER.debug("%s: _sleep_state_event, event: '%s'", self._name, event) + self._reset_take_over_control() await self._update_attrs_and_maybe_adapt_lights( transition=self._initial_transition, force=True ) @@ -540,6 +556,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ): # Tracks 'off' → 'on' state changes self._on_to_off_event[entity_id] = event + self.turn_on_off_listener.manually_controlled[entity_id] = False class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity): @@ -752,6 +769,8 @@ class TurnOnOffListener: self.turn_on_event: Dict[str, Event] = {} # Keeps 'asyncio.sleep` tasks that can be cancelled by 'light.turn_on' events self.sleep_tasks: Dict[str, asyncio.Task] = {} + # Tracks which lights are manually controlled + self.manually_controlled: Dict[str, bool] = {} self.remove_listener = self.hass.bus.async_listen( EVENT_CALL_SERVICE, self.turn_on_off_event_listener @@ -782,6 +801,7 @@ class TurnOnOffListener: ) for eid in entity_ids: self.turn_off_event[eid] = event + self.manually_controlled[eid] = False elif service == SERVICE_TURN_ON: _LOGGER.debug("Detected an 'light.turn_on('%s')' event", entity_ids) @@ -791,12 +811,35 @@ class TurnOnOffListener: task.cancel() self.turn_on_event[eid] = event - async def is_manually_adjusted(self, light: str, off_to_on_event: Optional[Event], adaptive_lighting_context: Context): + def is_manually_controlled( + self, + light: str, + force: bool, + adaptive_lighting_context: Context, + ): """Check if the light has been 'on' and is now manually being adjusted.""" - if off_to_on_event is None: - # No state change has been registered before, so we can't tell. - return False - return False + manually_controlled = self.manually_controlled[light] + if manually_controlled: + # Manually controlled until light is turned on and off + return True + + turn_on_event = self.turn_on_event.get(light) + if ( + turn_on_event is not None + and adaptive_lighting_context.id != turn_on_event.context.id + and not force + ): + # Light was already on and 'light.turn_on' was not called by + # the adaptive_lighting integration. + manually_controlled = self.manually_controlled[light] = True + _LOGGER.debug( + "'%s' was already on and 'light.turn_on' was not called by the" + " adaptive_lighting integration, the Adaptive Lighting will stop" + " adapting the light until the switch or the light turns off and" + " then on again.", + light, + ) + return manually_controlled async def maybe_cancel_adjusting( self, entity_id: str, off_to_on_event: Event, on_to_off_event: Optional[Event] diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index e89e0b5a..f9a82c98 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -38,7 +38,7 @@ "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", "sunset_offset": "sunset_offset, in +/- seconds", "sunset_time": "sunset_time, in 'HH:MM:SS' format", - "take_over_control": "take_over_control, (NOT YET IMPLEMENTED!) if manually adjusting the lights when they are already on", + "take_over_control": "take_over_control, (beta feature) if manually adjusting the lights when they are already on", "transition": "transition, in seconds" } } From 7f6deaa7da92c1ee20a6df471b2503916f08d7ba Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 3 Oct 2020 17:36:26 +0200 Subject: [PATCH 0234/1077] bug fixes --- .../adaptive_lighting/__init__.py | 3 -- custom_components/adaptive_lighting/switch.py | 35 +++++++------------ 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 88f81dc8..b5ec5bc5 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -28,7 +28,6 @@ import logging import voluptuous as vol -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry import homeassistant.helpers.config_validation as cv @@ -98,8 +97,6 @@ async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool: ) data = hass.data[DOMAIN] data[config_entry.entry_id][UNDO_UPDATE_LISTENER]() - switch = data[config_entry.entry_id][SWITCH_DOMAIN] - switch._remove_listeners() # pylint: disable=protected-access if len(data) == 1: # no more config_entries data.pop(ATTR_TURN_ON_OFF_LISTENER).remove_listener() diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 358bb777..2e5820c3 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -52,7 +52,6 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.sun import get_astral_location -from homeassistant.util import slugify from homeassistant.util.color import ( color_RGB_to_xy, color_temperature_kelvin_to_mired, @@ -259,7 +258,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set other attributes self._icon = ICON - self._entity_id = f"switch.{DOMAIN}_{slugify(self._name)}" self._state = None # Tracks 'off' → 'on' state changes @@ -287,20 +285,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): data, ) - @property - def entity_id(self): - """Return the entity ID of the switch.""" - return self._entity_id - @property def name(self): """Return the name of the device if any.""" return f"Adaptive Lighting: {self._name}" - # @property - # def unique_id(self): - # """Return the unique ID of entity.""" - # return self._name + @property + def unique_id(self): + """Return the unique ID of entity.""" + return self._name @property def is_on(self) -> Optional[bool]: @@ -323,6 +316,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._state = False assert not self.remove_listeners + async def async_will_remove_from_hass(self): + """Remove the listeners upon removing the component.""" + self._remove_listeners() + def _expand_light_groups(self) -> None: all_lights = _expand_light_groups(self.hass, self._lights) self.turn_on_off_listener.lights.update(all_lights) @@ -568,23 +565,17 @@ class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity): data = validate(config_entry) self._name = data[CONF_NAME] self._icon = ICON - self._entity_id = f"switch.{DOMAIN}_sleep_mode_{slugify(self._name)}" self._state = None - @property - def entity_id(self): - """Return the entity ID of the switch.""" - return self._entity_id - @property def name(self): """Return the name of the device if any.""" return f"Adaptive Lighting Sleep Mode: {self._name}" - # @property - # def unique_id(self): - # """Return the unique ID of entity.""" - # return f"{self._name}_sleep_mode" + @property + def unique_id(self): + """Return the unique ID of entity.""" + return f"{self._name}_sleep_mode" @property def icon(self) -> str: @@ -818,7 +809,7 @@ class TurnOnOffListener: adaptive_lighting_context: Context, ): """Check if the light has been 'on' and is now manually being adjusted.""" - manually_controlled = self.manually_controlled[light] + manually_controlled = self.manually_controlled.setdefault(light, False) if manually_controlled: # Manually controlled until light is turned on and off return True From 60cc02af268c50003f17963b632583ec22b374ff Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 4 Oct 2020 14:26:42 +0200 Subject: [PATCH 0235/1077] detect significant changes --- custom_components/adaptive_lighting/switch.py | 152 ++++++++++++++---- 1 file changed, 123 insertions(+), 29 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 2e5820c3..aec93ff2 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -14,6 +14,7 @@ import astral import voluptuous as vol from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, @@ -126,6 +127,7 @@ async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): data[CONF_ADAPT_BRIGHTNESS], data[CONF_ADAPT_COLOR_TEMP], data[CONF_ADAPT_RGB_COLOR], + force=True, ) @@ -211,6 +213,11 @@ def _supported_features(hass, light: str): return {key for key, value in _SUPPORT_OPTS.items() if supported_features & value} +def abs_rel_diff(a, b): + """Absolute relative difference in %.""" + return abs((a - b) / b) * 100 + + class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" @@ -238,7 +245,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._only_once = data[CONF_ONLY_ONCE] self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR] self._take_over_control = data[CONF_TAKE_OVER_CONTROL] - self._transition = data[CONF_TRANSITION] + self._transition = min( + data[CONF_TRANSITION], self._interval.total_seconds() // 2 + ) self._sun_light_settings = SunLightSettings( name=self._name, @@ -270,7 +279,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self.__context = Context() # self._context will be overwritten # Set in self._update_attrs_and_maybe_adapt_lights - self._light_settings = {} + self._settings: Dict[str, Any] = {} # Set and unset tracker in async_turn_on and async_turn_off self.remove_listeners = [] @@ -364,12 +373,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): def device_state_attributes(self) -> Dict[str, Any]: """Return the attributes of the switch.""" if not self.is_on: - return {key: None for key in self._light_settings} - return self._light_settings + return {key: None for key in self._settings} + return self._settings def _reset_take_over_control(self): - for light in self._lights: - self.turn_on_off_listener.manually_controlled[light] = False + self.turn_on_off_listener.reset(*self._lights) async def async_turn_on( # pylint: disable=arguments-differ self, adapt_lights: bool = True @@ -406,6 +414,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): adapt_brightness: Optional[bool] = None, adapt_color_temp: Optional[bool] = None, adapt_rgb_color: Optional[bool] = None, + force: bool = False, ) -> None: lock = self._locks.get(light) if lock is not None and lock.locked(): @@ -428,7 +437,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_TRANSITION] = transition if "brightness" in features and adapt_brightness: - service_data[ATTR_BRIGHTNESS_PCT] = self._light_settings["brightness_pct"] + service_data[ATTR_BRIGHTNESS_PCT] = self._settings["brightness_pct"] if ( "color_temp" in features @@ -437,18 +446,29 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ): attributes = self.hass.states.get(light).attributes min_mireds, max_mireds = attributes["min_mireds"], attributes["max_mireds"] - color_temp_mired = self._light_settings["color_temp_mired"] + color_temp_mired = self._settings["color_temp_mired"] color_temp_mired = max(min(color_temp_mired, max_mireds), min_mireds) service_data[ATTR_COLOR_TEMP] = color_temp_mired elif "color" in features and adapt_rgb_color: - service_data[ATTR_RGB_COLOR] = self._light_settings["rgb_color"] + service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"] + if ( + self._take_over_control + and not force + and self.turn_on_off_listener.significant_change( + light, + self._adapt_brightness, + self._adapt_color_temp, + self._adapt_rgb_color, + ) + ): + return + self.turn_on_off_listener.last_service_data[light] = service_data _LOGGER.debug( "%s: Scheduling 'light.turn_on' with the following 'service_data': %s", self._name, service_data, ) - await self.hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -464,7 +484,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ): _LOGGER.debug("%s: '_update_attrs_and_maybe_adapt_lights' called", self._name) assert self.is_on - self._light_settings = self._sun_light_settings.get_settings( + self._settings = self._sun_light_settings.get_settings( self.sleep_mode_switch.is_on ) self.async_write_ha_state() @@ -487,19 +507,21 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): for light in lights: if not is_on(self.hass, light): continue - if self._take_over_control: - if self.turn_on_off_listener.is_manually_controlled( + if ( + self._take_over_control + and self.turn_on_off_listener.is_manually_controlled( light, force, adaptive_lighting_context=self.__context, - ): - _LOGGER.debug( - "%s: '%s' is being manually controlled, stop adapting.", - self._name, - light, - ) - continue - await self._adapt_light(light, transition) + ) + ): + _LOGGER.debug( + "%s: '%s' is being manually controlled, stop adapting.", + self._name, + light, + ) + continue + await self._adapt_light(light, transition, force=force) async def _sleep_state_event(self, event: Event): if not match_state_event(event, ("on", "off")): @@ -553,7 +575,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ): # Tracks 'off' → 'on' state changes self._on_to_off_event[entity_id] = event - self.turn_on_off_listener.manually_controlled[entity_id] = False + self.turn_on_off_listener.reset(entity_id) class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity): @@ -762,23 +784,28 @@ class TurnOnOffListener: self.sleep_tasks: Dict[str, asyncio.Task] = {} # Tracks which lights are manually controlled self.manually_controlled: Dict[str, bool] = {} + # Track which settings were applied to a light + self.last_service_data: Dict[str, Dict[str, Any]] = {} self.remove_listener = self.hass.bus.async_listen( EVENT_CALL_SERVICE, self.turn_on_off_event_listener ) + def reset(self, *lights): + """Reset the 'manually_controlled' status of the lights.""" + for light in lights: + self.manually_controlled[light] = False + self.last_service_data.pop(light, None) + async def turn_on_off_event_listener(self, event: Event): """Track 'light.turn_off' and 'light.turn_on' service calls.""" domain = event.data.get(ATTR_DOMAIN) if domain != LIGHT_DOMAIN: return - service = event.data.get(ATTR_SERVICE) - service_data = event.data.get(ATTR_SERVICE_DATA, {}) - - entity_ids = service_data.get(ATTR_ENTITY_ID) - if isinstance(entity_ids, str): - entity_ids = [entity_ids] + service = event.data[ATTR_SERVICE] + service_data = event.data[ATTR_SERVICE_DATA] + entity_ids = cv.ensure_list(service_data[ATTR_ENTITY_ID]) if not any(eid in self.lights for eid in entity_ids): return @@ -792,7 +819,7 @@ class TurnOnOffListener: ) for eid in entity_ids: self.turn_off_event[eid] = event - self.manually_controlled[eid] = False + self.reset(eid) elif service == SERVICE_TURN_ON: _LOGGER.debug("Detected an 'light.turn_on('%s')' event", entity_ids) @@ -832,6 +859,73 @@ class TurnOnOffListener: ) return manually_controlled + def significant_change( + self, light, adapt_brightness, adapt_color_temp, adapt_rgb_color, threshold=5 + ): + """Has the light made a significant change since last update. + + This method will detect changes that were made to the light without + calling 'light.turn_on', so outside of Home Assistant. If a change is + detected, we mark the light as 'manually_controlled' until the light + or switch is turned 'off' and 'on' again. + """ + if light not in self.last_service_data: + return False + changed = False + service_data = self.last_service_data[light] + attributes = self.hass.states.get(light).attributes + if ( + adapt_brightness + and ATTR_BRIGHTNESS_PCT in service_data + and ATTR_BRIGHTNESS in attributes + ): + applied_brightness = round(255 * service_data[ATTR_BRIGHTNESS_PCT] / 100) + current_brightness = attributes["brightness"] + if abs_rel_diff(current_brightness, applied_brightness) > threshold: + _LOGGER.debug("Brightness of '%s' significantly changed", light) + changed = True + + if ( + adapt_color_temp + and ATTR_COLOR_TEMP in service_data + and ATTR_COLOR_TEMP in attributes + ): + applied_color_temp = service_data[ATTR_COLOR_TEMP] + current_color_temp = attributes[ATTR_COLOR_TEMP] + if abs_rel_diff(current_color_temp, applied_color_temp) > threshold: + _LOGGER.debug( + "Color temperature of '%s' significantly changed", + light, + ) + changed = True + + if ( + adapt_rgb_color + and ATTR_RGB_COLOR in service_data + and ATTR_RGB_COLOR in attributes + ): + applied_rgb_color = service_data[ATTR_RGB_COLOR] + current_rgb_color = attributes[ATTR_RGB_COLOR] + for col_applied, col_current in zip(applied_rgb_color, current_rgb_color): + if abs_rel_diff(col_applied, col_current) > threshold: + _LOGGER.debug( + "color RGB of '%s' significantly changed", + light, + ) + changed = True + + if (ATTR_RGB_COLOR in service_data and ATTR_RGB_COLOR not in attributes) or ( + ATTR_COLOR_TEMP in service_data and ATTR_COLOR_TEMP not in attributes + ): + # Light switched from RGB mode to color_temp or visa versa + _LOGGER.debug( + "'%s' switched from RGB mode to color_temp or visa versa", + light, + ) + changed = True + self.manually_controlled[light] = changed + return changed + async def maybe_cancel_adjusting( self, entity_id: str, off_to_on_event: Event, on_to_off_event: Optional[Event] ) -> bool: From 168484462acfeb11306d6dd67e0dc21ff4b44d7e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 4 Oct 2020 15:09:58 +0200 Subject: [PATCH 0236/1077] add detect_non_ha_changes option --- custom_components/adaptive_lighting/const.py | 7 ++++++- .../adaptive_lighting/strings.json | 3 ++- custom_components/adaptive_lighting/switch.py | 18 ++++++++++++++++-- .../adaptive_lighting/translations/en.json | 3 ++- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 5b11e768..3d749503 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -15,6 +15,10 @@ CONF_LIGHTS, DEFAULT_LIGHTS = "lights", [] CONF_ADAPT_BRIGHTNESS, DEFAULT_ADAPT_BRIGHTNESS = "adapt_brightness", True CONF_ADAPT_COLOR_TEMP, DEFAULT_ADAPT_COLOR_TEMP = "adapt_color_temp", True CONF_ADAPT_RGB_COLOR, DEFAULT_ADAPT_RGB_COLOR = "adapt_rgb_color", True +CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = ( + "detect_non_ha_changes", + False, +) CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100 @@ -29,7 +33,7 @@ CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 CONF_SUNRISE_TIME = "sunrise_time" CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 CONF_SUNSET_TIME = "sunset_time" -CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", False +CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener" @@ -52,6 +56,7 @@ VALIDATION_TUPLES = [ (CONF_ADAPT_BRIGHTNESS, DEFAULT_ADAPT_BRIGHTNESS, bool), (CONF_ADAPT_COLOR_TEMP, DEFAULT_ADAPT_COLOR_TEMP, bool), (CONF_ADAPT_RGB_COLOR, DEFAULT_ADAPT_RGB_COLOR, bool), + (CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool), (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), (CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)), diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index f9a82c98..d0ad5ce9 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -38,7 +38,8 @@ "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", "sunset_offset": "sunset_offset, in +/- seconds", "sunset_time": "sunset_time, in 'HH:MM:SS' format", - "take_over_control": "take_over_control, (beta feature) if manually adjusting the lights when they are already on", + "take_over_control": "take_over_control, if manually adjusting the lights when they are already on", + "detect_non_ha_changes": "detect_non_ha_changes, detect changes to lights made outside of HA (calls 'homeassistant.update_entity'!)", "transition": "transition, in seconds" } } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index aec93ff2..6867a59e 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -13,6 +13,10 @@ from typing import Any, Dict, List, Optional, Tuple, Union import astral import voluptuous as vol +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, @@ -66,6 +70,7 @@ from .const import ( CONF_ADAPT_BRIGHTNESS, CONF_ADAPT_COLOR_TEMP, CONF_ADAPT_RGB_COLOR, + CONF_DETECT_NON_HA_CHANGES, CONF_INITIAL_TRANSITION, CONF_INTERVAL, CONF_LIGHTS, @@ -240,6 +245,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._adapt_brightness = data[CONF_ADAPT_BRIGHTNESS] self._adapt_color_temp = data[CONF_ADAPT_COLOR_TEMP] self._adapt_rgb_color = data[CONF_ADAPT_RGB_COLOR] + self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] self._initial_transition = data[CONF_INITIAL_TRANSITION] self._interval = data[CONF_INTERVAL] self._only_once = data[CONF_ONLY_ONCE] @@ -454,8 +460,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if ( self._take_over_control + and self._detect_non_ha_changes and not force - and self.turn_on_off_listener.significant_change( + and await self.turn_on_off_listener.significant_change( light, self._adapt_brightness, self._adapt_color_temp, @@ -859,7 +866,7 @@ class TurnOnOffListener: ) return manually_controlled - def significant_change( + async def significant_change( self, light, adapt_brightness, adapt_color_temp, adapt_rgb_color, threshold=5 ): """Has the light made a significant change since last update. @@ -873,6 +880,12 @@ class TurnOnOffListener: return False changed = False service_data = self.last_service_data[light] + await self.hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: light}, + blocking=True, + ) attributes = self.hass.states.get(light).attributes if ( adapt_brightness @@ -913,6 +926,7 @@ class TurnOnOffListener: light, ) changed = True + break if (ATTR_RGB_COLOR in service_data and ATTR_RGB_COLOR not in attributes) or ( ATTR_COLOR_TEMP in service_data and ATTR_COLOR_TEMP not in attributes diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index f9a82c98..d0ad5ce9 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -38,7 +38,8 @@ "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", "sunset_offset": "sunset_offset, in +/- seconds", "sunset_time": "sunset_time, in 'HH:MM:SS' format", - "take_over_control": "take_over_control, (beta feature) if manually adjusting the lights when they are already on", + "take_over_control": "take_over_control, if manually adjusting the lights when they are already on", + "detect_non_ha_changes": "detect_non_ha_changes, detect changes to lights made outside of HA (calls 'homeassistant.update_entity'!)", "transition": "transition, in seconds" } } From e81d25983ff9839616d54106570be478ef6b81ef Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 4 Oct 2020 15:12:14 +0200 Subject: [PATCH 0237/1077] change opt order --- custom_components/adaptive_lighting/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 3d749503..e438321f 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -56,8 +56,8 @@ VALIDATION_TUPLES = [ (CONF_ADAPT_BRIGHTNESS, DEFAULT_ADAPT_BRIGHTNESS, bool), (CONF_ADAPT_COLOR_TEMP, DEFAULT_ADAPT_COLOR_TEMP, bool), (CONF_ADAPT_RGB_COLOR, DEFAULT_ADAPT_RGB_COLOR, bool), - (CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool), (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), + (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), (CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)), (CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP, int_between(1000, 10000)), @@ -72,7 +72,7 @@ VALIDATION_TUPLES = [ (CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int), (CONF_SUNSET_TIME, NONE_STR, str), (CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool), - (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), + (CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool), ] From c245298512bd93194767071886fba4122357be54 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 4 Oct 2020 23:15:12 +0200 Subject: [PATCH 0238/1077] sync PR --- .../adaptive_lighting/config_flow.py | 11 +++-- .../adaptive_lighting/strings.json | 4 +- custom_components/adaptive_lighting/switch.py | 48 +++++++++++++------ .../adaptive_lighting/translations/en.json | 4 +- 4 files changed, 44 insertions(+), 23 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index bb75cc07..575d7cd7 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -14,6 +14,7 @@ from .const import ( # pylint: disable=unused-import NONE_STR, VALIDATION_TUPLES, ) +from .switch import _supported_features _LOGGER = logging.getLogger(__name__) @@ -89,10 +90,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if not errors: return self.async_create_entry(title="", data=user_input) - all_lights = sorted(self.hass.states.async_entity_ids("light")) - to_replace = { - CONF_LIGHTS: cv.multi_select(all_lights), - } + all_lights = [ + light + for light in self.hass.states.async_entity_ids("light") + if _supported_features(self.hass, light) + ] + to_replace = {CONF_LIGHTS: cv.multi_select(sorted(all_lights))} options_schema = {} for name, default, validation in VALIDATION_TUPLES: diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index d0ad5ce9..d08cbc19 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -38,8 +38,8 @@ "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", "sunset_offset": "sunset_offset, in +/- seconds", "sunset_time": "sunset_time, in 'HH:MM:SS' format", - "take_over_control": "take_over_control, if manually adjusting the lights when they are already on", - "detect_non_ha_changes": "detect_non_ha_changes, detect changes to lights made outside of HA (calls 'homeassistant.update_entity'!)", + "take_over_control": "take_over_control, if anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", + "detect_non_ha_changes": "detect_non_ha_changes, detects all >5% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", "transition": "transition, in seconds" } } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 6867a59e..0ce86683 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -218,9 +218,12 @@ def _supported_features(hass, light: str): return {key for key, value in _SUPPORT_OPTS.items() if supported_features & value} -def abs_rel_diff(a, b): +def abs_rel_diff(val_a, val_b): """Absolute relative difference in %.""" - return abs((a - b) / b) * 100 + if val_b == 0: + # To avoid ZeroDivisionError + val_b = 1e-6 + return abs((val_a - val_b) / val_b) * 100 class AdaptiveSwitch(SwitchEntity, RestoreEntity): @@ -382,9 +385,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return {key: None for key in self._settings} return self._settings - def _reset_take_over_control(self): - self.turn_on_off_listener.reset(*self._lights) - async def async_turn_on( # pylint: disable=arguments-differ self, adapt_lights: bool = True ) -> None: @@ -395,7 +395,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self.is_on: return self._state = True - self._reset_take_over_control() + self.turn_on_off_listener.reset(*self._lights) await self._setup_listeners() if adapt_lights: await self._update_attrs_and_maybe_adapt_lights( @@ -408,7 +408,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return self._state = False self._remove_listeners() - self._reset_take_over_control() + self.turn_on_off_listener.reset(*self._lights) async def _async_update_at_interval(self, now=None) -> None: await self._update_attrs_and_maybe_adapt_lights(force=False) @@ -467,6 +467,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._adapt_brightness, self._adapt_color_temp, self._adapt_rgb_color, + self.__context, ) ): return @@ -534,7 +535,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if not match_state_event(event, ("on", "off")): return _LOGGER.debug("%s: _sleep_state_event, event: '%s'", self._name, event) - self._reset_take_over_control() + self.turn_on_off_listener.reset(*self._lights) await self._update_attrs_and_maybe_adapt_lights( transition=self._initial_transition, force=True ) @@ -552,6 +553,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug( "%s: Detected an 'off' → 'on' event for '%s'", self._name, entity_id ) + self.turn_on_off_listener.reset(entity_id) # Tracks 'off' → 'on' state changes self._off_to_on_event[entity_id] = event lock = self._locks.get(entity_id) @@ -867,7 +869,13 @@ class TurnOnOffListener: return manually_controlled async def significant_change( - self, light, adapt_brightness, adapt_color_temp, adapt_rgb_color, threshold=5 + self, + light, + adapt_brightness, + adapt_color_temp, + adapt_rgb_color, + context, + threshold=5, ): """Has the light made a significant change since last update. @@ -885,6 +893,7 @@ class TurnOnOffListener: SERVICE_UPDATE_ENTITY, {ATTR_ENTITY_ID: light}, blocking=True, + context=context, ) attributes = self.hass.states.get(light).attributes if ( @@ -895,7 +904,12 @@ class TurnOnOffListener: applied_brightness = round(255 * service_data[ATTR_BRIGHTNESS_PCT] / 100) current_brightness = attributes["brightness"] if abs_rel_diff(current_brightness, applied_brightness) > threshold: - _LOGGER.debug("Brightness of '%s' significantly changed", light) + _LOGGER.debug( + "Brightness of '%s' significantly changed from %s to %s", + light, + applied_brightness, + current_brightness, + ) changed = True if ( @@ -907,8 +921,10 @@ class TurnOnOffListener: current_color_temp = attributes[ATTR_COLOR_TEMP] if abs_rel_diff(current_color_temp, applied_color_temp) > threshold: _LOGGER.debug( - "Color temperature of '%s' significantly changed", + "Color temperature of '%s' significantly changed from %s to %s", light, + applied_color_temp, + current_color_temp, ) changed = True @@ -922,8 +938,10 @@ class TurnOnOffListener: for col_applied, col_current in zip(applied_rgb_color, current_rgb_color): if abs_rel_diff(col_applied, col_current) > threshold: _LOGGER.debug( - "color RGB of '%s' significantly changed", + "color RGB of '%s' significantly changed from %s to %s", light, + applied_rgb_color, + current_rgb_color, ) changed = True break @@ -962,8 +980,7 @@ class TurnOnOffListener: id_on_to_off = on_to_off_event.context.id turn_off_event = self.turn_off_event.get(entity_id) - id_turn_off = turn_off_event.context.id - transition = turn_off_event.data[ATTR_SERVICE_DATA].get(ATTR_TRANSITION) + transition = turn_off_event.data.get(ATTR_SERVICE_DATA, {}).get(ATTR_TRANSITION) turn_on_event = self.turn_on_event.get(entity_id) id_turn_on = turn_on_event.context.id @@ -975,7 +992,8 @@ class TurnOnOffListener: return False if ( - id_on_to_off == id_turn_off + turn_off_event is not None + and id_on_to_off == turn_off_event.context.id and id_on_to_off is not None and transition is not None # 'turn_off' is called with transition=... ): diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index d0ad5ce9..d08cbc19 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -38,8 +38,8 @@ "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", "sunset_offset": "sunset_offset, in +/- seconds", "sunset_time": "sunset_time, in 'HH:MM:SS' format", - "take_over_control": "take_over_control, if manually adjusting the lights when they are already on", - "detect_non_ha_changes": "detect_non_ha_changes, detect changes to lights made outside of HA (calls 'homeassistant.update_entity'!)", + "take_over_control": "take_over_control, if anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", + "detect_non_ha_changes": "detect_non_ha_changes, detects all >5% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", "transition": "transition, in seconds" } } From 51340c6d9775e2a75ad05c3acb2335a3daa3fe8f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 4 Oct 2020 23:55:56 +0200 Subject: [PATCH 0239/1077] add basic instructions --- README.md | 59 +++++-------------------------------------------------- 1 file changed, 5 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index dbba559b..ca434b95 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,14 @@ -# Circadian Lighting [[Home Assistant](https://www.home-assistant.io/) Component] -## Stay healthier and sleep better by syncing your lights with natural daylight to maintain your circadian rhythm! +# Adaptive Lighting component -![Circadian Light Rhythm|690x287](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/f/5fe7a780e9f8905fea4d1cbb66cdbe35858a6e36.jpg) +Try out this code by adding https://github.com/basnijholt/adaptive-lighting to your custom repos in HACS and install it! -Circadian Lighting slowly synchronizes your color changing lights with the regular naturally occurring color temperature of the sky throughout the day. This gives your environment a more natural feel, with cooler hues during the midday and warmer tints near twilight and dawn. +I have not written any docs yet, so I recommend to use the UI to add this integration. -In addition, Circadian Lighting can set your lights to a nice cool white at 1% in “Sleep” mode, which is far brighter than starlight but won’t reset your circadian rhythm or break down too much rhodopsin in your eyes. +See [this video on Reddit](https://www.reddit.com/r/homeassistant/comments/j09219/any_circadian_lighting_users_good_news_i_just/) to see how to add the integration and set the options. -
Expand for articles explaining the benefits of maintaining a natural Circadian rhythm - -* [Circadian Rhythms - National Institute of General Medical Sciences](https://www.nigms.nih.gov/Education/Pages/Factsheet_CircadianRhythms.aspx) -* [Circadian Rhythms Linked to Aging and Well-Being | Psychology Today](https://www.psychologytoday.com/us/blog/the-athletes-way/201306/circadian-rhythms-linked-aging-and-well-being) -* [Maintaining a daily rhythm is important for mental health, study suggests - CNN](https://www.cnn.com/2018/05/15/health/circadian-rhythm-mood-disorder-study/index.html) -* [How Nobel Winning Circadian Rhythm Research Benefits Pregnancy](https://www.healthypregnancy.com/how-nobel-prize-winning-circadian-rhythms-research-benefits-a-healthy-pregnancy/) -* [Body Clock & Sleep - National Sleep Foundation](https://sleepfoundation.org/sleep-topics/sleep-drive-and-your-body-clock) -* [How our body’s circadian clocks affect our health beyond sleep](https://www.theverge.com/2018/6/12/17453398/sleep-circadian-code-satchin-panda-clock-health-science) - -
- -### Visit the [Wiki](https://github.com/claytonjn/hass-circadian_lighting/wiki) for more information. -
- -## Basic Installation/Configuration Instructions: - -#### Installation: -Install `custom_component` files automatically using [HACS](https://github.com/claytonjn/hass-circadian_lighting/wiki/Installation-Instructions#hacs) or [Custom Updater](https://github.com/claytonjn/hass-circadian_lighting/wiki/Installation-Instructions#custom-updater), or install [Manually](https://github.com/claytonjn/hass-circadian_lighting/wiki/Installation-Instructions#manual-installation). - -[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs) - -#### Component Configuration: -```yaml -# Example configuration.yaml entry -circadian_lighting: -``` -[_Advanced Configuration_](https://github.com/claytonjn/hass-circadian_lighting/wiki/Advanced-Configuration#component-configuration-variables) - -#### Switch Configuration: -```yaml -# Example configuration.yaml entry -switch: - - platform: circadian_lighting - lights_ct: - - light.desk - - light.lamp -``` -Switch configuration variables: -* **name** (_Optional_): The name to use when displaying this switch. -* **lights_ct** (_Optional_): array: List of light entities which should be set in mireds. -* **lights_rgb** (_Optional_): array: List of light entities which should be set in RGB. -* **lights_xy** (_Optional_): array: List of light entities which should be set in XY. -* **lights_brightness** (_Optional_): array: List of light entities which should only have brightness adjusted. - -[_Advanced Configuration_](https://github.com/claytonjn/hass-circadian_lighting/wiki/Advanced-Configuration#switch-configuration-variables) - -
- ### Graphs! -These graphs were generated using the values calculated by the Circadian Lighting sensor/switch(es). +These graphs were generated using the values calculated by the Adaptive Lighting sensor/switch(es). ##### Sun Position: ![cl_percent|690x131](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/6/5/657ff98beb65a94598edeb4bdfd939095db1a22c.PNG) From 773a955b858656ca031c9e354bf0d40e5bec22ff Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 5 Oct 2020 09:37:57 +0200 Subject: [PATCH 0240/1077] bug fix --- custom_components/adaptive_lighting/switch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 0ce86683..4fa0627f 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -980,7 +980,10 @@ class TurnOnOffListener: id_on_to_off = on_to_off_event.context.id turn_off_event = self.turn_off_event.get(entity_id) - transition = turn_off_event.data.get(ATTR_SERVICE_DATA, {}).get(ATTR_TRANSITION) + if turn_off_event is not None: + transition = turn_off_event.data[ATTR_SERVICE_DATA].get(ATTR_TRANSITION) + else: + transition = None turn_on_event = self.turn_on_event.get(entity_id) id_turn_on = turn_on_event.context.id From 91735ebd58e41670c88a77a4ef04a610438b552c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 5 Oct 2020 12:17:06 +0200 Subject: [PATCH 0241/1077] register service after async_add_entities --- custom_components/adaptive_lighting/switch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 4fa0627f..9272413b 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -150,6 +150,8 @@ async def async_setup_entry(hass, config_entry: ConfigEntry, async_add_entities: data[config_entry.entry_id]["sleep_mode_switch"] = sleep_mode_switch data[config_entry.entry_id][SWITCH_DOMAIN] = switch + async_add_entities([switch, sleep_mode_switch], update_before_add=True) + # Register `apply` service platform = entity_platform.current_platform.get() platform.async_register_entity_service( @@ -167,7 +169,6 @@ async def async_setup_entry(hass, config_entry: ConfigEntry, async_add_entities: }, handle_apply, ) - async_add_entities([switch, sleep_mode_switch], update_before_add=True) def validate(config_entry): From 6c6fe59e6f3bf82dee6324444b865f443cc5cb69 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 5 Oct 2020 14:41:21 +0200 Subject: [PATCH 0242/1077] change order --- custom_components/adaptive_lighting/const.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index e438321f..d3d7ebed 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -24,7 +24,7 @@ CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100 CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP = "max_color_temp", 5500 CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS = "min_brightness", 1 -CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP = "min_color_temp", 2500 +CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP = "min_color_temp", 2000 CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE = "only_once", False CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1 @@ -34,7 +34,7 @@ CONF_SUNRISE_TIME = "sunrise_time" CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 CONF_SUNSET_TIME = "sunset_time" CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True -CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 60 +CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 45 ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener" UNDO_UPDATE_LISTENER = "undo_update_listener" @@ -59,18 +59,18 @@ VALIDATION_TUPLES = [ (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), - (CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)), - (CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP, int_between(1000, 10000)), (CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS, int_between(1, 100)), + (CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)), (CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP, int_between(1000, 10000)), - (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), + (CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP, int_between(1000, 10000)), (CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool), (CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)), (CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP, int_between(1000, 10000)), - (CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int), (CONF_SUNRISE_TIME, NONE_STR, str), - (CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int), + (CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int), (CONF_SUNSET_TIME, NONE_STR, str), + (CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int), + (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), (CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool), (CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool), ] From f1c210114810f5b0a8472e9fd7395c71f06fda55 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 5 Oct 2020 18:39:15 +0200 Subject: [PATCH 0243/1077] add 'manually_controlled' attribute to switch --- custom_components/adaptive_lighting/switch.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 9272413b..69d92b47 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -384,7 +384,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Return the attributes of the switch.""" if not self.is_on: return {key: None for key in self._settings} - return self._settings + manually_controlled = [ + light + for light in self._lights + if self.turn_on_off_listener.manually_controlled.get(light) + ] + return dict(self._settings, manually_controlled=manually_controlled) async def async_turn_on( # pylint: disable=arguments-differ self, adapt_lights: bool = True From 313c1addf48fd3b25a888e762b1c2d16a2e309db Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 5 Oct 2020 23:41:57 +0200 Subject: [PATCH 0244/1077] track state_changed --- .../adaptive_lighting/__init__.py | 15 ++- .../adaptive_lighting/config_flow.py | 11 +- .../adaptive_lighting/strings.json | 4 +- custom_components/adaptive_lighting/switch.py | 103 ++++++++++++------ .../adaptive_lighting/translations/en.json | 4 +- 5 files changed, 88 insertions(+), 49 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index b5ec5bc5..be8868f5 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -25,10 +25,13 @@ Resources: lights to 2700K (warm white) until your hub goes into "Sleep mode". """ import logging +from typing import Any, Dict import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_SOURCE +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from .const import ( @@ -45,7 +48,7 @@ PLATFORMS = ["switch"] def _all_unique_names(value): - """Validate that all enties have a unique profile name.""" + """Validate that all entities have a unique profile name.""" hosts = [device[CONF_NAME] for device in value] schema = vol.Schema(vol.Unique()) schema(hosts) @@ -58,20 +61,20 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: Dict[str, Any]): """Import integration from config.""" if DOMAIN in config: for entry in config[DOMAIN]: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=entry + DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=entry ) ) return True -async def async_setup_entry(hass, config_entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Set up the component.""" data = hass.data.setdefault(DOMAIN, {}) @@ -98,7 +101,9 @@ async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool: data = hass.data[DOMAIN] data[config_entry.entry_id][UNDO_UPDATE_LISTENER]() if len(data) == 1: # no more config_entries - data.pop(ATTR_TURN_ON_OFF_LISTENER).remove_listener() + turn_on_off_listener = data.pop(ATTR_TURN_ON_OFF_LISTENER) + turn_on_off_listener.remove_listener() + turn_on_off_listener.remove_listener2() if unload_ok: data.pop(config_entry.entry_id) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 575d7cd7..6bf5e430 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -4,6 +4,7 @@ import logging import voluptuous as vol from homeassistant import config_entries +from homeassistant.const import CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -29,24 +30,24 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - await self.async_set_unique_id(user_input["name"]) + await self.async_set_unique_id(user_input[CONF_NAME]) self._abort_if_unique_id_configured() - return self.async_create_entry(title=user_input["name"], data=user_input) + return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) return self.async_show_form( step_id="user", - data_schema=vol.Schema({vol.Required("name"): str}), + data_schema=vol.Schema({vol.Required(CONF_NAME): str}), errors=errors, ) async def async_step_import(self, user_input=None): """Handle configuration by yaml file.""" - await self.async_set_unique_id(user_input["name"]) + await self.async_set_unique_id(user_input[CONF_NAME]) for entry in self._async_current_entries(): if entry.unique_id == self.unique_id: self.hass.config_entries.async_update_entry(entry, data=user_input) self._abort_if_unique_id_configured() - return self.async_create_entry(title=user_input["name"], data=user_input) + return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) @staticmethod @callback diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index d08cbc19..57949ea0 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -6,12 +6,12 @@ "title": "Choose a name for the Adaptive Lighting", "description": "Every instance can contain multiple lights!", "data": { - "name": "Name" + "name": "[%key:common::config_flow::data::name%]" } } }, "abort": { - "already_configured": "This name is already configured." + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, "options": { diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 69d92b47..85fed2af 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -41,6 +41,7 @@ from homeassistant.const import ( CONF_NAME, EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_START, + EVENT_STATE_CHANGED, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, @@ -48,7 +49,7 @@ from homeassistant.const import ( SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) -from homeassistant.core import Context, Event, ServiceCall +from homeassistant.core import Context, Event, HomeAssistant, ServiceCall, State from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( @@ -114,6 +115,11 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=10) +# Consider it a significant change when attribute changes more than +BRIGHTNESS_CHANGE = 25 # ≈10% of total range +COLOR_TEMP_CHANGE = 250 # ≈5% of total range +RGB_CHANGE = 30 # ≈12% of total range per component + async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): """Handle the entity service apply.""" @@ -136,7 +142,9 @@ async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): ) -async def async_setup_entry(hass, config_entry: ConfigEntry, async_add_entities: bool): +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: bool +): """Set up the AdaptiveLighting switch.""" data = hass.data[DOMAIN] @@ -197,7 +205,7 @@ def match_state_event(event: Event, from_or_to_state: List[str]): return match -def _expand_light_groups(hass, lights: List[str]) -> List[str]: +def _expand_light_groups(hass: HomeAssistant, lights: List[str]) -> List[str]: all_lights = set() for light in lights: state = hass.states.get(light) @@ -213,13 +221,13 @@ def _expand_light_groups(hass, lights: List[str]) -> List[str]: return list(all_lights) -def _supported_features(hass, light: str): +def _supported_features(hass: HomeAssistant, light: str): state = hass.states.get(light) supported_features = state.attributes["supported_features"] return {key for key, value in _SUPPORT_OPTS.items() if supported_features & value} -def abs_rel_diff(val_a, val_b): +def abs_rel_diff(val_a, val_b) -> float: """Absolute relative difference in %.""" if val_b == 0: # To avoid ZeroDivisionError @@ -287,6 +295,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._locks: Dict[str, asyncio.Lock] = {} # To identify that this integration made a change self.__context = Context() # self._context will be overwritten + self.turn_on_off_listener.contexts.add(self.__context) # Set in self._update_attrs_and_maybe_adapt_lights self._settings: Dict[str, Any] = {} @@ -296,12 +305,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug( "%s: Setting up with '%s'," " config_entry.data: '%s'," - " config_entry.options: '%s', converted to '%s'.", + " config_entry.options: '%s', converted to '%s'," + " with context '%s'.", self._name, self._lights, config_entry.data, config_entry.options, data, + self.__context, ) @property @@ -477,7 +488,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) ): return - self.turn_on_off_listener.last_service_data[light] = service_data _LOGGER.debug( "%s: Scheduling 'light.turn_on' with the following 'service_data': %s", self._name, @@ -523,6 +533,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): continue if ( self._take_over_control + and False # XXX: REMOVE THIS and self.turn_on_off_listener.is_manually_controlled( light, force, @@ -538,7 +549,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): await self._adapt_light(light, transition, force=force) async def _sleep_state_event(self, event: Event): - if not match_state_event(event, ("on", "off")): + if not match_state_event(event, (STATE_ON, STATE_OFF)): return _LOGGER.debug("%s: _sleep_state_event, event: '%s'", self._name, event) self.turn_on_off_listener.reset(*self._lights) @@ -552,9 +563,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): entity_id = event.data.get("entity_id") if ( old_state is not None - and old_state.state == "off" + and old_state.state == STATE_OFF and new_state is not None - and new_state.state == "on" + and new_state.state == STATE_ON ): _LOGGER.debug( "%s: Detected an 'off' → 'on' event for '%s'", self._name, entity_id @@ -584,9 +595,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) elif ( old_state is not None - and old_state.state == "on" + and old_state.state == STATE_ON and new_state is not None - and new_state.state == "off" + and new_state.state == STATE_OFF ): # Tracks 'off' → 'on' state changes self._on_to_off_event[entity_id] = event @@ -596,7 +607,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" - def __init__(self, hass, config_entry): + def __init__(self, hass: HomeAssistant, config_entry): """Initialize the Adaptive Lighting switch.""" self.hass = hass data = validate(config_entry) @@ -790,6 +801,7 @@ class TurnOnOffListener: """Initialize the TurnOnOffListener that is shared among all switches.""" self.hass = hass self.lights = set() + self.contexts = set() # contexts of different AdaptiveSwitch instances # Tracks 'light.turn_off' service calls self.turn_off_event: Dict[str, Event] = {} @@ -799,18 +811,21 @@ class TurnOnOffListener: self.sleep_tasks: Dict[str, asyncio.Task] = {} # Tracks which lights are manually controlled self.manually_controlled: Dict[str, bool] = {} - # Track which settings were applied to a light - self.last_service_data: Dict[str, Dict[str, Any]] = {} + # Track 'state_changed' events of self.lights resulting from this integration + self.last_state_change: Dict[str, State] = {} self.remove_listener = self.hass.bus.async_listen( EVENT_CALL_SERVICE, self.turn_on_off_event_listener ) + self.remove_listener2 = self.hass.bus.async_listen( + EVENT_STATE_CHANGED, self.state_changed_event_listener + ) def reset(self, *lights): """Reset the 'manually_controlled' status of the lights.""" for light in lights: self.manually_controlled[light] = False - self.last_service_data.pop(light, None) + self.last_state_change.pop(light, None) async def turn_on_off_event_listener(self, event: Event): """Track 'light.turn_off' and 'light.turn_on' service calls.""" @@ -844,6 +859,25 @@ class TurnOnOffListener: task.cancel() self.turn_on_event[eid] = event + async def state_changed_event_listener(self, event: Event): + """Track 'state_changed' events.""" + entity_id = event.data.get(ATTR_ENTITY_ID, "") + if entity_id not in self.lights and entity_id.split(".")[0] != LIGHT_DOMAIN: + return + + new_state = event.data.get("new_state") + if ( + new_state is not None + and new_state.state == STATE_ON + and new_state.context in self.contexts + ): + _LOGGER.debug( + "Detected a '%s' 'state_changed' event: '%s'", + entity_id, + new_state.attributes, + ) + self.last_state_change[entity_id] = new_state + def is_manually_controlled( self, light: str, @@ -881,7 +915,6 @@ class TurnOnOffListener: adapt_color_temp, adapt_rgb_color, context, - threshold=5, ): """Has the light made a significant change since last update. @@ -890,10 +923,10 @@ class TurnOnOffListener: detected, we mark the light as 'manually_controlled' until the light or switch is turned 'off' and 'on' again. """ - if light not in self.last_service_data: + if light not in self.last_state_change: return False changed = False - service_data = self.last_service_data[light] + old_attributes = self.last_state_change[light].attributes await self.hass.services.async_call( HA_DOMAIN, SERVICE_UPDATE_ENTITY, @@ -904,56 +937,56 @@ class TurnOnOffListener: attributes = self.hass.states.get(light).attributes if ( adapt_brightness - and ATTR_BRIGHTNESS_PCT in service_data + and ATTR_BRIGHTNESS in old_attributes and ATTR_BRIGHTNESS in attributes ): - applied_brightness = round(255 * service_data[ATTR_BRIGHTNESS_PCT] / 100) - current_brightness = attributes["brightness"] - if abs_rel_diff(current_brightness, applied_brightness) > threshold: + last_brightness = old_attributes[ATTR_BRIGHTNESS] + current_brightness = attributes[ATTR_BRIGHTNESS] + if abs(current_brightness - last_brightness) > BRIGHTNESS_CHANGE: _LOGGER.debug( "Brightness of '%s' significantly changed from %s to %s", light, - applied_brightness, + last_brightness, current_brightness, ) changed = True if ( adapt_color_temp - and ATTR_COLOR_TEMP in service_data + and ATTR_COLOR_TEMP in old_attributes and ATTR_COLOR_TEMP in attributes ): - applied_color_temp = service_data[ATTR_COLOR_TEMP] + last_color_temp = old_attributes[ATTR_COLOR_TEMP] current_color_temp = attributes[ATTR_COLOR_TEMP] - if abs_rel_diff(current_color_temp, applied_color_temp) > threshold: + if abs(current_color_temp - last_color_temp) > COLOR_TEMP_CHANGE: _LOGGER.debug( "Color temperature of '%s' significantly changed from %s to %s", light, - applied_color_temp, + last_color_temp, current_color_temp, ) changed = True if ( adapt_rgb_color - and ATTR_RGB_COLOR in service_data + and ATTR_RGB_COLOR in old_attributes and ATTR_RGB_COLOR in attributes ): - applied_rgb_color = service_data[ATTR_RGB_COLOR] + last_rgb_color = old_attributes[ATTR_RGB_COLOR] current_rgb_color = attributes[ATTR_RGB_COLOR] - for col_applied, col_current in zip(applied_rgb_color, current_rgb_color): - if abs_rel_diff(col_applied, col_current) > threshold: + for last_col, current_col in zip(last_rgb_color, current_rgb_color): + if abs(last_col - current_col) > RGB_CHANGE: _LOGGER.debug( "color RGB of '%s' significantly changed from %s to %s", light, - applied_rgb_color, + last_rgb_color, current_rgb_color, ) changed = True break - if (ATTR_RGB_COLOR in service_data and ATTR_RGB_COLOR not in attributes) or ( - ATTR_COLOR_TEMP in service_data and ATTR_COLOR_TEMP not in attributes + if (ATTR_RGB_COLOR in old_attributes and ATTR_RGB_COLOR not in attributes) or ( + ATTR_COLOR_TEMP in old_attributes and ATTR_COLOR_TEMP not in attributes ): # Light switched from RGB mode to color_temp or visa versa _LOGGER.debug( diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index d08cbc19..57949ea0 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -6,12 +6,12 @@ "title": "Choose a name for the Adaptive Lighting", "description": "Every instance can contain multiple lights!", "data": { - "name": "Name" + "name": "[%key:common::config_flow::data::name%]" } } }, "abort": { - "already_configured": "This name is already configured." + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, "options": { From ba4411327797db9faf7dffc1467a3a7a00f8b651 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 6 Oct 2020 14:14:19 +0200 Subject: [PATCH 0245/1077] fixes --- custom_components/adaptive_lighting/switch.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 85fed2af..3f4bec69 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -40,7 +40,7 @@ from homeassistant.const import ( ATTR_SERVICE_DATA, CONF_NAME, EVENT_CALL_SERVICE, - EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, EVENT_STATE_CHANGED, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -117,7 +117,7 @@ SCAN_INTERVAL = timedelta(seconds=10) # Consider it a significant change when attribute changes more than BRIGHTNESS_CHANGE = 25 # ≈10% of total range -COLOR_TEMP_CHANGE = 250 # ≈5% of total range +COLOR_TEMP_CHANGE = 20 # ≈5% of total range RGB_CHANGE = 30 # ≈12% of total range per component @@ -227,14 +227,6 @@ def _supported_features(hass: HomeAssistant, light: str): return {key for key, value in _SUPPORT_OPTS.items() if supported_features & value} -def abs_rel_diff(val_a, val_b) -> float: - """Absolute relative difference in %.""" - if val_b == 0: - # To avoid ZeroDivisionError - val_b = 1e-6 - return abs((val_a - val_b) / val_b) * 100 - - class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" @@ -294,7 +286,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Locks that prevent light adjusting when waiting for a light to 'turn_off' self._locks: Dict[str, asyncio.Lock] = {} # To identify that this integration made a change - self.__context = Context() # self._context will be overwritten + self.__context = Context() # self._context would be overwritten self.turn_on_off_listener.contexts.add(self.__context) # Set in self._update_attrs_and_maybe_adapt_lights @@ -336,7 +328,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): await self._setup_listeners() else: self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, self._setup_listeners + EVENT_HOMEASSISTANT_STARTED, self._setup_listeners ) last_state = await self.async_get_last_state() is_new_entry = last_state is None # newly added to HA From 31176f8897d50229508d64ba66e9f95de214e0b6 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 7 Oct 2020 16:54:07 +0200 Subject: [PATCH 0246/1077] keep contexts around that identify where they come from --- custom_components/adaptive_lighting/switch.py | 362 +++++++++++++----- 1 file changed, 261 insertions(+), 101 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 3f4bec69..ffaf050b 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -7,6 +7,7 @@ from copy import deepcopy from dataclasses import dataclass import datetime from datetime import timedelta +import hashlib import logging from typing import Any, Dict, List, Optional, Tuple, Union @@ -20,9 +21,17 @@ from homeassistant.components.homeassistant import ( from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, + ATTR_BRIGHTNESS_STEP, + ATTR_BRIGHTNESS_STEP_PCT, + ATTR_COLOR_NAME, ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, + ATTR_WHITE_VALUE, + ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, @@ -120,6 +129,48 @@ BRIGHTNESS_CHANGE = 25 # ≈10% of total range COLOR_TEMP_CHANGE = 20 # ≈5% of total range RGB_CHANGE = 30 # ≈12% of total range per component +# Keep a short domain version for the context instances (which can only be 36 chars) +_DOMAIN_SHORT = "adapt_lgt" + +_DISABLE_ON = { + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_STEP, + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_STEP_PCT, + ATTR_BRIGHTNESS, + ATTR_COLOR_NAME, + ATTR_RGB_COLOR, + ATTR_XY_COLOR, + ATTR_HS_COLOR, + ATTR_COLOR_TEMP, + ATTR_KELVIN, + ATTR_WHITE_VALUE, + ATTR_EFFECT, +} + + +def _short_hash(string: str, length: int = 4) -> str: + """Creates a hash of 'string' with length 'length'.""" + return hashlib.sha1(string.encode("UTF-8")).hexdigest()[:length] + + +def create_context(name: str, which: str, index: int) -> Context: + """Create a context that can identify this integration.""" + # Use a hash for the name because otherwise the context might become + # too long (max len == 36) to fit in the database. + name_hash = _short_hash(name) + return Context(id=f"{_DOMAIN_SHORT}_{name_hash}_{which}_{index}") + + +def is_our_context(context: Optional[Context]) -> bool: + """Check whether this integration created 'context'.""" + if context is None: + return False + return context.id.startswith(_DOMAIN_SHORT) + async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): """Handle the entity service apply.""" @@ -207,6 +258,7 @@ def match_state_event(event: Event, from_or_to_state: List[str]): def _expand_light_groups(hass: HomeAssistant, lights: List[str]) -> List[str]: all_lights = set() + turn_on_off_listener = hass.data[DOMAIN][ATTR_TURN_ON_OFF_LISTENER] for light in lights: state = hass.states.get(light) if state is None: @@ -214,6 +266,7 @@ def _expand_light_groups(hass: HomeAssistant, lights: List[str]) -> List[str]: all_lights.add(light) elif "entity_id" in state.attributes: # it's a light group group = state.attributes["entity_id"] + turn_on_off_listener.lights.discard(light) all_lights.update(group) _LOGGER.debug("Expanded %s to %s", light, group) else: @@ -227,6 +280,86 @@ def _supported_features(hass: HomeAssistant, light: str): return {key for key, value in _SUPPORT_OPTS.items() if supported_features & value} +def _attributes_have_changed( + light, + old_attributes, + new_attributes, + adapt_brightness, + adapt_color_temp, + adapt_rgb_color, + context, +): + if ( + adapt_brightness + and ATTR_BRIGHTNESS in old_attributes + and ATTR_BRIGHTNESS in new_attributes + ): + last_brightness = old_attributes[ATTR_BRIGHTNESS] + current_brightness = new_attributes[ATTR_BRIGHTNESS] + if abs(current_brightness - last_brightness) > BRIGHTNESS_CHANGE: + _LOGGER.debug( + "Brightness of '%s' significantly changed from %s to %s with" + " context.id='%s'", + light, + last_brightness, + current_brightness, + context.id, + ) + return True + + if ( + adapt_color_temp + and ATTR_COLOR_TEMP in old_attributes + and ATTR_COLOR_TEMP in new_attributes + ): + last_color_temp = old_attributes[ATTR_COLOR_TEMP] + current_color_temp = new_attributes[ATTR_COLOR_TEMP] + if abs(current_color_temp - last_color_temp) > COLOR_TEMP_CHANGE: + _LOGGER.debug( + "Color temperature of '%s' significantly changed from %s to %s with" + " context.id='%s'", + light, + last_color_temp, + current_color_temp, + context.id, + ) + return True + + if ( + adapt_rgb_color + and ATTR_RGB_COLOR in old_attributes + and ATTR_RGB_COLOR in new_attributes + ): + last_rgb_color = old_attributes[ATTR_RGB_COLOR] + current_rgb_color = new_attributes[ATTR_RGB_COLOR] + for last_col, current_col in zip(last_rgb_color, current_rgb_color): + if abs(last_col - current_col) > RGB_CHANGE: + _LOGGER.debug( + "color RGB of '%s' significantly changed from %s to %s with" + " context.id='%s'", + light, + last_rgb_color, + current_rgb_color, + context.id, + ) + return True + + switched_color_temp = ( + ATTR_RGB_COLOR in old_attributes and ATTR_RGB_COLOR not in new_attributes + ) + switched_to_rgb_color = ( + ATTR_COLOR_TEMP in old_attributes and ATTR_COLOR_TEMP not in new_attributes + ) + if switched_color_temp or switched_to_rgb_color: + # Light switched from RGB mode to color_temp or visa versa + _LOGGER.debug( + "'%s' switched from RGB mode to color_temp or visa versa", + light, + ) + return True + return False + + class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" @@ -285,9 +418,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._off_to_on_event: Dict[str, Event] = {} # Locks that prevent light adjusting when waiting for a light to 'turn_off' self._locks: Dict[str, asyncio.Lock] = {} - # To identify that this integration made a change - self.__context = Context() # self._context would be overwritten - self.turn_on_off_listener.contexts.add(self.__context) + # To count the number of `Context` instances + self._context_cnt: int = 0 # Set in self._update_attrs_and_maybe_adapt_lights self._settings: Dict[str, Any] = {} @@ -297,14 +429,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug( "%s: Setting up with '%s'," " config_entry.data: '%s'," - " config_entry.options: '%s', converted to '%s'," - " with context '%s'.", + " config_entry.options: '%s', converted to '%s'.", self._name, self._lights, config_entry.data, config_entry.options, data, - self.__context, ) @property @@ -394,6 +524,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ] return dict(self._settings, manually_controlled=manually_controlled) + def create_context(self, which: str = "default") -> Context: + """Create a context that identifies this Adaptive Lighting instance.""" + # Right now the highest number of each context_id it can create is + # 'adapt_lgt_XXXX_turn_on_9999999999999' + # 'adapt_lgt_XXXX_interval_999999999999' + # 'adapt_lgt_XXXX_adapt_lights_99999999' + # 'adapt_lgt_XXXX_sleep_999999999999999' + # 'adapt_lgt_XXXX_light_event_999999999' + # So 100 million calls before we run into the 36 chars limit. + context = create_context(self._name, which, self._context_cnt) + self._context_cnt += 1 + return context + async def async_turn_on( # pylint: disable=arguments-differ self, adapt_lights: bool = True ) -> None: @@ -408,7 +551,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): await self._setup_listeners() if adapt_lights: await self._update_attrs_and_maybe_adapt_lights( - transition=self._initial_transition, force=True + transition=self._initial_transition, + force=True, + context=self.create_context("turn_on"), ) async def async_turn_off(self, **kwargs) -> None: @@ -420,7 +565,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self.turn_on_off_listener.reset(*self._lights) async def _async_update_at_interval(self, now=None) -> None: - await self._update_attrs_and_maybe_adapt_lights(force=False) + await self._update_attrs_and_maybe_adapt_lights( + force=False, context=self.create_context("interval") + ) async def _adapt_light( self, @@ -430,12 +577,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): adapt_color_temp: Optional[bool] = None, adapt_rgb_color: Optional[bool] = None, force: bool = False, + context: Optional[Context] = None, ) -> None: lock = self._locks.get(light) if lock is not None and lock.locked(): _LOGGER.debug("%s: '%s' is locked", self._name, light) return - service_data = {ATTR_ENTITY_ID: light} features = _supported_features(self.hass, light) @@ -466,7 +613,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_COLOR_TEMP] = color_temp_mired elif "color" in features and adapt_rgb_color: service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"] - + context = context or self.create_context("adapt_lights") if ( self._take_over_control and self._detect_non_ha_changes @@ -476,20 +623,22 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._adapt_brightness, self._adapt_color_temp, self._adapt_rgb_color, - self.__context, + context, ) ): return _LOGGER.debug( - "%s: Scheduling 'light.turn_on' with the following 'service_data': %s", + "%s: Scheduling 'light.turn_on' with the following 'service_data': %s" + " with context.id='%s'", self._name, service_data, + context.id, ) await self.hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, service_data, - context=self.__context, + context=context, ) async def _update_attrs_and_maybe_adapt_lights( @@ -497,7 +646,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): lights: Optional[List[str]] = None, transition: Optional[int] = None, force: bool = False, - ): + context: Optional[Context] = None, + ) -> None: _LOGGER.debug("%s: '_update_attrs_and_maybe_adapt_lights' called", self._name) assert self.is_on self._settings = self._sun_light_settings.get_settings( @@ -508,28 +658,31 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): lights = self._lights if (self._only_once and not force) or not lights: return - await self._adapt_lights(lights, transition, force) + await self._adapt_lights(lights, transition, force, context) async def _adapt_lights( - self, lights: List[str], transition: Optional[int], force: bool - ): + self, + lights: List[str], + transition: Optional[int], + force: bool, + context: Optional[Context], + ) -> None: _LOGGER.debug( - "%s: '_adapt_lights(%s, %s, %s)' called", + "%s: '_adapt_lights(%s, %s, force=%s, context.id=%s)' called", self.name, lights, transition, force, + context.id, ) for light in lights: if not is_on(self.hass, light): continue if ( self._take_over_control - and False # XXX: REMOVE THIS and self.turn_on_off_listener.is_manually_controlled( light, force, - adaptive_lighting_context=self.__context, ) ): _LOGGER.debug( @@ -538,18 +691,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): light, ) continue - await self._adapt_light(light, transition, force=force) + await self._adapt_light(light, transition, force=force, context=context) - async def _sleep_state_event(self, event: Event): + async def _sleep_state_event(self, event: Event) -> None: if not match_state_event(event, (STATE_ON, STATE_OFF)): return _LOGGER.debug("%s: _sleep_state_event, event: '%s'", self._name, event) self.turn_on_off_listener.reset(*self._lights) await self._update_attrs_and_maybe_adapt_lights( - transition=self._initial_transition, force=True + transition=self._initial_transition, + force=True, + context=self.create_context("sleep"), ) - async def _light_event(self, event: Event): + async def _light_event(self, event: Event) -> None: old_state = event.data.get("old_state") new_state = event.data.get("new_state") entity_id = event.data.get("entity_id") @@ -560,7 +715,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): and new_state.state == STATE_ON ): _LOGGER.debug( - "%s: Detected an 'off' → 'on' event for '%s'", self._name, entity_id + "%s: Detected an 'off' → 'on' event for '%s' with context.id='%s'", + self._name, + entity_id, + event.context.id, ) self.turn_on_off_listener.reset(entity_id) # Tracks 'off' → 'on' state changes @@ -584,6 +742,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): lights=[entity_id], transition=self._initial_transition, force=True, + context=self.create_context("light_event"), ) elif ( old_state is not None @@ -630,6 +789,7 @@ class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity): async def async_added_to_hass(self) -> None: """Call when entity about to be added to hass.""" last_state = await self.async_get_last_state() + # XXX: state isn't correctly restored! if last_state is None or STATE_OFF: # newly added to HA await self.async_turn_off() else: @@ -793,7 +953,6 @@ class TurnOnOffListener: """Initialize the TurnOnOffListener that is shared among all switches.""" self.hass = hass self.lights = set() - self.contexts = set() # contexts of different AdaptiveSwitch instances # Tracks 'light.turn_off' service calls self.turn_off_event: Dict[str, Event] = {} @@ -804,7 +963,7 @@ class TurnOnOffListener: # Tracks which lights are manually controlled self.manually_controlled: Dict[str, bool] = {} # Track 'state_changed' events of self.lights resulting from this integration - self.last_state_change: Dict[str, State] = {} + self.last_state_change: Dict[str, List[State]] = {} self.remove_listener = self.hass.bus.async_listen( EVENT_CALL_SERVICE, self.turn_on_off_event_listener @@ -844,7 +1003,11 @@ class TurnOnOffListener: self.reset(eid) elif service == SERVICE_TURN_ON: - _LOGGER.debug("Detected an 'light.turn_on('%s')' event", entity_ids) + _LOGGER.debug( + "Detected an 'light.turn_on('%s')' event with context.id='%s'", + entity_ids, + event.context.id, + ) for eid in entity_ids: task = self.sleep_tasks.get(eid) if task is not None: @@ -854,28 +1017,54 @@ class TurnOnOffListener: async def state_changed_event_listener(self, event: Event): """Track 'state_changed' events.""" entity_id = event.data.get(ATTR_ENTITY_ID, "") - if entity_id not in self.lights and entity_id.split(".")[0] != LIGHT_DOMAIN: + if entity_id not in self.lights or entity_id.split(".")[0] != LIGHT_DOMAIN: return new_state = event.data.get("new_state") if ( new_state is not None and new_state.state == STATE_ON - and new_state.context in self.contexts + and is_our_context(new_state.context) ): _LOGGER.debug( - "Detected a '%s' 'state_changed' event: '%s'", + "Detected a '%s' 'state_changed' event: '%s' with context.id='%s'", entity_id, new_state.attributes, + new_state.context.id, ) - self.last_state_change[entity_id] = new_state + # It is possible to have multiple state change events with the same context. + # This can happen because a `turn_on.light(brightness_pct=100, transition=30)` + # event leads to an instant state change of + # `new_state=dict(brightness=100, ...)`. However, after polling the light + # could still only be `new_state=dict(brightness=50, ...)`. + # We save both events because the first event change might indicate at what + # settings the light will be later *or* the second event might indicate a + # final state. The latter case happens for example when a light was + # called with a color_temp outside of its range (and HA reports the + # incorrect 'min_mireds' and 'max_mireds', which happens e.g., for + # Philips Hue White GU10 Bluetooth lights). + old_state: Optional[List[State]] = self.last_state_change.get(entity_id) + if ( + old_state is not None + and old_state[0].context.id == new_state.context.id + ): + # If there is already a state change event from this event (with this + # context) then append it to the already existing list. + _LOGGER.debug( + "State change event of '%s' is already in 'self.last_state_change' (%s)" + " adding this state also", + entity_id, + new_state.context.id, + ) + self.last_state_change[entity_id].append(new_state) + else: + self.last_state_change[entity_id] = [new_state] def is_manually_controlled( self, light: str, force: bool, - adaptive_lighting_context: Context, - ): + ) -> bool: """Check if the light has been 'on' and is now manually being adjusted.""" manually_controlled = self.manually_controlled.setdefault(light, False) if manually_controlled: @@ -885,7 +1074,7 @@ class TurnOnOffListener: turn_on_event = self.turn_on_event.get(light) if ( turn_on_event is not None - and adaptive_lighting_context.id != turn_on_event.context.id + and not is_our_context(turn_on_event.context) and not force ): # Light was already on and 'light.turn_on' was not called by @@ -898,16 +1087,28 @@ class TurnOnOffListener: " then on again.", light, ) + # if ( + # turn_on_event is not None + # and not is_our_context(turn_on_event.context) + # and any(_DISABLE_ON.intersection(turn_on_event.data[ATTR_SERVICE_DATA])) + # ): + # # XXX: add comment + # _LOGGER.debug( + # "'light.turn_on' was called on '%s' with settings '%s' so we stop adapting", + # light, + # turn_on_event.data[ATTR_SERVICE_DATA], + # ) + # manually_controlled = self.manually_controlled[light] = True return manually_controlled async def significant_change( self, - light, - adapt_brightness, - adapt_color_temp, - adapt_rgb_color, - context, - ): + light: str, + adapt_brightness: bool, + adapt_color_temp: bool, + adapt_rgb_color: bool, + context: Context, + ) -> bool: """Has the light made a significant change since last update. This method will detect changes that were made to the light without @@ -917,8 +1118,7 @@ class TurnOnOffListener: """ if light not in self.last_state_change: return False - changed = False - old_attributes = self.last_state_change[light].attributes + old_states: List[State] = self.last_state_change[light] await self.hass.services.async_call( HA_DOMAIN, SERVICE_UPDATE_ENTITY, @@ -926,66 +1126,26 @@ class TurnOnOffListener: blocking=True, context=context, ) - attributes = self.hass.states.get(light).attributes - if ( - adapt_brightness - and ATTR_BRIGHTNESS in old_attributes - and ATTR_BRIGHTNESS in attributes - ): - last_brightness = old_attributes[ATTR_BRIGHTNESS] - current_brightness = attributes[ATTR_BRIGHTNESS] - if abs(current_brightness - last_brightness) > BRIGHTNESS_CHANGE: - _LOGGER.debug( - "Brightness of '%s' significantly changed from %s to %s", - light, - last_brightness, - current_brightness, - ) - changed = True - - if ( - adapt_color_temp - and ATTR_COLOR_TEMP in old_attributes - and ATTR_COLOR_TEMP in attributes - ): - last_color_temp = old_attributes[ATTR_COLOR_TEMP] - current_color_temp = attributes[ATTR_COLOR_TEMP] - if abs(current_color_temp - last_color_temp) > COLOR_TEMP_CHANGE: - _LOGGER.debug( - "Color temperature of '%s' significantly changed from %s to %s", - light, - last_color_temp, - current_color_temp, - ) - changed = True - - if ( - adapt_rgb_color - and ATTR_RGB_COLOR in old_attributes - and ATTR_RGB_COLOR in attributes - ): - last_rgb_color = old_attributes[ATTR_RGB_COLOR] - current_rgb_color = attributes[ATTR_RGB_COLOR] - for last_col, current_col in zip(last_rgb_color, current_rgb_color): - if abs(last_col - current_col) > RGB_CHANGE: - _LOGGER.debug( - "color RGB of '%s' significantly changed from %s to %s", - light, - last_rgb_color, - current_rgb_color, - ) - changed = True - break - - if (ATTR_RGB_COLOR in old_attributes and ATTR_RGB_COLOR not in attributes) or ( - ATTR_COLOR_TEMP in old_attributes and ATTR_COLOR_TEMP not in attributes - ): - # Light switched from RGB mode to color_temp or visa versa - _LOGGER.debug( - "'%s' switched from RGB mode to color_temp or visa versa", + new_state = self.hass.states.get(light) + for index, old_state in enumerate(old_states): + changed = _attributes_have_changed( light, + old_state.attributes, + new_state.attributes, + adapt_brightness, + adapt_color_temp, + adapt_rgb_color, + context, ) - changed = True + if not changed: + _LOGGER.debug( + "States of '%s' didn't change wrt change event nr. %s (context.id=%s)", + light, + index, + context.id, + ) + break + self.manually_controlled[light] = changed return changed From a570ac8bb8a1ae115cfd2e477b746e45d27c8f71 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 8 Oct 2020 20:28:04 +0200 Subject: [PATCH 0247/1077] cleanup --- custom_components/adaptive_lighting/switch.py | 58 ++++--------------- 1 file changed, 12 insertions(+), 46 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index ffaf050b..c5b38fcb 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -21,17 +21,9 @@ from homeassistant.components.homeassistant import ( from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, - ATTR_BRIGHTNESS_STEP, - ATTR_BRIGHTNESS_STEP_PCT, - ATTR_COLOR_NAME, ATTR_COLOR_TEMP, - ATTR_EFFECT, - ATTR_HS_COLOR, - ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, - ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, @@ -132,28 +124,9 @@ RGB_CHANGE = 30 # ≈12% of total range per component # Keep a short domain version for the context instances (which can only be 36 chars) _DOMAIN_SHORT = "adapt_lgt" -_DISABLE_ON = { - ATTR_BRIGHTNESS, - ATTR_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT, - ATTR_BRIGHTNESS, - ATTR_BRIGHTNESS_STEP, - ATTR_BRIGHTNESS, - ATTR_BRIGHTNESS_STEP_PCT, - ATTR_BRIGHTNESS, - ATTR_COLOR_NAME, - ATTR_RGB_COLOR, - ATTR_XY_COLOR, - ATTR_HS_COLOR, - ATTR_COLOR_TEMP, - ATTR_KELVIN, - ATTR_WHITE_VALUE, - ATTR_EFFECT, -} - def _short_hash(string: str, length: int = 4) -> str: - """Creates a hash of 'string' with length 'length'.""" + """Create a hash of 'string' with length 'length'.""" return hashlib.sha1(string.encode("UTF-8")).hexdigest()[:length] @@ -648,7 +621,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): force: bool = False, context: Optional[Context] = None, ) -> None: - _LOGGER.debug("%s: '_update_attrs_and_maybe_adapt_lights' called", self._name) + _LOGGER.debug( + "%s: '_update_attrs_and_maybe_adapt_lights' called with context.id='%s'", + self._name, + context.id, + ) assert self.is_on self._settings = self._sun_light_settings.get_settings( self.sleep_mode_switch.is_on @@ -686,9 +663,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) ): _LOGGER.debug( - "%s: '%s' is being manually controlled, stop adapting.", + "%s: '%s' is being manually controlled, stop adapting, context.id=%s.", self._name, light, + context.id, ) continue await self._adapt_light(light, transition, force=force, context=context) @@ -789,7 +767,6 @@ class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity): async def async_added_to_hass(self) -> None: """Call when entity about to be added to hass.""" last_state = await self.async_get_last_state() - # XXX: state isn't correctly restored! if last_state is None or STATE_OFF: # newly added to HA await self.async_turn_off() else: @@ -1082,23 +1059,12 @@ class TurnOnOffListener: manually_controlled = self.manually_controlled[light] = True _LOGGER.debug( "'%s' was already on and 'light.turn_on' was not called by the" - " adaptive_lighting integration, the Adaptive Lighting will stop" - " adapting the light until the switch or the light turns off and" - " then on again.", + " adaptive_lighting integration (context.id='%s'), the Adaptive" + " Lighting will stop adapting the light until the switch or the" + " light turns off and then on again.", light, + turn_on_event.context.id, ) - # if ( - # turn_on_event is not None - # and not is_our_context(turn_on_event.context) - # and any(_DISABLE_ON.intersection(turn_on_event.data[ATTR_SERVICE_DATA])) - # ): - # # XXX: add comment - # _LOGGER.debug( - # "'light.turn_on' was called on '%s' with settings '%s' so we stop adapting", - # light, - # turn_on_event.data[ATTR_SERVICE_DATA], - # ) - # manually_controlled = self.manually_controlled[light] = True return manually_controlled async def significant_change( From a4d0074710e9317c46f36f800fe879af90274220 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 9 Oct 2020 09:43:37 +0200 Subject: [PATCH 0248/1077] Log all state change events --- custom_components/adaptive_lighting/switch.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index c5b38fcb..f36673e6 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -998,17 +998,19 @@ class TurnOnOffListener: return new_state = event.data.get("new_state") - if ( - new_state is not None - and new_state.state == STATE_ON - and is_our_context(new_state.context) - ): + if new_state is not None and new_state.state == STATE_ON: _LOGGER.debug( "Detected a '%s' 'state_changed' event: '%s' with context.id='%s'", entity_id, new_state.attributes, new_state.context.id, ) + + if ( + new_state is not None + and new_state.state == STATE_ON + and is_our_context(new_state.context) + ): # It is possible to have multiple state change events with the same context. # This can happen because a `turn_on.light(brightness_pct=100, transition=30)` # event leads to an instant state change of From 7521b862b0b4502e0f8f1f74845b21b45a139fef Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 9 Oct 2020 15:07:50 +0200 Subject: [PATCH 0249/1077] synx with PR --- custom_components/adaptive_lighting/switch.py | 83 +++++++++++-------- 1 file changed, 50 insertions(+), 33 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index f36673e6..6f615902 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -7,6 +7,7 @@ from copy import deepcopy from dataclasses import dataclass import datetime from datetime import timedelta +import functools import hashlib import logging from typing import Any, Dict, List, Optional, Tuple, Union @@ -20,7 +21,6 @@ from homeassistant.components.homeassistant import ( ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, @@ -203,7 +203,7 @@ async def async_setup_entry( ) -def validate(config_entry): +def validate(config_entry: ConfigEntry): """Get the options and data from the config_entry and add defaults.""" defaults = {key: default for key, default, _ in VALIDATION_TUPLES} data = deepcopy(defaults) @@ -254,14 +254,14 @@ def _supported_features(hass: HomeAssistant, light: str): def _attributes_have_changed( - light, - old_attributes, - new_attributes, - adapt_brightness, - adapt_color_temp, - adapt_rgb_color, - context, -): + light: str, + old_attributes: Dict[str, Any], + new_attributes: Dict[str, Any], + adapt_brightness: bool, + adapt_color_temp: bool, + adapt_rgb_color: bool, + context: Context, +) -> bool: if ( adapt_brightness and ATTR_BRIGHTNESS in old_attributes @@ -572,7 +572,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_TRANSITION] = transition if "brightness" in features and adapt_brightness: - service_data[ATTR_BRIGHTNESS_PCT] = self._settings["brightness_pct"] + brightness = round(255 * self._settings["brightness_pct"] / 100) + service_data[ATTR_BRIGHTNESS] = brightness if ( "color_temp" in features @@ -607,6 +608,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): service_data, context.id, ) + self.turn_on_off_listener.last_service_data[light] = service_data await self.hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -926,7 +928,7 @@ class SunLightSettings: class TurnOnOffListener: """Track 'light.turn_off' and 'light.turn_on' service calls.""" - def __init__(self, hass): + def __init__(self, hass: HomeAssistant): """Initialize the TurnOnOffListener that is shared among all switches.""" self.hass = hass self.lights = set() @@ -935,12 +937,14 @@ class TurnOnOffListener: self.turn_off_event: Dict[str, Event] = {} # Tracks 'light.turn_on' service calls self.turn_on_event: Dict[str, Event] = {} - # Keeps 'asyncio.sleep` tasks that can be cancelled by 'light.turn_on' events + # Keep 'asyncio.sleep' tasks that can be cancelled by 'light.turn_on' events self.sleep_tasks: Dict[str, asyncio.Task] = {} # Tracks which lights are manually controlled self.manually_controlled: Dict[str, bool] = {} # Track 'state_changed' events of self.lights resulting from this integration self.last_state_change: Dict[str, List[State]] = {} + # Track last 'service_data' to 'light.turn_on' resulting from this integration + self.last_service_data: Dict[str, Dict[str, Any]] = {} self.remove_listener = self.hass.bus.async_listen( EVENT_CALL_SERVICE, self.turn_on_off_event_listener @@ -949,13 +953,14 @@ class TurnOnOffListener: EVENT_STATE_CHANGED, self.state_changed_event_listener ) - def reset(self, *lights): + def reset(self, *lights) -> None: """Reset the 'manually_controlled' status of the lights.""" for light in lights: self.manually_controlled[light] = False self.last_state_change.pop(light, None) + self.last_service_data.pop(light, None) - async def turn_on_off_event_listener(self, event: Event): + async def turn_on_off_event_listener(self, event: Event) -> None: """Track 'light.turn_off' and 'light.turn_on' service calls.""" domain = event.data.get(ATTR_DOMAIN) if domain != LIGHT_DOMAIN: @@ -991,7 +996,7 @@ class TurnOnOffListener: task.cancel() self.turn_on_event[eid] = event - async def state_changed_event_listener(self, event: Event): + async def state_changed_event_listener(self, event: Event) -> None: """Track 'state_changed' events.""" entity_id = event.data.get(ATTR_ENTITY_ID, "") if entity_id not in self.lights or entity_id.split(".")[0] != LIGHT_DOMAIN: @@ -1016,17 +1021,16 @@ class TurnOnOffListener: # event leads to an instant state change of # `new_state=dict(brightness=100, ...)`. However, after polling the light # could still only be `new_state=dict(brightness=50, ...)`. - # We save both events because the first event change might indicate at what + # We save all events because the first event change might indicate at what # settings the light will be later *or* the second event might indicate a # final state. The latter case happens for example when a light was # called with a color_temp outside of its range (and HA reports the # incorrect 'min_mireds' and 'max_mireds', which happens e.g., for # Philips Hue White GU10 Bluetooth lights). old_state: Optional[List[State]] = self.last_state_change.get(entity_id) - if ( - old_state is not None - and old_state[0].context.id == new_state.context.id - ): + if old_state is None: + self.last_state_change[entity_id] = [new_state] + elif old_state[0].context.id == new_state.context.id: # If there is already a state change event from this event (with this # context) then append it to the already existing list. _LOGGER.debug( @@ -1036,8 +1040,6 @@ class TurnOnOffListener: new_state.context.id, ) self.last_state_change[entity_id].append(new_state) - else: - self.last_state_change[entity_id] = [new_state] def is_manually_controlled( self, @@ -1095,25 +1097,40 @@ class TurnOnOffListener: context=context, ) new_state = self.hass.states.get(light) + compare_to = functools.partial( + _attributes_have_changed, + light=light, + new_attributes=new_state.attributes, + adapt_brightness=adapt_brightness, + adapt_color_temp=adapt_color_temp, + adapt_rgb_color=adapt_rgb_color, + context=context, + ) for index, old_state in enumerate(old_states): - changed = _attributes_have_changed( - light, - old_state.attributes, - new_state.attributes, - adapt_brightness, - adapt_color_temp, - adapt_rgb_color, - context, - ) + changed = compare_to(old_attributes=old_state.attributes) if not changed: _LOGGER.debug( - "States of '%s' didn't change wrt change event nr. %s (context.id=%s)", + "State of '%s' didn't change wrt change event nr. %s (context.id=%s)", light, index, context.id, ) break + last_service_data = self.last_service_data.get(light) + if changed and last_service_data is not None: + # It can happen that the state change events that are associated + # with the last 'light.turn_on' call by this integration were not + # final states. Possibly a later EVENT_STATE_CHANGED happened, where + # the correct target brightness/color was reached. + changed = compare_to(old_attributes=last_service_data) + if not changed: + _LOGGER.debug( + "State of '%s' didn't change wrt 'last_service_data' (context.id=%s)", + light, + context.id, + ) + self.manually_controlled[light] = changed return changed From 6a1bb180b5080649d3976b58897abb91353af39a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 9 Oct 2020 15:13:09 +0200 Subject: [PATCH 0250/1077] change order --- custom_components/adaptive_lighting/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index d3d7ebed..fd75713e 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -56,6 +56,7 @@ VALIDATION_TUPLES = [ (CONF_ADAPT_BRIGHTNESS, DEFAULT_ADAPT_BRIGHTNESS, bool), (CONF_ADAPT_COLOR_TEMP, DEFAULT_ADAPT_COLOR_TEMP, bool), (CONF_ADAPT_RGB_COLOR, DEFAULT_ADAPT_RGB_COLOR, bool), + (CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool), (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), @@ -63,7 +64,6 @@ VALIDATION_TUPLES = [ (CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)), (CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP, int_between(1000, 10000)), (CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP, int_between(1000, 10000)), - (CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool), (CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)), (CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP, int_between(1000, 10000)), (CONF_SUNRISE_TIME, NONE_STR, str), From 3c1312f0f85eddfc52fc470f90bc48e0dcd98b47 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 9 Oct 2020 18:09:11 +0200 Subject: [PATCH 0251/1077] redmean --- custom_components/adaptive_lighting/switch.py | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 6f615902..52e5e356 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -10,6 +10,7 @@ from datetime import timedelta import functools import hashlib import logging +import math from typing import Any, Dict, List, Optional, Tuple, Union import astral @@ -119,7 +120,7 @@ SCAN_INTERVAL = timedelta(seconds=10) # Consider it a significant change when attribute changes more than BRIGHTNESS_CHANGE = 25 # ≈10% of total range COLOR_TEMP_CHANGE = 20 # ≈5% of total range -RGB_CHANGE = 30 # ≈12% of total range per component +RGB_REDMEAN_CHANGE = 80 # ≈10% of total range # Keep a short domain version for the context instances (which can only be 36 chars) _DOMAIN_SHORT = "adapt_lgt" @@ -253,6 +254,23 @@ def _supported_features(hass: HomeAssistant, light: str): return {key for key, value in _SUPPORT_OPTS.items() if supported_features & value} +def color_difference_redmean(rgb1, rgb2): + """The distance between colors in RGB space, known as redmean. + + The maximal distance between (255, 255, 255) and (0, 0, 0) ≈ 765. + + Sources: + - https://en.wikipedia.org/wiki/Color_difference#Euclidean + - https://www.compuphase.com/cmetric.htm + """ + r_hat = (rgb1[0] + rgb2[0]) / 2 + delta_r, delta_g, delta_b = [(col1 - col2) for col1, col2 in zip(rgb1, rgb2)] + red_term = (2 + r_hat / 256) * delta_r ** 2 + green_term = 4 * delta_g ** 2 + blue_term = (2 + (255 - r_hat) / 256) * delta_b ** 2 + return math.sqrt(red_term + green_term + blue_term) + + def _attributes_have_changed( light: str, old_attributes: Dict[str, Any], @@ -305,17 +323,17 @@ def _attributes_have_changed( ): last_rgb_color = old_attributes[ATTR_RGB_COLOR] current_rgb_color = new_attributes[ATTR_RGB_COLOR] - for last_col, current_col in zip(last_rgb_color, current_rgb_color): - if abs(last_col - current_col) > RGB_CHANGE: - _LOGGER.debug( - "color RGB of '%s' significantly changed from %s to %s with" - " context.id='%s'", - light, - last_rgb_color, - current_rgb_color, - context.id, - ) - return True + redmean_change = color_difference_redmean(last_rgb_color, current_rgb_color) + if redmean_change > RGB_REDMEAN_CHANGE: + _LOGGER.debug( + "color RGB of '%s' significantly changed from %s to %s with" + " context.id='%s'", + light, + last_rgb_color, + current_rgb_color, + context.id, + ) + return True switched_color_temp = ( ATTR_RGB_COLOR in old_attributes and ATTR_RGB_COLOR not in new_attributes From e655956a3f9c054124cc53f8886a4efc926a44c1 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 11 Oct 2020 11:35:03 +0200 Subject: [PATCH 0252/1077] style --- custom_components/adaptive_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 52e5e356..1fd9f4dd 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -255,7 +255,7 @@ def _supported_features(hass: HomeAssistant, light: str): def color_difference_redmean(rgb1, rgb2): - """The distance between colors in RGB space, known as redmean. + """Distance between colors in RGB space (redmean metric). The maximal distance between (255, 255, 255) and (0, 0, 0) ≈ 765. From d1714d800a21df14256d6155a7b06b0306e4679a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 11 Oct 2020 12:41:30 +0200 Subject: [PATCH 0253/1077] fix strings.json --- custom_components/adaptive_lighting/strings.json | 2 +- custom_components/adaptive_lighting/switch.py | 4 +++- custom_components/adaptive_lighting/translations/en.json | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 57949ea0..9d835bdc 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -6,7 +6,7 @@ "title": "Choose a name for the Adaptive Lighting", "description": "Every instance can contain multiple lights!", "data": { - "name": "[%key:common::config_flow::data::name%]" + "name": "Name" } } }, diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 1fd9f4dd..7c07ac50 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -254,7 +254,9 @@ def _supported_features(hass: HomeAssistant, light: str): return {key for key, value in _SUPPORT_OPTS.items() if supported_features & value} -def color_difference_redmean(rgb1, rgb2): +def color_difference_redmean( + rgb1: Tuple[float, float, float], rgb2: Tuple[float, float, float] +) -> float: """Distance between colors in RGB space (redmean metric). The maximal distance between (255, 255, 255) and (0, 0, 0) ≈ 765. diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 57949ea0..1ceb00df 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -6,12 +6,12 @@ "title": "Choose a name for the Adaptive Lighting", "description": "Every instance can contain multiple lights!", "data": { - "name": "[%key:common::config_flow::data::name%]" + "name": "Name" } } }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "Device is already configured" } }, "options": { From 86d1cad58806ff9dba34f1f9472dcdd668b0e3f0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 13 Oct 2020 22:39:38 +0200 Subject: [PATCH 0254/1077] sync with PR --- custom_components/adaptive_lighting/const.py | 1 + .../adaptive_lighting/services.yaml | 9 +++ custom_components/adaptive_lighting/switch.py | 64 ++++++++++++++++++- 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index fd75713e..0ec106d9 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -40,6 +40,7 @@ ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener" UNDO_UPDATE_LISTENER = "undo_update_listener" NONE_STR = "None" +SERVICE_NOT_MANUALLY_CONTROLLED = "not_manually_controlled" SERVICE_APPLY = "apply" CONF_TURN_ON_LIGHTS = "turn_on_lights" diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index dcc5bbfd..191192e5 100755 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -22,3 +22,12 @@ apply: turn_on_lights: description: "Turn on the lights that are off, default: false" example: false +not_manually_controlled: + description: Mark a light as not being 'manually_controlled'. + fields: + entity_id: + description: entity_id of the Adaptive Lighting switch. + example: switch.adaptive_lighting_default + lights: + description: entity_id(s) of lights. + example: light.bedroom_ceiling diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 7c07ac50..62023724 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import bisect +from collections import defaultdict from copy import deepcopy from dataclasses import dataclass import datetime @@ -51,7 +52,14 @@ from homeassistant.const import ( SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) -from homeassistant.core import Context, Event, HomeAssistant, ServiceCall, State +from homeassistant.core import ( + Context, + Event, + HomeAssistant, + ServiceCall, + State, + callback, +) from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( @@ -96,6 +104,7 @@ from .const import ( EXTRA_VALIDATION, ICON, SERVICE_APPLY, + SERVICE_NOT_MANUALLY_CONTROLLED, SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, TURNING_OFF_DELAY, @@ -167,6 +176,23 @@ async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): ) +async def handle_not_manually_controlled( + switch: AdaptiveSwitch, service_call: ServiceCall +): + """Remove lights from the 'manually_controlled' list.""" + all_lights = _expand_light_groups(switch.hass, service_call.data[CONF_LIGHTS]) + switch.turn_on_off_listener.reset(*all_lights) + + +@callback +def _fire_manually_controlled_event( + hass: HomeAssistant, light: str, context: Context, is_async=True +): + """Fire an event that 'light' is marked as manually_controlled.""" + fire = hass.bus.async_fire if is_async else hass.bus.fire + fire(f"{DOMAIN}.manually_controlled", {ATTR_ENTITY_ID: light}, context=context) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: bool ): @@ -203,6 +229,12 @@ async def async_setup_entry( handle_apply, ) + platform.async_register_entity_service( + SERVICE_NOT_MANUALLY_CONTROLLED, + {vol.Required(CONF_LIGHTS): cv.entity_ids}, + handle_not_manually_controlled, + ) + def validate(config_entry: ConfigEntry): """Get the options and data from the config_entry and add defaults.""" @@ -961,11 +993,17 @@ class TurnOnOffListener: self.sleep_tasks: Dict[str, asyncio.Task] = {} # Tracks which lights are manually controlled self.manually_controlled: Dict[str, bool] = {} + # Counts the number of times (in a row) a light had a changed state. + self.cnt_significant_changes: Dict[str, int] = defaultdict(int) # Track 'state_changed' events of self.lights resulting from this integration self.last_state_change: Dict[str, List[State]] = {} # Track last 'service_data' to 'light.turn_on' resulting from this integration self.last_service_data: Dict[str, Dict[str, Any]] = {} + # When a state is different `max_cnt_significant_changes` times in a row, + # mark it as manually_controlled. + self.max_cnt_significant_changes = 1 + self.remove_listener = self.hass.bus.async_listen( EVENT_CALL_SERVICE, self.turn_on_off_event_listener ) @@ -979,6 +1017,7 @@ class TurnOnOffListener: self.manually_controlled[light] = False self.last_state_change.pop(light, None) self.last_service_data.pop(light, None) + self.cnt_significant_changes[light] = 0 async def turn_on_off_event_listener(self, event: Event) -> None: """Track 'light.turn_off' and 'light.turn_on' service calls.""" @@ -1081,6 +1120,7 @@ class TurnOnOffListener: # Light was already on and 'light.turn_on' was not called by # the adaptive_lighting integration. manually_controlled = self.manually_controlled[light] = True + _fire_manually_controlled_event(self.hass, light, turn_on_event.context) _LOGGER.debug( "'%s' was already on and 'light.turn_on' was not called by the" " adaptive_lighting integration (context.id='%s'), the Adaptive" @@ -1151,7 +1191,27 @@ class TurnOnOffListener: context.id, ) - self.manually_controlled[light] = changed + n_changes = self.cnt_significant_changes[light] + if changed: + self.cnt_significant_changes[light] += 1 + if n_changes >= self.max_cnt_significant_changes: + # Only mark a light as significantly changing, changed==True `x` + # times in a row. We do this because sometimes a state changes + # happens only *after* a new update interval has already started. + self.manually_controlled[light] = True + _fire_manually_controlled_event( + self.hass, light, context, is_async=False + ) + else: + if n_changes > 1: + _LOGGER.debug( + "State of '%s' had 'cnt_significant_changes=%s' but the state" + " changed to the expected settings now", + light, + n_changes, + ) + self.cnt_significant_changes[light] = 0 + return changed async def maybe_cancel_adjusting( From d67479d017552a124531dcc46d1e51ac6382baab Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 14 Oct 2020 07:50:45 +0200 Subject: [PATCH 0255/1077] sync --- custom_components/adaptive_lighting/const.py | 3 +- .../adaptive_lighting/manifest.json | 2 +- .../adaptive_lighting/services.yaml | 7 ++- .../adaptive_lighting/strings.json | 4 +- custom_components/adaptive_lighting/switch.py | 61 +++++++++++++------ .../adaptive_lighting/translations/en.json | 4 +- 6 files changed, 55 insertions(+), 26 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 0ec106d9..ab732e66 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -40,7 +40,8 @@ ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener" UNDO_UPDATE_LISTENER = "undo_update_listener" NONE_STR = "None" -SERVICE_NOT_MANUALLY_CONTROLLED = "not_manually_controlled" +SERVICE_SET_MANUALLY_CONTROLLED = "set_manually_controlled" +CONF_MANUALLY_CONTROLLED = "manually_controlled" SERVICE_APPLY = "apply" CONF_TURN_ON_LIGHTS = "turn_on_lights" diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index dc730449..13461584 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/adaptive_lighting", "config_flow": true, "dependencies": [], - "codeowners": ["@basnijholt", "@claytonjn"], + "codeowners": ["@basnijholt"], "requirements": [] } diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 191192e5..713e559a 100755 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -22,12 +22,15 @@ apply: turn_on_lights: description: "Turn on the lights that are off, default: false" example: false -not_manually_controlled: - description: Mark a light as not being 'manually_controlled'. +set_manually_controlled: + description: Mark a light as (not) being 'manually_controlled'. fields: entity_id: description: entity_id of the Adaptive Lighting switch. example: switch.adaptive_lighting_default + manually_controlled: + description: "Whether to add ('true') or remove ('false') the light from the 'manually_controlled' list, default: true" + example: true lights: description: entity_id(s) of lights. example: light.bedroom_ceiling diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 9d835bdc..d45edc4b 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -35,9 +35,9 @@ "sleep_brightness": "sleep_brightness, in %", "sleep_color_temp": "sleep_color_temp, in Kelvin", "sunrise_offset": "sunrise_offset, in +/- seconds", - "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", + "sunrise_time": "sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location)", "sunset_offset": "sunset_offset, in +/- seconds", - "sunset_time": "sunset_time, in 'HH:MM:SS' format", + "sunset_time": "sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location)", "take_over_control": "take_over_control, if anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", "detect_non_ha_changes": "detect_non_ha_changes, detects all >5% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", "transition": "transition, in seconds" diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 62023724..eb496490 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -85,6 +85,7 @@ from .const import ( CONF_INITIAL_TRANSITION, CONF_INTERVAL, CONF_LIGHTS, + CONF_MANUALLY_CONTROLLED, CONF_MAX_BRIGHTNESS, CONF_MAX_COLOR_TEMP, CONF_MIN_BRIGHTNESS, @@ -104,7 +105,7 @@ from .const import ( EXTRA_VALIDATION, ICON, SERVICE_APPLY, - SERVICE_NOT_MANUALLY_CONTROLLED, + SERVICE_SET_MANUALLY_CONTROLLED, SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, TURNING_OFF_DELAY, @@ -176,21 +177,39 @@ async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): ) -async def handle_not_manually_controlled( +async def handle_set_manually_controlled( switch: AdaptiveSwitch, service_call: ServiceCall ): """Remove lights from the 'manually_controlled' list.""" all_lights = _expand_light_groups(switch.hass, service_call.data[CONF_LIGHTS]) - switch.turn_on_off_listener.reset(*all_lights) + _LOGGER.debug( + "Called 'adaptive_lighting.set_manually_controlled' service with '%s'", + service_call.data, + ) + if service_call.data[CONF_MANUALLY_CONTROLLED]: + for light in all_lights: + switch.turn_on_off_listener.manually_controlled[light] = True + _fire_manual_control_event(switch.hass, light, service_call.context) + else: + switch.turn_on_off_listener.reset(*all_lights) + # pylint: disable=protected-access + await switch._adapt_lights( + all_lights, + transition=switch._initial_transition, + force=True, + context=switch.create_context("service"), + ) @callback -def _fire_manually_controlled_event( +def _fire_manual_control_event( hass: HomeAssistant, light: str, context: Context, is_async=True ): """Fire an event that 'light' is marked as manually_controlled.""" fire = hass.bus.async_fire if is_async else hass.bus.fire - fire(f"{DOMAIN}.manually_controlled", {ATTR_ENTITY_ID: light}, context=context) + # Calling the event_type='manually_controlled' would be better, but + # event_type has a 32 character limit. + fire(f"{DOMAIN}.manual_control", {ATTR_ENTITY_ID: light}, context=context) async def async_setup_entry( @@ -230,9 +249,12 @@ async def async_setup_entry( ) platform.async_register_entity_service( - SERVICE_NOT_MANUALLY_CONTROLLED, - {vol.Required(CONF_LIGHTS): cv.entity_ids}, - handle_not_manually_controlled, + SERVICE_SET_MANUALLY_CONTROLLED, + { + vol.Required(CONF_LIGHTS): cv.entity_ids, + vol.Optional(CONF_MANUALLY_CONTROLLED, default=True): cv.boolean, + }, + handle_set_manually_controlled, ) @@ -557,6 +579,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # 'adapt_lgt_XXXX_adapt_lights_99999999' # 'adapt_lgt_XXXX_sleep_999999999999999' # 'adapt_lgt_XXXX_light_event_999999999' + # 'adapt_lgt_XXXX_service_9999999999999' # So 100 million calls before we run into the 36 chars limit. context = create_context(self._name, which, self._context_cnt) self._context_cnt += 1 @@ -639,6 +662,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_COLOR_TEMP] = color_temp_mired elif "color" in features and adapt_rgb_color: service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"] + context = context or self.create_context("adapt_lights") if ( self._take_over_control @@ -675,6 +699,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): force: bool = False, context: Optional[Context] = None, ) -> None: + assert context is not None _LOGGER.debug( "%s: '_update_attrs_and_maybe_adapt_lights' called with context.id='%s'", self._name, @@ -698,6 +723,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): force: bool, context: Optional[Context], ) -> None: + assert context is not None _LOGGER.debug( "%s: '_adapt_lights(%s, %s, force=%s, context.id=%s)' called", self.name, @@ -752,7 +778,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): entity_id, event.context.id, ) - self.turn_on_off_listener.reset(entity_id) + self.turn_on_off_listener.reset(entity_id, reset_manually_controlled=False) # Tracks 'off' → 'on' state changes self._off_to_on_event[entity_id] = event lock = self._locks.get(entity_id) @@ -1002,7 +1028,7 @@ class TurnOnOffListener: # When a state is different `max_cnt_significant_changes` times in a row, # mark it as manually_controlled. - self.max_cnt_significant_changes = 1 + self.max_cnt_significant_changes = 2 self.remove_listener = self.hass.bus.async_listen( EVENT_CALL_SERVICE, self.turn_on_off_event_listener @@ -1011,10 +1037,11 @@ class TurnOnOffListener: EVENT_STATE_CHANGED, self.state_changed_event_listener ) - def reset(self, *lights) -> None: + def reset(self, *lights, reset_manually_controlled=True) -> None: """Reset the 'manually_controlled' status of the lights.""" for light in lights: - self.manually_controlled[light] = False + if reset_manually_controlled: + self.manually_controlled[light] = False self.last_state_change.pop(light, None) self.last_service_data.pop(light, None) self.cnt_significant_changes[light] = 0 @@ -1120,7 +1147,7 @@ class TurnOnOffListener: # Light was already on and 'light.turn_on' was not called by # the adaptive_lighting integration. manually_controlled = self.manually_controlled[light] = True - _fire_manually_controlled_event(self.hass, light, turn_on_event.context) + _fire_manual_control_event(self.hass, light, turn_on_event.context) _LOGGER.debug( "'%s' was already on and 'light.turn_on' was not called by the" " adaptive_lighting integration (context.id='%s'), the Adaptive" @@ -1195,13 +1222,11 @@ class TurnOnOffListener: if changed: self.cnt_significant_changes[light] += 1 if n_changes >= self.max_cnt_significant_changes: - # Only mark a light as significantly changing, changed==True `x` - # times in a row. We do this because sometimes a state changes + # Only mark a light as significantly changing, if changed==True + # N times in a row. We do this because sometimes a state changes # happens only *after* a new update interval has already started. self.manually_controlled[light] = True - _fire_manually_controlled_event( - self.hass, light, context, is_async=False - ) + _fire_manual_control_event(self.hass, light, context, is_async=False) else: if n_changes > 1: _LOGGER.debug( diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 1ceb00df..f306cf60 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -35,9 +35,9 @@ "sleep_brightness": "sleep_brightness, in %", "sleep_color_temp": "sleep_color_temp, in Kelvin", "sunrise_offset": "sunrise_offset, in +/- seconds", - "sunrise_time": "sunrise_time, in 'HH:MM:SS' format", + "sunrise_time": "sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location)", "sunset_offset": "sunset_offset, in +/- seconds", - "sunset_time": "sunset_time, in 'HH:MM:SS' format", + "sunset_time": "sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location)", "take_over_control": "take_over_control, if anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", "detect_non_ha_changes": "detect_non_ha_changes, detects all >5% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", "transition": "transition, in seconds" From 466c4b62f37192beab43bfb997cb3fc8c40a7401 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 15 Oct 2020 19:50:02 +0200 Subject: [PATCH 0256/1077] link documentation --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ca434b95..91615e11 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,19 @@ Try out this code by adding https://github.com/basnijholt/adaptive-lighting to your custom repos in HACS and install it! -I have not written any docs yet, so I recommend to use the UI to add this integration. +See the documentation at https://deploy-preview-14877--home-assistant-docs.netlify.app/integrations/adaptive_lighting/ -See [this video on Reddit](https://www.reddit.com/r/homeassistant/comments/j09219/any_circadian_lighting_users_good_news_i_just/) to see how to add the integration and set the options. +See [this video on Reddit](https://www.reddit.com/r/homeassistant/comments/jabhso/ha_has_it_before_apple_has_even_finished_it_i/) to see how to add the integration and set the options. + +# Having problems? +Please enable debug logging by putting this in `configuration.yaml`: +```yaml +logger: + default: warning + logs: + custom_components.adaptive_lighting: debug +``` +and after the problem occurs please create an issue with the log. ### Graphs! From d3861163d7d3d60a8229fc46f4cab96ea1d5458b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 16 Oct 2020 14:14:44 +0200 Subject: [PATCH 0257/1077] rename 'manually_controlled' -> 'manual_control' and fix state save bug --- custom_components/adaptive_lighting/const.py | 4 +- .../adaptive_lighting/services.yaml | 8 +-- custom_components/adaptive_lighting/switch.py | 70 +++++++++---------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index ab732e66..a96e301b 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -40,8 +40,8 @@ ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener" UNDO_UPDATE_LISTENER = "undo_update_listener" NONE_STR = "None" -SERVICE_SET_MANUALLY_CONTROLLED = "set_manually_controlled" -CONF_MANUALLY_CONTROLLED = "manually_controlled" +SERVICE_SET_MANUAL_CONTROL = "set_manual_control" +CONF_MANUAL_CONTROL = "manual_control" SERVICE_APPLY = "apply" CONF_TURN_ON_LIGHTS = "turn_on_lights" diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 713e559a..ba56b77b 100755 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -22,14 +22,14 @@ apply: turn_on_lights: description: "Turn on the lights that are off, default: false" example: false -set_manually_controlled: - description: Mark a light as (not) being 'manually_controlled'. +set_manual_control: + description: Mark whether a light is 'manually controlled'. fields: entity_id: description: entity_id of the Adaptive Lighting switch. example: switch.adaptive_lighting_default - manually_controlled: - description: "Whether to add ('true') or remove ('false') the light from the 'manually_controlled' list, default: true" + manual_control: + description: "Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true" example: true lights: description: entity_id(s) of lights. diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index eb496490..23d460e8 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -85,7 +85,7 @@ from .const import ( CONF_INITIAL_TRANSITION, CONF_INTERVAL, CONF_LIGHTS, - CONF_MANUALLY_CONTROLLED, + CONF_MANUAL_CONTROL, CONF_MAX_BRIGHTNESS, CONF_MAX_COLOR_TEMP, CONF_MIN_BRIGHTNESS, @@ -105,7 +105,7 @@ from .const import ( EXTRA_VALIDATION, ICON, SERVICE_APPLY, - SERVICE_SET_MANUALLY_CONTROLLED, + SERVICE_SET_MANUAL_CONTROL, SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, TURNING_OFF_DELAY, @@ -177,18 +177,16 @@ async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): ) -async def handle_set_manually_controlled( - switch: AdaptiveSwitch, service_call: ServiceCall -): - """Remove lights from the 'manually_controlled' list.""" +async def handle_set_manual_control(switch: AdaptiveSwitch, service_call: ServiceCall): + """Set or unset lights as 'manually controlled'.""" all_lights = _expand_light_groups(switch.hass, service_call.data[CONF_LIGHTS]) _LOGGER.debug( - "Called 'adaptive_lighting.set_manually_controlled' service with '%s'", + "Called 'adaptive_lighting.set_manual_control' service with '%s'", service_call.data, ) - if service_call.data[CONF_MANUALLY_CONTROLLED]: + if service_call.data[CONF_MANUAL_CONTROL]: for light in all_lights: - switch.turn_on_off_listener.manually_controlled[light] = True + switch.turn_on_off_listener.manual_control[light] = True _fire_manual_control_event(switch.hass, light, service_call.context) else: switch.turn_on_off_listener.reset(*all_lights) @@ -205,10 +203,8 @@ async def handle_set_manually_controlled( def _fire_manual_control_event( hass: HomeAssistant, light: str, context: Context, is_async=True ): - """Fire an event that 'light' is marked as manually_controlled.""" + """Fire an event that 'light' is marked as manual_control.""" fire = hass.bus.async_fire if is_async else hass.bus.fire - # Calling the event_type='manually_controlled' would be better, but - # event_type has a 32 character limit. fire(f"{DOMAIN}.manual_control", {ATTR_ENTITY_ID: light}, context=context) @@ -249,12 +245,12 @@ async def async_setup_entry( ) platform.async_register_entity_service( - SERVICE_SET_MANUALLY_CONTROLLED, + SERVICE_SET_MANUAL_CONTROL, { vol.Required(CONF_LIGHTS): cv.entity_ids, - vol.Optional(CONF_MANUALLY_CONTROLLED, default=True): cv.boolean, + vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean, }, - handle_set_manually_controlled, + handle_set_manual_control, ) @@ -564,12 +560,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Return the attributes of the switch.""" if not self.is_on: return {key: None for key in self._settings} - manually_controlled = [ + manual_control = [ light for light in self._lights - if self.turn_on_off_listener.manually_controlled.get(light) + if self.turn_on_off_listener.manual_control.get(light) ] - return dict(self._settings, manually_controlled=manually_controlled) + return dict(self._settings, manual_control=manual_control) def create_context(self, which: str = "default") -> Context: """Create a context that identifies this Adaptive Lighting instance.""" @@ -778,7 +774,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): entity_id, event.context.id, ) - self.turn_on_off_listener.reset(entity_id, reset_manually_controlled=False) + self.turn_on_off_listener.reset(entity_id, reset_manual_control=False) # Tracks 'off' → 'on' state changes self._off_to_on_event[entity_id] = event lock = self._locks.get(entity_id) @@ -1018,7 +1014,7 @@ class TurnOnOffListener: # Keep 'asyncio.sleep' tasks that can be cancelled by 'light.turn_on' events self.sleep_tasks: Dict[str, asyncio.Task] = {} # Tracks which lights are manually controlled - self.manually_controlled: Dict[str, bool] = {} + self.manual_control: Dict[str, bool] = {} # Counts the number of times (in a row) a light had a changed state. self.cnt_significant_changes: Dict[str, int] = defaultdict(int) # Track 'state_changed' events of self.lights resulting from this integration @@ -1037,11 +1033,11 @@ class TurnOnOffListener: EVENT_STATE_CHANGED, self.state_changed_event_listener ) - def reset(self, *lights, reset_manually_controlled=True) -> None: - """Reset the 'manually_controlled' status of the lights.""" + def reset(self, *lights, reset_manual_control=True) -> None: + """Reset the 'manual_control' status of the lights.""" for light in lights: - if reset_manually_controlled: - self.manually_controlled[light] = False + if reset_manual_control: + self.manual_control[light] = False self.last_state_change.pop(light, None) self.last_service_data.pop(light, None) self.cnt_significant_changes[light] = 0 @@ -1062,9 +1058,10 @@ class TurnOnOffListener: if service == SERVICE_TURN_OFF: transition = service_data.get(ATTR_TRANSITION) _LOGGER.debug( - "Detected an 'light.turn_off('%s', transition=%s)' event", + "Detected an 'light.turn_off('%s', transition=%s)' event with context.id='%s'", entity_ids, transition, + event.context.id, ) for eid in entity_ids: self.turn_off_event[eid] = event @@ -1114,9 +1111,10 @@ class TurnOnOffListener: # incorrect 'min_mireds' and 'max_mireds', which happens e.g., for # Philips Hue White GU10 Bluetooth lights). old_state: Optional[List[State]] = self.last_state_change.get(entity_id) - if old_state is None: - self.last_state_change[entity_id] = [new_state] - elif old_state[0].context.id == new_state.context.id: + if ( + old_state is not None + and old_state[0].context.id == new_state.context.id + ): # If there is already a state change event from this event (with this # context) then append it to the already existing list. _LOGGER.debug( @@ -1126,15 +1124,17 @@ class TurnOnOffListener: new_state.context.id, ) self.last_state_change[entity_id].append(new_state) + else: + self.last_state_change[entity_id] = [new_state] def is_manually_controlled( self, light: str, force: bool, ) -> bool: - """Check if the light has been 'on' and is now manually being adjusted.""" - manually_controlled = self.manually_controlled.setdefault(light, False) - if manually_controlled: + """Check if the light has been 'on' and is now manually controlled.""" + manual_control = self.manual_control.setdefault(light, False) + if manual_control: # Manually controlled until light is turned on and off return True @@ -1146,7 +1146,7 @@ class TurnOnOffListener: ): # Light was already on and 'light.turn_on' was not called by # the adaptive_lighting integration. - manually_controlled = self.manually_controlled[light] = True + manual_control = self.manual_control[light] = True _fire_manual_control_event(self.hass, light, turn_on_event.context) _LOGGER.debug( "'%s' was already on and 'light.turn_on' was not called by the" @@ -1156,7 +1156,7 @@ class TurnOnOffListener: light, turn_on_event.context.id, ) - return manually_controlled + return manual_control async def significant_change( self, @@ -1170,7 +1170,7 @@ class TurnOnOffListener: This method will detect changes that were made to the light without calling 'light.turn_on', so outside of Home Assistant. If a change is - detected, we mark the light as 'manually_controlled' until the light + detected, we mark the light as 'manually controlled' until the light or switch is turned 'off' and 'on' again. """ if light not in self.last_state_change: @@ -1225,7 +1225,7 @@ class TurnOnOffListener: # Only mark a light as significantly changing, if changed==True # N times in a row. We do this because sometimes a state changes # happens only *after* a new update interval has already started. - self.manually_controlled[light] = True + self.manual_control[light] = True _fire_manual_control_event(self.hass, light, context, is_async=False) else: if n_changes > 1: From 7ce7f4e2c18dff72ba88c2c248a34c5cd0f7f799 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 17 Oct 2020 15:43:24 +0200 Subject: [PATCH 0258/1077] fix tz bug --- custom_components/adaptive_lighting/__init__.py | 10 +++++++--- custom_components/adaptive_lighting/switch.py | 11 ++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index be8868f5..fb68b80f 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -100,12 +100,16 @@ async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool: ) data = hass.data[DOMAIN] data[config_entry.entry_id][UNDO_UPDATE_LISTENER]() - if len(data) == 1: # no more config_entries + if unload_ok: + data.pop(config_entry.entry_id) + + if len(data) == 1 and ATTR_TURN_ON_OFF_LISTENER in data: + # no more config_entries turn_on_off_listener = data.pop(ATTR_TURN_ON_OFF_LISTENER) turn_on_off_listener.remove_listener() turn_on_off_listener.remove_listener2() - if unload_ok: - data.pop(config_entry.entry_id) + if not data: + hass.data.pop(DOMAIN) return unload_ok diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 23d460e8..5aa91f7d 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -213,6 +213,7 @@ async def async_setup_entry( ): """Set up the AdaptiveLighting switch.""" data = hass.data[DOMAIN] + assert config_entry.entry_id in data if ATTR_TURN_ON_OFF_LISTENER not in data: data[ATTR_TURN_ON_OFF_LISTENER] = TurnOnOffListener(hass) @@ -880,16 +881,12 @@ class SunLightSettings: def _replace_time(date: datetime.datetime, key: str) -> datetime.datetime: time = getattr(self, f"{key}_time") - date_time = datetime.datetime.combine(datetime.date.today(), time) + date_time = datetime.datetime.combine(date, time) utc_time = self.time_zone.localize(date_time).astimezone(dt_util.UTC) - return date.replace( - hour=utc_time.hour, - minute=utc_time.minute, - second=utc_time.second, - microsecond=utc_time.microsecond, - ) + return utc_time location = self.astral_location + sunrise = ( location.sunrise(date, local=False) if self.sunrise_time is None From 4b31281cf58491788a931210a57942789d87f400 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 17 Oct 2020 16:54:15 +0200 Subject: [PATCH 0259/1077] remove long doc-string --- .../adaptive_lighting/__init__.py | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index fb68b80f..786daae2 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -1,29 +1,4 @@ -"""Adaptive Lighting integration in Home-Assistant. - -This integration calculates color temperature and brightness to synchronize -your color-changing lights with the perceived color temperature of the sky -throughout the day. This gives your environment a more natural feel, with -cooler whites during the midday and warmer tints near twilight and dawn. - -Additionally, the integration sets your lights to a nice warm white at 1% in -"Sleep mode", which is far brighter than starlight but won't reset your -circadian rhythm or break down too much rhodopsin in your eyes. - -Human circadian rhythms are heavily influenced by ambient light levels and -hues. Hormone production, brainwave activity, mood, and wakefulness are -just some of the cognitive functions tied to cyclical natural light. - -Resources: -- http://en.wikipedia.org/wiki/Zeitgeber -- http://www.cambridgeincolour.com/tutorials/sunrise-sunset-calculator.htm -- http://en.wikipedia.org/wiki/Color_temperature - -## Notes -* Only your location is taken into account to calculate the the sun's position. -* Weather is not considered. -* The integration does not calculate a true "Blue Hour" -- it just sets the - lights to 2700K (warm white) until your hub goes into "Sleep mode". -""" +"""Adaptive Lighting integration in Home-Assistant.""" import logging from typing import Any, Dict From 5aed27eb0a3325a813c75c4e450fed5f3b268bee Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 19 Oct 2020 20:43:58 +0200 Subject: [PATCH 0260/1077] small changes that came up in implementing tests --- custom_components/adaptive_lighting/config_flow.py | 2 +- custom_components/adaptive_lighting/const.py | 5 ----- custom_components/adaptive_lighting/switch.py | 14 +------------- 3 files changed, 2 insertions(+), 19 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 6bf5e430..8fa74f5c 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -84,7 +84,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Handle options flow.""" conf = self.config_entry if conf.source == config_entries.SOURCE_IMPORT: - return self.async_show_form(step_id="init", data_schema={}) + return self.async_show_form(step_id="init", data_schema=None) errors = {} if user_input is not None: validate_options(user_input, errors) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index a96e301b..2bd9db96 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -86,11 +86,6 @@ def timedelta_as_int(value): return value.total_seconds() -def join_strings(lst): - """Join a list to comma-separated values string.""" - return ",".join(lst) - - # conf_option: (validator, coerce) tuples # these validators cannot be serialized but can be serialized when coerced by coerce. EXTRA_VALIDATION = { diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 5aa91f7d..0736f7de 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -17,10 +17,6 @@ from typing import Any, Dict, List, Optional, Tuple, Union import astral import voluptuous as vol -from homeassistant.components.homeassistant import ( - DOMAIN as HA_DOMAIN, - SERVICE_UPDATE_ENTITY, -) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -158,8 +154,6 @@ def is_our_context(context: Optional[Context]) -> bool: async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): """Handle the entity service apply.""" - if not isinstance(switch, AdaptiveSwitch): - raise ValueError("Apply can only be called for a AdaptiveSwitch.") hass = switch.hass data = service_call.data all_lights = _expand_light_groups(hass, data[CONF_LIGHTS]) @@ -1173,13 +1167,7 @@ class TurnOnOffListener: if light not in self.last_state_change: return False old_states: List[State] = self.last_state_change[light] - await self.hass.services.async_call( - HA_DOMAIN, - SERVICE_UPDATE_ENTITY, - {ATTR_ENTITY_ID: light}, - blocking=True, - context=context, - ) + await self.hass.helpers.entity_component.async_update_entity(light) new_state = self.hass.states.get(light) compare_to = functools.partial( _attributes_have_changed, From 7555638b6f877687bb58344eb70372fd0f7593e2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 20 Oct 2020 00:06:10 +0200 Subject: [PATCH 0261/1077] add percent --- custom_components/adaptive_lighting/const.py | 1 + custom_components/adaptive_lighting/switch.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 2bd9db96..c9dfd3a5 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -36,6 +36,7 @@ CONF_SUNSET_TIME = "sunset_time" CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 45 +SLEEP_MODE_SWITCH = "sleep_mode_switch" ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener" UNDO_UPDATE_LISTENER = "undo_update_listener" NONE_STR = "None" diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 0736f7de..8c662cf6 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -102,6 +102,7 @@ from .const import ( ICON, SERVICE_APPLY, SERVICE_SET_MANUAL_CONTROL, + SLEEP_MODE_SWITCH, SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, TURNING_OFF_DELAY, @@ -216,7 +217,7 @@ async def async_setup_entry( sleep_mode_switch = AdaptiveSleepModeSwitch(hass, config_entry) switch = AdaptiveSwitch(hass, config_entry, turn_on_off_listener, sleep_mode_switch) - data[config_entry.entry_id]["sleep_mode_switch"] = sleep_mode_switch + data[config_entry.entry_id][SLEEP_MODE_SWITCH] = sleep_mode_switch data[config_entry.entry_id][SWITCH_DOMAIN] = switch async_add_entities([switch, sleep_mode_switch], update_before_add=True) @@ -987,6 +988,7 @@ class SunLightSettings: "rgb_color": rgb_color, "xy_color": xy_color, "hs_color": hs_color, + "sun_position": percent, } From d1ba4b1be8d0a18fb773a1707bad7d755b4d329d Mon Sep 17 00:00:00 2001 From: Fabian1185 <73140943+Fabian1185@users.noreply.github.com> Date: Wed, 21 Oct 2020 11:11:31 +0200 Subject: [PATCH 0262/1077] Added german translation de.json added --- .../adaptive_lighting/translations/de.json | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/de.json diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json new file mode 100644 index 00000000..3e9c015d --- /dev/null +++ b/custom_components/adaptive_lighting/translations/de.json @@ -0,0 +1,51 @@ +{ + "title": "Adaptive Lighting", + "config": { + "step": { + "user": { + "title": "Benenne das Adaptive Lighting", + "description": "Jede Instanz kann mehrere Licht Entitäten beinhalten", + "data": { + "name": "Name" + } + } + }, + "abort": { + "already_configured": "Gerät ist bereits konfiguriert!" + } + }, + "options": { + "step": { + "init": { + "title": "Adaptive Lighting Optionen", + "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde.", + "data": { + "lights": "Lichter", + "adapt_brightness": "adapt_brightness, Helligkeit anpassen", + "adapt_color_temp": "adapt_color_temp, Farbtemperatur anpassen, wenn color_temp unterstüzt", + "adapt_rgb_color": "adapt_rgb_color, Farbtemperatur anpassen, wenn RGB/XY unterstüzt", + "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt", + "interval": "interval, Zeit zwischen Updates des Switches", + "max_brightness": "max_brightness, maximale Helligkeit in %", + "max_color_temp": "max_color_temp, maximale Farbtemperatur in Kelvin", + "min_brightness": "min_brightness, minimale Helligkeit in %", + "min_color_temp": "min_color_temp, minimale Farbtemperatur in Kelvin", + "only_once": "only_once, passe die Lichter nur beim Einschalten an", + "prefer_rgb_color": "prefer_rgb_color, nutze 'rgb_color' vor 'color_temp', wenn möglich", + "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %", + "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperaturin Kelvin", + "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- seconds", + "sunrise_time": "sunrise_time, Sonnenaufgangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenaufgangs an deiner Position verwendet)", + "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- seconds", + "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)", + "take_over_control": "take_over_control, wenn irgendetwas während ein Licht an ist außer Adaptive Lighting den Service 'light.turn_on' aufruft, stoppe die Anpassung des Lichtes (oder des Schalters) bis dieser wieder von off -> on geschaltet wird.", + "detect_non_ha_changes": "detect_non_ha_changes, entdeckt alle Änderungen über 5% am Licht (auch außerhalb von HA gemacht), 'take_over_control' muss aktiviert sein (ruft 'homeassistant.update_entity' jede 'interval' auf!)", + "transition": "transition, Wechselzeit in Sekunden" + } + } + }, + "error": { + "option_error": "Fehlerhafte Option" + } + } +} From 8b6527fd14b41ae8b9f9971557811e87da5b60f8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 21 Oct 2020 23:05:12 +0200 Subject: [PATCH 0263/1077] define a adapt_brightness and adapt_color switch and watch for color or brightness related data in light.turn_on --- custom_components/adaptive_lighting/const.py | 10 +- custom_components/adaptive_lighting/switch.py | 201 ++++++++++++------ 2 files changed, 138 insertions(+), 73 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index c9dfd3a5..ccfeffa8 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -12,9 +12,6 @@ SUN_EVENT_MIDNIGHT = "solar_midnight" CONF_NAME, DEFAULT_NAME = "name", "default" CONF_LIGHTS, DEFAULT_LIGHTS = "lights", [] -CONF_ADAPT_BRIGHTNESS, DEFAULT_ADAPT_BRIGHTNESS = "adapt_brightness", True -CONF_ADAPT_COLOR_TEMP, DEFAULT_ADAPT_COLOR_TEMP = "adapt_color_temp", True -CONF_ADAPT_RGB_COLOR, DEFAULT_ADAPT_RGB_COLOR = "adapt_rgb_color", True CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = ( "detect_non_ha_changes", False, @@ -37,9 +34,13 @@ CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 45 SLEEP_MODE_SWITCH = "sleep_mode_switch" +ADAPT_COLOR_SWITCH = "adapt_color_switch" +ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch" ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener" UNDO_UPDATE_LISTENER = "undo_update_listener" NONE_STR = "None" +ATTR_ADAPT_COLOR = "adapt_color" +ATTR_ADAPT_BRIGHTNESS = "adapt_brightness" SERVICE_SET_MANUAL_CONTROL = "set_manual_control" CONF_MANUAL_CONTROL = "manual_control" @@ -56,9 +57,6 @@ def int_between(min_int, max_int): VALIDATION_TUPLES = [ (CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), - (CONF_ADAPT_BRIGHTNESS, DEFAULT_ADAPT_BRIGHTNESS, bool), - (CONF_ADAPT_COLOR_TEMP, DEFAULT_ADAPT_COLOR_TEMP, bool), - (CONF_ADAPT_RGB_COLOR, DEFAULT_ADAPT_RGB_COLOR, bool), (CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool), (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 8c662cf6..1f16680f 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -19,9 +19,17 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_BRIGHTNESS_STEP, + ATTR_BRIGHTNESS_STEP_PCT, + ATTR_COLOR_NAME, ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, + ATTR_WHITE_VALUE, + ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, @@ -64,6 +72,7 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.sun import get_astral_location +from homeassistant.util import slugify from homeassistant.util.color import ( color_RGB_to_xy, color_temperature_kelvin_to_mired, @@ -73,10 +82,11 @@ from homeassistant.util.color import ( import homeassistant.util.dt as dt_util from .const import ( + ADAPT_BRIGHTNESS_SWITCH, + ADAPT_COLOR_SWITCH, + ATTR_ADAPT_BRIGHTNESS, + ATTR_ADAPT_COLOR, ATTR_TURN_ON_OFF_LISTENER, - CONF_ADAPT_BRIGHTNESS, - CONF_ADAPT_COLOR_TEMP, - CONF_ADAPT_RGB_COLOR, CONF_DETECT_NON_HA_CHANGES, CONF_INITIAL_TRANSITION, CONF_INTERVAL, @@ -129,6 +139,23 @@ BRIGHTNESS_CHANGE = 25 # ≈10% of total range COLOR_TEMP_CHANGE = 20 # ≈5% of total range RGB_REDMEAN_CHANGE = 80 # ≈10% of total range +COLOR_ATTRS = { # Should ATTR_PROFILE be in here? + ATTR_COLOR_NAME, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_KELVIN, + ATTR_RGB_COLOR, + ATTR_WHITE_VALUE, # Should this be here? + ATTR_XY_COLOR, +} + +BRIGHTNESS_ATTRS = { + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_BRIGHTNESS_STEP, + ATTR_BRIGHTNESS_STEP_PCT, +} + # Keep a short domain version for the context instances (which can only be 36 chars) _DOMAIN_SHORT = "adapt_lgt" @@ -165,9 +192,8 @@ async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): await switch._adapt_light( # pylint: disable=protected-access light, data[CONF_TRANSITION], - data[CONF_ADAPT_BRIGHTNESS], - data[CONF_ADAPT_COLOR_TEMP], - data[CONF_ADAPT_RGB_COLOR], + data[ATTR_ADAPT_BRIGHTNESS], + data[ATTR_ADAPT_COLOR], force=True, ) @@ -214,13 +240,27 @@ async def async_setup_entry( data[ATTR_TURN_ON_OFF_LISTENER] = TurnOnOffListener(hass) turn_on_off_listener = data[ATTR_TURN_ON_OFF_LISTENER] - sleep_mode_switch = AdaptiveSleepModeSwitch(hass, config_entry) - switch = AdaptiveSwitch(hass, config_entry, turn_on_off_listener, sleep_mode_switch) + sleep_mode_switch = SimpleSwitch("Sleep Mode", False, hass, config_entry) + adapt_color_switch = SimpleSwitch("Adapt Color", True, hass, config_entry) + adapt_brightness_switch = SimpleSwitch("Adapt Brightness", True, hass, config_entry) + switch = AdaptiveSwitch( + hass, + config_entry, + turn_on_off_listener, + sleep_mode_switch, + adapt_color_switch, + adapt_brightness_switch, + ) data[config_entry.entry_id][SLEEP_MODE_SWITCH] = sleep_mode_switch + data[config_entry.entry_id][ADAPT_COLOR_SWITCH] = adapt_color_switch + data[config_entry.entry_id][ADAPT_BRIGHTNESS_SWITCH] = adapt_brightness_switch data[config_entry.entry_id][SWITCH_DOMAIN] = switch - async_add_entities([switch, sleep_mode_switch], update_before_add=True) + async_add_entities( + [switch, sleep_mode_switch, adapt_color_switch, adapt_brightness_switch], + update_before_add=True, + ) # Register `apply` service platform = entity_platform.current_platform.get() @@ -232,9 +272,8 @@ async def async_setup_entry( CONF_TRANSITION, default=switch._initial_transition, # pylint: disable=protected-access ): VALID_TRANSITION, - vol.Optional(CONF_ADAPT_BRIGHTNESS, default=True): cv.boolean, - vol.Optional(CONF_ADAPT_COLOR_TEMP, default=True): cv.boolean, - vol.Optional(CONF_ADAPT_RGB_COLOR, default=True): cv.boolean, + vol.Optional(ATTR_ADAPT_BRIGHTNESS, default=True): cv.boolean, + vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean, vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean, }, handle_apply, @@ -264,7 +303,7 @@ def validate(config_entry: ConfigEntry): return data -def match_state_event(event: Event, from_or_to_state: List[str]): +def match_switch_state_event(event: Event, from_or_to_state: List[str]): """Match state event when either 'from_state' or 'to_state' matches.""" old_state = event.data.get("old_state") from_state_match = old_state is not None and old_state.state in from_or_to_state @@ -324,8 +363,7 @@ def _attributes_have_changed( old_attributes: Dict[str, Any], new_attributes: Dict[str, Any], adapt_brightness: bool, - adapt_color_temp: bool, - adapt_rgb_color: bool, + adapt_color: bool, context: Context, ) -> bool: if ( @@ -347,7 +385,7 @@ def _attributes_have_changed( return True if ( - adapt_color_temp + adapt_color and ATTR_COLOR_TEMP in old_attributes and ATTR_COLOR_TEMP in new_attributes ): @@ -365,7 +403,7 @@ def _attributes_have_changed( return True if ( - adapt_rgb_color + adapt_color and ATTR_RGB_COLOR in old_attributes and ATTR_RGB_COLOR in new_attributes ): @@ -407,20 +445,21 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): hass, config_entry: ConfigEntry, turn_on_off_listener: TurnOnOffListener, - sleep_mode_switch: AdaptiveSleepModeSwitch, + sleep_mode_switch: SimpleSwitch, + adapt_color_switch: SimpleSwitch, + adapt_brightness_switch: SimpleSwitch, ): """Initialize the Adaptive Lighting switch.""" self.hass = hass self.turn_on_off_listener = turn_on_off_listener self.sleep_mode_switch = sleep_mode_switch + self.adapt_color_switch = adapt_color_switch + self.adapt_brightness_switch = adapt_brightness_switch data = validate(config_entry) self._name = data[CONF_NAME] self._lights = data[CONF_LIGHTS] - self._adapt_brightness = data[CONF_ADAPT_BRIGHTNESS] - self._adapt_color_temp = data[CONF_ADAPT_COLOR_TEMP] - self._adapt_rgb_color = data[CONF_ADAPT_RGB_COLOR] self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] self._initial_transition = data[CONF_INITIAL_TRANSITION] self._interval = data[CONF_INTERVAL] @@ -527,12 +566,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): remove_interval = async_track_time_interval( self.hass, self._async_update_at_interval, self._interval ) - remove_sleep = async_track_state_change_event( - self.hass, - self.sleep_mode_switch.entity_id, - self._sleep_state_event, - ) - self.remove_listeners.extend([remove_interval, remove_sleep]) + remove_simple_switches = [ + async_track_state_change_event( + self.hass, + switch.entity_id, + self._simple_switch_state_event, + ) + for switch in ( + self.sleep_mode_switch, + self.adapt_brightness_switch, + self.adapt_color_switch, + ) + ] + + self.remove_listeners.extend([remove_interval, *remove_simple_switches]) if self._lights: self._expand_light_groups() @@ -614,8 +661,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): light: str, transition: Optional[int] = None, adapt_brightness: Optional[bool] = None, - adapt_color_temp: Optional[bool] = None, - adapt_rgb_color: Optional[bool] = None, + adapt_color: Optional[bool] = None, force: bool = False, context: Optional[Context] = None, ) -> None: @@ -629,11 +675,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if transition is None: transition = self._transition if adapt_brightness is None: - adapt_brightness = self._adapt_brightness - if adapt_color_temp is None: - adapt_color_temp = self._adapt_color_temp - if adapt_rgb_color is None: - adapt_rgb_color = self._adapt_rgb_color + adapt_brightness = self.adapt_brightness_switch.is_on + if adapt_color is None: + adapt_color = self.adapt_color_switch.is_on if "transition" in features: service_data[ATTR_TRANSITION] = transition @@ -644,7 +688,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if ( "color_temp" in features - and adapt_color_temp + and adapt_color and not (self._prefer_rgb_color and "color" in features) ): attributes = self.hass.states.get(light).attributes @@ -652,7 +696,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): color_temp_mired = self._settings["color_temp_mired"] color_temp_mired = max(min(color_temp_mired, max_mireds), min_mireds) service_data[ATTR_COLOR_TEMP] = color_temp_mired - elif "color" in features and adapt_rgb_color: + elif "color" in features and adapt_color: service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"] context = context or self.create_context("adapt_lights") @@ -662,9 +706,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): and not force and await self.turn_on_off_listener.significant_change( light, - self._adapt_brightness, - self._adapt_color_temp, - self._adapt_rgb_color, + adapt_brightness, + adapt_color, context, ) ): @@ -732,6 +775,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): and self.turn_on_off_listener.is_manually_controlled( light, force, + self.adapt_brightness_switch.is_on, + self.adapt_color_switch.is_on, ) ): _LOGGER.debug( @@ -743,15 +788,22 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): continue await self._adapt_light(light, transition, force=force, context=context) - async def _sleep_state_event(self, event: Event) -> None: - if not match_state_event(event, (STATE_ON, STATE_OFF)): + async def _simple_switch_state_event(self, event: Event) -> None: + if not match_switch_state_event(event, (STATE_ON, STATE_OFF)): return - _LOGGER.debug("%s: _sleep_state_event, event: '%s'", self._name, event) - self.turn_on_off_listener.reset(*self._lights) + which = { + self.sleep_mode_switch.entity_id: "sleep_sw", + self.adapt_color_switch.entity_id: "color_sw", + self.adapt_brightness_switch.entity_id: "brigt_sw", + }[event.data[ATTR_ENTITY_ID]] + _LOGGER.debug("%s: _simple_switch_state_event, event: '%s'", self._name, event) + if which == "sleep_sw": + # Reset the manually controlled status when the "sleep mode" changes + self.turn_on_off_listener.reset(*self._lights) await self._update_attrs_and_maybe_adapt_lights( transition=self._initial_transition, force=True, - context=self.create_context("sleep"), + context=self.create_context(which), ) async def _light_event(self, event: Event) -> None: @@ -805,26 +857,32 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self.turn_on_off_listener.reset(entity_id) -class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity): +class SimpleSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" - def __init__(self, hass: HomeAssistant, config_entry): + def __init__( + self, which: str, initial_state: bool, hass: HomeAssistant, config_entry + ): """Initialize the Adaptive Lighting switch.""" self.hass = hass data = validate(config_entry) self._name = data[CONF_NAME] self._icon = ICON self._state = None + self._which = which + self._unique_id = f"{self._name}_{slugify(self._which)}" + self._name = f"Adaptive Lighting {which}: {self._name}" + self._initial_state = initial_state @property def name(self): """Return the name of the device if any.""" - return f"Adaptive Lighting Sleep Mode: {self._name}" + return self._name @property def unique_id(self): """Return the unique ID of entity.""" - return f"{self._name}_sleep_mode" + return self._unique_id @property def icon(self) -> str: @@ -839,10 +897,15 @@ class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity): async def async_added_to_hass(self) -> None: """Call when entity about to be added to hass.""" last_state = await self.async_get_last_state() - if last_state is None or STATE_OFF: # newly added to HA - await self.async_turn_off() - else: + if last_state is None: # newly added to HA + if self._initial_state: + await self.async_turn_on() + else: + await self.async_turn_off() + elif STATE_ON: await self.async_turn_on() + elif STATE_OFF: + await self.async_turn_off() async def async_turn_on(self, **kwargs) -> None: """Turn on adaptive lighting sleep mode.""" @@ -1124,6 +1187,8 @@ class TurnOnOffListener: self, light: str, force: bool, + adapt_brightness: bool, + adapt_color: bool, ) -> bool: """Check if the light has been 'on' and is now manually controlled.""" manual_control = self.manual_control.setdefault(light, False) @@ -1137,26 +1202,29 @@ class TurnOnOffListener: and not is_our_context(turn_on_event.context) and not force ): - # Light was already on and 'light.turn_on' was not called by - # the adaptive_lighting integration. - manual_control = self.manual_control[light] = True - _fire_manual_control_event(self.hass, light, turn_on_event.context) - _LOGGER.debug( - "'%s' was already on and 'light.turn_on' was not called by the" - " adaptive_lighting integration (context.id='%s'), the Adaptive" - " Lighting will stop adapting the light until the switch or the" - " light turns off and then on again.", - light, - turn_on_event.context.id, - ) + keys = turn_on_event.data[ATTR_SERVICE_DATA].keys() + if (adapt_color and COLOR_ATTRS.intersection(keys)) or ( + adapt_brightness and BRIGHTNESS_ATTRS.intersection(keys) + ): + # Light was already on and 'light.turn_on' was not called by + # the adaptive_lighting integration. + manual_control = self.manual_control[light] = True + _fire_manual_control_event(self.hass, light, turn_on_event.context) + _LOGGER.debug( + "'%s' was already on and 'light.turn_on' was not called by the" + " adaptive_lighting integration (context.id='%s'), the Adaptive" + " Lighting will stop adapting the light until the switch or the" + " light turns off and then on again.", + light, + turn_on_event.context.id, + ) return manual_control async def significant_change( self, light: str, adapt_brightness: bool, - adapt_color_temp: bool, - adapt_rgb_color: bool, + adapt_color: bool, context: Context, ) -> bool: """Has the light made a significant change since last update. @@ -1176,8 +1244,7 @@ class TurnOnOffListener: light=light, new_attributes=new_state.attributes, adapt_brightness=adapt_brightness, - adapt_color_temp=adapt_color_temp, - adapt_rgb_color=adapt_rgb_color, + adapt_color=adapt_color, context=context, ) for index, old_state in enumerate(old_states): From 90e8544954db87e09eb0479c26b5688e069d7f67 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 21 Oct 2020 23:14:20 +0200 Subject: [PATCH 0264/1077] no need to track the state of adapt_brightness and color switches --- custom_components/adaptive_lighting/switch.py | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 1f16680f..cf7bbdb9 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -566,20 +566,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): remove_interval = async_track_time_interval( self.hass, self._async_update_at_interval, self._interval ) - remove_simple_switches = [ - async_track_state_change_event( - self.hass, - switch.entity_id, - self._simple_switch_state_event, - ) - for switch in ( - self.sleep_mode_switch, - self.adapt_brightness_switch, - self.adapt_color_switch, - ) - ] + remove_sleep = async_track_state_change_event( + self.hass, + self.sleep_mode_switch.entity_id, + self._sleep_mode_switch_state_event, + ) - self.remove_listeners.extend([remove_interval, *remove_simple_switches]) + self.remove_listeners.extend([remove_interval, remove_sleep]) if self._lights: self._expand_light_groups() @@ -788,22 +781,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): continue await self._adapt_light(light, transition, force=force, context=context) - async def _simple_switch_state_event(self, event: Event) -> None: + async def _sleep_mode_switch_state_event(self, event: Event) -> None: if not match_switch_state_event(event, (STATE_ON, STATE_OFF)): return - which = { - self.sleep_mode_switch.entity_id: "sleep_sw", - self.adapt_color_switch.entity_id: "color_sw", - self.adapt_brightness_switch.entity_id: "brigt_sw", - }[event.data[ATTR_ENTITY_ID]] - _LOGGER.debug("%s: _simple_switch_state_event, event: '%s'", self._name, event) - if which == "sleep_sw": - # Reset the manually controlled status when the "sleep mode" changes - self.turn_on_off_listener.reset(*self._lights) + _LOGGER.debug( + "%s: _sleep_mode_switch_state_event, event: '%s'", self._name, event + ) + # Reset the manually controlled status when the "sleep mode" changes + self.turn_on_off_listener.reset(*self._lights) await self._update_attrs_and_maybe_adapt_lights( transition=self._initial_transition, force=True, - context=self.create_context(which), + context=self.create_context("sleep"), ) async def _light_event(self, event: Event) -> None: From 9d0d069c4b771347da470039989c2764cc0dd917 Mon Sep 17 00:00:00 2001 From: Martin Myhrman Date: Thu, 22 Oct 2020 10:37:08 +0200 Subject: [PATCH 0265/1077] Add translation for Swedish --- .../adaptive_lighting/translations/sv.json | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/sv.json diff --git a/custom_components/adaptive_lighting/translations/sv.json b/custom_components/adaptive_lighting/translations/sv.json new file mode 100644 index 00000000..ed288402 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/sv.json @@ -0,0 +1,51 @@ +{ + "title": "Adaptiv Ljussättning", + "config": { + "step": { + "user": { + "title": "Välj ett namn för Adaptiv Ljussättning", + "description": "Varje konfiguration kan innehålla flera ljuskällor!", + "data": { + "name": "Namn" + } + } + }, + "abort": { + "already_configured": "Enheten är redan konfiguerad" + } + }, + "options": { + "step": { + "init": { + "title": "Adaptiv Ljussättning Inställningar", + "description": "Alla inställningar för en Adaptiv Ljussättning komponent. Titeln på inställningarna är desamma som i YAML konfigurationen. Inga inställningar visas om enheten redan är konfigurerad i YAML.", + "data": { + "lights": "lights, ljuskällor", + "adapt_brightness": "adapt_brightness, Adaptiv ljusstyrka", + "adapt_color_temp": "adapt_color_temp, Justera färgtemperatur genom att använda 'color_temp' om möjligt", + "adapt_rgb_color": "adapt_rgb_color, Justera färgtemperatur genom att använda RGB/XY om möjligt", + "initial_transition": "initial_transition, när ljuskällorna går från 'av' till 'på' eller när 'sleep_state' ändras", + "interval": "interval, Tid mellan uppdateringar i sekunder", + "max_brightness": "max_brightness, i procent %", + "max_color_temp": "max_color_temp, i Kelvin", + "min_brightness": "min_brightness, i %", + "min_color_temp": "min_color_temp, i Kelvin", + "only_once": "only_once, Adaptivt justera endast ljuskällorna när de sätts från 'av' till 'på'", + "prefer_rgb_color": "prefer_rgb_color, Använd 'rgb_color' över 'color_temp' om möjligt", + "sleep_brightness": "sleep_brightness, i %", + "sleep_color_temp": "sleep_color_temp, i Kelvin", + "sunrise_offset": "sunrise_offset, i +/- sekunder", + "sunrise_time": "sunrise_time, i 'HH:MM:SS' format (om 'None', används den faktiskta soluppgången för din position)", + "sunset_offset": "sunset_offset, i +/- sekunder", + "sunset_time": "sunset_time, i 'HH:MM:SS' format (om 'None', används den faktiskta solnedgången för din position)", + "take_over_control": "take_over_control, om något utöver 'Adaptiv Ljussättning' komponenten kallar på 'light.turn_on' när en ljuskälla redan är på, stängs den adaptiva justeringen av tills ljuskällan stängs av -> på igen, alternativt switchen för konfigurationen", + "detect_non_ha_changes": "detect_non_ha_changes, Upptäcker alla ändringar större än 5% gjorda på ljuskällorna som inte kommer från HA. Kräver att 'take_over_control' är påslaget.(Kallar på 'homeassistant.update_entity' vid varje 'interval'!)", + "transition": "transition, i sekunder" + } + } + }, + "error": { + "option_error": "Ogiltlig inställning" + } + } +} From 4de7e8d1e0f3935b6269cf86e7bf0b3684152296 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 22 Oct 2020 11:15:38 +0200 Subject: [PATCH 0266/1077] fix issue with initial state --- .../adaptive_lighting/strings.json | 3 - custom_components/adaptive_lighting/switch.py | 13 +-- .../adaptive_lighting/translations/en.json | 89 +++++++++---------- 3 files changed, 47 insertions(+), 58 deletions(-) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index d45edc4b..fe6d0dd8 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -21,9 +21,6 @@ "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", "data": { "lights": "lights", - "adapt_brightness": "adapt_brightness", - "adapt_color_temp": "adapt_color_temp, adapt color temperature using 'color_temp' if supported", - "adapt_rgb_color": "adapt_rgb_color, adapt color temperature using RGB/XY if supported", "initial_transition": "initial_transition, when lights go 'off' to 'on' or when 'sleep_state' changes", "interval": "interval, time between switch updates in seconds", "max_brightness": "max_brightness, in %", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index cf7bbdb9..611bd21a 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -855,12 +855,11 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): """Initialize the Adaptive Lighting switch.""" self.hass = hass data = validate(config_entry) - self._name = data[CONF_NAME] self._icon = ICON self._state = None self._which = which self._unique_id = f"{self._name}_{slugify(self._which)}" - self._name = f"Adaptive Lighting {which}: {self._name}" + self._name = f"Adaptive Lighting {which}: {data[CONF_NAME]}" self._initial_state = initial_state @property @@ -886,14 +885,10 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): async def async_added_to_hass(self) -> None: """Call when entity about to be added to hass.""" last_state = await self.async_get_last_state() - if last_state is None: # newly added to HA - if self._initial_state: - await self.async_turn_on() - else: - await self.async_turn_off() - elif STATE_ON: + _LOGGER.debug("%s: last state is %s", self._name, last_state) + if (last_state is None and self._initial_state) or last_state.state == STATE_ON: await self.async_turn_on() - elif STATE_OFF: + else: await self.async_turn_off() async def async_turn_on(self, **kwargs) -> None: diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index f306cf60..dc26be90 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -1,51 +1,48 @@ { - "title": "Adaptive Lighting", - "config": { - "step": { - "user": { - "title": "Choose a name for the Adaptive Lighting", - "description": "Every instance can contain multiple lights!", - "data": { - "name": "Name" + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "step": { + "user": { + "data": { + "name": "Name" + }, + "description": "Every instance can contain multiple lights!", + "title": "Choose a name for the Adaptive Lighting" + } } - } }, - "abort": { - "already_configured": "Device is already configured" - } - }, - "options": { - "step": { - "init": { - "title": "Adaptive Lighting options", - "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", - "data": { - "lights": "lights", - "adapt_brightness": "adapt_brightness", - "adapt_color_temp": "adapt_color_temp, adapt color temperature using 'color_temp' if supported", - "adapt_rgb_color": "adapt_rgb_color, adapt color temperature using RGB/XY if supported", - "initial_transition": "initial_transition, when lights go 'off' to 'on' or when 'sleep_state' changes", - "interval": "interval, time between switch updates in seconds", - "max_brightness": "max_brightness, in %", - "max_color_temp": "max_color_temp, in Kelvin", - "min_brightness": "min_brightness, in %", - "min_color_temp": "min_color_temp, in Kelvin", - "only_once": "only_once, only adapt the lights when turning them on", - "prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible", - "sleep_brightness": "sleep_brightness, in %", - "sleep_color_temp": "sleep_color_temp, in Kelvin", - "sunrise_offset": "sunrise_offset, in +/- seconds", - "sunrise_time": "sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location)", - "sunset_offset": "sunset_offset, in +/- seconds", - "sunset_time": "sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location)", - "take_over_control": "take_over_control, if anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", - "detect_non_ha_changes": "detect_non_ha_changes, detects all >5% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", - "transition": "transition, in seconds" + "options": { + "error": { + "option_error": "Invalid option" + }, + "step": { + "init": { + "data": { + "detect_non_ha_changes": "detect_non_ha_changes, detects all >5% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", + "initial_transition": "initial_transition, when lights go 'off' to 'on' or when 'sleep_state' changes", + "interval": "interval, time between switch updates in seconds", + "lights": "lights", + "max_brightness": "max_brightness, in %", + "max_color_temp": "max_color_temp, in Kelvin", + "min_brightness": "min_brightness, in %", + "min_color_temp": "min_color_temp, in Kelvin", + "only_once": "only_once, only adapt the lights when turning them on", + "prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible", + "sleep_brightness": "sleep_brightness, in %", + "sleep_color_temp": "sleep_color_temp, in Kelvin", + "sunrise_offset": "sunrise_offset, in +/- seconds", + "sunrise_time": "sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location)", + "sunset_offset": "sunset_offset, in +/- seconds", + "sunset_time": "sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location)", + "take_over_control": "take_over_control, if anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", + "transition": "transition, in seconds" + }, + "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", + "title": "Adaptive Lighting options" + } } - } }, - "error": { - "option_error": "Invalid option" - } - } -} + "title": "Adaptive Lighting" +} \ No newline at end of file From da0b2552b9667b840ecc3b82bfe160fa05294e89 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 22 Oct 2020 12:34:36 +0200 Subject: [PATCH 0267/1077] fix name bug --- custom_components/adaptive_lighting/switch.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 611bd21a..c3979028 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -858,8 +858,9 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): self._icon = ICON self._state = None self._which = which - self._unique_id = f"{self._name}_{slugify(self._which)}" - self._name = f"Adaptive Lighting {which}: {data[CONF_NAME]}" + name = data[CONF_NAME] + self._unique_id = f"{name}_{slugify(self._which)}" + self._name = f"Adaptive Lighting {which}: {name}" self._initial_state = initial_state @property From 4ffa6fc0e4ee313764aba824dca9e5fb9c0cf06c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 22 Oct 2020 23:04:35 +0200 Subject: [PATCH 0268/1077] fix bug: make sure 'last_state is not None' --- custom_components/adaptive_lighting/switch.py | 4 +++- custom_components/adaptive_lighting/translations/de.json | 9 +++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index c3979028..2d09eaaf 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -887,7 +887,9 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): """Call when entity about to be added to hass.""" last_state = await self.async_get_last_state() _LOGGER.debug("%s: last state is %s", self._name, last_state) - if (last_state is None and self._initial_state) or last_state.state == STATE_ON: + if (last_state is None and self._initial_state) or ( + last_state is not None and last_state.state == STATE_ON + ): await self.async_turn_on() else: await self.async_turn_off() diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json index 3e9c015d..b8d06afd 100644 --- a/custom_components/adaptive_lighting/translations/de.json +++ b/custom_components/adaptive_lighting/translations/de.json @@ -18,12 +18,9 @@ "step": { "init": { "title": "Adaptive Lighting Optionen", - "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde.", + "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde.", "data": { "lights": "Lichter", - "adapt_brightness": "adapt_brightness, Helligkeit anpassen", - "adapt_color_temp": "adapt_color_temp, Farbtemperatur anpassen, wenn color_temp unterstüzt", - "adapt_rgb_color": "adapt_rgb_color, Farbtemperatur anpassen, wenn RGB/XY unterstüzt", "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt", "interval": "interval, Zeit zwischen Updates des Switches", "max_brightness": "max_brightness, maximale Helligkeit in %", @@ -39,7 +36,7 @@ "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- seconds", "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)", "take_over_control": "take_over_control, wenn irgendetwas während ein Licht an ist außer Adaptive Lighting den Service 'light.turn_on' aufruft, stoppe die Anpassung des Lichtes (oder des Schalters) bis dieser wieder von off -> on geschaltet wird.", - "detect_non_ha_changes": "detect_non_ha_changes, entdeckt alle Änderungen über 5% am Licht (auch außerhalb von HA gemacht), 'take_over_control' muss aktiviert sein (ruft 'homeassistant.update_entity' jede 'interval' auf!)", + "detect_non_ha_changes": "detect_non_ha_changes, entdeckt alle Änderungen über 10% am Licht (auch außerhalb von HA gemacht), 'take_over_control' muss aktiviert sein (ruft 'homeassistant.update_entity' jede 'interval' auf!)", "transition": "transition, Wechselzeit in Sekunden" } } @@ -48,4 +45,4 @@ "option_error": "Fehlerhafte Option" } } -} +} \ No newline at end of file From 3352fa3b9c51982b4460827ddc796e5e7edfd4f3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 25 Oct 2020 09:35:54 +0100 Subject: [PATCH 0269/1077] fix apply service, closes #24 --- .../adaptive_lighting/services.yaml | 10 +-- custom_components/adaptive_lighting/switch.py | 7 +- .../adaptive_lighting/translations/en.json | 86 +++++++++---------- 3 files changed, 54 insertions(+), 49 deletions(-) diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index ba56b77b..71d723e0 100755 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -13,12 +13,12 @@ apply: adapt_brightness: description: "Adapt the 'brightness', default: true" example: true - adapt_color_temp: - description: "Adapt the 'color_temp', default: true" - example: true - adapt_rgb_color: - description: "Adapt the 'rgb_color', default: true" + adapt_color: + description: "Adapt the color_temp/color_rgb, default: true" example: true + prefer_rgb_color: + description: "Prefer to use color_rgb over color_temp if possible, default: false" + example: false turn_on_lights: description: "Turn on the lights that are off, default: false" example: false diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 2d09eaaf..6982494c 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -194,6 +194,7 @@ async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): data[CONF_TRANSITION], data[ATTR_ADAPT_BRIGHTNESS], data[ATTR_ADAPT_COLOR], + data[CONF_PREFER_RGB_COLOR], force=True, ) @@ -274,6 +275,7 @@ async def async_setup_entry( ): VALID_TRANSITION, vol.Optional(ATTR_ADAPT_BRIGHTNESS, default=True): cv.boolean, vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean, + vol.Optional(CONF_PREFER_RGB_COLOR, default=False): cv.boolean, vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean, }, handle_apply, @@ -655,6 +657,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): transition: Optional[int] = None, adapt_brightness: Optional[bool] = None, adapt_color: Optional[bool] = None, + prefer_rgb_color: Optional[bool] = None, force: bool = False, context: Optional[Context] = None, ) -> None: @@ -671,6 +674,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): adapt_brightness = self.adapt_brightness_switch.is_on if adapt_color is None: adapt_color = self.adapt_color_switch.is_on + if prefer_rgb_color is None: + prefer_rgb_color = self._prefer_rgb_color if "transition" in features: service_data[ATTR_TRANSITION] = transition @@ -682,7 +687,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if ( "color_temp" in features and adapt_color - and not (self._prefer_rgb_color and "color" in features) + and not (prefer_rgb_color and "color" in features) ): attributes = self.hass.states.get(light).attributes min_mireds, max_mireds = attributes["min_mireds"], attributes["max_mireds"] diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index dc26be90..c2c31dbb 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -1,48 +1,48 @@ { - "config": { - "abort": { - "already_configured": "Device is already configured" - }, - "step": { - "user": { - "data": { - "name": "Name" - }, - "description": "Every instance can contain multiple lights!", - "title": "Choose a name for the Adaptive Lighting" - } + "title": "Adaptive Lighting", + "config": { + "step": { + "user": { + "title": "Choose a name for the Adaptive Lighting", + "description": "Every instance can contain multiple lights!", + "data": { + "name": "Name" } + } }, - "options": { - "error": { - "option_error": "Invalid option" - }, - "step": { - "init": { - "data": { - "detect_non_ha_changes": "detect_non_ha_changes, detects all >5% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", - "initial_transition": "initial_transition, when lights go 'off' to 'on' or when 'sleep_state' changes", - "interval": "interval, time between switch updates in seconds", - "lights": "lights", - "max_brightness": "max_brightness, in %", - "max_color_temp": "max_color_temp, in Kelvin", - "min_brightness": "min_brightness, in %", - "min_color_temp": "min_color_temp, in Kelvin", - "only_once": "only_once, only adapt the lights when turning them on", - "prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible", - "sleep_brightness": "sleep_brightness, in %", - "sleep_color_temp": "sleep_color_temp, in Kelvin", - "sunrise_offset": "sunrise_offset, in +/- seconds", - "sunrise_time": "sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location)", - "sunset_offset": "sunset_offset, in +/- seconds", - "sunset_time": "sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location)", - "take_over_control": "take_over_control, if anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", - "transition": "transition, in seconds" - }, - "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", - "title": "Adaptive Lighting options" - } + "abort": { + "already_configured": "Device is already configured" + } + }, + "options": { + "step": { + "init": { + "title": "Adaptive Lighting options", + "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", + "data": { + "lights": "lights", + "initial_transition": "initial_transition, when lights go 'off' to 'on' or when 'sleep_state' changes", + "interval": "interval, time between switch updates in seconds", + "max_brightness": "max_brightness, in %", + "max_color_temp": "max_color_temp, in Kelvin", + "min_brightness": "min_brightness, in %", + "min_color_temp": "min_color_temp, in Kelvin", + "only_once": "only_once, only adapt the lights when turning them on", + "prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible", + "sleep_brightness": "sleep_brightness, in %", + "sleep_color_temp": "sleep_color_temp, in Kelvin", + "sunrise_offset": "sunrise_offset, in +/- seconds", + "sunrise_time": "sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location)", + "sunset_offset": "sunset_offset, in +/- seconds", + "sunset_time": "sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location)", + "take_over_control": "take_over_control, if anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", + "detect_non_ha_changes": "detect_non_ha_changes, detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", + "transition": "transition, in seconds" } + } }, - "title": "Adaptive Lighting" -} \ No newline at end of file + "error": { + "option_error": "Invalid option" + } + } +} From f92a4b9e0aa83eea518651758407501c41e2522b Mon Sep 17 00:00:00 2001 From: Matt Forster Date: Mon, 26 Oct 2020 13:34:58 -0600 Subject: [PATCH 0270/1077] feat: add white value to service data White value is available as a channel for brightness. This adds the support for lights that use it, using the same calculated value as brightness. --- custom_components/adaptive_lighting/switch.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 6982494c..13159769 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -22,6 +22,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS_STEP_PCT, + ATTR_WHITE_VALUE, ATTR_COLOR_NAME, ATTR_COLOR_TEMP, ATTR_HS_COLOR, @@ -35,6 +36,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, + SUPPORT_WHITE_VALUE, VALID_TRANSITION, is_on, ) @@ -122,6 +124,7 @@ from .const import ( _SUPPORT_OPTS = { "brightness": SUPPORT_BRIGHTNESS, + "white_value": SUPPORT_WHITE_VALUE, "color_temp": SUPPORT_COLOR_TEMP, "color": SUPPORT_COLOR, "transition": SUPPORT_TRANSITION, @@ -145,12 +148,12 @@ COLOR_ATTRS = { # Should ATTR_PROFILE be in here? ATTR_HS_COLOR, ATTR_KELVIN, ATTR_RGB_COLOR, - ATTR_WHITE_VALUE, # Should this be here? ATTR_XY_COLOR, } BRIGHTNESS_ATTRS = { ATTR_BRIGHTNESS, + ATTR_WHITE_VALUE, ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS_STEP_PCT, @@ -386,6 +389,25 @@ def _attributes_have_changed( ) return True + # White value is treated the same as brightness in all respects + if ( + adapt_brightness + and ATTR_WHITE_VALUE in old_attributes + and ATTR_WHITE_VALUE in new_attributes + ): + last_white_value = old_attributes[ATTR_WHITE_VALUE] + current_white_value = new_attributes[ATTR_WHITE_VALUE] + if abs(current_white_value - last_white_value) > BRIGHTNESS_CHANGE: + _LOGGER.debug( + "White Value of '%s' significantly changed from %s to %s with" + " context.id='%s'", + light, + last_white_value, + current_white_value, + context.id, + ) + return True + if ( adapt_color and ATTR_COLOR_TEMP in old_attributes @@ -684,6 +706,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): brightness = round(255 * self._settings["brightness_pct"] / 100) service_data[ATTR_BRIGHTNESS] = brightness + if "white_value" in features and adapt_brightness: + white_value = round(255 * self._settings["brightness_pct"] / 100) + service_data[ATTR_WHITE_VALUE] = white_value + if ( "color_temp" in features and adapt_color From ea87e69d3d752263fe1e2441c320de2bc31a75ff Mon Sep 17 00:00:00 2001 From: Matt Forster Date: Mon, 26 Oct 2020 13:50:47 -0600 Subject: [PATCH 0271/1077] chore: move comment --- custom_components/adaptive_lighting/switch.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 13159769..e412e0a4 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -153,7 +153,7 @@ COLOR_ATTRS = { # Should ATTR_PROFILE be in here? BRIGHTNESS_ATTRS = { ATTR_BRIGHTNESS, - ATTR_WHITE_VALUE, + ATTR_WHITE_VALUE, # White value is treated the same as brightness in all respects ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS_STEP_PCT, @@ -389,7 +389,6 @@ def _attributes_have_changed( ) return True - # White value is treated the same as brightness in all respects if ( adapt_brightness and ATTR_WHITE_VALUE in old_attributes From c8620c333832e899485521b32193155cd0a81f48 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 27 Oct 2020 17:52:37 +0100 Subject: [PATCH 0272/1077] if lights is not specified in set_manual_control, select all lights Closes #28 --- custom_components/adaptive_lighting/services.yaml | 2 +- custom_components/adaptive_lighting/switch.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 71d723e0..10bfd2a8 100755 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -32,5 +32,5 @@ set_manual_control: description: "Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true" example: true lights: - description: entity_id(s) of lights. + description: entity_id(s) of lights, if not specified, all lights in the switch are selected. example: light.bedroom_ceiling diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index e412e0a4..497316bd 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -22,7 +22,6 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS_STEP_PCT, - ATTR_WHITE_VALUE, ATTR_COLOR_NAME, ATTR_COLOR_TEMP, ATTR_HS_COLOR, @@ -47,6 +46,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, + ATTR_SUPPORTED_FEATURES, CONF_NAME, EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_STARTED, @@ -153,7 +153,7 @@ COLOR_ATTRS = { # Should ATTR_PROFILE be in here? BRIGHTNESS_ATTRS = { ATTR_BRIGHTNESS, - ATTR_WHITE_VALUE, # White value is treated the same as brightness in all respects + ATTR_WHITE_VALUE, ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS_STEP_PCT, @@ -204,7 +204,11 @@ async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): async def handle_set_manual_control(switch: AdaptiveSwitch, service_call: ServiceCall): """Set or unset lights as 'manually controlled'.""" - all_lights = _expand_light_groups(switch.hass, service_call.data[CONF_LIGHTS]) + lights = service_call.data[CONF_LIGHTS] + if not lights: + all_lights = switch._lights + else: + all_lights = _expand_light_groups(switch.hass, lights) _LOGGER.debug( "Called 'adaptive_lighting.set_manual_control' service with '%s'", service_call.data, @@ -287,7 +291,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_SET_MANUAL_CONTROL, { - vol.Required(CONF_LIGHTS): cv.entity_ids, + vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean, }, handle_set_manual_control, @@ -340,7 +344,7 @@ def _expand_light_groups(hass: HomeAssistant, lights: List[str]) -> List[str]: def _supported_features(hass: HomeAssistant, light: str): state = hass.states.get(light) - supported_features = state.attributes["supported_features"] + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] return {key for key, value in _SUPPORT_OPTS.items() if supported_features & value} From 8c396623188438afec6060b57ab0f2aaa5a3de55 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 27 Oct 2020 20:39:05 +0100 Subject: [PATCH 0273/1077] add separate_turn_on_commands option for MQTT lights Closes https://github.com/basnijholt/adaptive-lighting/issues/29 --- custom_components/adaptive_lighting/const.py | 5 ++++ .../adaptive_lighting/strings.json | 1 + custom_components/adaptive_lighting/switch.py | 25 +++++++++++++------ .../adaptive_lighting/translations/de.json | 1 + .../adaptive_lighting/translations/en.json | 1 + .../adaptive_lighting/translations/sv.json | 1 + 6 files changed, 27 insertions(+), 7 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index ccfeffa8..e1f2e3cb 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -24,6 +24,10 @@ CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS = "min_brightness", 1 CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP = "min_color_temp", 2000 CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE = "only_once", False CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False +CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS = ( + "separate_turn_on_commands", + False, +) CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1 CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP = "sleep_color_temp", 1000 CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 @@ -74,6 +78,7 @@ VALIDATION_TUPLES = [ (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), (CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool), (CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool), + (CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool), ] diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index fe6d0dd8..274569da 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -29,6 +29,7 @@ "min_color_temp": "min_color_temp, in Kelvin", "only_once": "only_once, only adapt the lights when turning them on", "prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible", + "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.", "sleep_brightness": "sleep_brightness, in %", "sleep_color_temp": "sleep_color_temp, in Kelvin", "sunrise_offset": "sunrise_offset, in +/- seconds", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 497316bd..5a613a20 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -100,6 +100,7 @@ from .const import ( CONF_MIN_COLOR_TEMP, CONF_ONLY_ONCE, CONF_PREFER_RGB_COLOR, + CONF_SEPARATE_TURN_ON_COMMANDS, CONF_SLEEP_BRIGHTNESS, CONF_SLEEP_COLOR_TEMP, CONF_SUNRISE_OFFSET, @@ -206,7 +207,7 @@ async def handle_set_manual_control(switch: AdaptiveSwitch, service_call: Servic """Set or unset lights as 'manually controlled'.""" lights = service_call.data[CONF_LIGHTS] if not lights: - all_lights = switch._lights + all_lights = switch._lights # pylint: disable=protected-access else: all_lights = _expand_light_groups(switch.hass, lights) _LOGGER.debug( @@ -492,6 +493,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._interval = data[CONF_INTERVAL] self._only_once = data[CONF_ONLY_ONCE] self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR] + self._separate_turn_on_commands = data[CONF_SEPARATE_TURN_ON_COMMANDS] self._take_over_control = data[CONF_TAKE_OVER_CONTROL] self._transition = min( data[CONF_TRANSITION], self._interval.total_seconds() // 2 @@ -747,12 +749,21 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context.id, ) self.turn_on_off_listener.last_service_data[light] = service_data - await self.hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - service_data, - context=context, - ) + if self._separate_turn_on_commands: + service_datas = [ + {ATTR_ENTITY_ID: light, key: value} + for key, value in service_data.items() + if key != ATTR_ENTITY_ID + ] + else: + service_datas = [service_data] + for service_data in service_datas: + await self.hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + service_data, + context=context, + ) async def _update_attrs_and_maybe_adapt_lights( self, diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json index b8d06afd..dae5af4e 100644 --- a/custom_components/adaptive_lighting/translations/de.json +++ b/custom_components/adaptive_lighting/translations/de.json @@ -29,6 +29,7 @@ "min_color_temp": "min_color_temp, minimale Farbtemperatur in Kelvin", "only_once": "only_once, passe die Lichter nur beim Einschalten an", "prefer_rgb_color": "prefer_rgb_color, nutze 'rgb_color' vor 'color_temp', wenn möglich", + "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.", "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %", "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperaturin Kelvin", "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- seconds", diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index c2c31dbb..ed1d205b 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -29,6 +29,7 @@ "min_color_temp": "min_color_temp, in Kelvin", "only_once": "only_once, only adapt the lights when turning them on", "prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible", + "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.", "sleep_brightness": "sleep_brightness, in %", "sleep_color_temp": "sleep_color_temp, in Kelvin", "sunrise_offset": "sunrise_offset, in +/- seconds", diff --git a/custom_components/adaptive_lighting/translations/sv.json b/custom_components/adaptive_lighting/translations/sv.json index ed288402..8c9c4bf7 100644 --- a/custom_components/adaptive_lighting/translations/sv.json +++ b/custom_components/adaptive_lighting/translations/sv.json @@ -32,6 +32,7 @@ "min_color_temp": "min_color_temp, i Kelvin", "only_once": "only_once, Adaptivt justera endast ljuskällorna när de sätts från 'av' till 'på'", "prefer_rgb_color": "prefer_rgb_color, Använd 'rgb_color' över 'color_temp' om möjligt", + "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.", "sleep_brightness": "sleep_brightness, i %", "sleep_color_temp": "sleep_color_temp, i Kelvin", "sunrise_offset": "sunrise_offset, i +/- sekunder", From c785dcd2022c98968d838b5344f79f78c72c6f92 Mon Sep 17 00:00:00 2001 From: Justin Paupore Date: Tue, 27 Oct 2020 21:41:33 -0700 Subject: [PATCH 0274/1077] Clean up context ID generation. Using a sha256 hash is major overkill for the purposes of finding a short hash of a string. This change switches _short_hash to use Python's built-in hash() function (used for hashing in dicts and sets). which works just fine for this, and is much faster. In addition, since context IDs are length-limited, use base-85 encoding to pack as many bits of the hash and index into the context as possible. --- custom_components/adaptive_lighting/switch.py | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 5a613a20..40b1bd31 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +import base64 import bisect from collections import defaultdict from copy import deepcopy @@ -9,7 +10,6 @@ from dataclasses import dataclass import datetime from datetime import timedelta import functools -import hashlib import logging import math from typing import Any, Dict, List, Optional, Tuple, Union @@ -163,10 +163,17 @@ BRIGHTNESS_ATTRS = { # Keep a short domain version for the context instances (which can only be 36 chars) _DOMAIN_SHORT = "adapt_lgt" +def _int_to_bytes(i: int, signed: bool = False) -> bytes: + bits = i.bit_length() + if signed: + # Make room for the sign bit. + bits += 1 + return i.to_bytes((bits + 7) // 8, 'little', signed=signed) def _short_hash(string: str, length: int = 4) -> str: """Create a hash of 'string' with length 'length'.""" - return hashlib.sha1(string.encode("UTF-8")).hexdigest()[:length] + str_hash_bytes = _int_to_bytes(hash(string), signed=True) + return base64.b85encode(str_hash_bytes)[:length] def create_context(name: str, which: str, index: int) -> Context: @@ -174,7 +181,11 @@ def create_context(name: str, which: str, index: int) -> Context: # Use a hash for the name because otherwise the context might become # too long (max len == 36) to fit in the database. name_hash = _short_hash(name) - return Context(id=f"{_DOMAIN_SHORT}_{name_hash}_{which}_{index}") + # Pack index with base85 to maximize the number of contexts we can create + # before we exceed the 36-character limit and are forced to wrap. + index_packed = base64.b85encode(_int_to_bytes(index, signed=False)) + context_id = f"{_DOMAIN_SHORT}:{name_hash}:{which}:{index_packed}"[:36] + return Context(id=context_id) def is_our_context(context: Optional[Context]) -> bool: @@ -635,13 +646,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): def create_context(self, which: str = "default") -> Context: """Create a context that identifies this Adaptive Lighting instance.""" # Right now the highest number of each context_id it can create is - # 'adapt_lgt_XXXX_turn_on_9999999999999' - # 'adapt_lgt_XXXX_interval_999999999999' - # 'adapt_lgt_XXXX_adapt_lights_99999999' - # 'adapt_lgt_XXXX_sleep_999999999999999' - # 'adapt_lgt_XXXX_light_event_999999999' - # 'adapt_lgt_XXXX_service_9999999999999' - # So 100 million calls before we run into the 36 chars limit. + # 'adapt_lgt:XXXX:turn_on:*************' + # 'adapt_lgt:XXXX:interval:************' + # 'adapt_lgt:XXXX:adapt_lights:********' + # 'adapt_lgt:XXXX:sleep:***************' + # 'adapt_lgt:XXXX:light_event:*********' + # 'adapt_lgt:XXXX:service:*************' + # The smallest space we have is for adapt_lights, which has + # 8 characters. In base85 encoding, that's enough space to hold values + # up to 2**48 - 1, which should give us plenty of calls before we wrap. context = create_context(self._name, which, self._context_cnt) self._context_cnt += 1 return context From 17ac3951240a65e75e79a548b85d50d7319d9f9b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 30 Oct 2020 07:04:41 +0100 Subject: [PATCH 0275/1077] improve separate_turn_on_commands option --- custom_components/adaptive_lighting/switch.py | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 5a613a20..bb1c5cda 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -184,6 +184,28 @@ def is_our_context(context: Optional[Context]) -> bool: return context.id.startswith(_DOMAIN_SHORT) +def _copy_and_pop(dct, keys): + """Copy a dictionary and remove 'keys' if they exist.""" + copy = dct.copy() + for key in keys: + copy.pop(key, None) + return copy + + +def _split_service_data(service_data, adapt_brightness, adapt_color): + """Split service_data into two dictionaries (for color and brightness).""" + service_datas = [] + if adapt_color: + service_datas.append( + _copy_and_pop(service_data, (ATTR_WHITE_VALUE, ATTR_BRIGHTNESS)) + ) + if adapt_brightness: + service_datas.append( + _copy_and_pop(service_data, (ATTR_RGB_COLOR, ATTR_COLOR_TEMP)) + ) + return service_datas + + async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): """Handle the entity service apply.""" hass = switch.hass @@ -741,23 +763,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) ): return - _LOGGER.debug( - "%s: Scheduling 'light.turn_on' with the following 'service_data': %s" - " with context.id='%s'", - self._name, - service_data, - context.id, - ) self.turn_on_off_listener.last_service_data[light] = service_data - if self._separate_turn_on_commands: - service_datas = [ - {ATTR_ENTITY_ID: light, key: value} - for key, value in service_data.items() - if key != ATTR_ENTITY_ID - ] - else: - service_datas = [service_data] + service_datas = ( + _split_service_data(service_data, adapt_brightness, adapt_color) + if self._separate_turn_on_commands + else [service_data] + ) for service_data in service_datas: + _LOGGER.debug( + "%s: Scheduling 'light.turn_on' with the following 'service_data': %s" + " with context.id='%s'", + self._name, + service_data, + context.id, + ) await self.hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, From 44c2850e32028c61536c5f8033f03f8b76e31028 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 19 Dec 2020 13:56:38 +0100 Subject: [PATCH 0276/1077] add switch entity_id to adaptive_lighting.manual_control event --- custom_components/adaptive_lighting/switch.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index bb1c5cda..3c373782 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -239,7 +239,7 @@ async def handle_set_manual_control(switch: AdaptiveSwitch, service_call: Servic if service_call.data[CONF_MANUAL_CONTROL]: for light in all_lights: switch.turn_on_off_listener.manual_control[light] = True - _fire_manual_control_event(switch.hass, light, service_call.context) + _fire_manual_control_event(switch, light, service_call.context) else: switch.turn_on_off_listener.reset(*all_lights) # pylint: disable=protected-access @@ -253,11 +253,16 @@ async def handle_set_manual_control(switch: AdaptiveSwitch, service_call: Servic @callback def _fire_manual_control_event( - hass: HomeAssistant, light: str, context: Context, is_async=True + switch: AdaptiveSwitch, light: str, context: Context, is_async=True ): """Fire an event that 'light' is marked as manual_control.""" + hass = switch.hass fire = hass.bus.async_fire if is_async else hass.bus.fire - fire(f"{DOMAIN}.manual_control", {ATTR_ENTITY_ID: light}, context=context) + fire( + f"{DOMAIN}.manual_control", + {ATTR_ENTITY_ID: light, SWITCH_DOMAIN: switch.entity_id}, + context=context, + ) async def async_setup_entry( @@ -756,6 +761,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): and self._detect_non_ha_changes and not force and await self.turn_on_off_listener.significant_change( + self, light, adapt_brightness, adapt_color, @@ -830,6 +836,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if ( self._take_over_control and self.turn_on_off_listener.is_manually_controlled( + self, light, force, self.adapt_brightness_switch.is_on, @@ -1236,6 +1243,7 @@ class TurnOnOffListener: def is_manually_controlled( self, + switch: AdaptiveSwitch, light: str, force: bool, adapt_brightness: bool, @@ -1260,7 +1268,7 @@ class TurnOnOffListener: # Light was already on and 'light.turn_on' was not called by # the adaptive_lighting integration. manual_control = self.manual_control[light] = True - _fire_manual_control_event(self.hass, light, turn_on_event.context) + _fire_manual_control_event(switch, light, turn_on_event.context) _LOGGER.debug( "'%s' was already on and 'light.turn_on' was not called by the" " adaptive_lighting integration (context.id='%s'), the Adaptive" @@ -1273,6 +1281,7 @@ class TurnOnOffListener: async def significant_change( self, + switch: AdaptiveSwitch, light: str, adapt_brightness: bool, adapt_color: bool, @@ -1331,7 +1340,7 @@ class TurnOnOffListener: # N times in a row. We do this because sometimes a state changes # happens only *after* a new update interval has already started. self.manual_control[light] = True - _fire_manual_control_event(self.hass, light, context, is_async=False) + _fire_manual_control_event(switch, light, context, is_async=False) else: if n_changes > 1: _LOGGER.debug( From ff2321ece9790abff7256cb0a26528f42c763575 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 19 Dec 2020 14:06:48 +0100 Subject: [PATCH 0277/1077] simplify _split_service_data --- custom_components/adaptive_lighting/switch.py | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 3c373782..921a228a 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -184,25 +184,19 @@ def is_our_context(context: Optional[Context]) -> bool: return context.id.startswith(_DOMAIN_SHORT) -def _copy_and_pop(dct, keys): - """Copy a dictionary and remove 'keys' if they exist.""" - copy = dct.copy() - for key in keys: - copy.pop(key, None) - return copy - - def _split_service_data(service_data, adapt_brightness, adapt_color): """Split service_data into two dictionaries (for color and brightness).""" service_datas = [] if adapt_color: - service_datas.append( - _copy_and_pop(service_data, (ATTR_WHITE_VALUE, ATTR_BRIGHTNESS)) - ) + service_data_color = service_data.copy() + service_data_color.pop(ATTR_WHITE_VALUE, None) + service_data_color.pop(ATTR_BRIGHTNESS, None) + service_datas.append(service_data_color) if adapt_brightness: - service_datas.append( - _copy_and_pop(service_data, (ATTR_RGB_COLOR, ATTR_COLOR_TEMP)) - ) + service_data_brightness = service_data.copy() + service_data_brightness.pop(ATTR_RGB_COLOR, None) + service_data_brightness.pop(ATTR_COLOR_TEMP, None) + service_datas.append(service_data_brightness) return service_datas From b9ec138c7db7f2ebacbe0119d309126ac2e55519 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 19 Dec 2020 14:09:26 +0100 Subject: [PATCH 0278/1077] log fire adaptive_lighting.manual_control event --- custom_components/adaptive_lighting/switch.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 921a228a..4fd2bf89 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -252,6 +252,11 @@ def _fire_manual_control_event( """Fire an event that 'light' is marked as manual_control.""" hass = switch.hass fire = hass.bus.async_fire if is_async else hass.bus.fire + _LOGGER.debug( + "'adaptive_lighting.manual_control' event fired for %s for light %s", + switch.entity_id, + light, + ) fire( f"{DOMAIN}.manual_control", {ATTR_ENTITY_ID: light, SWITCH_DOMAIN: switch.entity_id}, From 91b27490ef16fa85ddaccbc5223e33cd8e4c205f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 19 Dec 2020 14:34:47 +0100 Subject: [PATCH 0279/1077] wait between turn_on commands for transition/2 if separate_turn_on_commands is used See #49 --- custom_components/adaptive_lighting/switch.py | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 4fd2bf89..2575a3e5 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -186,6 +186,10 @@ def is_our_context(context: Optional[Context]) -> bool: def _split_service_data(service_data, adapt_brightness, adapt_color): """Split service_data into two dictionaries (for color and brightness).""" + transition = service_data.get(ATTR_TRANSITION) + if transition is not None: + # Split the transition over both commands + service_data[ATTR_TRANSITION] /= 2 service_datas = [] if adapt_color: service_data_color = service_data.copy() @@ -769,12 +773,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ): return self.turn_on_off_listener.last_service_data[light] = service_data - service_datas = ( - _split_service_data(service_data, adapt_brightness, adapt_color) - if self._separate_turn_on_commands - else [service_data] - ) - for service_data in service_datas: + + async def turn_on(service_data): _LOGGER.debug( "%s: Scheduling 'light.turn_on' with the following 'service_data': %s" " with context.id='%s'", @@ -789,6 +789,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context=context, ) + if not self._separate_turn_on_commands: + await turn_on(service_data) + else: + service_data_color, service_data_brightness = _split_service_data( + service_data, adapt_brightness, adapt_color + ) + await turn_on(service_data_color) + transition = service_data_color.get(ATTR_TRANSITION) + if transition is not None: + await asyncio.sleep(transition) + await turn_on(service_data_brightness) + async def _update_attrs_and_maybe_adapt_lights( self, lights: Optional[List[str]] = None, From 8a97480672cfff6f1c0bd948eba92fa04e022ac1 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 19 Dec 2020 15:14:35 +0100 Subject: [PATCH 0280/1077] README update with documentation --- README.md | 167 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 162 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 91615e11..46d45d02 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,167 @@ -# Adaptive Lighting component +# Adaptive Lighting component for Home Assistant -Try out this code by adding https://github.com/basnijholt/adaptive-lighting to your custom repos in HACS and install it! +![](https://github.com/home-assistant/brands/raw/b4a168b9af282ef916e120d31091ecd5e3c35e66/core_integrations/adaptive_lighting/icon.png) -See the documentation at https://deploy-preview-14877--home-assistant-docs.netlify.app/integrations/adaptive_lighting/ +_Try out this code by adding https://github.com/basnijholt/adaptive-lighting to your custom repos in HACS and install it!_ +*This `custom_component` is also being added to `core`, see [this PR](https://github.com/home-assistant/core/pull/40626), although it might take months before it makes it in.* -See [this video on Reddit](https://www.reddit.com/r/homeassistant/comments/jabhso/ha_has_it_before_apple_has_even_finished_it_i/) to see how to add the integration and set the options. +The `adaptive_lighting` platform changes the settings of your lights throughout the day. +It uses the position of the sun to calculate the color temperature and brightness that is most fitting for that time of the day. +Scientific research has shown that this helps to maintain your natural circadian rhythm (your biological clock) and might lead to improved sleep, mood, and general well-being. + +In practical terms, this means that after the sun sets, the brightness of your lights will decrease to a certain minimum brightness, while the color temperature will be at its coolest color temperature at noon, after which it will decrease and reach its warmest color at sunset. +Around sunrise, the opposite will happen. + +Additionally, the integration provides a way to define and set your lights in "sleep mode". +When "sleep mode" is enabled, the lights will be at a minimal brightness and have a very warm color. + +The integration creates 4 switches (in this example the component's name is `"living_room"`): +1. `switch.adaptive_lighting_living_room`, which turns the Adaptive Lighting integration on or off. It has several attributes that show the current light settings. +2. `switch.adaptive_lighting_sleep_mode_living_room`, which when activated, turns on "sleep mode" (you can set a specific `sleep_brightness` and `sleep_color_temp`). +3. `switch.adaptive_lighting_adapt_brightness_living_room`, which sets whether the integration should adapt the brightness of the lights (if supported by the light). +4. `switch.adaptive_lighting_adapt_color_living_room`, which sets whether the integration should adapt the color of the lights (if supported by the light). + +## Taking back control + +Although having your lights automatically adapt is great most of the time, there might be times at which you want to set the lights to a different color/brightness and keep it that way. +For this purpose, the integration (when `take_over_control` is enabled) automatically detects whether someone (e.g., person toggling the light switch) or something (automation) changes the lights. +If this happens *and* the light is already on, the light that was changed gets marked as "manually controlled" and the Adaptive Lighting component will stop adapting that light until it turns off and on again (or if you use the service call `adaptive_lighting.set_manual_control`). +This mechanism works by listening to all `light.turn_on` calls that change the color or brightness and by noting that the component did not make the call. +Additionally, there is an option to detect all state changes (when `detect_non_ha_changes` is enabled), so also changes to the lights that were not made by a `light.turn_on` call (e.g., through an app or via something outside of Home Assistant.) +It does this by comparing a light's state to Adaptive Lighting's previously used settings. +Whenever a light gets marked as "manually controlled", an `adaptive_lighting.manual_control` event is fired, such that one can use this information in automations. + +## Configuration + +This integration is both fully configurable through YAML _and_ the frontend. (**Configuration** -> **Integrations** -> **Adaptive Lighting**, **Adaptive Lighting** -> **Options**) +Here, the options in the frontend and in YAML have the same names. + +```yaml +# Example configuration.yaml entry +adaptive_lighting: + lights: + - light.living_room_lights +``` + +### Options +| option | description | required | default | type | +|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|-----------|---------| +| name | The name to use when displaying this switch. | False | default | string | +| lights | List of light entities for Adaptive Lighting to control (may be empty). | False | list | [] | +| prefer_rgb_color | Whether to use RGB color adjustment instead of native light color temperature. | False | False | boolean | +| initial_transition | How long the first transition is when the lights go from `off` to `on` (or when "sleep mode" is toggled). | False | 1 | time | +| transition | How long the transition is when the lights change, in seconds. | False | 45 | integer | +| interval | How often to adapt the lights, in seconds. | False | 90 | integer | +| min_brightness | The minimum percent of brightness to set the lights to. | False | 1 | integer | +| max_brightness | The maximum percent of brightness to set the lights to. | False | 100 | integer | +| min_color_temp | The warmest color temperature to set the lights to, in Kelvin. | False | 2000 | integer | +| max_color_temp | The coldest color temperature to set the lights to, in Kelvin. | False | 5500 | integer | +| sleep_brightness | Brightness of lights while the sleep mode is enabled. | False | 1 | integer | +| sleep_color_temp | Color temperature of lights while the sleep mode is enabled. | False | 1000 | integer | +| sunrise_time | Override the sunrise time with a fixed time. | False | time | | +| sunrise_offset | Change the sunrise time with a positive or negative offset. | False | 0 | time | +| sunset_time | Override the sunset time with a fixed time. | False | time | | +| sunset_offset | Change the sunset time with a positive or negative offset. | False | 0 | time | +| only_once | Whether to keep adapting the lights (false) or to only adapt the lights as soon as they are turned on (true). | False | False | boolean | +| take_over_control | If another source calls `light.turn_on` while the lights are on and being adapted, disable Adaptive Lighting. | False | True | boolean | +| detect_non_ha_changes | Whether to detect state changes and stop adapting lights, even not from `light.turn_on`. Needs `take_over_control` to be enabled. Note that by enabling this option, it calls 'homeassistant.update_entity' every 'interval'! | inclusive | False | boolean | + +Full example: + +```yaml +# Example configuration.yaml entry +adaptive_lighting: +- name: "default" + lights: [] + prefer_rgb_color: false + transition: 45 + initial_transition: 1 + interval: 90 + min_brightness: 1 + max_brightness: 100 + min_color_temp: 2000 + max_color_temp: 5500 + sleep_brightness: 1 + sleep_color_temp: 1000 + sunrise_time: "08:00:00" # override the sunrise time + sunrise_offset: + sunset_time: + sunset_offset: 1800 # in seconds or '00:15:00' + take_over_control: true + detect_non_ha_changes: false + only_once: false + +``` + +### Services + +`adaptive_lighting.apply` applies Adaptive Lighting settings to lights on demand. + +| Service data attribute | Optional | Description | +|---------------------------|----------|-------------------------------------------------------------------------| +| `entity_id` | no | The `entity_id` of the switch with the settings to apply. | +| `lights` | no | A light (or list of lights) to apply the settings to. | +| `transition` | yes | The number of seconds for the transition. | +| `adapt_brightness` | yes | Whether to change the brightness of the light or not. | +| `adapt_color` | yes | Whether to adapt the color on supporting lights. | +| `prefer_rgb_color` | yes | Whether to prefer RGB color adjustment over of native light color temperature when possible. | +| `turn_on_lights` | yes | Whether to turn on lights that are currently off. | + +`adaptive_lighting.set_manual_control` can mark (or unmark) whether a light is "manually controlled", meaning that when a light has `manual_control`, the light is not adapted. + +| Service data attribute | Optional | Description | +|------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------| +| `entity_id` | no | The `entity_id` of the switch in which to (un)mark the light as being "manually controlled". | +| `lights` | no | A light (or list of lights) to apply the settings to. | +| `manual_control` | no | Whether to mark (true) or unmark (false) the light as "manually controlled", when not specified it selects all lights in the switch. | + + +## Automation examples + +Reset the `manual_control` status of a light after an hour. +```yaml +- alias: "Adaptive lighting: reset manual_control after 1 hour" + mode: parallel + trigger: + platform: event + event_type: adaptive_lighting.manual_control + variables: + light: "{{ trigger.event.data.entity_id }}" + switch: "{{ trigger.event.data.switch }}" + action: + - delay: "01:00:00" + - condition: template + value_template: "{{ light in state_attr(switch, 'manual_control') }}" + - service: adaptive_lighting.set_manual_control + data: + entity_id: "{{ switch }}" + lights: "{{ light }}" + manual_control: false +``` + +Toggle multiple Adaptive Lighting switches to "sleep mode" using an `input_boolean.sleep_mode`. + +```yaml +- alias: "Adaptive lighting: toggle 'sleep mode'" + trigger: + - platform: state + entity_id: input_boolean.sleep_mode + - platform: homeassistant + event: start # in case the states aren't properly restored + variables: + sleep_mode: "{{ states('input_boolean.sleep_mode') }}" + action: + service: "switch.turn_{{ sleep_mode }}" + entity_id: + - switch.adaptive_lighting_sleep_mode_living_room + - switch.adaptive_lighting_sleep_mode_bedroom +``` + +# Other + +See the documentation of the PR at https://deploy-preview-14877--home-assistant-docs.netlify.app/integrations/adaptive_lighting/ and [this video on Reddit](https://www.reddit.com/r/homeassistant/comments/jabhso/ha_has_it_before_apple_has_even_finished_it_i/) to see how to add the integration and set the options. + +This integration was originally based of the great work of @claytonjn https://github.com/claytonjn/hass-circadian_lighting, but has been 100% rewritten and extended with new features. # Having problems? Please enable debug logging by putting this in `configuration.yaml`: @@ -14,7 +171,7 @@ logger: logs: custom_components.adaptive_lighting: debug ``` -and after the problem occurs please create an issue with the log. +and after the problem occurs please create an issue with the log (`/config/home-assistant.log`). ### Graphs! From fa9ad171a5a42a7aea45b82126f93d51506e408d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 19 Dec 2020 15:41:03 +0100 Subject: [PATCH 0281/1077] remove info.md --- info.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 info.md diff --git a/info.md b/info.md deleted file mode 100644 index 564c61b7..00000000 --- a/info.md +++ /dev/null @@ -1,7 +0,0 @@ -## Stay healthier and sleep better by syncing your lights with natural daylight to maintain your circadian rhythm! - - - -Circadian Lighting slowly synchronizes your color changing lights with the regular naturally occurring color temperature of the sky throughout the day. This gives your environment a more natural feel, with cooler hues during the midday and warmer tints near twilight and dawn. - -In addition, Circadian Lighting can set your lights to a nice cool white at 1% in “Sleep” mode, which is far brighter than starlight but won’t reset your circadian rhythm or break down too much rhodopsin in your eyes. \ No newline at end of file From 4257d27deeffeb2f9be9acb287f23c5be6c2a296 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 19 Dec 2020 16:33:33 +0100 Subject: [PATCH 0282/1077] deal with 'light.turn_on' with multiple lights, which appear as csv list Solves the bug reported in #39 (https://github.com/basnijholt/adaptive-lighting/issues/39) --- custom_components/adaptive_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 2575a3e5..597928f3 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1175,7 +1175,7 @@ class TurnOnOffListener: service = event.data[ATTR_SERVICE] service_data = event.data[ATTR_SERVICE_DATA] - entity_ids = cv.ensure_list(service_data[ATTR_ENTITY_ID]) + entity_ids = cv.ensure_list_csv(service_data[ATTR_ENTITY_ID]) if not any(eid in self.lights for eid in entity_ids): return From 008c5e444da8b27951d49e85d82b6401a4d9b449 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 19 Dec 2020 19:11:11 +0100 Subject: [PATCH 0283/1077] add separate_turn_on_commands to README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 46d45d02..6d4aa685 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,8 @@ adaptive_lighting: | sunset_offset | Change the sunset time with a positive or negative offset. | False | 0 | time | | only_once | Whether to keep adapting the lights (false) or to only adapt the lights as soon as they are turned on (true). | False | False | boolean | | take_over_control | If another source calls `light.turn_on` while the lights are on and being adapted, disable Adaptive Lighting. | False | True | boolean | -| detect_non_ha_changes | Whether to detect state changes and stop adapting lights, even not from `light.turn_on`. Needs `take_over_control` to be enabled. Note that by enabling this option, it calls 'homeassistant.update_entity' every 'interval'! | inclusive | False | boolean | +| detect_non_ha_changes | Whether to detect state changes and stop adapting lights, even not from `light.turn_on`. Needs `take_over_control` to be enabled. Note that by enabling this option, it calls 'homeassistant.update_entity' every 'interval'! | False | False | boolean | +| separate_turn_on_commands | Whether to use separate `light.turn_on` calls for color and brightness, needed for some types of lights | False | False | boolean | Full example: From 1ec6866189f80ac17715527799341ade7b9e08f1 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 27 Dec 2020 13:05:05 +0100 Subject: [PATCH 0284/1077] fix bug when resetting switch and it's off --- custom_components/adaptive_lighting/switch.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 597928f3..0869accf 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -241,12 +241,13 @@ async def handle_set_manual_control(switch: AdaptiveSwitch, service_call: Servic else: switch.turn_on_off_listener.reset(*all_lights) # pylint: disable=protected-access - await switch._adapt_lights( - all_lights, - transition=switch._initial_transition, - force=True, - context=switch.create_context("service"), - ) + if switch.is_on: + await switch._update_attrs_and_maybe_adapt_lights( + all_lights, + transition=switch._initial_transition, + force=True, + context=switch.create_context("service"), + ) @callback From 73ce5f15431a21b9a8fc0e7b5b616c8a40f0a674 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 27 Dec 2020 13:35:43 +0100 Subject: [PATCH 0285/1077] separate_turn_on_commands fix when _split_service_data returns 1 item --- custom_components/adaptive_lighting/switch.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 0869accf..03ad8d93 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -793,14 +793,16 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if not self._separate_turn_on_commands: await turn_on(service_data) else: - service_data_color, service_data_brightness = _split_service_data( + # Could be a list of length 1 or 2 + service_datas = _split_service_data( service_data, adapt_brightness, adapt_color ) - await turn_on(service_data_color) - transition = service_data_color.get(ATTR_TRANSITION) - if transition is not None: - await asyncio.sleep(transition) - await turn_on(service_data_brightness) + await turn_on(service_datas[0]) + if len(service_datas) == 2: + transition = service_datas[0].get(ATTR_TRANSITION) + if transition is not None: + await asyncio.sleep(transition) + await turn_on(service_datas[1]) async def _update_attrs_and_maybe_adapt_lights( self, From 36c10075224699d9e9e8b84acd50b541fccd5c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCri=20Rebane?= <46962963+Repsionu@users.noreply.github.com> Date: Thu, 3 Dec 2020 13:12:49 +0200 Subject: [PATCH 0286/1077] Create et.json Made Estonian (et-EE) translation. Best, JR --- .../adaptive_lighting/translations/et.json | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/et.json diff --git a/custom_components/adaptive_lighting/translations/et.json b/custom_components/adaptive_lighting/translations/et.json new file mode 100644 index 00000000..7c9af5d2 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/et.json @@ -0,0 +1,49 @@ +{ + "title": "Kohanduv valgus", + "config": { + "step": { + "user": { + "title": "Vali kohanduva valguse üksuse nimi", + "description": "Igas üksuses võib olla mitu valgustit!", + "data": { + "name": "Nimi" + } + } + }, + "abort": { + "already_configured": "Üksus on juba seadistatud" + } + }, + "options": { + "step": { + "init": { + "title": "Kohanduva valguse suvandid", + "description": "Kohanduva valguse suvandid. Valikute nimetused ühtuvad YAML kirjes olevatega. Valikuid ei kuvata kui seadistus on tehtud YAML kirjes.", + "data": { + "lights": "valgustid", + "initial_transition": "Algne üleminek kui valgustid lülituvad sisse/välja või unerežiim muutub", + "interval": "Intervall, aeg muutuste vahel sekundites", + "max_brightness": "Suurim heledus %", + "max_color_temp": "Suurim värvustemperatuur Kelvinites", + "min_brightness": "Vähim heledus %", + "min_color_temp": "Vähim värvustemperatuur Kelvinites", + "only_once": "Ainult üks kord, rakendub ainult valgusti sisselülitamisel", + "prefer_rgb_color": "Eelista RGB värve, võimalusel kasuta RGB sätteid värvustemperatuuri asemel", + "separate_turn_on_commands": "Eraldi lülitused iga valiku (värvus, heledus jne.) sisselülitamiseks, mõned valgustid vajavad seda.", + "sleep_brightness": "Unerežiimi heledus %", + "sleep_color_temp": "Uneržiimi värvus Kelvinites", + "sunrise_offset": "Nihe päikesetõusust, +/- sekundit", + "sunrise_time": "Päikesetõusu aeg 'HH:MM:SS' vormingus. (Kui jätta tühjaks kasutatakse asukohajärgset)", + "sunset_offset": "Nihe päikeseloojangust, +/- sekundit", + "sunset_time": "Päikeseloojangu aeg 'HH:MM:SS' vormingus. (Kui jätta tühjaks kasutatakse asukohajärgset)", + "take_over_control": "Käsitsi juhtimine: kui miski peale kohanduva valguse enda lültiab valgusti sisse ja see juba põleb, katkesta kohandamine kuni järgmise välise lülitamiseni.", + "detect_non_ha_changes": "Märka väliseid lülitusi: kui mõni säte muutub üle 10% (isegi väljaspoolt HA juhituna) siis peab käsitsi juhtimine olema lubatud (kutsutakse 'homeassistant.update_entity')'interval'!)", + "transition": "Üleminekud, sekundites" + } + } + }, + "error": { + "option_error": "Vigane suvand" + } + } +} From 1eaca9fc36f82067a41de506439acba01d4466b2 Mon Sep 17 00:00:00 2001 From: Travis Pew Date: Fri, 1 Jan 2021 14:44:11 -0500 Subject: [PATCH 0287/1077] Update README.md set_manual_control I'd been scratching my head over this for a little bit when I realized that the optional value column had not been updated to reflect the description which clearly states it is optional. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6d4aa685..f477ffd5 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ adaptive_lighting: |------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------| | `entity_id` | no | The `entity_id` of the switch in which to (un)mark the light as being "manually controlled". | | `lights` | no | A light (or list of lights) to apply the settings to. | -| `manual_control` | no | Whether to mark (true) or unmark (false) the light as "manually controlled", when not specified it selects all lights in the switch. | +| `manual_control` | yes | Whether to mark (true) or unmark (false) the light as "manually controlled", when not specified it selects all lights in the switch. | ## Automation examples From 90b1837c7f7e03b43061be5fb255329a18b86591 Mon Sep 17 00:00:00 2001 From: Will Puckett Date: Mon, 4 Jan 2021 10:16:42 -0800 Subject: [PATCH 0288/1077] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6d4aa685..0835b5a4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![](https://github.com/home-assistant/brands/raw/b4a168b9af282ef916e120d31091ecd5e3c35e66/core_integrations/adaptive_lighting/icon.png) -_Try out this code by adding https://github.com/basnijholt/adaptive-lighting to your custom repos in HACS and install it!_ +_Try out this code by adding https://github.com/basnijholt/adaptive-lighting to your custom repos in [HACS (Home Assistant Community Store)](https://hacs.xyz/) and install it!_ *This `custom_component` is also being added to `core`, see [this PR](https://github.com/home-assistant/core/pull/40626), although it might take months before it makes it in.* The `adaptive_lighting` platform changes the settings of your lights throughout the day. From 30d85e3ae7f69d1dbe98eceabdaaf724dcf060b4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 1 Mar 2021 19:20:18 +0100 Subject: [PATCH 0289/1077] add version string in manifest.json --- custom_components/adaptive_lighting/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 13461584..ee4c828b 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -5,5 +5,6 @@ "config_flow": true, "dependencies": [], "codeowners": ["@basnijholt"], + "version": "1.0.0", "requirements": [] } From b5eed585caf1264d47bea2af574803b7935b811e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 30 Apr 2021 10:46:14 +0200 Subject: [PATCH 0290/1077] support Astral v2 --- custom_components/adaptive_lighting/switch.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 03ad8d93..6fa23bf2 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -529,10 +529,17 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._transition = min( data[CONF_TRANSITION], self._interval.total_seconds() // 2 ) + _loc = get_astral_location(self.hass) + if isinstance(_loc, tuple): + # Astral v2.2 + location, _ = _loc + else: + # Astral v1 + location = _loc self._sun_light_settings = SunLightSettings( name=self._name, - astral_location=get_astral_location(self.hass), + astral_location=location, max_brightness=data[CONF_MAX_BRIGHTNESS], max_color_temp=data[CONF_MAX_COLOR_TEMP], min_brightness=data[CONF_MIN_BRIGHTNESS], From 20a37b41035fc38c470ea611777fe7d962a05512 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 30 Apr 2021 11:00:06 +0200 Subject: [PATCH 0291/1077] fix 'AttributeError: 'Location' object has no attribute 'solar_noon'' --- custom_components/adaptive_lighting/switch.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 6fa23bf2..45783ee8 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1036,8 +1036,14 @@ class SunLightSettings: ) + self.sunset_offset if self.sunrise_time is None and self.sunset_time is None: - solar_noon = location.solar_noon(date, local=False) - solar_midnight = location.solar_midnight(date, local=False) + try: + # Astral v1 + solar_noon = location.solar_noon(date, local=False) + solar_midnight = location.solar_midnight(date, local=False) + except AttributeError: + # Astral v2 + solar_noon = location.noon(date, local=False) + solar_midnight = location.midnight(date, local=False) else: solar_noon = sunrise + (sunset - sunrise) / 2 solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset) / 2 From 99049415dcbe1a427890871ed715d1eb25e6fd51 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 10 May 2021 18:26:26 +0200 Subject: [PATCH 0292/1077] support ATTR_SUPPORTED_COLOR_MODES --- custom_components/adaptive_lighting/switch.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 45783ee8..841e9d3f 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -38,7 +38,13 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, VALID_TRANSITION, is_on, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_BRIGHTNESS, + ATTR_SUPPORTED_COLOR_MODES, ) + from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -377,7 +383,17 @@ def _expand_light_groups(hass: HomeAssistant, lights: List[str]) -> List[str]: def _supported_features(hass: HomeAssistant, light: str): state = hass.states.get(light) supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] - return {key for key, value in _SUPPORT_OPTS.items() if supported_features & value} + supported = {key for key, value in _SUPPORT_OPTS.items() if supported_features & value} + supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES, set()) + if COLOR_MODE_RGB in supported_color_modes: + supported.add("color") + if COLOR_MODE_RGBW in supported_color_modes: + supported.add("color") + if COLOR_MODE_COLOR_TEMP in supported_color_modes: + supported.add("color_temp") + if COLOR_MODE_BRIGHTNESS in supported_color_modes: + supported.add("brightness") + return supported def color_difference_redmean( From 41d149d9bb34cd785caa26129fefdc62071a1d17 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 10 May 2021 20:19:47 +0200 Subject: [PATCH 0293/1077] always add brightness when color is supported, see comment by @DigitalFeonix https://github.com/basnijholt/adaptive-lighting/issues/112#issuecomment-836944011 --- custom_components/adaptive_lighting/switch.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 841e9d3f..8eb86f8c 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -383,14 +383,21 @@ def _expand_light_groups(hass: HomeAssistant, lights: List[str]) -> List[str]: def _supported_features(hass: HomeAssistant, light: str): state = hass.states.get(light) supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] - supported = {key for key, value in _SUPPORT_OPTS.items() if supported_features & value} + supported = { + key for key, value in _SUPPORT_OPTS.items() if supported_features & value + } supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES, set()) if COLOR_MODE_RGB in supported_color_modes: supported.add("color") + # Adding brightness here, see + # comment https://github.com/basnijholt/adaptive-lighting/issues/112#issuecomment-836944011 + supported.add("brightness") if COLOR_MODE_RGBW in supported_color_modes: supported.add("color") + supported.add("brightness") # see above url if COLOR_MODE_COLOR_TEMP in supported_color_modes: supported.add("color_temp") + supported.add("brightness") # see above url if COLOR_MODE_BRIGHTNESS in supported_color_modes: supported.add("brightness") return supported From deb348a535425ed3f6b8a06ec163b7bf19b42810 Mon Sep 17 00:00:00 2001 From: David Stenbeck Date: Thu, 27 May 2021 15:30:11 +0200 Subject: [PATCH 0294/1077] Improvements to setting descriptions. --- .../adaptive_lighting/translations/en.json | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index ed1d205b..2689d56d 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -3,42 +3,42 @@ "config": { "step": { "user": { - "title": "Choose a name for the Adaptive Lighting", - "description": "Every instance can contain multiple lights!", + "title": "Choose a name for the Adaptive Lighting instance", + "description": "Pick a name this instance. You can run several instances of Adaptive lighting, each of these can contain multiple lights!", "data": { "name": "Name" } } }, "abort": { - "already_configured": "Device is already configured" + "already_configured": "This device is already configured" } }, "options": { "step": { "init": { "title": "Adaptive Lighting options", - "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", + "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have the adaptive_lighting entry defined in your YAML configuration.", "data": { "lights": "lights", - "initial_transition": "initial_transition, when lights go 'off' to 'on' or when 'sleep_state' changes", - "interval": "interval, time between switch updates in seconds", - "max_brightness": "max_brightness, in %", - "max_color_temp": "max_color_temp, in Kelvin", - "min_brightness": "min_brightness, in %", - "min_color_temp": "min_color_temp, in Kelvin", - "only_once": "only_once, only adapt the lights when turning them on", - "prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible", - "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.", - "sleep_brightness": "sleep_brightness, in %", - "sleep_color_temp": "sleep_color_temp, in Kelvin", - "sunrise_offset": "sunrise_offset, in +/- seconds", - "sunrise_time": "sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location)", - "sunset_offset": "sunset_offset, in +/- seconds", - "sunset_time": "sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location)", - "take_over_control": "take_over_control, if anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", - "detect_non_ha_changes": "detect_non_ha_changes, detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", - "transition": "transition, in seconds" + "initial_transition": "initial_transition: When lights turn 'off' to 'on' or when 'sleep_state' changes. (seconds)", + "interval": "interval: Time between switch updates. (seconds)", + "max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)", + "max_color_temp": "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)", + "min_brightness": "min_brightness: Lowest brightness of lights during a cycle. (%)", + "min_color_temp": "min_color_temp, Warmest hue of the color temperature cycle. (%)", + "only_once": "only_once: Only adapt the lights when turning them on.", + "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.", + "separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).", + "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)", + "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)", + "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- seconds)", + "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- seconds)", + "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", + "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", + "transition": "Transition time when applying a change to the lights (seconds)" } } }, From b45f8d7f32cbe438f8ec39919cfc96c08b31b5bf Mon Sep 17 00:00:00 2001 From: David Stenbeck Date: Thu, 27 May 2021 15:33:46 +0200 Subject: [PATCH 0295/1077] Spelling error --- custom_components/adaptive_lighting/translations/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 2689d56d..e66b6769 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -4,7 +4,7 @@ "step": { "user": { "title": "Choose a name for the Adaptive Lighting instance", - "description": "Pick a name this instance. You can run several instances of Adaptive lighting, each of these can contain multiple lights!", + "description": "Pick a name for this instance. You can run several instances of Adaptive lighting, each of these can contain multiple lights!", "data": { "name": "Name" } From 40bc5dad17045facfa2bb5f384591aeea0256e9e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 3 Jun 2021 09:56:38 +0200 Subject: [PATCH 0296/1077] fix time_zone AttributeError, fixes #128 Thanks @yurnih! --- custom_components/adaptive_lighting/switch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 8eb86f8c..cfe073c1 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1042,7 +1042,10 @@ class SunLightSettings: def _replace_time(date: datetime.datetime, key: str) -> datetime.datetime: time = getattr(self, f"{key}_time") date_time = datetime.datetime.combine(date, time) - utc_time = self.time_zone.localize(date_time).astimezone(dt_util.UTC) + try: # HA ≤2021.05, https://github.com/basnijholt/adaptive-lighting/issues/128 + utc_time = self.time_zone.localize(date_time).astimezone(dt_util.UTC) + except AttributeError: # HA ≥2021.06 + utc_time = date_time.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) return utc_time location = self.astral_location From 18e057fd423d25fc54212324887abc046dd1bae7 Mon Sep 17 00:00:00 2001 From: Nicholai Nissen Date: Sun, 20 Jun 2021 13:06:42 +0200 Subject: [PATCH 0297/1077] i18n: Add Danish translation --- .../adaptive_lighting/translations/da.json | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/da.json diff --git a/custom_components/adaptive_lighting/translations/da.json b/custom_components/adaptive_lighting/translations/da.json new file mode 100644 index 00000000..2a881e5d --- /dev/null +++ b/custom_components/adaptive_lighting/translations/da.json @@ -0,0 +1,49 @@ +{ + "title": "Adaptiv Belysning", + "config": { + "step": { + "user": { + "title": "Vælg et navn for denne Adaptive Belysning", + "description": "Vælg et navn til denne konfiguration. Du kan køre flere konfigurationer af Adaptiv Belysning, og hver af dem kan indeholde flere lys!", + "data": { + "name": "Navn" + } + } + }, + "abort": { + "already_configured": "Denne enhed er allerede konfigureret" + } + }, + "options": { + "step": { + "init": { + "title": "Adaptiv Belysnings indstillinger", + "description": "Alle indstillinger tilhørende en Adaptiv Belysnings komponent. Indstillingernes navne svarer til YAML indstillingernes. Ingen indstillinger vises hvis du allerede har konfigureret den i YAML.", + "data": { + "lights": "lights: lyskilder", + "initial_transition": "initial_transition: Hvor lang overgang når lyset går fra 'off' til 'on' eller når 'sleep_state' skiftes. (i sekunder)", + "interval": "interval: Tid imellem opdateringer (i sekunder)", + "max_brightness": "max_brightness: Højeste lysstyrke i cyklussen. (%)", + "max_color_temp": "max_color_temp: Koldeste lystemperatur i cyklussen. (Kelvin)", + "min_brightness": "min_brightness: Laveste lysstyrke i cyklussen. (%)", + "min_color_temp": "min_color_temp: Varmeste lystemperatur i cyklussen. (Kelvin)", + "only_once": "only_once: Juster udelukkende lysene adaptivt i øjeblikket de tændes.", + "prefer_rgb_color": "prefer_rgb_color: Brug 'rgb_color' istedet for 'color_temp' når muligt.", + "separate_turn_on_commands": "separate_turn_on_commands: Adskil kommandoerne for hver attribut (color, brightness, etc.) ved 'light.turn_on' (nødvendigt for bestemte lys).", + "sleep_brightness": "sleep_brightness, Lysstyrke for Sleep Mode. (%)", + "sleep_color_temp": "sleep_color_temp: Farvetemperatur under Sleep Mode. (Kelvin)", + "sunrise_offset": "sunrise_offset: Hvor længe før (-) eller efter (+) at definere solopgangen i cyklussen (+/- sekunder)", + "sunrise_time": "sunrise_time: Manuel overstyring af solopgangstidspunktet, hvis 'None', bruges det egentlige tidspunkt din lokation. (HH:MM:SS)", + "sunset_offset": "sunset_offset: Hvor længe før (-) eller efter (+) at definere solnedgangen i cyklussen (+/- sekunder)", + "sunset_time": "sunset_time: Manuel overstyring af solnedgangstidspunktet, hvis 'None', bruges det egentlige tidspunkt for din lokation. (HH:MM:SS)", + "take_over_control": "take_over_control: Hvis andet end Adaptiv Belysning kalder 'light.turn_on' på et lys der allerede er tændt, afbryd adaptering af lyset indtil at det tændes igen.", + "detect_non_ha_changes": "detect_non_ha_changes: Registrer alle ændringer på >10% på et lys (også udenfor HA), kræver at 'take_over_control' er slået til (kalder 'homeassistant.update_entity' hvert 'interval'!)", + "transition": "Overgangsperiode når en ændring i lyset udføres (i sekunder)" + } + } + }, + "error": { + "option_error": "Ugyldig indstilling" + } + } +} From cd439ea58fe158b56317e76df760a5640a2e7356 Mon Sep 17 00:00:00 2001 From: Denys Dovhan Date: Sun, 20 Jun 2021 16:51:33 +0300 Subject: [PATCH 0298/1077] Add Ukrainian --- .../adaptive_lighting/translations/uk.json | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/uk.json diff --git a/custom_components/adaptive_lighting/translations/uk.json b/custom_components/adaptive_lighting/translations/uk.json new file mode 100644 index 00000000..c71d5e63 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/uk.json @@ -0,0 +1,49 @@ +{ + "title": "Адаптивне освітлення", + "config": { + "step": { + "user": { + "title": "Оберіть ім’я для екземпляра адаптивного освітлення", + "description": "Оберіть ім’я для цього екземпляра. Ви можете мати декілька екземплярів адаптивного освітлення, кожен може містити декілька приладів!", + "data": { + "name": "Ім’я" + } + } + }, + "abort": { + "already_configured": "Цей пристрій вже налаштовано" + } + }, + "options": { + "step": { + "init": { + "title": "Опції адаптивного освітлення", + "description": "Всі налаштування компонента адаптивного освітлення. Назви опцій відповідають налаштуванням у YAML. Опції не відображаються, якщо ви вже визначили їх у компоненті adaptive_lighting вашої YAML-конфігурації.", + "data": { + "lights": "прилади", + "initial_transition": "initial_transition: Коли прилад вимикається (off), вмикається (on), або змінює 'sleep_state'. (секунди)", + "interval": "interval: Час між оновленнями перемикача. (секунди)", + "max_brightness": "max_brightness: Найвища яскравість світла під час циклу. (%)", + "max_color_temp": "max_color_temp: Найхолодніший відтінок циклу кольорової температури. (Кельвін)", + "min_brightness": "min_brightness: Найнижча яскравість світла під час циклу. (%)", + "min_color_temp": "min_color_temp: Найтепліший відтінок циклу кольорової температури. (%)", + "only_once": "only_once: Адаптувати світло лише після початкового увімкнення.", + "prefer_rgb_color": "prefer_rgb_color: Використовувати 'rgb_color' замість 'color_temp', коли можливо.", + "separate_turn_on_commands": "separate_turn_on_commands: Окремі команди для кожного атрибута (колір, яскравість, тощо.) в 'light.turn_on' (необхідні для деяких приладів).", + "sleep_brightness": "sleep_brightness: Налаштування яскравості для Режиму сну. (%)", + "sleep_color_temp": "sleep_color_temp: Температура кольору для Режиму сну. (Кельвін)", + "sunrise_offset": "sunrise_offset: Як за довго до(-) або після(+) визначати точку сходу сонця для циклу (+/- секунд)", + "sunrise_time": "sunrise_time: Ручний перезапис часу сходу сонця, якщо 'None', тоді використовується час сходу сонця у вашій локації (HH:MM:SS)", + "sunset_offset": "sunset_offset: Як за довго до(-) або після(+) визначати точку заходу сонця для циклу (+/- секунд)", + "sunset_time": "sunset_time: Ручний перезапис часу заходу сонця, якщо 'None', тоді використовується час заходу сонця у вашій локації (HH:MM:SS)", + "take_over_control": "take_over_control: Якщо що-небудь, окрім Адаптивного освітлення, викликає 'light.turn_on', коли світло вже увімкнено, чи адаптувати освітлення допоки світло (або перемикач) перемкнеться (off -> on).", + "detect_non_ha_changes": "detect_non_ha_changes: виявляти всі зміни >10% до освітлення (включаючи ті, що зроблені поза HA), вимагає, щоб 'take_over_control' був включений (виклик 'homeassistant.update_entity' кожного оновлення 'interval'!)", + "transition": "Час переходу, який застосовується до освітлення (секунди)" + } + } + }, + "error": { + "option_error": "Хибна опція" + } + } +} From c17170c507f015880c79e41219a8c48d60334068 Mon Sep 17 00:00:00 2001 From: Michael Kirsch Date: Sun, 18 Jul 2021 21:12:34 +0200 Subject: [PATCH 0299/1077] add xy and hs as alternative color modes --- custom_components/adaptive_lighting/switch.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index cfe073c1..d4f49c8c 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -40,6 +40,8 @@ from homeassistant.components.light import ( is_on, COLOR_MODE_RGB, COLOR_MODE_RGBW, + COLOR_MODE_HS, + COLOR_MODE_XY, COLOR_MODE_COLOR_TEMP, COLOR_MODE_BRIGHTNESS, ATTR_SUPPORTED_COLOR_MODES, @@ -395,6 +397,12 @@ def _supported_features(hass: HomeAssistant, light: str): if COLOR_MODE_RGBW in supported_color_modes: supported.add("color") supported.add("brightness") # see above url + if COLOR_MODE_XY in supported_color_modes: + supported.add("color") + supported.add("brightness") # see above url + if COLOR_MODE_HS in supported_color_modes: + supported.add("color") + supported.add("brightness") # see above url if COLOR_MODE_COLOR_TEMP in supported_color_modes: supported.add("color_temp") supported.add("brightness") # see above url From 458cd45964b43e10425ba68bd3d01532540d89fa Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 1 Aug 2021 11:37:29 +0200 Subject: [PATCH 0300/1077] add Maintainers section --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 4df681b1..ffe75355 100644 --- a/README.md +++ b/README.md @@ -186,3 +186,9 @@ These graphs were generated using the values calculated by the Adaptive Lighting ##### Brightness: ![cl_brightness|690x130](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/8/58ebd994b62a8b1abfb3497a5288d923ff4e2330.PNG) + +# Maintainers + +- @basnijholt +- @RubenKelevra + From 2001a737ff593f294cf5fc9b9aeb255357812351 Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Sun, 1 Aug 2021 16:41:16 +0200 Subject: [PATCH 0301/1077] Create config.yml Source: https://github.com/ipfs/go-ipfs/blob/master/.github/config.yml --- .github/config.yml | 64 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/config.yml diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 00000000..915efced --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,64 @@ +# Configuration for welcome - https://github.com/behaviorbot/welcome + +# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome +# Comment to be posted to on first time issues +newIssueWelcomeComment: > + Thank you for submitting your first issue to this repository! A maintainer + will be here shortly to triage and review. + + In the meantime, please double-check that you have provided all the + necessary information to make this process easy! Any information that can + help save additional round trips is useful! We currently aim to give + initial feedback within **two business days**. If this does not happen, feel + free to leave a comment. + + Please keep an eye on how this issue will be labeled, as labels give an + overview of priorities, assignments and additional actions requested by the + maintainers: + + - "Priority" labels will show how urgent this is for the team. + - "Status" labels will show if this is ready to be worked on, blocked, or in progress. + - "Need" labels will indicate if additional input or analysis is required. + + Finally, remember to use [the discussion tab](https://github.com/basnijholt/adaptive-lighting/discussions) if you just need general + support. + +# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome +# Comment to be posted to on PRs from first time contributors in your repository +newPRWelcomeComment: > + Thank you for submitting this PR! + + A maintainer will be here shortly to review it. + + We are super grateful! Help us by making sure that: + + * The context for this PR is clear, with relevant discussion, decisions + and stakeholders linked/mentioned. + + * Your contribution itself is clear (code comments, self-review for the + rest) and in its best form. + + Getting other community members to do a review would be great help too on + complex PRs. If you are unsure about something, just leave us a comment. + + Next steps: + + * A maintainer will triage and assign priority to this PR, commenting on + any missing things and potentially assigning a reviewer for high + priority items. + + * The PR gets reviews, discussed and approvals as needed. + + * The PR is merged by maintainers when it has been approved and comments addressed. + + We currently aim to provide initial feedback/triaging within **two business + days**. Please keep an eye on any labelling actions, as these will indicate + priorities and status of your contribution. + + We are very grateful for your contribution! + + +# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge +# Comment to be posted to on pull requests merged by a first time user +# Currently disabled +#firstPRMergeComment: "" From e9164e7f63eb17b9090f3b54c7600f2249f6dd73 Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Sun, 1 Aug 2021 16:43:09 +0200 Subject: [PATCH 0302/1077] Create auto-comment.yml Source: https://github.com/ipfs/go-ipfs/blob/master/.github/auto-comment.yml --- .github/auto-comment.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/auto-comment.yml diff --git a/.github/auto-comment.yml b/.github/auto-comment.yml new file mode 100644 index 00000000..c1272240 --- /dev/null +++ b/.github/auto-comment.yml @@ -0,0 +1,6 @@ +# Comment to a new issue. +# Disabled +# issueOpened: "" + +# Disabled +# pullRequestOpened: "" From cd5abe80a4fb47ca3a716df26703344b77e24819 Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Sun, 1 Aug 2021 16:54:42 +0200 Subject: [PATCH 0303/1077] Create bug-report.md Source: https://github.com/ipfs/go-ipfs/blob/08e058427f760d8d171a6666d955ca7146cd352a/.github/ISSUE_TEMPLATE/bug-report.md --- .github/ISSUE_TEMPLATE/bug-report.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.md diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 00000000..c5bcb3a4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,17 @@ +--- +name: 'Bug Report' +about: 'Report a bug in adaptive-lighting.' +labels: kind/bug, need/triage +--- + +#### Version information: + + +#### Description: + From d9639e0dccd8d9a656ea1e20aac5965db7a28661 Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Sun, 1 Aug 2021 16:57:15 +0200 Subject: [PATCH 0304/1077] Create doc.md Source: https://github.com/ipfs/go-ipfs/blob/08e058427f760d8d171a6666d955ca7146cd352a/.github/ISSUE_TEMPLATE/doc.md --- .github/ISSUE_TEMPLATE/doc.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/doc.md diff --git a/.github/ISSUE_TEMPLATE/doc.md b/.github/ISSUE_TEMPLATE/doc.md new file mode 100644 index 00000000..98c9a008 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/doc.md @@ -0,0 +1,13 @@ +--- +name: 'Documentation Issue' +about: 'Report missing, erroneous docs, broken links or propose new docs' +labels: kind/docs_issue, need/triage +--- + +#### Location + + + +#### Description + + From aeca253ade1074fcad58f02bfbff6feb016505d6 Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Sun, 1 Aug 2021 16:58:09 +0200 Subject: [PATCH 0305/1077] Create enhancement.md --- .github/ISSUE_TEMPLATE/enhancement.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/enhancement.md diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md new file mode 100644 index 00000000..71501f54 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -0,0 +1,6 @@ +--- +name: 'Enhancement' +about: 'Suggest an improvement to an existing feature.' +labels: kind/enhancement need/triage +--- + From d083af2149241413bf868a9fa10c32f550127738 Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Sun, 1 Aug 2021 16:58:52 +0200 Subject: [PATCH 0306/1077] Create feature.md --- .github/ISSUE_TEMPLATE/feature.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature.md diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 00000000..c4b787df --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,5 @@ +--- +name: 'Feature' +about: 'Suggest a new feature' +labels: kind/feature, need/triage +--- From 4edd1191d986149d97351042911a11b62240bce6 Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Sun, 1 Aug 2021 17:02:39 +0200 Subject: [PATCH 0307/1077] Create config.yml --- .github/ISSUE_TEMPLATE/config.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..c4eeda14 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: Getting Help on adaptive-lighting + url: https://github.com/basnijholt/adaptive-lighting/discussions/categories/q-a + about: Q&A section of the discussion tab + - name: Share your idea + url: https://github.com/basnijholt/adaptive-lighting/discussions/categories/ideas + about: And discuss it with the community + - name: General discussions about this component + url: https://github.com/basnijholt/adaptive-lighting/discussions/categories/general + about: General discussions about this component + - name: Share your setup with adaptive-lighting + url: https://github.com/basnijholt/adaptive-lighting/discussions/categories/show-and-tell + about: Or see what other people do with this component From a24e8d16b0ad0523fe7949e11b3fb29b81836c63 Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Sun, 1 Aug 2021 17:05:13 +0200 Subject: [PATCH 0308/1077] ISSUE_TEMPLATE/enhancement.md: add missing comma --- .github/ISSUE_TEMPLATE/enhancement.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md index 71501f54..fcd16fc6 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.md +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -1,6 +1,6 @@ --- name: 'Enhancement' about: 'Suggest an improvement to an existing feature.' -labels: kind/enhancement need/triage +labels: kind/enhancement, need/triage --- From f7afdb1ba2da6d1e34869d6fc17ad54c5a5e6636 Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Sun, 1 Aug 2021 17:08:40 +0200 Subject: [PATCH 0309/1077] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ffe75355..e311694e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![](https://github.com/home-assistant/brands/raw/b4a168b9af282ef916e120d31091ecd5e3c35e66/core_integrations/adaptive_lighting/icon.png) _Try out this code by adding https://github.com/basnijholt/adaptive-lighting to your custom repos in [HACS (Home Assistant Community Store)](https://hacs.xyz/) and install it!_ -*This `custom_component` is also being added to `core`, see [this PR](https://github.com/home-assistant/core/pull/40626), although it might take months before it makes it in.* + The `adaptive_lighting` platform changes the settings of your lights throughout the day. It uses the position of the sun to calculate the color temperature and brightness that is most fitting for that time of the day. From 449de6510ff006207c9cd1a8d2355bd7fc50df5d Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Sun, 1 Aug 2021 17:47:20 +0200 Subject: [PATCH 0310/1077] add validation workflow for HACS --- .github/workflows/validate.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/validate.yml diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 00000000..fc1b5f91 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,17 @@ +name: Validate + +on: + push: + pull_request: + schedule: + - cron: "0 0 * * *" + +jobs: + validate: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v2" + - name: HACS validation + uses: "hacs/action@main" + with: + category: "integration" From 523ddb17d6c9383feaf55c57039902ac02e7ee84 Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Sun, 1 Aug 2021 17:48:31 +0200 Subject: [PATCH 0311/1077] add hassfest validation --- .github/workflows/hassfest.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/hassfest.yaml diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml new file mode 100644 index 00000000..18c7d193 --- /dev/null +++ b/.github/workflows/hassfest.yaml @@ -0,0 +1,14 @@ +name: Validate with hassfest + +on: + push: + pull_request: + schedule: + - cron: "0 0 * * *" + +jobs: + validate: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v2" + - uses: home-assistant/actions/hassfest@master From 038e7c8a32a2f0a689bc92e02bfb97d51ac3751c Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Sun, 1 Aug 2021 18:06:38 +0200 Subject: [PATCH 0312/1077] manifest.json: fix informations/add missing ones - fix documentation link - add issue_tracker link - add iot_class - fix version --- custom_components/adaptive_lighting/manifest.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index ee4c828b..b7716d87 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -1,10 +1,12 @@ { "domain": "adaptive_lighting", "name": "Adaptive Lighting", - "documentation": "https://www.home-assistant.io/integrations/adaptive_lighting", + "documentation": "https://github.com/basnijholt/adaptive-lighting#readme", + "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "config_flow": true, "dependencies": [], "codeowners": ["@basnijholt"], - "version": "1.0.0", - "requirements": [] + "version": "1.0.13", + "requirements": [], + "iot_class": "calculated" } From 995ecb4ebf9543ae469bb68d02af2966aa7a47b3 Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Sun, 1 Aug 2021 22:14:36 +0200 Subject: [PATCH 0313/1077] update code owners in manifest - fix formatting - update version --- custom_components/adaptive_lighting/manifest.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index b7716d87..d4f9a091 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -2,11 +2,11 @@ "domain": "adaptive_lighting", "name": "Adaptive Lighting", "documentation": "https://github.com/basnijholt/adaptive-lighting#readme", - "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", + "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "config_flow": true, "dependencies": [], - "codeowners": ["@basnijholt"], - "version": "1.0.13", + "codeowners": ["@basnijholt", "@RubenKelevra"], + "version": "1.0.14", "requirements": [], "iot_class": "calculated" } From 64e48dfd542645832e870493fdd4047645536c50 Mon Sep 17 00:00:00 2001 From: Mike Roberts Date: Thu, 26 Aug 2021 10:15:34 -0700 Subject: [PATCH 0314/1077] calculate light settings at end of transition period --- custom_components/adaptive_lighting/switch.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index d4f49c8c..c2603f06 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -557,9 +557,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR] self._separate_turn_on_commands = data[CONF_SEPARATE_TURN_ON_COMMANDS] self._take_over_control = data[CONF_TAKE_OVER_CONTROL] - self._transition = min( - data[CONF_TRANSITION], self._interval.total_seconds() // 2 - ) + self._transition = data[CONF_TRANSITION] _loc = get_astral_location(self.hass) if isinstance(_loc, tuple): # Astral v2.2 @@ -582,6 +580,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): sunset_offset=data[CONF_SUNSET_OFFSET], sunset_time=data[CONF_SUNSET_TIME], time_zone=self.hass.config.time_zone, + transition=data[CONF_TRANSITION], ) # Set other attributes @@ -744,7 +743,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _async_update_at_interval(self, now=None) -> None: await self._update_attrs_and_maybe_adapt_lights( - force=False, context=self.create_context("interval") + transition=self._transition, force=False, context=self.create_context("interval") ) async def _adapt_light( @@ -857,7 +856,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) assert self.is_on self._settings = self._sun_light_settings.get_settings( - self.sleep_mode_switch.is_on + self.sleep_mode_switch.is_on, transition ) self.async_write_ha_state() if lights is None: @@ -1043,6 +1042,7 @@ class SunLightSettings: sunset_offset: Optional[datetime.timedelta] sunset_time: Optional[datetime.time] time_zone: datetime.tzinfo + transition: int def get_sun_events(self, date: datetime.datetime) -> Dict[str, float]: """Get the four sun event's timestamps at 'date'.""" @@ -1113,11 +1113,13 @@ class SunLightSettings: i_now = bisect.bisect([ts for _, ts in events], now.timestamp()) return events[i_now - 1 : i_now + 1] - def calc_percent(self) -> float: + def calc_percent(self, transition: int) -> float: """Calculate the position of the sun in %.""" now = dt_util.utcnow() - now_ts = now.timestamp() - today = self.relevant_events(now) + + target_time = now + timedelta(seconds=transition) + target_ts = target_time.timestamp() + today = self.relevant_events(target_time) (_, prev_ts), (next_event, next_ts) = today h, x = ( # pylint: disable=invalid-name (prev_ts, next_ts) @@ -1125,7 +1127,7 @@ class SunLightSettings: else (next_ts, prev_ts) ) k = 1 if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_NOON) else -1 - percentage = (0 - k) * ((now_ts - h) / (h - x)) ** 2 + k + percentage = (0 - k) * ((target_ts - h) / (h - x)) ** 2 + k return percentage def calc_brightness_pct(self, percent: float, is_sleep: bool) -> float: @@ -1148,13 +1150,13 @@ class SunLightSettings: return self.min_color_temp def get_settings( - self, is_sleep + self, is_sleep, transition ) -> Dict[str, Union[float, Tuple[float, float], Tuple[float, float, float]]]: """Get all light settings. Calculating all values takes <0.5ms. """ - percent = self.calc_percent() + percent = self.calc_percent(transition) if transition is not None else self.calc_percent(0) brightness_pct = self.calc_brightness_pct(percent, is_sleep) color_temp_kelvin = self.calc_color_temp_kelvin(percent, is_sleep) color_temp_mired: float = color_temperature_kelvin_to_mired(color_temp_kelvin) From 7581c8bf82493abfb52e8a46c8f253aab517d470 Mon Sep 17 00:00:00 2001 From: "Michael \"Chishm\" Chisholm" Date: Wed, 8 Sep 2021 13:27:55 +1000 Subject: [PATCH 0315/1077] Associate contexts with causing events/services via parent_id --- custom_components/adaptive_lighting/switch.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index d4f49c8c..2cf6a597 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -177,12 +177,17 @@ def _short_hash(string: str, length: int = 4) -> str: return hashlib.sha1(string.encode("UTF-8")).hexdigest()[:length] -def create_context(name: str, which: str, index: int) -> Context: +def create_context( + name: str, which: str, index: int, parent: Optional[Context] = None +) -> Context: """Create a context that can identify this integration.""" # Use a hash for the name because otherwise the context might become # too long (max len == 36) to fit in the database. name_hash = _short_hash(name) - return Context(id=f"{_DOMAIN_SHORT}_{name_hash}_{which}_{index}") + parent_id = parent.id if parent else None + return Context( + id=f"{_DOMAIN_SHORT}_{name_hash}_{which}_{index}", parent_id=parent_id + ) def is_our_context(context: Optional[Context]) -> bool: @@ -228,6 +233,7 @@ async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): data[ATTR_ADAPT_COLOR], data[CONF_PREFER_RGB_COLOR], force=True, + context=switch.create_context("service", parent=service_call.context), ) @@ -254,7 +260,7 @@ async def handle_set_manual_control(switch: AdaptiveSwitch, service_call: Servic all_lights, transition=switch._initial_transition, force=True, - context=switch.create_context("service"), + context=switch.create_context("service", parent=service_call.context), ) @@ -701,7 +707,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ] return dict(self._settings, manual_control=manual_control) - def create_context(self, which: str = "default") -> Context: + def create_context( + self, which: str = "default", parent: Optional[Context] = None + ) -> Context: """Create a context that identifies this Adaptive Lighting instance.""" # Right now the highest number of each context_id it can create is # 'adapt_lgt_XXXX_turn_on_9999999999999' @@ -711,7 +719,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # 'adapt_lgt_XXXX_light_event_999999999' # 'adapt_lgt_XXXX_service_9999999999999' # So 100 million calls before we run into the 36 chars limit. - context = create_context(self._name, which, self._context_cnt) + context = create_context(self._name, which, self._context_cnt, parent=parent) self._context_cnt += 1 return context @@ -915,7 +923,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): await self._update_attrs_and_maybe_adapt_lights( transition=self._initial_transition, force=True, - context=self.create_context("sleep"), + context=self.create_context("sleep", parent=event.context), ) async def _light_event(self, event: Event) -> None: @@ -956,7 +964,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): lights=[entity_id], transition=self._initial_transition, force=True, - context=self.create_context("light_event"), + context=self.create_context("light_event", parent=event.context), ) elif ( old_state is not None From fa10d95305338e0dc09fe80ac71cc8d382fc2518 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Thu, 16 Sep 2021 14:15:36 +1000 Subject: [PATCH 0316/1077] Make the transition time to/from sleep mode configurable Signed-off-by: Avi Miller --- custom_components/adaptive_lighting/const.py | 2 ++ custom_components/adaptive_lighting/strings.json | 3 ++- custom_components/adaptive_lighting/switch.py | 4 +++- custom_components/adaptive_lighting/translations/en.json | 3 ++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index e1f2e3cb..b182ed82 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -17,6 +17,7 @@ CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = ( False, ) CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 +CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION = "sleep_transition", 1 CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100 CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP = "max_color_temp", 5500 @@ -63,6 +64,7 @@ VALIDATION_TUPLES = [ (CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), (CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool), (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), + (CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION, VALID_TRANSITION), (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), (CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS, int_between(1, 100)), diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 274569da..72fdcb3b 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -21,7 +21,8 @@ "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", "data": { "lights": "lights", - "initial_transition": "initial_transition, when lights go 'off' to 'on' or when 'sleep_state' changes", + "initial_transition": "initial_transition, when lights go 'off' to 'on'", + "sleep_transition": "sleep_transition, when 'sleep_state' changes", "interval": "interval, time between switch updates in seconds", "max_brightness": "max_brightness, in %", "max_color_temp": "max_color_temp, in Kelvin", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 2cf6a597..0d571119 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -99,6 +99,7 @@ from .const import ( ATTR_TURN_ON_OFF_LISTENER, CONF_DETECT_NON_HA_CHANGES, CONF_INITIAL_TRANSITION, + CONF_SLEEP_TRANSITION, CONF_INTERVAL, CONF_LIGHTS, CONF_MANUAL_CONTROL, @@ -558,6 +559,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] self._initial_transition = data[CONF_INITIAL_TRANSITION] + self._sleep_transition = data[CONF_SLEEP_TRANSITION] self._interval = data[CONF_INTERVAL] self._only_once = data[CONF_ONLY_ONCE] self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR] @@ -921,7 +923,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Reset the manually controlled status when the "sleep mode" changes self.turn_on_off_listener.reset(*self._lights) await self._update_attrs_and_maybe_adapt_lights( - transition=self._initial_transition, + transition=self._sleep_transition, force=True, context=self.create_context("sleep", parent=event.context), ) diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index e66b6769..fb1c7f04 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -21,7 +21,8 @@ "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have the adaptive_lighting entry defined in your YAML configuration.", "data": { "lights": "lights", - "initial_transition": "initial_transition: When lights turn 'off' to 'on' or when 'sleep_state' changes. (seconds)", + "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (seconds)", + "sleep_transition": "sleep_transition: When 'sleep_state' changes. (seconds)", "interval": "interval: Time between switch updates. (seconds)", "max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)", "max_color_temp": "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)", From 4492cfe6155cd8020b3cee15cb7126e5e680fb48 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Thu, 16 Sep 2021 22:09:18 +1000 Subject: [PATCH 0317/1077] Update README.md to include sleep_transition Signed-off-by: Avi Miller --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e311694e..224c8bf5 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,8 @@ adaptive_lighting: | name | The name to use when displaying this switch. | False | default | string | | lights | List of light entities for Adaptive Lighting to control (may be empty). | False | list | [] | | prefer_rgb_color | Whether to use RGB color adjustment instead of native light color temperature. | False | False | boolean | -| initial_transition | How long the first transition is when the lights go from `off` to `on` (or when "sleep mode" is toggled). | False | 1 | time | +| initial_transition | How long the first transition is when the lights go from `off` to `on`. | False | 1 | time | +| sleep_transition | How long the transition is when when "sleep mode" is toggled | False | 1 | time | | transition | How long the transition is when the lights change, in seconds. | False | 45 | integer | | interval | How often to adapt the lights, in seconds. | False | 90 | integer | | min_brightness | The minimum percent of brightness to set the lights to. | False | 1 | integer | @@ -191,4 +192,3 @@ These graphs were generated using the values calculated by the Adaptive Lighting - @basnijholt - @RubenKelevra - From f3a3c1cd483ba471a3cf951b77b66592f5d42ab9 Mon Sep 17 00:00:00 2001 From: Michel Peterson Date: Wed, 22 Sep 2021 11:22:10 +0300 Subject: [PATCH 0318/1077] Default `lights` of the `apply` service The apply service is meant to apply the adaptive lighting parameters to a specific set of light(s). This set of lights need to be passed to the service, even though each Adaptive Lighting switch already has this list configured on itself. While having the flexibility to apply to some, it also might be useful to apply to all lights that the switch manages. This patch makes the lights paramater optional and defaults it to the lights configured on the corresponding configuration entry of the switch being called. --- custom_components/adaptive_lighting/services.yaml | 2 +- custom_components/adaptive_lighting/switch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 10bfd2a8..8f449a77 100755 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -5,7 +5,7 @@ apply: description: entity_id of the Adaptive Lighting switch. example: switch.adaptive_lighting_default lights: - description: entity_id(s) of lights. + description: "entity_id(s) of lights, default: lights of the switch" example: light.bedroom_ceiling transition: description: Transition of the lights. diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index ffd8bebd..be877dff 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -322,7 +322,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_APPLY, { - vol.Required(CONF_LIGHTS): cv.entity_ids, + vol.Optional(CONF_LIGHTS, default=switch._lights): cv.entity_ids, # pylint: disable=protected-access vol.Optional( CONF_TRANSITION, default=switch._initial_transition, # pylint: disable=protected-access From 346dda9c6c4bc81c21252a16d92104665ae082fe Mon Sep 17 00:00:00 2001 From: Sindre Broch Date: Wed, 22 Sep 2021 21:40:21 +0200 Subject: [PATCH 0319/1077] Fix spelling in description to Kelvin --- custom_components/adaptive_lighting/translations/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index fb1c7f04..cc32a722 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -27,7 +27,7 @@ "max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)", "max_color_temp": "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)", "min_brightness": "min_brightness: Lowest brightness of lights during a cycle. (%)", - "min_color_temp": "min_color_temp, Warmest hue of the color temperature cycle. (%)", + "min_color_temp": "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)", "only_once": "only_once: Only adapt the lights when turning them on.", "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.", "separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).", From ebbf6673c1740244ee7029667af85a341cf33f7d Mon Sep 17 00:00:00 2001 From: Shulyaka Date: Fri, 24 Sep 2021 01:47:53 +0300 Subject: [PATCH 0320/1077] Add .gitignore from python template --- .gitignore | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b6e47617 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ From e2212c11448da1bcdf719f5db411687b7afc7fdf Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 29 Sep 2021 10:07:00 +0200 Subject: [PATCH 0321/1077] fix docs on set_manual_control --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 224c8bf5..2b4edb82 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,8 @@ adaptive_lighting: | Service data attribute | Optional | Description | |------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------| | `entity_id` | no | The `entity_id` of the switch in which to (un)mark the light as being "manually controlled". | -| `lights` | no | A light (or list of lights) to apply the settings to. | -| `manual_control` | yes | Whether to mark (true) or unmark (false) the light as "manually controlled", when not specified it selects all lights in the switch. | +| `lights` | yes | entity_id(s) of lights, if not specified, all lights in the switch are selected. | +| `manual_control` | yes | Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true | ## Automation examples From 249d6f82c8cd2b9f2ce2dd3916b879fe206b6c80 Mon Sep 17 00:00:00 2001 From: vapescherov Date: Mon, 8 Nov 2021 03:34:50 +0500 Subject: [PATCH 0322/1077] Allow sunsets after midnight --- custom_components/adaptive_lighting/switch.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index be877dff..6c9cd7d6 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1066,6 +1066,18 @@ class SunLightSettings: utc_time = date_time.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) return utc_time + def calculate_noon_and_midnight( + sunset: datetime.datetime, sunrise: datetime.datetime + ) -> (datetime.datetime, datetime.datetime): + middle = abs(sunset - sunrise) / 2 + if sunset > sunrise: + noon = sunrise + middle + midnight = noon + timedelta(hours=12) * (1 if noon.hour < 12 else -1) + else: + midnight = sunset + middle + noon = midnight + timedelta(hours=12) * (1 if midnight.hour < 12 else -1) + return noon, midnight + location = self.astral_location sunrise = ( @@ -1089,8 +1101,7 @@ class SunLightSettings: solar_noon = location.noon(date, local=False) solar_midnight = location.midnight(date, local=False) else: - solar_noon = sunrise + (sunset - sunrise) / 2 - solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset) / 2 + (solar_noon, solar_midnight) = calculate_noon_and_midnight(sunset, sunrise) events = [ (SUN_EVENT_SUNRISE, sunrise.timestamp()), From 65fad194539a308aab1435919fdb4c08a4a3627a Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Tue, 9 Nov 2021 01:29:56 +0100 Subject: [PATCH 0323/1077] add some batches stolen from https://github.com/bramstroker/homeassistant-powercalc/edit/master/README.md ;) Source: https://github.com/bramstroker/homeassistant-powercalc/edit/master/README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 2b4edb82..d0dadacc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs) +![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting) + # Adaptive Lighting component for Home Assistant ![](https://github.com/home-assistant/brands/raw/b4a168b9af282ef916e120d31091ecd5e3c35e66/core_integrations/adaptive_lighting/icon.png) From cc90c512555977dcd204d4c1a06d5e5ee1304b25 Mon Sep 17 00:00:00 2001 From: Simon Gurcke Date: Sat, 13 Nov 2021 23:49:03 +1000 Subject: [PATCH 0324/1077] Fix default lights for adaptive_lighting.apply service --- custom_components/adaptive_lighting/switch.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 6c9cd7d6..c07f8b5c 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -222,9 +222,15 @@ async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): """Handle the entity service apply.""" hass = switch.hass data = service_call.data - all_lights = _expand_light_groups(hass, data[CONF_LIGHTS]) + all_lights = data[CONF_LIGHTS] + if not all_lights: + all_lights = switch._lights + all_lights = _expand_light_groups(hass, all_lights) switch.turn_on_off_listener.lights.update(all_lights) - + _LOGGER.debug( + "Called 'adaptive_lighting.apply' service with '%s'", + data, + ) for light in all_lights: if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light): await switch._adapt_light( # pylint: disable=protected-access @@ -322,7 +328,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_APPLY, { - vol.Optional(CONF_LIGHTS, default=switch._lights): cv.entity_ids, # pylint: disable=protected-access + vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, # pylint: disable=protected-access vol.Optional( CONF_TRANSITION, default=switch._initial_transition, # pylint: disable=protected-access From 169042ce9dcf12ba63fad2bd8946a68ff22377c8 Mon Sep 17 00:00:00 2001 From: covid10 <71146231+covid10@users.noreply.github.com> Date: Fri, 10 Dec 2021 17:14:50 +0100 Subject: [PATCH 0325/1077] Create nb.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Norwegian (norsk bokmål) translation --- .../adaptive_lighting/translations/nb.json | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/nb.json diff --git a/custom_components/adaptive_lighting/translations/nb.json b/custom_components/adaptive_lighting/translations/nb.json new file mode 100644 index 00000000..fbbfafee --- /dev/null +++ b/custom_components/adaptive_lighting/translations/nb.json @@ -0,0 +1,49 @@ +{ + "title":"Adaptiv Belysning", + "config":{ + "step":{ + "user":{ + "title":"Velg et navn", + "description":"Velg et navn for denne konfigurasjonen for adaptiv belysning - hver konfigurasjon kan inneholde flere lyskilder!", + "data":{ + "name":"Navn" + } + } + }, + "abort":{ + "already_configured":"Denne enheten er allerede konfigurert!" + } + }, + "options":{ + "step":{ + "init":{ + "title":"Adaptiv Belysning Innstillinger", + "description":"Alle innstillinger for en adaptiv belysning konfigurasjon. Innstillingene er identiske med innstillingene for YAML konfigurasjon. Ingen innstillinger vises dersom du har definert adaptive_lighting i din YAML konfigurasjon.", + "data":{ + "lights":"Lys / Lyskilder", + "initial_transition":"'initial_transition': varigheten på startovergangen (i sekunder) når lysene skrus av eller på - eller når 'sleep_state' endres", + "interval":"'interval': tiden mellom oppdateringer (i sekunder)", + "max_brightness":"'max_brightness': den høyeste lysstyrken (i prosent) på lysene i løpet av en syklus (fra soloppgang til solnedgang)", + "max_color_temp":"'max_color_temp': den høyeste fargetemperaturen (i kelvin) på lysene i løpet av en syklus (fra soloppgang til solnedgang)", + "min_brightness":"'min_brightness': den laveste lysstyrken (i prosent) på lysene i løpet av en syklus (fra soloppgang til solnedgang)", + "min_color_temp":"'min_color_temp': den laveste fargetemperaturen (i kelvin) på lysene i løpet av en syklus (fra soloppgang til solnedgang)", + "only_once":"'only_once': anvend innstillingene for adaptiv belysning kun når lysene skrus av eller på", + "prefer_rgb_color":"'prefer_rgb_color': benytt rgb i stedet for fargetemperatur dersom det er mulig", + "separate_turn_on_commands":"'separate_turn_on_commands': separer kommandone i 'light.turn_on' for hver attributt (farge, lysstyrke, osv.). Dette kan være nødvendig for enkelte typer lys / lyskilder", + "sleep_brightness":"'sleep_brightness': lysstyrken på lysene (i prosent) når 'sleep_mode' (søvnmodus) er aktiv", + "sleep_color_temp":"'sleep_color_temp': fargetemperaturen på lysene (i kelvin) når 'sleep_mode' (søvnmodus) er aktiv", + "sunrise_offset":"'sunrise_offset': utligningen i tidspunktet for soloppgang - hvor lenge før (-) eller etter (+) tidspunktet solen står opp (lokalt) skal defineres som soloppgang (i sekunder - f. eks: '-1800' vil definere tidspunktet for soloppgang en halvtime tidligere enn det faktiske tidspunktet for soloppgang)", + "sunrise_time":"'sunrise_time': definer tidspunktet for soloppgang manuelt (i følgende format: TT:MM:SS - f. eks: '08:00:00' vil definere tidspunktet for soloppgang som klokken 8 på morgenen)", + "sunset_offset":"'sunset_offset': utligningen i tidspunktet for solnedgang - hvor lenge før (-) eller etter (+) tidspunktet solen går ned (lokalt) skal defineres som solnedgang (i sekunder - f. eks: '+3600' vil definere tidspunktet for solnedgang en time senere enn det faktiske tidspunktet for solnedgang)", + "sunset_time":"'sunset_time': definer tidspunktet for solnedgang manuelt (i følgende format: TT:MM:SS - f. eks: '20:30:00' vil definere tidspunktet for solnegang som halv-ni på kvelden)", + "take_over_control":"'take_over_control': dersom en annen tjeneste enn adaptiv belysning skrur lysene av eller på, vil automatisk adaptering av lyset stoppes inntil lyset (eller den tilhørende bryteren for adaptiv belysning) blir slått av - og på igjen", + "detect_non_ha_changes":"'detect_non_ha_changes': registrerer alle endringer i lysstyrke over 10% med opprinnelse utenfor Home Assistant - krever at 'take_over_control' er aktivert (OBS: tilkaller 'homeassistant.update_entity' ved hvert 'interval'!)", + "transition":"'transition': varigheten (i sekunder) på overgangen når lysene oppdateres (f.eks: dersom '45' er oppgitt, vil det være en 45 sekunders overgangsfase fra gjeldende lysinnstillinger og over til oppdaterte lysinnstillinger)" + } + } + }, + "error":{ + "option_error":"En eller flere valgte innstillinger er ugyldige" + } + } +} From 741225ab90efeca4b648acd5fcfeecc5e2697e02 Mon Sep 17 00:00:00 2001 From: covid10 <71146231+covid10@users.noreply.github.com> Date: Fri, 10 Dec 2021 17:27:42 +0100 Subject: [PATCH 0326/1077] Norwegian translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Norwegian (norsk bokmål) translation --- .../adaptive_lighting/translations/nb.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/nb.json b/custom_components/adaptive_lighting/translations/nb.json index fbbfafee..2abeae9b 100644 --- a/custom_components/adaptive_lighting/translations/nb.json +++ b/custom_components/adaptive_lighting/translations/nb.json @@ -21,24 +21,24 @@ "description":"Alle innstillinger for en adaptiv belysning konfigurasjon. Innstillingene er identiske med innstillingene for YAML konfigurasjon. Ingen innstillinger vises dersom du har definert adaptive_lighting i din YAML konfigurasjon.", "data":{ "lights":"Lys / Lyskilder", - "initial_transition":"'initial_transition': varigheten på startovergangen (i sekunder) når lysene skrus av eller på - eller når 'sleep_state' endres", + "initial_transition":"'initial_transition': overgangen (i sekunder) når lysene skrus av eller på - eller når 'sleep_state' endres", "interval":"'interval': tiden mellom oppdateringer (i sekunder)", - "max_brightness":"'max_brightness': den høyeste lysstyrken (i prosent) på lysene i løpet av en syklus (fra soloppgang til solnedgang)", - "max_color_temp":"'max_color_temp': den høyeste fargetemperaturen (i kelvin) på lysene i løpet av en syklus (fra soloppgang til solnedgang)", - "min_brightness":"'min_brightness': den laveste lysstyrken (i prosent) på lysene i løpet av en syklus (fra soloppgang til solnedgang)", - "min_color_temp":"'min_color_temp': den laveste fargetemperaturen (i kelvin) på lysene i løpet av en syklus (fra soloppgang til solnedgang)", + "max_brightness":"'max_brightness': den høyeste lysstyrken (i prosent) på lysene i løpet av en syklus", + "max_color_temp":"'max_color_temp': den høyeste fargetemperaturen (i kelvin) på lysene i løpet av en syklus", + "min_brightness":"'min_brightness': den laveste lysstyrken (i prosent) på lysene i løpet av en syklus", + "min_color_temp":"'min_color_temp': den laveste fargetemperaturen (i kelvin) på lysene i løpet av en syklus", "only_once":"'only_once': anvend innstillingene for adaptiv belysning kun når lysene skrus av eller på", "prefer_rgb_color":"'prefer_rgb_color': benytt rgb i stedet for fargetemperatur dersom det er mulig", "separate_turn_on_commands":"'separate_turn_on_commands': separer kommandone i 'light.turn_on' for hver attributt (farge, lysstyrke, osv.). Dette kan være nødvendig for enkelte typer lys / lyskilder", "sleep_brightness":"'sleep_brightness': lysstyrken på lysene (i prosent) når 'sleep_mode' (søvnmodus) er aktiv", "sleep_color_temp":"'sleep_color_temp': fargetemperaturen på lysene (i kelvin) når 'sleep_mode' (søvnmodus) er aktiv", - "sunrise_offset":"'sunrise_offset': utligningen i tidspunktet for soloppgang - hvor lenge før (-) eller etter (+) tidspunktet solen står opp (lokalt) skal defineres som soloppgang (i sekunder - f. eks: '-1800' vil definere tidspunktet for soloppgang en halvtime tidligere enn det faktiske tidspunktet for soloppgang)", - "sunrise_time":"'sunrise_time': definer tidspunktet for soloppgang manuelt (i følgende format: TT:MM:SS - f. eks: '08:00:00' vil definere tidspunktet for soloppgang som klokken 8 på morgenen)", - "sunset_offset":"'sunset_offset': utligningen i tidspunktet for solnedgang - hvor lenge før (-) eller etter (+) tidspunktet solen går ned (lokalt) skal defineres som solnedgang (i sekunder - f. eks: '+3600' vil definere tidspunktet for solnedgang en time senere enn det faktiske tidspunktet for solnedgang)", + "sunrise_offset":"'sunrise_offset': hvor lenge før (-) eller etter (+) tidspunktet solen står opp (lokalt) skal defineres som soloppgang (i sekunder)", + "sunrise_time":"'sunrise_time': definer tidspunktet for soloppgang manuelt (i følgende format: TT:MM:SS)", + "sunset_offset":"'sunset_offset': hvor lenge før (-) eller etter (+) tidspunktet solen går ned (lokalt) skal defineres som solnedgang (i sekunder)", "sunset_time":"'sunset_time': definer tidspunktet for solnedgang manuelt (i følgende format: TT:MM:SS - f. eks: '20:30:00' vil definere tidspunktet for solnegang som halv-ni på kvelden)", "take_over_control":"'take_over_control': dersom en annen tjeneste enn adaptiv belysning skrur lysene av eller på, vil automatisk adaptering av lyset stoppes inntil lyset (eller den tilhørende bryteren for adaptiv belysning) blir slått av - og på igjen", "detect_non_ha_changes":"'detect_non_ha_changes': registrerer alle endringer i lysstyrke over 10% med opprinnelse utenfor Home Assistant - krever at 'take_over_control' er aktivert (OBS: tilkaller 'homeassistant.update_entity' ved hvert 'interval'!)", - "transition":"'transition': varigheten (i sekunder) på overgangen når lysene oppdateres (f.eks: dersom '45' er oppgitt, vil det være en 45 sekunders overgangsfase fra gjeldende lysinnstillinger og over til oppdaterte lysinnstillinger)" + "transition":"'transition': varigheten (i sekunder) på overgangen når lysene oppdateres " } } }, From 21f088e82576a25ce4fd2795312e0d0bf14155df Mon Sep 17 00:00:00 2001 From: covid10 <71146231+covid10@users.noreply.github.com> Date: Sat, 11 Dec 2021 13:23:31 +0100 Subject: [PATCH 0327/1077] device_state_attributes warnings 2021.12.0b (#230) * device_state_attributes warnings 2021.12.0b Fix for device_state_attributes warnings which began in release 2021.12.0b * device_state_attributes warnings 2021.12.0b Fix for device_state_attributes warnings which began in release 2021.12.0b --- custom_components/adaptive_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index c07f8b5c..af826196 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -703,7 +703,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return self._icon @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> Dict[str, Any]: """Return the attributes of the switch.""" if not self.is_on: return {key: None for key in self._settings} From b4138c7827df9106fe54ac750764f229743c3fe7 Mon Sep 17 00:00:00 2001 From: bedaes Date: Sat, 11 Dec 2021 22:01:26 +0100 Subject: [PATCH 0328/1077] Fix tuple expression in type annotation --- custom_components/adaptive_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index af826196..7e023134 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1074,7 +1074,7 @@ class SunLightSettings: def calculate_noon_and_midnight( sunset: datetime.datetime, sunrise: datetime.datetime - ) -> (datetime.datetime, datetime.datetime): + ) -> Tuple[datetime.datetime, datetime.datetime]: middle = abs(sunset - sunrise) / 2 if sunset > sunrise: noon = sunrise + middle From 87ba587d0fb7897b3fc908474102be15b81425b9 Mon Sep 17 00:00:00 2001 From: gvssr <61377476+gvssr@users.noreply.github.com> Date: Thu, 16 Dec 2021 09:22:33 +0100 Subject: [PATCH 0329/1077] Prettify integration name in HACS Add capitalized naming and removed underscore ( _ ) --- hacs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hacs.json b/hacs.json index 1de0dd51..500d0ebd 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { - "name": "adaptive_lighting", + "name": "Adaptive Lighting", "render_readme": true, "domains": ["switch"] } From c509bd81c04d03fa1385df1e9adcc746b4dc2f63 Mon Sep 17 00:00:00 2001 From: MangoScango Date: Fri, 24 Dec 2021 11:44:34 -0500 Subject: [PATCH 0330/1077] add adapt_delay config Option to set a delay between when a lightstate off -> on event is detected, and lights are adapted. Trying to adapt lights that are still going through their initial turning on fade in transition can cause flickering, so setting this to a number higher than the transition time avoids the problem. --- custom_components/adaptive_lighting/const.py | 2 ++ custom_components/adaptive_lighting/switch.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index b182ed82..03cf9ca3 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -52,6 +52,7 @@ CONF_MANUAL_CONTROL = "manual_control" SERVICE_APPLY = "apply" CONF_TURN_ON_LIGHTS = "turn_on_lights" +CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0 TURNING_OFF_DELAY = 5 @@ -81,6 +82,7 @@ VALIDATION_TUPLES = [ (CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool), (CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool), (CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool), + (CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, int_between(0, 100)), ] diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 7e023134..2dffd1f6 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -128,6 +128,7 @@ from .const import ( SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, TURNING_OFF_DELAY, + CONF_ADAPT_DELAY, VALIDATION_TUPLES, replace_none_str, ) @@ -572,6 +573,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._separate_turn_on_commands = data[CONF_SEPARATE_TURN_ON_COMMANDS] self._take_over_control = data[CONF_TAKE_OVER_CONTROL] self._transition = data[CONF_TRANSITION] + self._adapt_delay = data[CONF_ADAPT_DELAY] _loc = get_astral_location(self.hass) if isinstance(_loc, tuple): # Astral v2.2 @@ -966,6 +968,21 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): "%s: Cancelling adjusting lights for %s", self._name, entity_id ) return + + if self._adapt_delay > 0: + _LOGGER.debug( + "%s: sleep started for '%s' with context.id='%s'", + self._name, + entity_id, + event.context.id, + ) + await asyncio.sleep(self._adapt_delay) + _LOGGER.debug( + "%s: sleep ended for '%s' with context.id='%s'", + self._name, + entity_id, + event.context.id, + ) await self._update_attrs_and_maybe_adapt_lights( lights=[entity_id], From d56852a197f2da9f9115aca98a6fb66fdb689884 Mon Sep 17 00:00:00 2001 From: MangoScango Date: Fri, 24 Dec 2021 12:46:02 -0500 Subject: [PATCH 0331/1077] Update Strings --- custom_components/adaptive_lighting/const.py | 2 +- custom_components/adaptive_lighting/strings.json | 3 ++- custom_components/adaptive_lighting/translations/en.json | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 03cf9ca3..e7dfb1de 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -82,7 +82,7 @@ VALIDATION_TUPLES = [ (CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool), (CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool), (CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool), - (CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, int_between(0, 100)), + (CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, int_between(0, 10000)), ] diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 72fdcb3b..9bae4d3e 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -39,7 +39,8 @@ "sunset_time": "sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location)", "take_over_control": "take_over_control, if anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", "detect_non_ha_changes": "detect_non_ha_changes, detects all >5% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", - "transition": "transition, in seconds" + "transition": "transition, in seconds", + "adapt_delay": "Wait time between light turn on, and Adaptive Lights applying changes to the light state. May avoid flickering." } } }, diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index cc32a722..ecc6eff8 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -39,7 +39,8 @@ "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", - "transition": "Transition time when applying a change to the lights (seconds)" + "transition": "Transition time when applying a change to the lights (seconds)", + "adapt_delay": "Wait time between light turn on, and Adaptive Lights applying changes to the light state. May avoid flickering." } } }, From c96a35186414f4e2f16231dec33f49d237540b8d Mon Sep 17 00:00:00 2001 From: Lynilia <89228568+Lynilia@users.noreply.github.com> Date: Fri, 11 Mar 2022 02:36:13 +0100 Subject: [PATCH 0332/1077] Add French translation --- .../adaptive_lighting/translations/fr.json | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/fr.json diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json new file mode 100644 index 00000000..966967ce --- /dev/null +++ b/custom_components/adaptive_lighting/translations/fr.json @@ -0,0 +1,50 @@ +{ + "title": "Éclairage adaptatif", + "config": { + "step": { + "user": { + "title": "Choisissez un nom pour cette instance d'éclairage adaptatif", + "description": "Choisissez un nom pour cette instance. Vous pouvez configurer plusieurs instances d'éclairage adaptatif, chacune pouvant contrôler plusieurs lampes !", + "data": { + "name": "Nom" + } + } + }, + "abort": { + "already_configured": "Cet appareil est déjà configuré" + } + }, + "options": { + "step": { + "init": { + "title": "Options d'éclairage adaptatif", + "description": "Tous les paramètres de l'instance d'éclairage adaptatif. Les noms des options correspondent aux paramètres YAML. Aucune option n'est affichée si l'entrée adaptive_lighting est définie dans votre configuration YAML.", + "data": { + "lights": "lights : Les lampes à contrôler", + "initial_transition": "initial_transition : Transition (en secondes) lorsque l'état d'une lampe passe d'« éteinte » à « allumée ».", + "sleep_transition": "sleep_transition : Transition (en secondes) lorsque « sleep_state » est commuté.", + "interval": "interval : Temps (en secondes) entre deux mises à jour du commutateur.", + "max_brightness": "max_brightness : Luminosité maximale des lampes (en pourcentage) au cours d'un cycle.", + "max_color_temp": "max_color_temp : Couleur la plus froide (en kelvins) du cycle de température de couleur.", + "min_brightness": "min_brightness : Luminosité minimale des lampes (en pourcentage) au cours d'un cycle.", + "min_color_temp": "min_color_temp : Couleur la plus chaude (en kelvins) du cycle de température de couleur.", + "only_once": "only_once : Adapter les lampes uniquement au moment où elles sont allumées.", + "prefer_rgb_color": "prefer_rgb_color : Utiliser « rgb_color » plutôt que « color_temp » lorsque cela est possible.", + "separate_turn_on_commands": "separate_turn_on_commands : Séparer les commandes pour chaque attribut (couleur, luminosité, etc.) de « light.turn_on » (nécessaire pour certaines lampes).", + "sleep_brightness": "sleep_brightness : Luminosité (en pourcentage) du mode nuit.", + "sleep_color_temp": "sleep_color_temp : Température de couleur (en kelvins) du mode nuit.", + "sunrise_offset": "sunrise_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au lever du soleil.", + "sunrise_time": "sunrise_time : Heure (HH:MM:SS) du lever du soleil. Si « None », utilise l'heure correspondant à votre emplacement.", + "sunset_offset": "sunset_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au coucher du soleil.", + "sunset_time": "sunset_time : Heure (HH:MM:SS) du coucher du soleil. Si « None », utilise l'heure correspondant à votre emplacement.", + "take_over_control": "take_over_control : Si quelque chose d'autre que l'éclairage adaptatif appelle « light.turn_on » alors qu'une lampe est déjà allumée, cesser d'adapter cette lampe jusqu'à ce qu'elle (ou le commutateur) soit éteinte puis rallumée.", + "detect_non_ha_changes": "detect_non_ha_changes : Détecter tout changement de plus de 10 % appliqué aux lampes (même en dehors de HA). Nécessite que « take_over_control » soit activé. (Appelle « homeassistant.update_entity » tous les « interval » !)", + "transition": "transition : Durée de la transition (en secondes) des changements appliqués aux lampes." + } + } + }, + "error": { + "option_error": "Option non valide" + } + } +} From 13ff09dc2f0fdccbed1d3b9ea2c0e30c4bd40f9e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 17 Apr 2022 19:50:11 -0700 Subject: [PATCH 0333/1077] run black --- custom_components/adaptive_lighting/switch.py | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 7e023134..cc66762a 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -328,7 +328,9 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_APPLY, { - vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, # pylint: disable=protected-access + vol.Optional( + CONF_LIGHTS, default=[] + ): cv.entity_ids, # pylint: disable=protected-access vol.Optional( CONF_TRANSITION, default=switch._initial_transition, # pylint: disable=protected-access @@ -437,9 +439,9 @@ def color_difference_redmean( """ r_hat = (rgb1[0] + rgb2[0]) / 2 delta_r, delta_g, delta_b = [(col1 - col2) for col1, col2 in zip(rgb1, rgb2)] - red_term = (2 + r_hat / 256) * delta_r ** 2 - green_term = 4 * delta_g ** 2 - blue_term = (2 + (255 - r_hat) / 256) * delta_b ** 2 + red_term = (2 + r_hat / 256) * delta_r**2 + green_term = 4 * delta_g**2 + blue_term = (2 + (255 - r_hat) / 256) * delta_b**2 return math.sqrt(red_term + green_term + blue_term) @@ -759,7 +761,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _async_update_at_interval(self, now=None) -> None: await self._update_attrs_and_maybe_adapt_lights( - transition=self._transition, force=False, context=self.create_context("interval") + transition=self._transition, + force=False, + context=self.create_context("interval"), ) async def _adapt_light( @@ -1068,12 +1072,14 @@ class SunLightSettings: date_time = datetime.datetime.combine(date, time) try: # HA ≤2021.05, https://github.com/basnijholt/adaptive-lighting/issues/128 utc_time = self.time_zone.localize(date_time).astimezone(dt_util.UTC) - except AttributeError: # HA ≥2021.06 - utc_time = date_time.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + except AttributeError: # HA ≥2021.06 + utc_time = date_time.replace( + tzinfo=dt_util.DEFAULT_TIME_ZONE + ).astimezone(dt_util.UTC) return utc_time def calculate_noon_and_midnight( - sunset: datetime.datetime, sunrise: datetime.datetime + sunset: datetime.datetime, sunrise: datetime.datetime ) -> Tuple[datetime.datetime, datetime.datetime]: middle = abs(sunset - sunrise) / 2 if sunset > sunrise: @@ -1081,7 +1087,9 @@ class SunLightSettings: midnight = noon + timedelta(hours=12) * (1 if noon.hour < 12 else -1) else: midnight = sunset + middle - noon = midnight + timedelta(hours=12) * (1 if midnight.hour < 12 else -1) + noon = midnight + timedelta(hours=12) * ( + 1 if midnight.hour < 12 else -1 + ) return noon, midnight location = self.astral_location @@ -1183,7 +1191,11 @@ class SunLightSettings: Calculating all values takes <0.5ms. """ - percent = self.calc_percent(transition) if transition is not None else self.calc_percent(0) + percent = ( + self.calc_percent(transition) + if transition is not None + else self.calc_percent(0) + ) brightness_pct = self.calc_brightness_pct(percent, is_sleep) color_temp_kelvin = self.calc_color_temp_kelvin(percent, is_sleep) color_temp_mired: float = color_temperature_kelvin_to_mired(color_temp_kelvin) From 44a7e41e80bbce133914e1b7448f904075a8520a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 17 Apr 2022 19:50:55 -0700 Subject: [PATCH 0334/1077] use pyupgrade --- .../adaptive_lighting/__init__.py | 2 +- custom_components/adaptive_lighting/switch.py | 96 +++++++++---------- 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 786daae2..ccf8760a 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -36,7 +36,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: Dict[str, Any]): +async def async_setup(hass: HomeAssistant, config: dict[str, Any]): """Import integration from config.""" if DOMAIN in config: diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index cc66762a..ae79f4d1 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -179,7 +179,7 @@ def _short_hash(string: str, length: int = 4) -> str: def create_context( - name: str, which: str, index: int, parent: Optional[Context] = None + name: str, which: str, index: int, parent: Context | None = None ) -> Context: """Create a context that can identify this integration.""" # Use a hash for the name because otherwise the context might become @@ -191,7 +191,7 @@ def create_context( ) -def is_our_context(context: Optional[Context]) -> bool: +def is_our_context(context: Context | None) -> bool: """Check whether this integration created 'context'.""" if context is None: return False @@ -367,7 +367,7 @@ def validate(config_entry: ConfigEntry): return data -def match_switch_state_event(event: Event, from_or_to_state: List[str]): +def match_switch_state_event(event: Event, from_or_to_state: list[str]): """Match state event when either 'from_state' or 'to_state' matches.""" old_state = event.data.get("old_state") from_state_match = old_state is not None and old_state.state in from_or_to_state @@ -379,7 +379,7 @@ def match_switch_state_event(event: Event, from_or_to_state: List[str]): return match -def _expand_light_groups(hass: HomeAssistant, lights: List[str]) -> List[str]: +def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]: all_lights = set() turn_on_off_listener = hass.data[DOMAIN][ATTR_TURN_ON_OFF_LISTENER] for light in lights: @@ -427,7 +427,7 @@ def _supported_features(hass: HomeAssistant, light: str): def color_difference_redmean( - rgb1: Tuple[float, float, float], rgb2: Tuple[float, float, float] + rgb1: tuple[float, float, float], rgb2: tuple[float, float, float] ) -> float: """Distance between colors in RGB space (redmean metric). @@ -438,7 +438,7 @@ def color_difference_redmean( - https://www.compuphase.com/cmetric.htm """ r_hat = (rgb1[0] + rgb2[0]) / 2 - delta_r, delta_g, delta_b = [(col1 - col2) for col1, col2 in zip(rgb1, rgb2)] + delta_r, delta_g, delta_b = ((col1 - col2) for col1, col2 in zip(rgb1, rgb2)) red_term = (2 + r_hat / 256) * delta_r**2 green_term = 4 * delta_g**2 blue_term = (2 + (255 - r_hat) / 256) * delta_b**2 @@ -447,8 +447,8 @@ def color_difference_redmean( def _attributes_have_changed( light: str, - old_attributes: Dict[str, Any], - new_attributes: Dict[str, Any], + old_attributes: dict[str, Any], + new_attributes: dict[str, Any], adapt_brightness: bool, adapt_color: bool, context: Context, @@ -604,16 +604,16 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._state = None # Tracks 'off' → 'on' state changes - self._on_to_off_event: Dict[str, Event] = {} + self._on_to_off_event: dict[str, Event] = {} # Tracks 'on' → 'off' state changes - self._off_to_on_event: Dict[str, Event] = {} + self._off_to_on_event: dict[str, Event] = {} # Locks that prevent light adjusting when waiting for a light to 'turn_off' - self._locks: Dict[str, asyncio.Lock] = {} + self._locks: dict[str, asyncio.Lock] = {} # To count the number of `Context` instances self._context_cnt: int = 0 # Set in self._update_attrs_and_maybe_adapt_lights - self._settings: Dict[str, Any] = {} + self._settings: dict[str, Any] = {} # Set and unset tracker in async_turn_on and async_turn_off self.remove_listeners = [] @@ -639,7 +639,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return self._name @property - def is_on(self) -> Optional[bool]: + def is_on(self) -> bool | None: """Return true if adaptive lighting is on.""" return self._state @@ -705,7 +705,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return self._icon @property - def extra_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the attributes of the switch.""" if not self.is_on: return {key: None for key in self._settings} @@ -717,7 +717,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return dict(self._settings, manual_control=manual_control) def create_context( - self, which: str = "default", parent: Optional[Context] = None + self, which: str = "default", parent: Context | None = None ) -> Context: """Create a context that identifies this Adaptive Lighting instance.""" # Right now the highest number of each context_id it can create is @@ -769,12 +769,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _adapt_light( self, light: str, - transition: Optional[int] = None, - adapt_brightness: Optional[bool] = None, - adapt_color: Optional[bool] = None, - prefer_rgb_color: Optional[bool] = None, + transition: int | None = None, + adapt_brightness: bool | None = None, + adapt_color: bool | None = None, + prefer_rgb_color: bool | None = None, force: bool = False, - context: Optional[Context] = None, + context: Context | None = None, ) -> None: lock = self._locks.get(light) if lock is not None and lock.locked(): @@ -863,10 +863,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _update_attrs_and_maybe_adapt_lights( self, - lights: Optional[List[str]] = None, - transition: Optional[int] = None, + lights: list[str] | None = None, + transition: int | None = None, force: bool = False, - context: Optional[Context] = None, + context: Context | None = None, ) -> None: assert context is not None _LOGGER.debug( @@ -887,10 +887,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _adapt_lights( self, - lights: List[str], - transition: Optional[int], + lights: list[str], + transition: int | None, force: bool, - context: Optional[Context], + context: Context | None, ) -> None: assert context is not None _LOGGER.debug( @@ -1021,7 +1021,7 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): return self._icon @property - def is_on(self) -> Optional[bool]: + def is_on(self) -> bool | None: """Return true if adaptive lighting is on.""" return self._state @@ -1057,14 +1057,14 @@ class SunLightSettings: min_color_temp: int sleep_brightness: int sleep_color_temp: int - sunrise_offset: Optional[datetime.timedelta] - sunrise_time: Optional[datetime.time] - sunset_offset: Optional[datetime.timedelta] - sunset_time: Optional[datetime.time] + sunrise_offset: datetime.timedelta | None + sunrise_time: datetime.time | None + sunset_offset: datetime.timedelta | None + sunset_time: datetime.time | None time_zone: datetime.tzinfo transition: int - def get_sun_events(self, date: datetime.datetime) -> Dict[str, float]: + def get_sun_events(self, date: datetime.datetime) -> dict[str, float]: """Get the four sun event's timestamps at 'date'.""" def _replace_time(date: datetime.datetime, key: str) -> datetime.datetime: @@ -1080,7 +1080,7 @@ class SunLightSettings: def calculate_noon_and_midnight( sunset: datetime.datetime, sunrise: datetime.datetime - ) -> Tuple[datetime.datetime, datetime.datetime]: + ) -> tuple[datetime.datetime, datetime.datetime]: middle = abs(sunset - sunrise) / 2 if sunset > sunrise: noon = sunrise + middle @@ -1138,7 +1138,7 @@ class SunLightSettings: return events - def relevant_events(self, now: datetime.datetime) -> List[Tuple[str, float]]: + def relevant_events(self, now: datetime.datetime) -> list[tuple[str, float]]: """Get the previous and next sun event.""" events = [ self.get_sun_events(now + timedelta(days=days)) for days in [-1, 0, 1] @@ -1186,7 +1186,7 @@ class SunLightSettings: def get_settings( self, is_sleep, transition - ) -> Dict[str, Union[float, Tuple[float, float], Tuple[float, float, float]]]: + ) -> dict[str, float | tuple[float, float] | tuple[float, float, float]]: """Get all light settings. Calculating all values takes <0.5ms. @@ -1199,11 +1199,11 @@ class SunLightSettings: brightness_pct = self.calc_brightness_pct(percent, is_sleep) color_temp_kelvin = self.calc_color_temp_kelvin(percent, is_sleep) color_temp_mired: float = color_temperature_kelvin_to_mired(color_temp_kelvin) - rgb_color: Tuple[float, float, float] = color_temperature_to_rgb( + rgb_color: tuple[float, float, float] = color_temperature_to_rgb( color_temp_kelvin ) - xy_color: Tuple[float, float] = color_RGB_to_xy(*rgb_color) - hs_color: Tuple[float, float] = color_xy_to_hs(*xy_color) + xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color) + hs_color: tuple[float, float] = color_xy_to_hs(*xy_color) return { "brightness_pct": brightness_pct, "color_temp_kelvin": color_temp_kelvin, @@ -1224,19 +1224,19 @@ class TurnOnOffListener: self.lights = set() # Tracks 'light.turn_off' service calls - self.turn_off_event: Dict[str, Event] = {} + self.turn_off_event: dict[str, Event] = {} # Tracks 'light.turn_on' service calls - self.turn_on_event: Dict[str, Event] = {} + self.turn_on_event: dict[str, Event] = {} # Keep 'asyncio.sleep' tasks that can be cancelled by 'light.turn_on' events - self.sleep_tasks: Dict[str, asyncio.Task] = {} + self.sleep_tasks: dict[str, asyncio.Task] = {} # Tracks which lights are manually controlled - self.manual_control: Dict[str, bool] = {} + self.manual_control: dict[str, bool] = {} # Counts the number of times (in a row) a light had a changed state. - self.cnt_significant_changes: Dict[str, int] = defaultdict(int) + self.cnt_significant_changes: dict[str, int] = defaultdict(int) # Track 'state_changed' events of self.lights resulting from this integration - self.last_state_change: Dict[str, List[State]] = {} + self.last_state_change: dict[str, list[State]] = {} # Track last 'service_data' to 'light.turn_on' resulting from this integration - self.last_service_data: Dict[str, Dict[str, Any]] = {} + self.last_service_data: dict[str, dict[str, Any]] = {} # When a state is different `max_cnt_significant_changes` times in a row, # mark it as manually_controlled. @@ -1326,7 +1326,7 @@ class TurnOnOffListener: # called with a color_temp outside of its range (and HA reports the # incorrect 'min_mireds' and 'max_mireds', which happens e.g., for # Philips Hue White GU10 Bluetooth lights). - old_state: Optional[List[State]] = self.last_state_change.get(entity_id) + old_state: list[State] | None = self.last_state_change.get(entity_id) if ( old_state is not None and old_state[0].context.id == new_state.context.id @@ -1398,7 +1398,7 @@ class TurnOnOffListener: """ if light not in self.last_state_change: return False - old_states: List[State] = self.last_state_change[light] + old_states: list[State] = self.last_state_change[light] await self.hass.helpers.entity_component.async_update_entity(light) new_state = self.hass.states.get(light) compare_to = functools.partial( @@ -1456,7 +1456,7 @@ class TurnOnOffListener: return changed async def maybe_cancel_adjusting( - self, entity_id: str, off_to_on_event: Event, on_to_off_event: Optional[Event] + self, entity_id: str, off_to_on_event: Event, on_to_off_event: Event | None ) -> bool: """Cancel the adjusting of a light if it has just been turned off. From b6419bd09e3c3c97336e883d197027f865b778b9 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 17 Apr 2022 19:51:44 -0700 Subject: [PATCH 0335/1077] remove unused imports --- custom_components/adaptive_lighting/__init__.py | 2 +- custom_components/adaptive_lighting/switch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index ccf8760a..f7be6292 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -1,6 +1,6 @@ """Adaptive Lighting integration in Home-Assistant.""" import logging -from typing import Any, Dict +from typing import Any import voluptuous as vol diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index ae79f4d1..1de61655 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -12,7 +12,7 @@ import functools import hashlib import logging import math -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any import astral import voluptuous as vol From 86be7c63eb59809ed5ed65c3d5fc9a0eee77168c Mon Sep 17 00:00:00 2001 From: Joscha Wagner Date: Thu, 19 May 2022 05:55:46 +0200 Subject: [PATCH 0336/1077] Update manifest.json --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index d4f9a091..bf8465ab 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -6,7 +6,7 @@ "config_flow": true, "dependencies": [], "codeowners": ["@basnijholt", "@RubenKelevra"], - "version": "1.0.14", + "version": "1.0.15", "requirements": [], "iot_class": "calculated" } From f26b2d19e53fec880275012d86711a6c79cbce7a Mon Sep 17 00:00:00 2001 From: Sven Serlier <85389871+wrt54g@users.noreply.github.com> Date: Fri, 27 May 2022 22:56:02 +0200 Subject: [PATCH 0337/1077] Update HACS URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d0dadacc..e420da65 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs) +[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting) # Adaptive Lighting component for Home Assistant From 2539723a5e7aab64819ce2a200453ca1f36103c4 Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Tue, 21 Jun 2022 13:50:43 +0200 Subject: [PATCH 0338/1077] Make the badges flat --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e420da65..73f05073 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration) -![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting) +[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) +![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) # Adaptive Lighting component for Home Assistant From 995a74b7797616c8fc31931f961b73765996ed93 Mon Sep 17 00:00:00 2001 From: LukaszP2 <44735995+LukaszP2@users.noreply.github.com> Date: Sun, 3 Jul 2022 12:01:03 +0200 Subject: [PATCH 0339/1077] Create pl.json Polish translotion --- .../adaptive_lighting/translations/pl.json | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/pl.json diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json new file mode 100644 index 00000000..07cb8d79 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/pl.json @@ -0,0 +1,50 @@ +{ + "title": "Adaptacyjne oświetlenie", + "config": { + "step": { + "user": { + "title": "Wybierz nazwę grupy dla Adaptacyjnego oświetlenia", + "description": "Wybierz nazwę dla grupy. Możesz użyć wiele grup Adaptacyjnego oświetlenia, każda może mieć dowolną konfigurację świateł!", + "data": { + "name": "Nazwa" + } + } + }, + "abort": { + "already_configured": "Już skonfigurowane!" + } + }, + "options": { + "step": { + "init": { + "title": "Adaptacyjne oświetlenie opcje", + "description": "Wszystkie ustawienia dla Adaptacyjnego oświetlenia. Nazwy opcji odpowiadają ustawieniom YAML. Żadne opcje nie są wyświetlane, jeśli masz wpis adaptive_lighting zdefiniowany w konfiguracji YAML.", + "data": { + "lights": "światła", + "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (sekund)", + "sleep_transition": "sleep_transition: When 'sleep_state' changes. (sekund)", + "interval": "interval: Time between switch updates. (sekund)", + "max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)", + "max_color_temp": "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)", + "min_brightness": "min_brightness: Lowest brightness of lights during a cycle. (%)", + "min_color_temp": "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)", + "only_once": "only_once: Only adapt the lights when turning them on.", + "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.", + "separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).", + "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)", + "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)", + "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- sekund)", + "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- sekund)", + "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", + "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", + "transition": "Transition time when applying a change to the lights (sekund)" + } + } + }, + "error": { + "option_error": "Błędne opcje" + } + } +} From 91a5feff8f8c037e4631543566dcd798a6cef155 Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Tue, 12 Jul 2022 11:13:36 +0200 Subject: [PATCH 0340/1077] add funding file for github --- .github/FUNDING.yaml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yaml diff --git a/.github/FUNDING.yaml b/.github/FUNDING.yaml new file mode 100644 index 00000000..e42b9e64 --- /dev/null +++ b/.github/FUNDING.yaml @@ -0,0 +1 @@ +github: [basnijholz, RubenKelevra] From 4cc38949ba632d1432d2f0a904e36d532b77510e Mon Sep 17 00:00:00 2001 From: "@RubenKelevra" Date: Tue, 12 Jul 2022 11:15:00 +0200 Subject: [PATCH 0341/1077] Move funding file --- .github/{FUNDING.yaml => FUNDING.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{FUNDING.yaml => FUNDING.yml} (100%) diff --git a/.github/FUNDING.yaml b/.github/FUNDING.yml similarity index 100% rename from .github/FUNDING.yaml rename to .github/FUNDING.yml From f4e6ab4c585762216aed8dd72deadb01362432ba Mon Sep 17 00:00:00 2001 From: Hudson Brendon Date: Sat, 6 Aug 2022 03:48:08 -0300 Subject: [PATCH 0342/1077] Create pt-br.json --- .../adaptive_lighting/translations/pt-br.json | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/pt-br.json diff --git a/custom_components/adaptive_lighting/translations/pt-br.json b/custom_components/adaptive_lighting/translations/pt-br.json new file mode 100644 index 00000000..87d74547 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/pt-br.json @@ -0,0 +1,50 @@ +{ + "title": "Iluminação Adaptativa", + "config": { + "step": { + "user": { + "title": "Escolha um nome para a instância da Iluminação Adaptativa", + "description": "Escolha um nome para esta instância. Você pode executar várias instâncias de iluminação adaptativa, cada uma delas pode conter várias luzes!", + "data": { + "name": "Nome" + } + } + }, + "abort": { + "already_configured": "Este dispositivo já está configurado" + } + }, + "options": { + "step": { + "init": { + "title": "Opções da iluminação adaptiva", + "description": "Todas as configurações de um componente de iluminação adaptativa. Os nomes das opções correspondem às configurações de YAML. Nenhuma opção será exibida se você tiver a entrada adaptive_lighting definida em sua configuração YAML.", + "data": { + "lights": "luzes", + "initial_transition": "initial_transition: Quando as luzes mudam de 'off' para 'on'. (segundos)", + "sleep_transition": "sleep_transition: Quando 'sleep_state' muda. (segundos)", + "interval": "interval: Tempo entre as atualizações do switch. (segundos)", + "max_brightness": "max_brightness: Maior brilho das luzes durante um ciclo. (%)", + "max_color_temp": "max_color_temp: Matiz mais frio do ciclo de temperatura de cor. (Kelvin)", + "min_brightness": "min_brightness: Menor brilho das luzes durante um ciclo. (%)", + "min_color_temp": "min_color_temp, matiz mais quente do ciclo de temperatura de cor. (Kelvin)", + "only_once": "only_once: Apenas adapte as luzes ao ligá-las.", + "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' em vez de 'color_temp' quando possível.", + "separate_turn_on_commands": "separar_turn_on_commands: Separe os comandos para cada atributo (cor, brilho, etc.) em 'light.turn_on' (necessário para algumas luzes).", + "sleep_brightness": "sleep_brightness, configuração de brilho para o modo de suspensão. (%)", + "sleep_color_temp": "sleep_color_temp: configuração de temperatura de cor para o modo de suspensão. (Kelvin)", + "sunrise_offset": "sunrise_offset: Quanto tempo antes (-) ou depois (+) para definir o ponto do nascer do sol do ciclo (+/- segundos)", + "sunrise_time": "sunrise_time: substituição manual do horário do nascer do sol, se 'Nenhum', ele usa o horário real do nascer do sol em sua localização (HH:MM:SS)", + "sunset_offset": "Sunset_offset: Quanto tempo antes (-) ou depois (+) para definir o ponto de pôr do sol do ciclo (+/- segundos)", + "sunset_time": "sunset_time: substituição manual do horário do pôr do sol, se 'Nenhum', ele usa o horário real do nascer do sol em sua localização (HH:MM:SS)", + "take_over_control": "take_over_control: Se qualquer coisa, exceto Adaptive Lighting, chamar 'light.turn_on' quando uma luz já estiver acesa, pare de adaptar essa luz até que ela (ou o interruptor) desligue -> ligue.", + "detect_non_ha_changes": "detect_non_ha_changes: detecta todas as alterações > 10% feitas nas luzes (também fora do HA), requer que 'take_over_control' seja ativado (chama 'homeassistant.update_entity' a cada 'intervalo'!)", + "transition": "Tempo de transição ao aplicar uma mudança nas luzes (segundos)" + } + } + }, + "error": { + "option_error": "Opção inválida" + } + } +} From b2790d4a197d5abecccdaf94ee4b30c41395ca85 Mon Sep 17 00:00:00 2001 From: Hudson Brendon Date: Sat, 6 Aug 2022 04:41:23 -0300 Subject: [PATCH 0343/1077] Rename pt-br.json to pt-BR.json --- .../adaptive_lighting/translations/{pt-br.json => pt-BR.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename custom_components/adaptive_lighting/translations/{pt-br.json => pt-BR.json} (100%) diff --git a/custom_components/adaptive_lighting/translations/pt-br.json b/custom_components/adaptive_lighting/translations/pt-BR.json similarity index 100% rename from custom_components/adaptive_lighting/translations/pt-br.json rename to custom_components/adaptive_lighting/translations/pt-BR.json From 34a855d75f5d85569fb1a705075e067cd6ae023d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 11:19:40 -0700 Subject: [PATCH 0344/1077] Remove support for "white_value", closes #313 --- custom_components/adaptive_lighting/switch.py | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 1de61655..cd68e8d0 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -28,14 +28,12 @@ from homeassistant.components.light import ( ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, - SUPPORT_WHITE_VALUE, VALID_TRANSITION, is_on, COLOR_MODE_RGB, @@ -134,7 +132,6 @@ from .const import ( _SUPPORT_OPTS = { "brightness": SUPPORT_BRIGHTNESS, - "white_value": SUPPORT_WHITE_VALUE, "color_temp": SUPPORT_COLOR_TEMP, "color": SUPPORT_COLOR, "transition": SUPPORT_TRANSITION, @@ -163,7 +160,6 @@ COLOR_ATTRS = { # Should ATTR_PROFILE be in here? BRIGHTNESS_ATTRS = { ATTR_BRIGHTNESS, - ATTR_WHITE_VALUE, ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS_STEP_PCT, @@ -207,7 +203,6 @@ def _split_service_data(service_data, adapt_brightness, adapt_color): service_datas = [] if adapt_color: service_data_color = service_data.copy() - service_data_color.pop(ATTR_WHITE_VALUE, None) service_data_color.pop(ATTR_BRIGHTNESS, None) service_datas.append(service_data_color) if adapt_brightness: @@ -471,24 +466,6 @@ def _attributes_have_changed( ) return True - if ( - adapt_brightness - and ATTR_WHITE_VALUE in old_attributes - and ATTR_WHITE_VALUE in new_attributes - ): - last_white_value = old_attributes[ATTR_WHITE_VALUE] - current_white_value = new_attributes[ATTR_WHITE_VALUE] - if abs(current_white_value - last_white_value) > BRIGHTNESS_CHANGE: - _LOGGER.debug( - "White Value of '%s' significantly changed from %s to %s with" - " context.id='%s'", - light, - last_white_value, - current_white_value, - context.id, - ) - return True - if ( adapt_color and ATTR_COLOR_TEMP in old_attributes @@ -799,10 +776,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): brightness = round(255 * self._settings["brightness_pct"] / 100) service_data[ATTR_BRIGHTNESS] = brightness - if "white_value" in features and adapt_brightness: - white_value = round(255 * self._settings["brightness_pct"] / 100) - service_data[ATTR_WHITE_VALUE] = white_value - if ( "color_temp" in features and adapt_color From 150c7f9c925df1cb9b8fd5956cc8ecb199beef59 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 11:22:03 -0700 Subject: [PATCH 0345/1077] remove domains from hacs.json --- hacs.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hacs.json b/hacs.json index 500d0ebd..1a865d2d 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,4 @@ { "name": "Adaptive Lighting", - "render_readme": true, - "domains": ["switch"] + "render_readme": true } From 57d51183fe060df0a23066110b1d8cd414140038 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 11:30:20 -0700 Subject: [PATCH 0346/1077] Update version number --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index bf8465ab..e2a9cdc7 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -6,7 +6,7 @@ "config_flow": true, "dependencies": [], "codeowners": ["@basnijholt", "@RubenKelevra"], - "version": "1.0.15", + "version": "1.0.16", "requirements": [], "iot_class": "calculated" } From 10e16dc40f02262ca10f6aa1c9300403d24477fa Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:05:31 -0700 Subject: [PATCH 0347/1077] Copy tests from https://github.com/home-assistant/core/pull/40626 --- tests/__init__.py | 1 + tests/test_config_flow.py | 131 ++++++ tests/test_init.py | 55 +++ tests/test_switch.py | 874 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 1061 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_config_flow.py create mode 100644 tests/test_init.py create mode 100644 tests/test_switch.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..5ae9fe68 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for the Adaptive Lighting integration.""" diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py new file mode 100644 index 00000000..53901cf9 --- /dev/null +++ b/tests/test_config_flow.py @@ -0,0 +1,131 @@ +"""Test Adaptive Lighting config flow.""" +from homeassistant import data_entry_flow +from homeassistant.components.adaptive_lighting.const import ( + CONF_SUNRISE_TIME, + CONF_SUNSET_TIME, + DEFAULT_NAME, + DOMAIN, + NONE_STR, + VALIDATION_TUPLES, +) +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_NAME + +from tests.common import MockConfigEntry + +DEFAULT_DATA = {key: default for key, default, _ in VALIDATION_TUPLES} + + +async def test_flow_manual_configuration(hass): + """Test that config flow works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["handler"] == "adaptive_lighting" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_NAME: "living room"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "living room" + + +async def test_import_success(hass): + """Test import step is successful.""" + data = DEFAULT_DATA.copy() + data[CONF_NAME] = DEFAULT_NAME + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "import"}, + data=data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + for key, value in data.items(): + assert result["data"][key] == value + + +async def test_options(hass): + """Test updating options.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_NAME, + data={CONF_NAME: DEFAULT_NAME}, + options={}, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + data = DEFAULT_DATA.copy() + data[CONF_SUNRISE_TIME] = NONE_STR + data[CONF_SUNSET_TIME] = NONE_STR + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=data, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + for key, value in data.items(): + assert result["data"][key] == value + + +async def test_incorrect_options(hass): + """Test updating incorrect options.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_NAME, + data={CONF_NAME: DEFAULT_NAME}, + options={}, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + result = await hass.config_entries.options.async_init(entry.entry_id) + data = DEFAULT_DATA.copy() + data[CONF_SUNRISE_TIME] = "yolo" + data[CONF_SUNSET_TIME] = "yolo" + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=data, + ) + + +async def test_import_twice(hass): + """Test importing twice.""" + data = DEFAULT_DATA.copy() + data[CONF_NAME] = DEFAULT_NAME + for _ in range(2): + _ = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "import"}, + data=data, + ) + + +async def test_changing_options_when_using_yaml(hass): + """Test changing options when using YAML.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_NAME, + data={CONF_NAME: DEFAULT_NAME}, + source=SOURCE_IMPORT, + options={}, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + result = await hass.config_entries.options.async_init(entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={}, + ) diff --git a/tests/test_init.py b/tests/test_init.py new file mode 100644 index 00000000..53f05c61 --- /dev/null +++ b/tests/test_init.py @@ -0,0 +1,55 @@ +"""Tests for Adaptive Lighting integration.""" +from homeassistant import config_entries +from homeassistant.components import adaptive_lighting +from homeassistant.components.adaptive_lighting.const import ( + DEFAULT_NAME, + UNDO_UPDATE_LISTENER, +) +from homeassistant.const import CONF_NAME +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_setup_with_config(hass): + """Test that we import the config and setup the integration.""" + config = { + adaptive_lighting.DOMAIN: { + adaptive_lighting.CONF_NAME: DEFAULT_NAME, + } + } + assert await async_setup_component(hass, adaptive_lighting.DOMAIN, config) + assert adaptive_lighting.DOMAIN in hass.data + + +async def test_successful_config_entry(hass): + """Test that Adaptive Lighting is configured successfully.""" + + entry = MockConfigEntry( + domain=adaptive_lighting.DOMAIN, + data={CONF_NAME: DEFAULT_NAME}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state == config_entries.ENTRY_STATE_LOADED + + assert UNDO_UPDATE_LISTENER in hass.data[adaptive_lighting.DOMAIN][entry.entry_id] + + +async def test_unload_entry(hass): + """Test removing Adaptive Lighting.""" + entry = MockConfigEntry( + domain=adaptive_lighting.DOMAIN, + data={CONF_NAME: DEFAULT_NAME}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert adaptive_lighting.DOMAIN not in hass.data diff --git a/tests/test_switch.py b/tests/test_switch.py new file mode 100644 index 00000000..68f76fda --- /dev/null +++ b/tests/test_switch.py @@ -0,0 +1,874 @@ +"""Tests for Adaptive Lighting switches.""" +# pylint: disable=protected-access +import asyncio +import datetime +from random import randint + +import pytest + +from homeassistant.components.adaptive_lighting.const import ( + ADAPT_BRIGHTNESS_SWITCH, + ADAPT_COLOR_SWITCH, + ATTR_TURN_ON_OFF_LISTENER, + CONF_DETECT_NON_HA_CHANGES, + CONF_INITIAL_TRANSITION, + CONF_MANUAL_CONTROL, + CONF_MIN_COLOR_TEMP, + CONF_PREFER_RGB_COLOR, + CONF_SEPARATE_TURN_ON_COMMANDS, + CONF_SUNRISE_OFFSET, + CONF_SUNRISE_TIME, + CONF_SUNSET_TIME, + CONF_TRANSITION, + CONF_TURN_ON_LIGHTS, + DEFAULT_MAX_BRIGHTNESS, + DEFAULT_NAME, + DEFAULT_SLEEP_BRIGHTNESS, + DEFAULT_SLEEP_COLOR_TEMP, + DOMAIN, + SERVICE_APPLY, + SERVICE_SET_MANUAL_CONTROL, + SLEEP_MODE_SWITCH, + UNDO_UPDATE_LISTENER, +) +from homeassistant.components.adaptive_lighting.switch import ( + _attributes_have_changed, + _expand_light_groups, + color_difference_redmean, + create_context, + is_our_context, +) +from homeassistant.components.demo.light import DemoLight +from homeassistant.components.group import DOMAIN as GROUP_DOMAIN +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_TEMP, + ATTR_RGB_COLOR, + DOMAIN as LIGHT_DOMAIN, + SERVICE_TURN_OFF, +) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +import homeassistant.config as config_util +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_LIGHTS, + CONF_NAME, + CONF_PLATFORM, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import Context, State +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.async_mock import patch +from tests.common import MockConfigEntry +from tests.components.demo.test_light import ENTITY_LIGHT + +SUNRISE = datetime.datetime( + year=2020, + month=10, + day=17, + hour=6, +) +SUNSET = datetime.datetime( + year=2020, + month=10, + day=17, + hour=22, +) + +LAT_LONG_TZS = [ + (39, -1, "Europe/Madrid"), + (60, 50, "GMT"), + (55, 13, "Europe/Copenhagen"), + (52.379189, 4.899431, "Europe/Amsterdam"), + (32.87336, -117.22743, "US/Pacific"), +] + +_SWITCH_FMT = f"{SWITCH_DOMAIN}.{DOMAIN}" +ENTITY_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}" +ENTITY_SLEEP_MODE_SWITCH = f"{_SWITCH_FMT}_sleep_mode_{DEFAULT_NAME}" +ENTITY_ADAPT_BRIGHTNESS_SWITCH = f"{_SWITCH_FMT}_adapt_brightness_{DEFAULT_NAME}" +ENTITY_ADAPT_COLOR_SWITCH = f"{_SWITCH_FMT}_adapt_color_{DEFAULT_NAME}" + +ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE + + +@pytest.fixture +def reset_time_zone(): + """Reset time zone.""" + yield + dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE + + +async def setup_switch(hass, extra_data): + """Create the switch entry.""" + entry = MockConfigEntry(domain=DOMAIN, data={CONF_NAME: DEFAULT_NAME, **extra_data}) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + switch = hass.data[DOMAIN][entry.entry_id][SWITCH_DOMAIN] + return entry, switch + + +async def setup_lights(hass): + """Set up 3 light entities using the 'test' platform.""" + platform = getattr(hass.components, "test.light") + while platform.ENTITIES: + # Make sure it is empty + platform.ENTITIES.pop() + lights = [ + DemoLight( + unique_id="light_1", + name="Bed Light", + state=True, + ct=200, + ), + DemoLight( + unique_id="light_2", + name="Ceiling Lights", + state=True, + ct=380, + ), + DemoLight( + unique_id="light_3", + name="Kitchen Lights", + state=False, + hs_color=(345, 75), + ct=240, + ), + ] + platform.ENTITIES.extend(lights) + assert await async_setup_component( + hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {CONF_PLATFORM: "test"}} + ) + await hass.async_block_till_done() + return lights + + +async def setup_lights_and_switch(hass, extra_conf=None): + """Create switch and demo lights.""" + # Setup demo lights and turn on + lights_instances = await setup_lights(hass) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT}, + blocking=True, + ) + + # Setup switch + lights = [ + "light.bed_light", + "light.ceiling_lights", + ] + assert all(hass.states.get(light) is not None for light in lights) + _, switch = await setup_switch( + hass, + { + CONF_LIGHTS: lights, + CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour), + CONF_SUNSET_TIME: datetime.time(SUNSET.hour), + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_PREFER_RGB_COLOR: False, + CONF_MIN_COLOR_TEMP: 2500, # to not coincide with sleep_color_temp + **(extra_conf or {}), + }, + ) + await hass.async_block_till_done() + return switch, lights_instances + + +async def test_adaptive_lighting_switches(hass): + """Test switches created for adaptive_lighting integration.""" + entry, _ = await setup_switch(hass, {}) + + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 4 + assert set(hass.states.async_entity_ids(SWITCH_DOMAIN)) == { + ENTITY_SWITCH, + ENTITY_SLEEP_MODE_SWITCH, + ENTITY_ADAPT_COLOR_SWITCH, + ENTITY_ADAPT_BRIGHTNESS_SWITCH, + } + assert ATTR_TURN_ON_OFF_LISTENER in hass.data[DOMAIN] + assert entry.entry_id in hass.data[DOMAIN] + assert len(hass.data[DOMAIN].keys()) == 2 + + data = hass.data[DOMAIN][entry.entry_id] + assert SLEEP_MODE_SWITCH in data + assert SWITCH_DOMAIN in data + assert ADAPT_COLOR_SWITCH in data + assert ADAPT_BRIGHTNESS_SWITCH in data + assert UNDO_UPDATE_LISTENER in data + assert len(data.keys()) == 5 + + +@pytest.mark.parametrize("lat,long,timezone", LAT_LONG_TZS) +async def test_adaptive_lighting_time_zones_with_default_settings( + hass, lat, long, timezone, reset_time_zone # pylint: disable=redefined-outer-name +): + """Test setting up the Adaptive Lighting switches with different timezones.""" + await config_util.async_process_ha_core_config( + hass, + {"latitude": lat, "longitude": long, "time_zone": timezone}, + ) + _, switch = await setup_switch(hass, {}) + # Shouldn't raise an exception ever + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test") + ) + + +@pytest.mark.parametrize("lat,long,timezone", LAT_LONG_TZS) +async def test_adaptive_lighting_time_zones_and_sun_settings( + hass, lat, long, timezone, reset_time_zone # pylint: disable=redefined-outer-name +): + """Test setting up the Adaptive Lighting switches with different timezones. + + Also test the (sleep) brightness and color temperature settings. + """ + await config_util.async_process_ha_core_config( + hass, + {"latitude": lat, "longitude": long, "time_zone": timezone}, + ) + _, switch = await setup_switch( + hass, + { + CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour), + CONF_SUNSET_TIME: datetime.time(SUNSET.hour), + }, + ) + + context = switch.create_context("test") # needs to be passed to update method + min_color_temp = switch._sun_light_settings.min_color_temp + + sunset = hass.config.time_zone.localize(SUNSET).astimezone(dt_util.UTC) + before_sunset = sunset - datetime.timedelta(hours=1) + after_sunset = sunset + datetime.timedelta(hours=1) + sunrise = hass.config.time_zone.localize(SUNRISE).astimezone(dt_util.UTC) + before_sunrise = sunrise - datetime.timedelta(hours=1) + after_sunrise = sunrise + datetime.timedelta(hours=1) + + async def patch_time_and_update(time): + with patch("homeassistant.util.dt.utcnow", return_value=time): + await switch._update_attrs_and_maybe_adapt_lights(context=context) + await hass.async_block_till_done() + + # At sunset the brightness should be max and color_temp at the smallest value + await patch_time_and_update(sunset) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == min_color_temp + + # One hour before sunset the brightness should be max and color_temp + # not at the smallest value yet. + await patch_time_and_update(before_sunset) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] > min_color_temp + + # One hour after sunset the brightness should be down + await patch_time_and_update(after_sunset) + assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == min_color_temp + + # At sunrise the brightness should be max and color_temp at the smallest value + await patch_time_and_update(sunrise) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == min_color_temp + + # One hour before sunrise the brightness should smaller than max + # and color_temp at the min value. + await patch_time_and_update(before_sunrise) + assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == min_color_temp + + # One hour after sunrise the brightness should be up + await patch_time_and_update(after_sunrise) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] > min_color_temp + + # Turn on sleep mode which make the brightness and color_temp + # deterministic regardless of the time + await switch.sleep_mode_switch.async_turn_on() + await switch._update_attrs_and_maybe_adapt_lights(context=context) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_SLEEP_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == DEFAULT_SLEEP_COLOR_TEMP + + +async def test_light_settings(hass): + """Test that light settings are correctly applied.""" + switch, _ = await setup_lights_and_switch(hass) + lights = switch._lights + + # Turn on "sleep mode" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_SLEEP_MODE_SWITCH}, + blocking=True, + ) + await hass.async_block_till_done() + light_states = [hass.states.get(light) for light in lights] + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] == round( + 255 * switch._settings[ATTR_BRIGHTNESS_PCT] / 100 + ) + last_service_data = switch.turn_on_off_listener.last_service_data[ + state.entity_id + ] + assert state.attributes[ATTR_BRIGHTNESS] == last_service_data[ATTR_BRIGHTNESS] + assert state.attributes[ATTR_COLOR_TEMP] == last_service_data[ATTR_COLOR_TEMP] + + # Turn off "sleep mode" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_SLEEP_MODE_SWITCH}, + blocking=True, + ) + await hass.async_block_till_done() + + # Test with different times + sunset = hass.config.time_zone.localize(SUNSET).astimezone(dt_util.UTC) + before_sunset = sunset - datetime.timedelta(hours=1) + after_sunset = sunset + datetime.timedelta(hours=1) + sunrise = hass.config.time_zone.localize(SUNRISE).astimezone(dt_util.UTC) + before_sunrise = sunrise - datetime.timedelta(hours=1) + after_sunrise = sunrise + datetime.timedelta(hours=1) + + context = switch.create_context("test") # needs to be passed to update method + + async def patch_time_and_get_updated_states(time): + with patch("homeassistant.util.dt.utcnow", return_value=time): + await switch._update_attrs_and_maybe_adapt_lights( + transition=0, context=context, force=True + ) + await hass.async_block_till_done() + return [hass.states.get(light) for light in lights] + + def assert_expected_color_temp(state): + last_service_data = switch.turn_on_off_listener.last_service_data[ + state.entity_id + ] + assert state.attributes[ATTR_COLOR_TEMP] == last_service_data[ATTR_COLOR_TEMP] + + # At sunset the brightness should be max and color_temp at the smallest value + light_states = await patch_time_and_get_updated_states(sunset) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert_expected_color_temp(state) + + # One hour before sunset the brightness should be max and color_temp + # not at the smallest value yet. + light_states = await patch_time_and_get_updated_states(before_sunset) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert_expected_color_temp(state) + + # One hour after sunset the brightness should be down + light_states = await patch_time_and_get_updated_states(after_sunset) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] < 255 + assert_expected_color_temp(state) + + # At sunrise the brightness should be max and color_temp at the smallest value + light_states = await patch_time_and_get_updated_states(sunrise) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert_expected_color_temp(state) + + # One hour before sunrise the brightness should smaller than max + # and color_temp at the min value. + light_states = await patch_time_and_get_updated_states(before_sunrise) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] < 255 + assert_expected_color_temp(state) + + # One hour after sunrise the brightness should be up + light_states = await patch_time_and_get_updated_states(after_sunrise) + for state in light_states: + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert_expected_color_temp(state) + + +async def test_turn_on_off_listener_not_tracking_untracked_lights(hass): + """Test that lights that are not in a Adaptive Lighting switch aren't tracked.""" + switch, _ = await setup_lights_and_switch(hass) + light = "light.kitchen_lights" + assert light not in switch._lights + for state in [True, False]: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: light}, + blocking=True, + ) + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test") + ) + await hass.async_block_till_done() + assert light not in switch.turn_on_off_listener.lights + + +async def test_manual_control(hass): + """Test the 'manual control' tracking.""" + switch, (light, *_) = await setup_lights_and_switch(hass) + context = switch.create_context("test") # needs to be passed to update method + manual_control = switch.turn_on_off_listener.manual_control + + async def update(): + await switch._update_attrs_and_maybe_adapt_lights(transition=0, context=context) + await hass.async_block_till_done() + + async def turn_light(state, **kwargs): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs}, + blocking=True, + ) + await hass.async_block_till_done() + await update() + + async def turn_switch(state, entity_id): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + async def change_manual_control(set_to, extra_service_data=None): + if extra_service_data is None: + extra_service_data = {CONF_LIGHTS: [ENTITY_LIGHT]} + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_MANUAL_CONTROL: set_to, + **extra_service_data, + }, + blocking=True, + ) + await hass.async_block_till_done() + await update() + + def increased_brightness(): + return (light._brightness + 100) % 255 + + def increased_color_temp(): + return max((light._ct + 100) % light.max_mireds, light.min_mireds) + + # Nothing is manually controlled + await update() + assert not manual_control[ENTITY_LIGHT] + # Call light.turn_on for ENTITY_LIGHT + await turn_light(True, brightness=increased_brightness()) + # Check that ENTITY_LIGHT is manually controlled + assert manual_control[ENTITY_LIGHT] + # Test adaptive_lighting.set_manual_control + await change_manual_control(False) + # Check that ENTITY_LIGHT is not manually controlled + assert not manual_control[ENTITY_LIGHT] + + # Check that toggling light off to on resets manual control + await change_manual_control(True) + assert manual_control[ENTITY_LIGHT] + await turn_light(False) + await turn_light(True, brightness=increased_brightness()) + assert not manual_control[ENTITY_LIGHT] + + # Check that toggling (sleep mode) switch resets manual control + for entity_id in [ENTITY_SWITCH, ENTITY_SLEEP_MODE_SWITCH]: + await change_manual_control(True) + assert manual_control[ENTITY_LIGHT] + await turn_switch(False, entity_id) + await turn_switch(True, entity_id) + assert not manual_control[ENTITY_LIGHT] + + # Check that when 'adapt_brightness' is off, changing the brightness + # doesn't mark it as manually controlled but changing color_temp + # does + await turn_light(False) # reset manually controlled status + await turn_light(True) + assert not manual_control[ENTITY_LIGHT] + await switch.adapt_brightness_switch.async_turn_off() + await turn_light(True, brightness=increased_brightness()) + assert not manual_control[ENTITY_LIGHT] + await turn_light(True, color_temp=(light._ct + 100) % 500) + assert manual_control[ENTITY_LIGHT] + await switch.adapt_brightness_switch.async_turn_on() # turn on again + + # Check that when 'adapt_color' is off, changing the color + # doesn't mark it as manually controlled but changing brightness + # does + await turn_light(False) # reset manually controlled status + await turn_light(True) + assert not manual_control[ENTITY_LIGHT] + await switch.adapt_color_switch.async_turn_off() + await turn_light(True, color_temp=increased_color_temp()) + assert not manual_control[ENTITY_LIGHT] + await turn_light(True, brightness=increased_brightness()) + assert manual_control[ENTITY_LIGHT] + + # Check that when 'adapt_color' adapt_brightness are both off + # nothing marks it as manually controlled + await turn_light(False) # reset manually controlled status + await turn_light(True) + await switch.adapt_color_switch.async_turn_off() + await switch.adapt_brightness_switch.async_turn_off() + assert not manual_control[ENTITY_LIGHT] + await turn_light(True, color_temp=increased_color_temp()) + await turn_light(True, brightness=increased_brightness()) + await turn_light( + True, + color_temp=increased_color_temp(), + brightness=increased_brightness(), + ) + assert not manual_control[ENTITY_LIGHT] + # Turn switches on again + await switch.adapt_color_switch.async_turn_on() + await switch.adapt_brightness_switch.async_turn_on() + + # Check that when no lights are specified, all are reset + await change_manual_control(True, {CONF_LIGHTS: switch._lights}) + assert all([manual_control[eid] for eid in switch._lights]) + # do not pass "lights" so reset all + await change_manual_control(False, {}) + assert all([not manual_control[eid] for eid in switch._lights]) + + +async def test_apply_service(hass): + """Test adaptive_lighting.apply service.""" + switch, (_, _, light) = await setup_lights_and_switch(hass) + entity_id = light.entity_id + assert entity_id not in switch._lights + + def increased_brightness(): + return (light._brightness + 100) % 255 + + def increased_color_temp(): + return max((light._ct + 100) % light.max_mireds, light.min_mireds) + + async def change_light(): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: entity_id, + ATTR_BRIGHTNESS: increased_brightness(), + ATTR_COLOR_TEMP: increased_color_temp(), + }, + blocking=True, + ) + await hass.async_block_till_done() + + async def apply(**kwargs): + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: ENTITY_SWITCH, + CONF_LIGHTS: [entity_id], + CONF_TURN_ON_LIGHTS: True, + **kwargs, + }, + blocking=True, + ) + + # Test turn on with defaults + assert hass.states.get(entity_id).state == STATE_OFF + await apply() + assert hass.states.get(entity_id).state == STATE_ON + await change_light() + + # Test only changing color + old_state = hass.states.get(entity_id).attributes + await apply(adapt_color=True, adapt_brightness=False) + new_state = hass.states.get(entity_id).attributes + assert old_state[ATTR_BRIGHTNESS] == new_state[ATTR_BRIGHTNESS] + assert old_state[ATTR_COLOR_TEMP] != new_state[ATTR_COLOR_TEMP] + + # Test only changing brightness + await change_light() + old_state = hass.states.get(entity_id).attributes + await apply(adapt_color=False, adapt_brightness=True) + new_state = hass.states.get(entity_id).attributes + assert old_state[ATTR_BRIGHTNESS] != new_state[ATTR_BRIGHTNESS] + assert old_state[ATTR_COLOR_TEMP] == new_state[ATTR_COLOR_TEMP] + + +async def test_switch_off_on_off(hass): + """Test switch rapid off_on_off.""" + + async def turn_light(state, **kwargs): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs}, + blocking=True, + ) + await hass.async_block_till_done() + + async def update(): + await switch._update_attrs_and_maybe_adapt_lights( + transition=0, context=switch.create_context("test") + ) + await hass.async_block_till_done() + + switch, _ = await setup_lights_and_switch(hass) + + for turn_light_state_at_end in [True, False]: + # Turn light on + await turn_light(True) + # Turn light off with transition + await turn_light(False, transition=1) + + assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + # Set state to on after a second (like happens IRL) + await asyncio.sleep(1e-3) + hass.states.async_set(ENTITY_LIGHT, STATE_ON) + # Set state to off after a second (like happens IRL) + await asyncio.sleep(1e-3) + hass.states.async_set(ENTITY_LIGHT, STATE_OFF) + + # Now we test whether the sleep task is there + assert ENTITY_LIGHT in switch.turn_on_off_listener.sleep_tasks + sleep_task = switch.turn_on_off_listener.sleep_tasks[ENTITY_LIGHT] + assert not sleep_task.cancelled() + + # A 'light.turn_on' event should cancel that task + await turn_light(turn_light_state_at_end) + await update() + state = hass.states.get(ENTITY_LIGHT).state + if turn_light_state_at_end: + assert sleep_task.cancelled() + assert state == STATE_ON + else: + assert state == STATE_OFF + + +async def test_significant_change(hass): + """Test significant change.""" + + async def turn_light(state, **kwargs): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs}, + blocking=True, + ) + await hass.async_block_till_done() + + async def update(force): + await switch._update_attrs_and_maybe_adapt_lights( + transition=0, + context=switch.create_context("test"), + force=force, + ) + await hass.async_block_till_done() + + switch, (bed_light_instance, *_) = await setup_lights_and_switch(hass) + await turn_light(True) + await update(force=True) # removes manual control + assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + + # Change brightness by setting state (not using 'light.turn_on') + attributes = hass.states.get(ENTITY_LIGHT).attributes + new_attributes = attributes.copy() + new_brightness = (attributes[ATTR_BRIGHTNESS] + 100) % 255 + new_attributes[ATTR_BRIGHTNESS] = new_brightness + bed_light_instance._brightness = new_brightness + assert switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None + for _ in range(switch.turn_on_off_listener.max_cnt_significant_changes): + await update(force=False) + assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + # On next update the light should be marked as manually controlled + await update(force=False) + assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + + +def test_color_difference_redmean(): + """Test color_difference_redmean function.""" + for _ in range(10): + rgb_1 = (randint(0, 255), randint(0, 255), randint(0, 255)) + rgb_2 = (randint(0, 255), randint(0, 255), randint(0, 255)) + color_difference_redmean(rgb_1, rgb_2) + color_difference_redmean((0, 0, 0), (255, 255, 255)) + + +def test_is_our_context(): + """Test is_our_context function.""" + context = create_context(DOMAIN, "test", 0) + assert is_our_context(context) + assert not is_our_context(None) + assert not is_our_context(Context()) + + +def test_attributes_have_changed(): + """Test _attributes_have_changed function.""" + attributes_1 = {ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (0, 0, 0), ATTR_COLOR_TEMP: 100} + attributes_2 = { + ATTR_BRIGHTNESS: 100, + ATTR_RGB_COLOR: (255, 0, 0), + ATTR_COLOR_TEMP: 300, + } + kwargs = dict( + light="light.test", + adapt_brightness=True, + adapt_color=True, + context=Context(), + ) + assert not _attributes_have_changed( + old_attributes=attributes_1, new_attributes=attributes_1, **kwargs + ) + for key, value in attributes_2.items(): + attrs = dict(attributes_1) + attrs[key] = value + assert _attributes_have_changed( + old_attributes=attributes_1, new_attributes=attrs, **kwargs + ) + # Switch from rgb_color to color_temp + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP: 100}, + new_attributes={ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (0, 0, 0)}, + **kwargs, + ) + + +@pytest.mark.parametrize("wait", [True, False]) +async def test_expand_light_groups(hass, wait): + """Test expanding light groups.""" + await setup_switch(hass, {}) + lights = ["light.ceiling_lights", "light.kitchen_lights"] + await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "demo"}, + { + "platform": GROUP_DOMAIN, + "entities": lights, + }, + ] + }, + ) + if wait: + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + expanded = set(_expand_light_groups(hass, ["light.light_group"])) + if wait: + assert expanded == set(lights) + else: + # Cannot expand yet because state is None + assert expanded == {"light.light_group"} + + +async def test_unload_switch(hass): + """Test removing Adaptive Lighting.""" + entry, _ = await setup_switch(hass, {}) + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert DOMAIN not in hass.data + + +@pytest.mark.parametrize("state", [STATE_ON, STATE_OFF, None]) +async def test_restore_off_state(hass, state): + """Test that the 'off' and 'on' states are propoperly restored.""" + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=State(ENTITY_SWITCH, state) if state is not None else None, + ): + await hass.async_start() + await hass.async_block_till_done() + _, switch = await setup_switch(hass, {}) + if state == STATE_ON: + assert switch.is_on + elif state == STATE_OFF: + assert not switch.is_on + elif state is None: + assert switch.is_on + + for _switch, initial_state in [ + (switch.sleep_mode_switch, False), + (switch.adapt_brightness_switch, True), + (switch.adapt_color_switch, True), + ]: + if state == STATE_ON: + assert _switch.is_on + elif state == STATE_OFF: + assert not _switch.is_on + elif state is None: + if initial_state: + assert _switch.is_on + else: + assert not _switch.is_on + + +@pytest.mark.xfail(reason="Offset is larger than half a day") +async def test_offset_too_large(hass): + """Test that update fails when the offset is too large.""" + _, switch = await setup_switch(hass, {CONF_SUNRISE_OFFSET: 3600 * 12}) + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test") + ) + await hass.async_block_till_done() + + +async def test_turn_on_and_off_when_already_at_that_state(hass): + """Test 'switch.turn_on/off' when switch is on/off.""" + _, switch = await setup_switch(hass, {}) + + await switch.async_turn_on() + await hass.async_block_till_done() + await switch.async_turn_on() + await hass.async_block_till_done() + + await switch.async_turn_off() + await hass.async_block_till_done() + await switch.async_turn_off() + await hass.async_block_till_done() + + +async def test_async_update_at_interval(hass): + """Test '_async_update_at_interval' method.""" + _, switch = await setup_switch(hass, {}) + await switch._async_update_at_interval() + + +@pytest.mark.parametrize("separate_turn_on_commands", (True, False)) +async def test_separate_turn_on_commands(hass, separate_turn_on_commands): + """Test 'separate_turn_on_commands' argument.""" + switch, (light, *_) = await setup_lights_and_switch( + hass, {CONF_SEPARATE_TURN_ON_COMMANDS: separate_turn_on_commands} + ) + # We just turn sleep mode on and off which should change the + # brightness and color. We don't test whether the number are exactly + # what we expect because we do this in other tests already, we merely + # check whether the brightness and color_temp change. + context = switch.create_context("test") # needs to be passed to update method + brightness = light.brightness + color_temp = light.color_temp + await switch.sleep_mode_switch.async_turn_on() + await switch._update_attrs_and_maybe_adapt_lights(context=context) + await hass.async_block_till_done() + sleep_brightness = light.brightness + sleep_color_temp = light.color_temp + assert sleep_brightness != brightness + assert sleep_color_temp != color_temp + await switch.sleep_mode_switch.async_turn_off() + await switch._update_attrs_and_maybe_adapt_lights(context=context) + await hass.async_block_till_done() + brightness = light.brightness + color_temp = light.color_temp + assert sleep_brightness != brightness + assert sleep_color_temp != color_temp From d14135581747f378caf931ce0f7eef40669b088b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:26:57 -0700 Subject: [PATCH 0348/1077] Add .github/workflows/ci.yaml --- .github/workflows/ci.yaml | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..1d1b037d --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,57 @@ +name: CI + +# yamllint disable-line rule:truthy +on: + push: + branches: + - dev + - rc + - master + pull_request: ~ + +env: + CACHE_VERSION: 1 + PIP_CACHE_VERSION: 1 + HA_SHORT_VERSION: 2022.9 + DEFAULT_PYTHON: 3.9 + PRE_COMMIT_CACHE: ~/.cache/pre-commit + PIP_CACHE: /tmp/pip-cache + SQLALCHEMY_WARN_20: 1 + PYTHONASYNCIODEBUG: 1 + HASS_CI: 1 + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + + base: + name: Prepare dependencies + runs-on: ubuntu-20.04 + needs: info + timeout-minutes: 60 + strategy: + matrix: + python-version: ["3.9", "3.10"] + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.0.2 + - name: Check out code from GitHub + uses: actions/checkout@v3.0.2 + with: + repository: home-assistant/core + path: homeassistant + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v4.1.0 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -e homeassistant/ + - name: Run pytest + timeout-minutes: 60 + run: | + pytest tests From b03e4c435570a7d8f982bce9c6683626b7259e65 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:27:55 -0700 Subject: [PATCH 0349/1077] Cleanup CI --- .github/workflows/ci.yaml | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1d1b037d..0cdb8cef 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,28 +1,8 @@ -name: CI +name: pytest -# yamllint disable-line rule:truthy on: push: - branches: - - dev - - rc - - master - pull_request: ~ - -env: - CACHE_VERSION: 1 - PIP_CACHE_VERSION: 1 - HA_SHORT_VERSION: 2022.9 - DEFAULT_PYTHON: 3.9 - PRE_COMMIT_CACHE: ~/.cache/pre-commit - PIP_CACHE: /tmp/pip-cache - SQLALCHEMY_WARN_20: 1 - PYTHONASYNCIODEBUG: 1 - HASS_CI: 1 - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + pull_request: jobs: From bf911730f0a76881d9e42c110ecfe9fbf271b1f9 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:28:36 -0700 Subject: [PATCH 0350/1077] remove requirements for CI --- .github/workflows/ci.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0cdb8cef..54525a90 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,10 +6,9 @@ on: jobs: - base: + pytest: name: Prepare dependencies runs-on: ubuntu-20.04 - needs: info timeout-minutes: 60 strategy: matrix: From 377333beb22d55fa5e9f07664d5489aedb019d4c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:29:26 -0700 Subject: [PATCH 0351/1077] Do not duplicate tests --- .github/workflows/ci.yaml | 1 + .github/workflows/hassfest.yaml | 1 + .github/workflows/validate.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 54525a90..7e3a8106 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,6 +2,7 @@ name: pytest on: push: + branches: [master] pull_request: jobs: diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml index 18c7d193..2845b7dc 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/hassfest.yaml @@ -2,6 +2,7 @@ name: Validate with hassfest on: push: + branches: [master] pull_request: schedule: - cron: "0 0 * * *" diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index fc1b5f91..aec72c30 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -2,6 +2,7 @@ name: Validate on: push: + branches: [master] pull_request: schedule: - cron: "0 0 * * *" From f6c9d138c5bfaaeccfed28ae39003e6c452fef3b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:30:28 -0700 Subject: [PATCH 0352/1077] install pytest --- .github/workflows/ci.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7e3a8106..e35521b5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -29,8 +29,9 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install -e homeassistant/ + pip install --upgrade pip + pip install --upgrade pip pytest + pip install -e homeassistant/ - name: Run pytest timeout-minutes: 60 run: | From a9428ed93645eecd39d8d1bf4b217df662386708 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:36:37 -0700 Subject: [PATCH 0353/1077] Fix PYTHONPATH --- .github/workflows/ci.yaml | 2 +- tests/test_config_flow.py | 2 +- tests/test_init.py | 4 ++-- tests/test_switch.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e35521b5..7d134fd5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -35,4 +35,4 @@ jobs: - name: Run pytest timeout-minutes: 60 run: | - pytest tests + PYTHONPATH=${PYTHONPATH}:custom_components/:homeassistant/tests pytest tests diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 53901cf9..5666cea0 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,6 +1,6 @@ """Test Adaptive Lighting config flow.""" from homeassistant import data_entry_flow -from homeassistant.components.adaptive_lighting.const import ( +from adaptive_lighting.const import ( CONF_SUNRISE_TIME, CONF_SUNSET_TIME, DEFAULT_NAME, diff --git a/tests/test_init.py b/tests/test_init.py index 53f05c61..ed87a4ba 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,7 +1,7 @@ """Tests for Adaptive Lighting integration.""" from homeassistant import config_entries -from homeassistant.components import adaptive_lighting -from homeassistant.components.adaptive_lighting.const import ( +import adaptive_lighting +from adaptive_lighting.const import ( DEFAULT_NAME, UNDO_UPDATE_LISTENER, ) diff --git a/tests/test_switch.py b/tests/test_switch.py index 68f76fda..f61170e9 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -6,7 +6,7 @@ from random import randint import pytest -from homeassistant.components.adaptive_lighting.const import ( +from adaptive_lighting.const import ( ADAPT_BRIGHTNESS_SWITCH, ADAPT_COLOR_SWITCH, ATTR_TURN_ON_OFF_LISTENER, @@ -31,7 +31,7 @@ from homeassistant.components.adaptive_lighting.const import ( SLEEP_MODE_SWITCH, UNDO_UPDATE_LISTENER, ) -from homeassistant.components.adaptive_lighting.switch import ( +from adaptive_lighting.switch import ( _attributes_have_changed, _expand_light_groups, color_difference_redmean, From 965c0e0d3d852df91e4a9923bd35075f9c1e687f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:39:01 -0700 Subject: [PATCH 0354/1077] Install test requirements --- .github/workflows/ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7d134fd5..d0789e73 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,7 +30,8 @@ jobs: - name: Install dependencies run: | pip install --upgrade pip - pip install --upgrade pip pytest + pip install --upgrade pytest + pip install -r homeassistant/requirements_test.txt pip install -e homeassistant/ - name: Run pytest timeout-minutes: 60 From e380e5ccaa09078892e37faffa11d1da62a775d0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:43:21 -0700 Subject: [PATCH 0355/1077] install requirements.txt --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d0789e73..05f5461b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -31,6 +31,7 @@ jobs: run: | pip install --upgrade pip pip install --upgrade pytest + pip install -r homeassistant/requirements.txt pip install -r homeassistant/requirements_test.txt pip install -e homeassistant/ - name: Run pytest From da1e52142d708e51f08b67b4302bfa3eb1241c21 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 12:46:49 -0700 Subject: [PATCH 0356/1077] install homeassistant/requirements_test_all.txt --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 05f5461b..12074486 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -32,7 +32,7 @@ jobs: pip install --upgrade pip pip install --upgrade pytest pip install -r homeassistant/requirements.txt - pip install -r homeassistant/requirements_test.txt + pip install -r homeassistant/requirements_test_all.txt pip install -e homeassistant/ - name: Run pytest timeout-minutes: 60 From b9ff3d6f9a4df29f9089cac19d3cd338a196dc45 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 13:42:55 -0700 Subject: [PATCH 0357/1077] Try to copy over files --- .github/workflows/ci.yaml | 14 +++++++++----- test_dependencies.py | 27 +++++++++++++++++++++++++++ tests/test_config_flow.py | 2 +- tests/test_init.py | 4 ++-- tests/test_switch.py | 6 +++--- 5 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 test_dependencies.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 12074486..4e8ec83a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v3.0.2 with: repository: home-assistant/core - path: homeassistant + path: core - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v4.1.0 @@ -31,10 +31,14 @@ jobs: run: | pip install --upgrade pip pip install --upgrade pytest - pip install -r homeassistant/requirements.txt - pip install -r homeassistant/requirements_test_all.txt - pip install -e homeassistant/ + pip install -r core/requirements.txt + pip install -r core/requirements_test.txt + pip install -e core/ + pip install $(python test_dependencies.py) - name: Run pytest timeout-minutes: 60 run: | - PYTHONPATH=${PYTHONPATH}:custom_components/:homeassistant/tests pytest tests + cp -r custom_components/adaptive_lighting core/homeassistant/components/adaptive_lighting + cp -r tests/ core/tests/components/adaptive_lighting + cd core + pytest tests/components/adaptive_lighting diff --git a/test_dependencies.py b/test_dependencies.py new file mode 100644 index 00000000..3886b747 --- /dev/null +++ b/test_dependencies.py @@ -0,0 +1,27 @@ +from collections import defaultdict + +with open("core/requirements_test_all.txt") as f: + lines = f.readlines() + +components = [] +packages = [] +deps = {} +for i, line in enumerate(lines): + line = line.strip() + if line.startswith("# homeassistant."): + component = line.split("# homeassistant.")[1] + components.append(component) + elif components and line: + packages.append(line) + else: + for component in components: + for package in packages: + deps.setdefault(component, []).append(package) + components = [] + packages = [] + +required = ["components.recorder", "components.mqtt", "components.zeroconf"] +to_install = [] +for r in required: + to_install.extend(deps[r]) +print(" ".join(to_install)) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 5666cea0..53901cf9 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,6 +1,6 @@ """Test Adaptive Lighting config flow.""" from homeassistant import data_entry_flow -from adaptive_lighting.const import ( +from homeassistant.components.adaptive_lighting.const import ( CONF_SUNRISE_TIME, CONF_SUNSET_TIME, DEFAULT_NAME, diff --git a/tests/test_init.py b/tests/test_init.py index ed87a4ba..53f05c61 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,7 +1,7 @@ """Tests for Adaptive Lighting integration.""" from homeassistant import config_entries -import adaptive_lighting -from adaptive_lighting.const import ( +from homeassistant.components import adaptive_lighting +from homeassistant.components.adaptive_lighting.const import ( DEFAULT_NAME, UNDO_UPDATE_LISTENER, ) diff --git a/tests/test_switch.py b/tests/test_switch.py index f61170e9..e2bb19bd 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -6,7 +6,7 @@ from random import randint import pytest -from adaptive_lighting.const import ( +from homeassistant.components.adaptive_lighting.const import ( ADAPT_BRIGHTNESS_SWITCH, ADAPT_COLOR_SWITCH, ATTR_TURN_ON_OFF_LISTENER, @@ -31,7 +31,7 @@ from adaptive_lighting.const import ( SLEEP_MODE_SWITCH, UNDO_UPDATE_LISTENER, ) -from adaptive_lighting.switch import ( +from homeassistant.components.adaptive_lighting.switch import ( _attributes_have_changed, _expand_light_groups, color_difference_redmean, @@ -63,7 +63,7 @@ from homeassistant.core import Context, State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch +from unittest.mock import patch from tests.common import MockConfigEntry from tests.components.demo.test_light import ENTITY_LIGHT From 850badeb1b31661726d51bc0e530fb56f21e2493 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 13:43:22 -0700 Subject: [PATCH 0358/1077] pytest exists in core/requirements_test.txt --- .github/workflows/ci.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4e8ec83a..fa4f32a7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,7 +30,6 @@ jobs: - name: Install dependencies run: | pip install --upgrade pip - pip install --upgrade pytest pip install -r core/requirements.txt pip install -r core/requirements_test.txt pip install -e core/ From 472f516582f44039cd4f8b3b872b34b869c4e925 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 13:47:19 -0700 Subject: [PATCH 0359/1077] Copy pytest call from core --- .github/workflows/ci.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fa4f32a7..2b317092 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -40,4 +40,13 @@ jobs: cp -r custom_components/adaptive_lighting core/homeassistant/components/adaptive_lighting cp -r tests/ core/tests/components/adaptive_lighting cd core - pytest tests/components/adaptive_lighting + python3 -X dev -m pytest \ + -qq \ + --timeout=9 \ + --durations=10 \ + --dist=loadfile \ + --cov="homeassistant" \ + --cov-report=xml \ + -o console_output_style=count \ + -p no:sugar \ + tests/components/adaptive_lighting From 7808d1036cbbefea957c0d0f7de988dfe3784c76 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 13:59:50 -0700 Subject: [PATCH 0360/1077] add pre-commit --- .pre-commit-config.yaml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..4394d4bb --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-added-large-files + - id: trailing-whitespace + - id: end-of-file-fixer + - id: mixed-line-ending + args: ["--fix=lf"] + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + - repo: https://github.com/ambv/black + rev: 22.6.0 + hooks: + - id: black + - repo: https://github.com/asottile/pyupgrade + rev: v2.37.3 + hooks: + - id: pyupgrade + args: ["--py39-plus"] + - repo: https://github.com/timothycrosley/isort + rev: 5.10.1 + hooks: + - id: isort From e8446fb2325ee6f73652ce712e11069efd63eb38 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 14:01:10 -0700 Subject: [PATCH 0361/1077] Use symlinks --- .github/workflows/ci.yaml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2b317092..33dc1d6d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,14 +37,22 @@ jobs: - name: Run pytest timeout-minutes: 60 run: | - cp -r custom_components/adaptive_lighting core/homeassistant/components/adaptive_lighting - cp -r tests/ core/tests/components/adaptive_lighting cd core + + # Link homeassitant.components.adaptive_lighting + cd homeassistant/components + ln -fs ../../../custom_components/adaptive_lighting adaptive_lighting + cd - + + # Link adaptive_lighting tests + cd tests/components/ + ln -fs ../../../tests adaptive_lighting + cd - + python3 -X dev -m pytest \ -qq \ --timeout=9 \ --durations=10 \ - --dist=loadfile \ --cov="homeassistant" \ --cov-report=xml \ -o console_output_style=count \ From 0016818beb155f2a603f6f5477302c4d419cf176 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 14:13:33 -0700 Subject: [PATCH 0362/1077] Fix TZ test --- .github/workflows/ci.yaml | 1 - setup.cfg | 11 +++++++++++ tests/test_switch.py | 17 ++++++++--------- 3 files changed, 19 insertions(+), 10 deletions(-) create mode 100644 setup.cfg diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 33dc1d6d..3067ec99 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -29,7 +29,6 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - pip install --upgrade pip pip install -r core/requirements.txt pip install -r core/requirements_test.txt pip install -e core/ diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..284326f5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,11 @@ +[isort] +force_sort_within_sections=True +profile=black + +[flake8] +ignore = E203, E266, W503 +max-line-length = 100 +max-complexity = 18 +select = B,C,E,F,W,T4,B9 +per-file-ignores = + code_example.py: E402, E501 diff --git a/tests/test_switch.py b/tests/test_switch.py index e2bb19bd..adfed407 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -3,8 +3,7 @@ import asyncio import datetime from random import randint - -import pytest +from unittest.mock import patch from homeassistant.components.adaptive_lighting.const import ( ADAPT_BRIGHTNESS_SWITCH, @@ -45,9 +44,9 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, - DOMAIN as LIGHT_DOMAIN, - SERVICE_TURN_OFF, ) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.light import SERVICE_TURN_OFF from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN import homeassistant.config as config_util from homeassistant.const import ( @@ -62,8 +61,8 @@ from homeassistant.const import ( from homeassistant.core import Context, State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +import pytest -from unittest.mock import patch from tests.common import MockConfigEntry from tests.components.demo.test_light import ENTITY_LIGHT @@ -247,10 +246,10 @@ async def test_adaptive_lighting_time_zones_and_sun_settings( context = switch.create_context("test") # needs to be passed to update method min_color_temp = switch._sun_light_settings.min_color_temp - sunset = hass.config.time_zone.localize(SUNSET).astimezone(dt_util.UTC) + sunset = SUNSET.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) before_sunset = sunset - datetime.timedelta(hours=1) after_sunset = sunset + datetime.timedelta(hours=1) - sunrise = hass.config.time_zone.localize(SUNRISE).astimezone(dt_util.UTC) + sunrise = SUNRISE.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) before_sunrise = sunrise - datetime.timedelta(hours=1) after_sunrise = sunrise + datetime.timedelta(hours=1) @@ -333,10 +332,10 @@ async def test_light_settings(hass): await hass.async_block_till_done() # Test with different times - sunset = hass.config.time_zone.localize(SUNSET).astimezone(dt_util.UTC) + sunset = SUNSET.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) before_sunset = sunset - datetime.timedelta(hours=1) after_sunset = sunset + datetime.timedelta(hours=1) - sunrise = hass.config.time_zone.localize(SUNRISE).astimezone(dt_util.UTC) + sunrise = SUNRISE.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) before_sunrise = sunrise - datetime.timedelta(hours=1) after_sunrise = sunrise + datetime.timedelta(hours=1) From 167530d77c079483e54d7ac472cbe19889214ad0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 14:30:24 -0700 Subject: [PATCH 0363/1077] Fix test_successful_config_entry and test_unload_entry --- tests/test_init.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index 53f05c61..b6f48e0b 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,10 +1,10 @@ """Tests for Adaptive Lighting integration.""" -from homeassistant import config_entries from homeassistant.components import adaptive_lighting from homeassistant.components.adaptive_lighting.const import ( DEFAULT_NAME, UNDO_UPDATE_LISTENER, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_NAME from homeassistant.setup import async_setup_component @@ -33,7 +33,7 @@ async def test_successful_config_entry(hass): assert await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state == ConfigEntryState.LOADED assert UNDO_UPDATE_LISTENER in hass.data[adaptive_lighting.DOMAIN][entry.entry_id] @@ -51,5 +51,5 @@ async def test_unload_entry(hass): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state == ConfigEntryState.NOT_LOADED assert adaptive_lighting.DOMAIN not in hass.data From 4baa564f427981871f23f9c30971f63573ef42ee Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 14:39:53 -0700 Subject: [PATCH 0364/1077] Fix setting up lights --- tests/test_switch.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_switch.py b/tests/test_switch.py index adfed407..33614e6a 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -140,11 +140,18 @@ async def setup_lights(hass): ct=240, ), ] + for light in lights: + light.hass = hass + slug = light.name.lower().replace(" ", "_") + light.entity_id = f"light.{slug}" + await light.async_update_ha_state() + platform.ENTITIES.extend(lights) assert await async_setup_component( hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() + assert all(hass.states.get(light.entity_id) is not None for light in lights) return lights From b6309e89d367e5f6909e7e0d8e46ba6f7f97f10b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 14:44:24 -0700 Subject: [PATCH 0365/1077] Fix assert and block till done --- tests/test_switch.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_switch.py b/tests/test_switch.py index 33614e6a..77d96341 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -49,6 +49,7 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.light import SERVICE_TURN_OFF from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN import homeassistant.config as config_util +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, CONF_LIGHTS, @@ -109,6 +110,7 @@ async def setup_switch(hass, extra_data): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + assert entry.state is ConfigEntryState.LOADED switch = hass.data[DOMAIN][entry.entry_id][SWITCH_DOMAIN] return entry, switch @@ -587,6 +589,7 @@ async def test_apply_service(hass): }, blocking=True, ) + await hass.async_block_till_done() # Test turn on with defaults assert hass.states.get(entity_id).state == STATE_OFF From 5c760b5f4af8a22c6b4a4e38918bc0d7f14b5615 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 15:07:07 -0700 Subject: [PATCH 0366/1077] call platform.init() --- tests/test_switch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_switch.py b/tests/test_switch.py index 77d96341..ff759537 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -149,6 +149,7 @@ async def setup_lights(hass): await light.async_update_ha_state() platform.ENTITIES.extend(lights) + platform.init() assert await async_setup_component( hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {CONF_PLATFORM: "test"}} ) From 01fd7f96e20743ea91645e04e388e52ef8d00fd9 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 15:08:03 -0700 Subject: [PATCH 0367/1077] Run all pre-commit filters --- .github/FUNDING.yml | 2 +- .github/ISSUE_TEMPLATE/enhancement.md | 1 - .../adaptive_lighting/__init__.py | 3 +-- .../adaptive_lighting/config_flow.py | 3 +-- custom_components/adaptive_lighting/const.py | 3 +-- custom_components/adaptive_lighting/switch.py | 27 ++++++++++--------- .../adaptive_lighting/translations/de.json | 2 +- test_dependencies.py | 2 -- tests/test_switch.py | 2 +- 9 files changed, 20 insertions(+), 25 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e42b9e64..e6701aec 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: [basnijholz, RubenKelevra] +github: [basnijholz, RubenKelevra] diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md index fcd16fc6..cc515a20 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.md +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -3,4 +3,3 @@ name: 'Enhancement' about: 'Suggest an improvement to an existing feature.' labels: kind/enhancement, need/triage --- - diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index f7be6292..33881c75 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -2,12 +2,11 @@ import logging from typing import Any -import voluptuous as vol - from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +import voluptuous as vol from .const import ( _DOMAIN_SCHEMA, diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 8fa74f5c..d0f0bf2d 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -1,12 +1,11 @@ """Config flow for Adaptive Lighting integration.""" import logging -import voluptuous as vol - from homeassistant import config_entries from homeassistant.const import CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +import voluptuous as vol from .const import ( # pylint: disable=unused-import CONF_LIGHTS, diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index b182ed82..105a95fc 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,8 +1,7 @@ """Constants for the Adaptive Lighting integration.""" -import voluptuous as vol - from homeassistant.components.light import VALID_TRANSITION import homeassistant.helpers.config_validation as cv +import voluptuous as vol ICON = "mdi:theme-light-dark" diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index cd68e8d0..c7a39b17 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -15,8 +15,6 @@ import math from typing import Any import astral -import voluptuous as vol - from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, @@ -27,25 +25,27 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_KELVIN, ATTR_RGB_COLOR, + ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_XY_COLOR, - DOMAIN as LIGHT_DOMAIN, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_XY, +) +from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, VALID_TRANSITION, is_on, - COLOR_MODE_RGB, - COLOR_MODE_RGBW, - COLOR_MODE_HS, - COLOR_MODE_XY, - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_BRIGHTNESS, - ATTR_SUPPORTED_COLOR_MODES, ) - -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DOMAIN, @@ -88,6 +88,7 @@ from homeassistant.util.color import ( color_xy_to_hs, ) import homeassistant.util.dt as dt_util +import voluptuous as vol from .const import ( ADAPT_BRIGHTNESS_SWITCH, @@ -97,7 +98,6 @@ from .const import ( ATTR_TURN_ON_OFF_LISTENER, CONF_DETECT_NON_HA_CHANGES, CONF_INITIAL_TRANSITION, - CONF_SLEEP_TRANSITION, CONF_INTERVAL, CONF_LIGHTS, CONF_MANUAL_CONTROL, @@ -110,6 +110,7 @@ from .const import ( CONF_SEPARATE_TURN_ON_COMMANDS, CONF_SLEEP_BRIGHTNESS, CONF_SLEEP_COLOR_TEMP, + CONF_SLEEP_TRANSITION, CONF_SUNRISE_OFFSET, CONF_SUNRISE_TIME, CONF_SUNSET_OFFSET, diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json index dae5af4e..24c1d07e 100644 --- a/custom_components/adaptive_lighting/translations/de.json +++ b/custom_components/adaptive_lighting/translations/de.json @@ -46,4 +46,4 @@ "option_error": "Fehlerhafte Option" } } -} \ No newline at end of file +} diff --git a/test_dependencies.py b/test_dependencies.py index 3886b747..a9c37648 100644 --- a/test_dependencies.py +++ b/test_dependencies.py @@ -1,5 +1,3 @@ -from collections import defaultdict - with open("core/requirements_test_all.txt") as f: lines = f.readlines() diff --git a/tests/test_switch.py b/tests/test_switch.py index ff759537..de19045f 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -755,7 +755,7 @@ def test_attributes_have_changed(): @pytest.mark.parametrize("wait", [True, False]) async def test_expand_light_groups(hass, wait): """Test expanding light groups.""" - await setup_switch(hass, {}) + await setup_lights_and_switch(hass, {}) lights = ["light.ceiling_lights", "light.kitchen_lights"] await async_setup_component( hass, From 26bce85318b5d52e1d05e49a1f99f549dff958d9 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 16:11:23 -0700 Subject: [PATCH 0368/1077] Setup demo platform --- tests/test_switch.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/test_switch.py b/tests/test_switch.py index de19045f..9689a05a 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access import asyncio import datetime +import logging from random import randint from unittest.mock import patch @@ -67,6 +68,8 @@ import pytest from tests.common import MockConfigEntry from tests.components.demo.test_light import ENTITY_LIGHT +_LOGGER = logging.getLogger(__name__) + SUNRISE = datetime.datetime( year=2020, month=10, @@ -97,6 +100,13 @@ ENTITY_ADAPT_COLOR_SWITCH = f"{_SWITCH_FMT}_adapt_color_{DEFAULT_NAME}" ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE +@pytest.fixture(autouse=True) +async def setup_comp(hass): + """Set up demo component.""" + await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) + await hass.async_block_till_done() + + @pytest.fixture def reset_time_zone(): """Reset time zone.""" @@ -117,6 +127,9 @@ async def setup_switch(hass, extra_data): async def setup_lights(hass): """Set up 3 light entities using the 'test' platform.""" + await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) + await hass.async_block_till_done() + platform = getattr(hass.components, "test.light") while platform.ENTITIES: # Make sure it is empty @@ -442,6 +455,7 @@ async def test_manual_control(hass): ) await hass.async_block_till_done() await update() + _LOGGER.debug("Turn light %s, to %s", state, kwargs) async def turn_switch(state, entity_id): await hass.services.async_call( @@ -491,7 +505,8 @@ async def test_manual_control(hass): assert manual_control[ENTITY_LIGHT] await turn_light(False) await turn_light(True, brightness=increased_brightness()) - assert not manual_control[ENTITY_LIGHT] + assert hass.states.get(ENTITY_LIGHT).state == STATE_ON + assert not manual_control[ENTITY_LIGHT], manual_control # Check that toggling (sleep mode) switch resets manual control for entity_id in [ENTITY_SWITCH, ENTITY_SLEEP_MODE_SWITCH]: From bc825f035c09fdeeb2253369f4be86aa4dd8efea Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 16:12:12 -0700 Subject: [PATCH 0369/1077] Remove fixture that is not neede --- tests/test_switch.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/test_switch.py b/tests/test_switch.py index 9689a05a..9aeb131a 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -100,13 +100,6 @@ ENTITY_ADAPT_COLOR_SWITCH = f"{_SWITCH_FMT}_adapt_color_{DEFAULT_NAME}" ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE -@pytest.fixture(autouse=True) -async def setup_comp(hass): - """Set up demo component.""" - await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) - await hass.async_block_till_done() - - @pytest.fixture def reset_time_zone(): """Reset time zone.""" From 3b03482593f2d185d2721dc05afe9e3849a150fe Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 16:17:41 -0700 Subject: [PATCH 0370/1077] Never return an empty list, fixes #81 --- custom_components/adaptive_lighting/switch.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index c7a39b17..b013d17e 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -211,6 +211,9 @@ def _split_service_data(service_data, adapt_brightness, adapt_color): service_data_brightness.pop(ATTR_RGB_COLOR, None) service_data_brightness.pop(ATTR_COLOR_TEMP, None) service_datas.append(service_data_brightness) + + if not service_datas: # neither adapt_brightness nor adapt_color + return [service_data] return service_datas From 60179cbe38c3179766877d954cf7538fb8fd315a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 22:02:52 -0700 Subject: [PATCH 0371/1077] Fix test_separate_turn_on_commands --- custom_components/adaptive_lighting/switch.py | 3 +++ tests/test_switch.py | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index b013d17e..349f36e7 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -902,6 +902,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _sleep_mode_switch_state_event(self, event: Event) -> None: if not match_switch_state_event(event, (STATE_ON, STATE_OFF)): + _LOGGER.debug("%s: Ignoring sleep event %s", self._name, event) return _LOGGER.debug( "%s: _sleep_mode_switch_state_event, event: '%s'", self._name, event @@ -1015,10 +1016,12 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): async def async_turn_on(self, **kwargs) -> None: """Turn on adaptive lighting sleep mode.""" + _LOGGER.debug("%s: Turning on", self._name) self._state = True async def async_turn_off(self, **kwargs) -> None: """Turn off adaptive lighting sleep mode.""" + _LOGGER.debug("%s: Turning off", self._name) self._state = False diff --git a/tests/test_switch.py b/tests/test_switch.py index 9aeb131a..5c785bce 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -879,14 +879,22 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands): await switch.sleep_mode_switch.async_turn_on() await switch._update_attrs_and_maybe_adapt_lights(context=context) await hass.async_block_till_done() - sleep_brightness = light.brightness - sleep_color_temp = light.color_temp + + # TODO: figure out why `light.brightness` is not updating + attrs = hass.states.get(light.entity_id).attributes + sleep_brightness = attrs["brightness"] + sleep_color_temp = attrs["color_temp"] + assert sleep_brightness != brightness assert sleep_color_temp != color_temp + await switch.sleep_mode_switch.async_turn_off() await switch._update_attrs_and_maybe_adapt_lights(context=context) await hass.async_block_till_done() - brightness = light.brightness - color_temp = light.color_temp + + attrs = hass.states.get(light.entity_id).attributes + brightness = attrs["brightness"] + color_temp = attrs["color_temp"] + assert sleep_brightness != brightness assert sleep_color_temp != color_temp From f1980715841a6641563b3a1ebcc91e52078cc0c3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 22:24:36 -0700 Subject: [PATCH 0372/1077] Use variable --- tests/test_switch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_switch.py b/tests/test_switch.py index 5c785bce..e290c335 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -120,7 +120,9 @@ async def setup_switch(hass, extra_data): async def setup_lights(hass): """Set up 3 light entities using the 'test' platform.""" - await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) + await async_setup_component( + hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": "demo"}} + ) await hass.async_block_till_done() platform = getattr(hass.components, "test.light") From 26617659906fcd14699abe3e874a655bfd292e1a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 22:25:13 -0700 Subject: [PATCH 0373/1077] Remove test_expand_light_groups --- tests/test_switch.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/tests/test_switch.py b/tests/test_switch.py index e290c335..6773c2d1 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -762,37 +762,6 @@ def test_attributes_have_changed(): ) -@pytest.mark.parametrize("wait", [True, False]) -async def test_expand_light_groups(hass, wait): - """Test expanding light groups.""" - await setup_lights_and_switch(hass, {}) - lights = ["light.ceiling_lights", "light.kitchen_lights"] - await async_setup_component( - hass, - LIGHT_DOMAIN, - { - LIGHT_DOMAIN: [ - {"platform": "demo"}, - { - "platform": GROUP_DOMAIN, - "entities": lights, - }, - ] - }, - ) - if wait: - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - - expanded = set(_expand_light_groups(hass, ["light.light_group"])) - if wait: - assert expanded == set(lights) - else: - # Cannot expand yet because state is None - assert expanded == {"light.light_group"} - - async def test_unload_switch(hass): """Test removing Adaptive Lighting.""" entry, _ = await setup_switch(hass, {}) From 412ca6cf52ebf15fa4968678aba29f82676af03f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 28 Aug 2022 22:28:36 -0700 Subject: [PATCH 0374/1077] Add components.http dependencies --- test_dependencies.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test_dependencies.py b/test_dependencies.py index a9c37648..384abe84 100644 --- a/test_dependencies.py +++ b/test_dependencies.py @@ -18,7 +18,12 @@ for i, line in enumerate(lines): components = [] packages = [] -required = ["components.recorder", "components.mqtt", "components.zeroconf"] +required = [ + "components.recorder", + "components.mqtt", + "components.zeroconf", + "components.http", +] to_install = [] for r in required: to_install.extend(deps[r]) From 5aee7ae1d55c9381d61f874140ccc193a03a6ac7 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 29 Aug 2022 08:31:05 -0700 Subject: [PATCH 0375/1077] Fix hassfest error --- custom_components/adaptive_lighting/strings.json | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 72fdcb3b..cda583e9 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -1,5 +1,4 @@ { - "title": "Adaptive Lighting", "config": { "step": { "user": { From 54eba57baf83c3c4cfe58e2043865b01f32d44ca Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 29 Aug 2022 08:38:37 -0700 Subject: [PATCH 0376/1077] Rename steps in CI --- .github/workflows/ci.yaml | 2 +- .github/workflows/hassfest.yaml | 2 +- .github/workflows/validate.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3067ec99..84769d86 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,7 +8,7 @@ on: jobs: pytest: - name: Prepare dependencies + name: Run pytest runs-on: ubuntu-20.04 timeout-minutes: 60 strategy: diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml index 2845b7dc..157d5415 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/hassfest.yaml @@ -8,7 +8,7 @@ on: - cron: "0 0 * * *" jobs: - validate: + validate_hassfest: runs-on: "ubuntu-latest" steps: - uses: "actions/checkout@v2" diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index aec72c30..2bb88b96 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -8,7 +8,7 @@ on: - cron: "0 0 * * *" jobs: - validate: + validate_hacs: runs-on: "ubuntu-latest" steps: - uses: "actions/checkout@v2" From 166044074a58e010ed1244b47247ad09e5930a3c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 29 Aug 2022 09:22:35 -0700 Subject: [PATCH 0377/1077] Add pre-commit CI --- .github/workflows/pre-commit.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/pre-commit.yaml diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml new file mode 100644 index 00000000..f46e01d4 --- /dev/null +++ b/.github/workflows/pre-commit.yaml @@ -0,0 +1,14 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [master] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - uses: pre-commit/action@v3.0.0 From 6b3686696b0bd47036fcfc1d8f84855957c27022 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 29 Aug 2022 09:25:10 -0700 Subject: [PATCH 0378/1077] Fix pre-commit issues --- custom_components/adaptive_lighting/switch.py | 4 +++- tests/test_switch.py | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index bf064f73..59c110f6 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -169,12 +169,14 @@ BRIGHTNESS_ATTRS = { # Keep a short domain version for the context instances (which can only be 36 chars) _DOMAIN_SHORT = "adapt_lgt" + def _int_to_bytes(i: int, signed: bool = False) -> bytes: bits = i.bit_length() if signed: # Make room for the sign bit. bits += 1 - return i.to_bytes((bits + 7) // 8, 'little', signed=signed) + return i.to_bytes((bits + 7) // 8, "little", signed=signed) + def _short_hash(string: str, length: int = 4) -> str: """Create a hash of 'string' with length 'length'.""" diff --git a/tests/test_switch.py b/tests/test_switch.py index 6773c2d1..2038fe8d 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -33,13 +33,11 @@ from homeassistant.components.adaptive_lighting.const import ( ) from homeassistant.components.adaptive_lighting.switch import ( _attributes_have_changed, - _expand_light_groups, color_difference_redmean, create_context, is_our_context, ) from homeassistant.components.demo.light import DemoLight -from homeassistant.components.group import DOMAIN as GROUP_DOMAIN from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, From b9f28dd0ab87702e9fe25b9b8232874297ba2f4a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 29 Aug 2022 09:30:20 -0700 Subject: [PATCH 0379/1077] Fix linting issues --- custom_components/adaptive_lighting/switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index cf4dcddf..73f6ed31 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -96,6 +96,7 @@ from .const import ( ATTR_ADAPT_BRIGHTNESS, ATTR_ADAPT_COLOR, ATTR_TURN_ON_OFF_LISTENER, + CONF_ADAPT_DELAY, CONF_DETECT_NON_HA_CHANGES, CONF_INITIAL_TRANSITION, CONF_INTERVAL, @@ -127,7 +128,6 @@ from .const import ( SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, TURNING_OFF_DELAY, - CONF_ADAPT_DELAY, VALIDATION_TUPLES, replace_none_str, ) @@ -963,7 +963,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): "%s: Cancelling adjusting lights for %s", self._name, entity_id ) return - + if self._adapt_delay > 0: _LOGGER.debug( "%s: sleep started for '%s' with context.id='%s'", From 7cabb0d100ca01f8415c27072e47ffcc9158a763 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 29 Aug 2022 09:35:39 -0700 Subject: [PATCH 0380/1077] Add adapt_delay to README docs --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 73f05073..2fe5fd06 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,8 @@ adaptive_lighting: | name | The name to use when displaying this switch. | False | default | string | | lights | List of light entities for Adaptive Lighting to control (may be empty). | False | list | [] | | prefer_rgb_color | Whether to use RGB color adjustment instead of native light color temperature. | False | False | boolean | -| initial_transition | How long the first transition is when the lights go from `off` to `on`. | False | 1 | time | -| sleep_transition | How long the transition is when when "sleep mode" is toggled | False | 1 | time | +| initial_transition | How long the first transition is when the lights go from `off` to `on`. | False | 1 | time | +| sleep_transition | How long the transition is when when "sleep mode" is toggled | False | 1 | time | | transition | How long the transition is when the lights change, in seconds. | False | 45 | integer | | interval | How often to adapt the lights, in seconds. | False | 90 | integer | | min_brightness | The minimum percent of brightness to set the lights to. | False | 1 | integer | @@ -68,8 +68,9 @@ adaptive_lighting: | sunset_offset | Change the sunset time with a positive or negative offset. | False | 0 | time | | only_once | Whether to keep adapting the lights (false) or to only adapt the lights as soon as they are turned on (true). | False | False | boolean | | take_over_control | If another source calls `light.turn_on` while the lights are on and being adapted, disable Adaptive Lighting. | False | True | boolean | -| detect_non_ha_changes | Whether to detect state changes and stop adapting lights, even not from `light.turn_on`. Needs `take_over_control` to be enabled. Note that by enabling this option, it calls 'homeassistant.update_entity' every 'interval'! | False | False | boolean | -| separate_turn_on_commands | Whether to use separate `light.turn_on` calls for color and brightness, needed for some types of lights | False | False | boolean | +| detect_non_ha_changes | Whether to detect state changes and stop adapting lights, even not from `light.turn_on`. Needs `take_over_control` to be enabled. Note that by enabling this option, it calls 'homeassistant.update_entity' every 'interval'! | False | False | boolean | +| separate_turn_on_commands | Whether to use separate `light.turn_on` calls for color and brightness, needed for some types of lights | False | False | boolean | +| adapt_delay | Wait time in seconds between light turn on, and Adaptive Lights applying changes to the light state. May avoid flickering. | False | 0 | integer | Full example: From 17061f360f4d3870d70e714b67a018deb832cd23 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 29 Aug 2022 18:28:54 -0700 Subject: [PATCH 0381/1077] Add test_area test, reproduce issue of #75 --- tests/test_switch.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/tests/test_switch.py b/tests/test_switch.py index 2038fe8d..1473a29e 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -59,11 +59,12 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import Context, State +from homeassistant.helpers import device_registry, entity_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util import pytest -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_device_registry, mock_registry from tests.components.demo.test_light import ENTITY_LIGHT _LOGGER = logging.getLogger(__name__) @@ -867,3 +868,32 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands): assert sleep_brightness != brightness assert sleep_color_temp != color_temp + + +async def test_area(hass): + _, (light, *_) = await setup_lights_and_switch(hass) + device_in_area = device_registry.DeviceEntry(area_id="test-area") + + mock_device_registry(hass, {device_in_area.id: device_in_area}) + entity_in_area = entity_registry.RegistryEntry( + entity_id=light.entity_id, + unique_id="in-area-id", + platform="test", + device_id=device_in_area.id, + ) + mock_registry(hass, {entity_in_area.entity_id: entity_in_area}) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {"area_id": "in-area-id"}, + blocking=True, + ) + await hass.async_block_till_done() From b64c44fc8dcbef3fac728686522de666f01d71e3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 29 Aug 2022 18:51:24 -0700 Subject: [PATCH 0382/1077] Use area_entities to extract entity_ids from area --- custom_components/adaptive_lighting/switch.py | 13 ++++++++++++- tests/test_switch.py | 7 ++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 73f6ed31..a8349f65 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -48,6 +48,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_AREA_ID, ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_SERVICE, @@ -80,6 +81,7 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.sun import get_astral_location +from homeassistant.helpers.template import area_entities from homeassistant.util import slugify from homeassistant.util.color import ( color_RGB_to_xy, @@ -1276,7 +1278,16 @@ class TurnOnOffListener: service = event.data[ATTR_SERVICE] service_data = event.data[ATTR_SERVICE_DATA] - entity_ids = cv.ensure_list_csv(service_data[ATTR_ENTITY_ID]) + if ATTR_ENTITY_ID in service_data: + entity_ids = cv.ensure_list_csv(service_data[ATTR_ENTITY_ID]) + elif ATTR_AREA_ID in service_data: + area_ids = cv.ensure_list_csv(service_data[ATTR_AREA_ID]) + entity_ids = [] + for area_id in area_ids: + entity_ids.extend(area_entities(self.hass, area_id)) + _LOGGER.debug( + "Found entity_ids '%s' in area area_id %s: %s", entity_ids, area_id + ) if not any(eid in self.lights for eid in entity_ids): return diff --git a/tests/test_switch.py b/tests/test_switch.py index 1473a29e..1de95909 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -871,7 +871,7 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands): async def test_area(hass): - _, (light, *_) = await setup_lights_and_switch(hass) + switch, (light, *_) = await setup_lights_and_switch(hass) device_in_area = device_registry.DeviceEntry(area_id="test-area") mock_device_registry(hass, {device_in_area.id: device_in_area}) @@ -897,3 +897,8 @@ async def test_area(hass): blocking=True, ) await hass.async_block_till_done() + + _LOGGER.debug( + "switch.turn_on_off_listener.last_service_data: %s", + switch.turn_on_off_listener.last_service_data, + ) From acd506aeebc152676ab9d496d90ea202a6df07bb Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 29 Aug 2022 19:07:34 -0700 Subject: [PATCH 0383/1077] Only add the lights for an area --- custom_components/adaptive_lighting/switch.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index a8349f65..00555ef0 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1284,9 +1284,12 @@ class TurnOnOffListener: area_ids = cv.ensure_list_csv(service_data[ATTR_AREA_ID]) entity_ids = [] for area_id in area_ids: - entity_ids.extend(area_entities(self.hass, area_id)) + area_entity_ids = area_entities(self.hass, area_id) + for entity_id in area_entity_ids: + if entity_id.startswith(LIGHT_DOMAIN): + entity_ids.append(entity_id) _LOGGER.debug( - "Found entity_ids '%s' in area area_id %s: %s", entity_ids, area_id + "Found entity_ids '%s' for area_id '%s'", entity_ids, area_id ) if not any(eid in self.lights for eid in entity_ids): From 7bcd70cbecfa7e72bb1a28ee66e15bd4460dddb0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 29 Aug 2022 19:25:04 -0700 Subject: [PATCH 0384/1077] Add TODO to the test --- tests/test_switch.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test_switch.py b/tests/test_switch.py index 1de95909..38f9ea91 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -50,6 +50,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN import homeassistant.config as config_util from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( + ATTR_AREA_ID, ATTR_ENTITY_ID, CONF_LIGHTS, CONF_NAME, @@ -59,12 +60,12 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import Context, State -from homeassistant.helpers import device_registry, entity_registry +from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util import pytest -from tests.common import MockConfigEntry, mock_device_registry, mock_registry +from tests.common import MockConfigEntry from tests.components.demo.test_light import ENTITY_LIGHT _LOGGER = logging.getLogger(__name__) @@ -872,16 +873,14 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands): async def test_area(hass): switch, (light, *_) = await setup_lights_and_switch(hass) - device_in_area = device_registry.DeviceEntry(area_id="test-area") - - mock_device_registry(hass, {device_in_area.id: device_in_area}) - entity_in_area = entity_registry.RegistryEntry( - entity_id=light.entity_id, - unique_id="in-area-id", - platform="test", - device_id=device_in_area.id, + # TODO: this doesn't set up the area correctly because I get: + # MainThread ... Unable to find referenced areas test_area or it + # is/they are currently not available + # Therefore the area currently doesn't report to have lights in it. + entity = entity_registry.async_get(hass).async_get_or_create( + LIGHT_DOMAIN, "demo", light.unique_id, area_id="test_area" ) - mock_registry(hass, {entity_in_area.entity_id: entity_in_area}) + _LOGGER.debug("test_area entity: %s", entity) await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -893,7 +892,7 @@ async def test_area(hass): await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {"area_id": "in-area-id"}, + {ATTR_AREA_ID: entity.area_id}, blocking=True, ) await hass.async_block_till_done() @@ -902,3 +901,4 @@ async def test_area(hass): "switch.turn_on_off_listener.last_service_data: %s", switch.turn_on_off_listener.last_service_data, ) + raise Exception(str(switch.turn_on_off_listener)) From 48b7bc43985beecb3f31693f9fee1f8752cf680b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 29 Aug 2022 19:36:15 -0700 Subject: [PATCH 0385/1077] Fix test_area --- tests/test_switch.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_switch.py b/tests/test_switch.py index 38f9ea91..4739855e 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -65,7 +65,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util import pytest -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_area_registry from tests.components.demo.test_light import ENTITY_LIGHT _LOGGER = logging.getLogger(__name__) @@ -873,10 +873,10 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands): async def test_area(hass): switch, (light, *_) = await setup_lights_and_switch(hass) - # TODO: this doesn't set up the area correctly because I get: - # MainThread ... Unable to find referenced areas test_area or it - # is/they are currently not available - # Therefore the area currently doesn't report to have lights in it. + + area_registry = mock_area_registry(hass) + area_registry.async_create("test_area") + entity = entity_registry.async_get(hass).async_get_or_create( LIGHT_DOMAIN, "demo", light.unique_id, area_id="test_area" ) @@ -884,11 +884,11 @@ async def test_area(hass): await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: light.entity_id}, + {ATTR_AREA_ID: entity.area_id}, blocking=True, ) await hass.async_block_till_done() - + assert light.entity_id in switch.turn_on_off_listener.last_service_data await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, @@ -901,4 +901,4 @@ async def test_area(hass): "switch.turn_on_off_listener.last_service_data: %s", switch.turn_on_off_listener.last_service_data, ) - raise Exception(str(switch.turn_on_off_listener)) + assert light.entity_id not in switch.turn_on_off_listener.last_service_data From 1fd6787505adb0fafed26869bf8029fc8d295021 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 29 Aug 2022 19:40:14 -0700 Subject: [PATCH 0386/1077] Bump version to 1.0.17 --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index e2a9cdc7..c6ac3cd6 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -6,7 +6,7 @@ "config_flow": true, "dependencies": [], "codeowners": ["@basnijholt", "@RubenKelevra"], - "version": "1.0.16", + "version": "1.0.17", "requirements": [], "iot_class": "calculated" } From c208139aa161f393ef492ac17875957f0c010609 Mon Sep 17 00:00:00 2001 From: Gleb Date: Tue, 30 Aug 2022 13:04:21 +0300 Subject: [PATCH 0387/1077] Adds Russian translation --- .../adaptive_lighting/translations/ru.json | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/ru.json diff --git a/custom_components/adaptive_lighting/translations/ru.json b/custom_components/adaptive_lighting/translations/ru.json new file mode 100644 index 00000000..1f18a68a --- /dev/null +++ b/custom_components/adaptive_lighting/translations/ru.json @@ -0,0 +1,51 @@ +{ + "title": "Adaptive Lighting", + "config": { + "step": { + "user": { + "title": "Выберите имя для экземпляра Adaptive Lighting", + "description": "Выберите имя для этого экземпляра. Вы можете запустить несколько экземпляров Adaptive Lighting, каждый из которых может содержать несколько источников света!", + "data": { + "name": "Имя" + } + } + }, + "abort": { + "already_configured": "Это устройство уже настроено" + } + }, + "options": { + "step": { + "init": { + "title": "Настройки Adaptive Lighting", + "description": "Все настройки компонента Adaptive Lighting. Названия опций соответствуют настройкам в YAML. Параметры не отображаются, если в конфигурации YAML определена запись adaptive_lighting.", + "data": { + "lights": "Осветительные приборы", + "initial_transition": "initial_transition: Начальный переход, когда свет переключается с 'off' на 'on'. (секунды)", + "sleep_transition": "sleep_transition: Когда прибор переходит в Режима Сна (Sleep Mode) и 'sleep_state' изменяется. (секунды)", + "interval": "interval: Интервал между обновлениями переключателя. (секунды)", + "max_brightness": "max_brightness: Максимальная яркость света во время цикла. (%)", + "max_color_temp": "max_color_temp: Самый холодный оттенок цветовой температуры во время цикла. (Kelvin)", + "min_brightness": "min_brightness: Минимальная яркость света во время цикла. (%)", + "min_color_temp": "min_color_temp: Самый теплый оттенок цветовой температуры во время цикла. (Kelvin)", + "only_once": "only_once: Адаптировать свет только при включении.", + "prefer_rgb_color": "prefer_rgb_color: По возможности использовать 'rgb_color' вместо 'color_temp'.", + "separate_turn_on_commands": "separate_turn_on_commands: Раздельные команды для каждого атрибута (цвет, яркость и т.д.) в 'light.turn_on' (требуется для некоторых источников света).", + "sleep_brightness": "sleep_brightness: Настройка яркости для Режима Сна (Sleep Mode). (%)", + "sleep_color_temp": "sleep_color_temp: Настройка цветовой температуры для Режима Сна (Sleep Mode). (Kelvin)", + "sunrise_offset": "sunrise_offset: За сколько времени до (-) или после (+) переопределить время восхода во время цикла. (+/- секунды)", + "sunrise_time": "sunrise_time: Ручное изменение времени восхода солнца, если указано 'None', используется фактическое время восхода в Вашем местоположении. (ЧЧ:ММ:СС)", + "sunset_offset": "sunset_offset: За сколько времени до (-) или после (+) переопределить время заката во время цикла. (+/- секунды)", + "sunset_time": "sunset_time: Ручное изменение времени заката солнца, если указано 'None', используется фактическое время заката в Вашем местоположении. (ЧЧ:ММ:СС)", + "take_over_control": "take_over_control: Если что-либо, кроме Adaptive Lighting, вызывает службу 'light.turn_on', когда свет уже включен, прекратить адаптацию этого осветительного прибора, пока он (или переключатель) не переключится off -> on.", + "detect_non_ha_changes": "detect_non_ha_changes: Обнаруживает все изменения на >10% примененные к освещению (также и из-за пределов Home Assistant), требует включения 'take_over_control' (вызывает 'homeassistant.update_entity' каждый 'interval'!)", + "transition": "Время перехода при применении изменения к источникам света. (секунды)", + "adapt_delay": "Время ожидания между включением света и применением адаптации. Может помочь избежать мерцания. (секунды)" + } + } + }, + "error": { + "option_error": "Ошибка в настройках!" + } + } +} From 9e22e70d63ad30347611c906de52c04ea776efea Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 30 Aug 2022 09:45:05 -0700 Subject: [PATCH 0388/1077] Take care of edge case where no area_id or entity_id in service_data --- custom_components/adaptive_lighting/switch.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 00555ef0..5eb29a9a 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1291,6 +1291,11 @@ class TurnOnOffListener: _LOGGER.debug( "Found entity_ids '%s' for area_id '%s'", entity_ids, area_id ) + else: + _LOGGER.debug( + "No entity_ids or area_ids found in service_data: %s", service_data + ) + return if not any(eid in self.lights for eid in entity_ids): return From 52220b0328dbaad380a719423a80c39be28d6493 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 30 Aug 2022 09:46:13 -0700 Subject: [PATCH 0389/1077] Bump version to 1.0.18 --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index c6ac3cd6..4ac645d0 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -6,7 +6,7 @@ "config_flow": true, "dependencies": [], "codeowners": ["@basnijholt", "@RubenKelevra"], - "version": "1.0.17", + "version": "1.0.18", "requirements": [], "iot_class": "calculated" } From dee6eed67f20cb7eca892c4fd710e0e88c7a337b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 30 Aug 2022 10:29:32 -0700 Subject: [PATCH 0390/1077] Take care of the turn_on_event not having registered an event for entity_id --- custom_components/adaptive_lighting/switch.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 5eb29a9a..7f910702 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1512,6 +1512,11 @@ class TurnOnOffListener: transition = None turn_on_event = self.turn_on_event.get(entity_id) + if turn_on_event is None: + # This means that the light never got a 'turn_on' call that we + # registered. I am not 100% sure why this happens, but it does. + # This is a fix for #170 and #232 + return False id_turn_on = turn_on_event.context.id id_off_to_on = off_to_on_event.context.id From 1642ae8da35ec36b6cd719e00966f64b93257889 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 30 Aug 2022 10:34:36 -0700 Subject: [PATCH 0391/1077] Bump to 1.0.19 --- custom_components/adaptive_lighting/manifest.json | 2 +- custom_components/adaptive_lighting/switch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 4ac645d0..8b431d69 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -6,7 +6,7 @@ "config_flow": true, "dependencies": [], "codeowners": ["@basnijholt", "@RubenKelevra"], - "version": "1.0.18", + "version": "1.0.19", "requirements": [], "iot_class": "calculated" } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 7f910702..4fdf1413 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1515,7 +1515,7 @@ class TurnOnOffListener: if turn_on_event is None: # This means that the light never got a 'turn_on' call that we # registered. I am not 100% sure why this happens, but it does. - # This is a fix for #170 and #232 + # This is a fix for #170 and #232. return False id_turn_on = turn_on_event.context.id From 26797b677d8fd2fa9ad8cfcfbbe0666c4eb38a30 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 30 Aug 2022 10:48:09 -0700 Subject: [PATCH 0392/1077] Make sure that 'settings' dict is populated when switch is off Closes #249 and #190 --- custom_components/adaptive_lighting/switch.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 4fdf1413..922d1e1d 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -793,6 +793,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if "transition" in features: service_data[ATTR_TRANSITION] = transition + # The switch might be off and not have _settings set. + self._settings = self._sun_light_settings.get_settings( + self.sleep_mode_switch.is_on, transition + ) + if "brightness" in features and adapt_brightness: brightness = round(255 * self._settings["brightness_pct"] / 100) service_data[ATTR_BRIGHTNESS] = brightness From c0bc8d23da186368799abe76b4963f0c34d40f38 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 30 Aug 2022 10:49:24 -0700 Subject: [PATCH 0393/1077] Bump to 1.0.20 --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 8b431d69..529820f3 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -6,7 +6,7 @@ "config_flow": true, "dependencies": [], "codeowners": ["@basnijholt", "@RubenKelevra"], - "version": "1.0.19", + "version": "1.0.20", "requirements": [], "iot_class": "calculated" } From 6dc90ea81df0e5a9b349615926ece1e238e42a09 Mon Sep 17 00:00:00 2001 From: awashingmachine Date: Tue, 30 Aug 2022 20:17:05 +0200 Subject: [PATCH 0394/1077] Adds italian translation --- .../adaptive_lighting/translations/it.json | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/it.json diff --git a/custom_components/adaptive_lighting/translations/it.json b/custom_components/adaptive_lighting/translations/it.json new file mode 100644 index 00000000..3069838d --- /dev/null +++ b/custom_components/adaptive_lighting/translations/it.json @@ -0,0 +1,51 @@ +{ + "title": "Illuminazione Adattiva", + "config": { + "step": { + "user": { + "title": "Scegli un nome per l'istanza Illuminazione Adattiva", + "description": "Scegli un nome per questa istanza. Puoi eseguire più istanze di Illuminazione adattiva, ognuna delle quali può contenere più luci!", + "data": { + "name": "Nome" + } + } + }, + "abort": { + "already_configured": "Questo dispositivo è già configurato" + } + }, + "options": { + "step": { + "init": { + "title": "Opzioni Illuminazione Adattiva", + "description": "Tutte le opzioni per il componente Illuminazione Adattiva. I nomi delle opzioni corrispondono con le impostazioni YAML. Non sono mostrate opzioni se hai la voce adaptive-lighting definita nella tua configurazione YAML.", + "data": { + "lights": "luci", + "initial_transition": "initial_transition: Quando le luci vanno da 'off' a 'on'. (secondi)", + "sleep_transition": "sleep_transition: Quando 'sleep_state' cambia. (secondi)", + "interval": "interval: Tempo tra i cambiamenti dello switch. (secondi)", + "max_brightness": "max_brightness: Illuminazione massima delle luci durante un ciclo. (%)", + "max_color_temp": "max_color_temp: Gradazione più fredda del ciclo di temperatura del colore. (Kelvin)", + "min_brightness": "min_brightness: Illuminazione minima delle luci durante un ciclo. (%)", + "min_color_temp": "min_color_temp, Gradazione più calda del ciclo di temperatura del colore. (Kelvin)", + "only_once": "only_once: Adatta le luci solo quando vengono accese.", + "prefer_rgb_color": "prefer_rgb_color: Usa 'rgb_color' al posto di 'color_temp' quando possibile.", + "separate_turn_on_commands": "separate_turn_on_commands: Separa i comandi per ogni attributo (color, brightness, etc.) in 'light.turn_on' (richiesto per alcune luci).", + "sleep_brightness": "sleep_brightness, Impostazione della luminosità per la modalità notturna. (%)", + "sleep_color_temp": "sleep_color_temp: Impostazione della temperatura colore per la modalità notturna. (Kelvin)", + "sunrise_offset": "sunrise_offset: Imposta quanto anticipare(-) o ritardare(+) l'alba nel ciclo (+/- secondi)", + "sunrise_time": "sunrise_time: Imposta manualmente l'ora dell'alba, se 'None', usa l'ora effettiva dell'alba alla tua posizione (HH:MM:SS)", + "sunset_offset": "sunset_offset: Imposta quanto anticipare(-) o ritardare(+) il tramonto nel ciclo (+/- secondi)", + "sunset_time": "sunset_time: Imposta manualmente l'ora del tramonto, se 'None', usa l'ora effettiva del tramonto alla tua posizione (HH:MM:SS)", + "take_over_control": "take_over_control: Se viene chiamato il servizio 'lights.turn_on' (non da Illuminazione Adattiva) quando una luce è già accesa, interrompi l'adattamento della luce finquando essa o l'interruttore non vengono riaccesi (off -> on.)", + "detect_non_ha_changes": "detect_non_ha_changes: rileva tutti i cambiamenti >10% applicati alle luci (anche fuori da HA), richiede che 'take_over_control' sia abilitato (chiama 'homeassistant.update_entity' ad ogni 'intervallo'!)", + "transition": "Tempo di transizione quando viene applicato un cambiamento alle luci (secondi)", + "adapt_delay": "Tempo di attesa tra l'accensione della luce, e Illuminazione Adattiva che applica i cambiamenti allo stato della luce. Potrebbe evitare lo sfarfallio." + } + } + }, + "error": { + "option_error": "Opzione non valida" + } + } + } \ No newline at end of file From 6fd16e34f46e88f6494ed6c73a450d24fed64cbb Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 30 Aug 2022 11:35:25 -0700 Subject: [PATCH 0395/1077] Run pre-commit --- .../adaptive_lighting/translations/it.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/it.json b/custom_components/adaptive_lighting/translations/it.json index 3069838d..b00b663b 100644 --- a/custom_components/adaptive_lighting/translations/it.json +++ b/custom_components/adaptive_lighting/translations/it.json @@ -24,14 +24,14 @@ "initial_transition": "initial_transition: Quando le luci vanno da 'off' a 'on'. (secondi)", "sleep_transition": "sleep_transition: Quando 'sleep_state' cambia. (secondi)", "interval": "interval: Tempo tra i cambiamenti dello switch. (secondi)", - "max_brightness": "max_brightness: Illuminazione massima delle luci durante un ciclo. (%)", - "max_color_temp": "max_color_temp: Gradazione più fredda del ciclo di temperatura del colore. (Kelvin)", + "max_brightness": "max_brightness: Illuminazione massima delle luci durante un ciclo. (%)", + "max_color_temp": "max_color_temp: Gradazione più fredda del ciclo di temperatura del colore. (Kelvin)", "min_brightness": "min_brightness: Illuminazione minima delle luci durante un ciclo. (%)", "min_color_temp": "min_color_temp, Gradazione più calda del ciclo di temperatura del colore. (Kelvin)", - "only_once": "only_once: Adatta le luci solo quando vengono accese.", + "only_once": "only_once: Adatta le luci solo quando vengono accese.", "prefer_rgb_color": "prefer_rgb_color: Usa 'rgb_color' al posto di 'color_temp' quando possibile.", "separate_turn_on_commands": "separate_turn_on_commands: Separa i comandi per ogni attributo (color, brightness, etc.) in 'light.turn_on' (richiesto per alcune luci).", - "sleep_brightness": "sleep_brightness, Impostazione della luminosità per la modalità notturna. (%)", + "sleep_brightness": "sleep_brightness, Impostazione della luminosità per la modalità notturna. (%)", "sleep_color_temp": "sleep_color_temp: Impostazione della temperatura colore per la modalità notturna. (Kelvin)", "sunrise_offset": "sunrise_offset: Imposta quanto anticipare(-) o ritardare(+) l'alba nel ciclo (+/- secondi)", "sunrise_time": "sunrise_time: Imposta manualmente l'ora dell'alba, se 'None', usa l'ora effettiva dell'alba alla tua posizione (HH:MM:SS)", @@ -48,4 +48,4 @@ "option_error": "Opzione non valida" } } - } \ No newline at end of file + } From 7d10259bc8ba1a39bac07af7544ad1b41d63ed72 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 31 Aug 2022 22:01:21 -0700 Subject: [PATCH 0396/1077] Use RGB selector --- custom_components/adaptive_lighting/const.py | 7 +++++++ custom_components/adaptive_lighting/switch.py | 19 ++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 6d144ee2..e860fa74 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,5 +1,6 @@ """Constants for the Adaptive Lighting integration.""" from homeassistant.components.light import VALID_TRANSITION +from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv import voluptuous as vol @@ -30,6 +31,7 @@ CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS = ( ) CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1 CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP = "sleep_color_temp", 1000 +CONF_SLEEP_RGB_COLOR, DEFAULT_SLEEP_RGB_COLOR = "sleep_rgb_color", [255, 56, 0] CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 CONF_SUNRISE_TIME = "sunrise_time" CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 @@ -73,6 +75,11 @@ VALIDATION_TUPLES = [ (CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP, int_between(1000, 10000)), (CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)), (CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP, int_between(1000, 10000)), + ( + CONF_SLEEP_RGB_COLOR, + DEFAULT_SLEEP_RGB_COLOR, + selector.ColorRGBSelector(selector.ColorRGBSelectorConfig()), + ), (CONF_SUNRISE_TIME, NONE_STR, str), (CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int), (CONF_SUNSET_TIME, NONE_STR, str), diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 922d1e1d..bfea3b48 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -113,6 +113,7 @@ from .const import ( CONF_SEPARATE_TURN_ON_COMMANDS, CONF_SLEEP_BRIGHTNESS, CONF_SLEEP_COLOR_TEMP, + CONF_SLEEP_RGB_COLOR, CONF_SLEEP_TRANSITION, CONF_SUNRISE_OFFSET, CONF_SUNRISE_TIME, @@ -587,6 +588,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): min_color_temp=data[CONF_MIN_COLOR_TEMP], sleep_brightness=data[CONF_SLEEP_BRIGHTNESS], sleep_color_temp=data[CONF_SLEEP_COLOR_TEMP], + sleep_rgb_color=data[CONF_SLEEP_RGB_COLOR], sunrise_offset=data[CONF_SUNRISE_OFFSET], sunrise_time=data[CONF_SUNRISE_TIME], sunset_offset=data[CONF_SUNSET_OFFSET], @@ -1074,6 +1076,7 @@ class SunLightSettings: min_color_temp: int sleep_brightness: int sleep_color_temp: int + sleep_rgb_color: tuple[int, int, int] sunrise_offset: datetime.timedelta | None sunrise_time: datetime.time | None sunset_offset: datetime.timedelta | None @@ -1192,10 +1195,8 @@ class SunLightSettings: percent = 1 + percent return (delta_brightness * percent) + self.min_brightness - def calc_color_temp_kelvin(self, percent: float, is_sleep: bool) -> float: + def calc_color_temp_kelvin(self, percent: float) -> float: """Calculate the color temperature in Kelvin.""" - if is_sleep: - return self.sleep_color_temp if percent > 0: delta = self.max_color_temp - self.min_color_temp return (delta * percent) + self.min_color_temp @@ -1214,11 +1215,15 @@ class SunLightSettings: else self.calc_percent(0) ) brightness_pct = self.calc_brightness_pct(percent, is_sleep) - color_temp_kelvin = self.calc_color_temp_kelvin(percent, is_sleep) + if is_sleep: + color_temp_kelvin = self.sleep_color_temp + rgb_color: tuple[float, float, float] = self.sleep_rgb_color + else: + color_temp_kelvin = self.calc_color_temp_kelvin(percent) + rgb_color: tuple[float, float, float] = color_temperature_to_rgb( + color_temp_kelvin + ) color_temp_mired: float = color_temperature_kelvin_to_mired(color_temp_kelvin) - rgb_color: tuple[float, float, float] = color_temperature_to_rgb( - color_temp_kelvin - ) xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color) hs_color: tuple[float, float] = color_xy_to_hs(*xy_color) return { From d3dce3166ed265fa49912af925ed1ba801493e8b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 31 Aug 2022 22:05:31 -0700 Subject: [PATCH 0397/1077] Add strings and options --- custom_components/adaptive_lighting/const.py | 5 +++++ custom_components/adaptive_lighting/strings.json | 2 ++ 2 files changed, 7 insertions(+) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index e860fa74..fc767408 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -32,6 +32,10 @@ CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS = ( CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1 CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP = "sleep_color_temp", 1000 CONF_SLEEP_RGB_COLOR, DEFAULT_SLEEP_RGB_COLOR = "sleep_rgb_color", [255, 56, 0] +CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = ( + "sleep_rgb_or_color_temp", + "color_temp", +) CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 CONF_SUNRISE_TIME = "sunrise_time" CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 @@ -80,6 +84,7 @@ VALIDATION_TUPLES = [ DEFAULT_SLEEP_RGB_COLOR, selector.ColorRGBSelector(selector.ColorRGBSelectorConfig()), ), + (CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP, bool), (CONF_SUNRISE_TIME, NONE_STR, str), (CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int), (CONF_SUNSET_TIME, NONE_STR, str), diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 5a185ea3..c20335bc 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -31,7 +31,9 @@ "prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible", "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.", "sleep_brightness": "sleep_brightness, in %", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, use 'rgb' or 'color_temp'", "sleep_color_temp": "sleep_color_temp, in Kelvin", + "sleep_rgb_color": "sleep_rgb_color, in RGB", "sunrise_offset": "sunrise_offset, in +/- seconds", "sunrise_time": "sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location)", "sunset_offset": "sunset_offset, in +/- seconds", From 29a4b8a33c36afd6711c92a3c2a82de2a667b378 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 31 Aug 2022 22:15:24 -0700 Subject: [PATCH 0398/1077] Add selection between RGB and color temp --- custom_components/adaptive_lighting/const.py | 15 ++++++++++++++- custom_components/adaptive_lighting/switch.py | 5 ++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index fc767408..596c29d4 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,4 +1,5 @@ """Constants for the Adaptive Lighting integration.""" + from homeassistant.components.light import VALID_TRANSITION from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv @@ -84,7 +85,19 @@ VALIDATION_TUPLES = [ DEFAULT_SLEEP_RGB_COLOR, selector.ColorRGBSelector(selector.ColorRGBSelectorConfig()), ), - (CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP, bool), + ( + CONF_SLEEP_RGB_OR_COLOR_TEMP, + DEFAULT_SLEEP_RGB_OR_COLOR_TEMP, + selector.SelectSelector( + selector.SelectSelectorConfig( + options=["color_temp", "rgb"], + multiple=False, + mode=selector.SelectSelectorMode.DROPDOWN, + ), + ) + # vol.Any(vol.Literal("color_temp"), vol.Literal("rgb")), + # cv.multi_select(["color_temp", "rgb_color"]), + ), (CONF_SUNRISE_TIME, NONE_STR, str), (CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int), (CONF_SUNSET_TIME, NONE_STR, str), diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index bfea3b48..95c52e55 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -12,7 +12,7 @@ from datetime import timedelta import functools import logging import math -from typing import Any +from typing import Any, Literal import astral from homeassistant.components.light import ( @@ -114,6 +114,7 @@ from .const import ( CONF_SLEEP_BRIGHTNESS, CONF_SLEEP_COLOR_TEMP, CONF_SLEEP_RGB_COLOR, + CONF_SLEEP_RGB_OR_COLOR_TEMP, CONF_SLEEP_TRANSITION, CONF_SUNRISE_OFFSET, CONF_SUNRISE_TIME, @@ -589,6 +590,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): sleep_brightness=data[CONF_SLEEP_BRIGHTNESS], sleep_color_temp=data[CONF_SLEEP_COLOR_TEMP], sleep_rgb_color=data[CONF_SLEEP_RGB_COLOR], + sleep_rgb_or_color_temp=data[CONF_SLEEP_RGB_OR_COLOR_TEMP], sunrise_offset=data[CONF_SUNRISE_OFFSET], sunrise_time=data[CONF_SUNRISE_TIME], sunset_offset=data[CONF_SUNSET_OFFSET], @@ -1075,6 +1077,7 @@ class SunLightSettings: min_brightness: int min_color_temp: int sleep_brightness: int + sleep_rgb_or_color_temp: Literal["color_temp", "rgb"] sleep_color_temp: int sleep_rgb_color: tuple[int, int, int] sunrise_offset: datetime.timedelta | None From 37304f3d3546fda6dbcfa737344d1abda3ad6d7d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 31 Aug 2022 22:32:57 -0700 Subject: [PATCH 0399/1077] test --- custom_components/adaptive_lighting/config_flow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index d0f0bf2d..0f078263 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -99,6 +99,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): options_schema = {} for name, default, validation in VALIDATION_TUPLES: + if name == "sleep_rgb_or_color_temp": + continue key = vol.Optional(name, default=conf.options.get(name, default)) value = to_replace.get(name, validation) options_schema[key] = value From f500a3758efc5dd49543d45d0fe505e1d624d9a8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 31 Aug 2022 22:44:07 -0700 Subject: [PATCH 0400/1077] Update to translations and cleanup --- .../adaptive_lighting/config_flow.py | 2 - custom_components/adaptive_lighting/const.py | 6 +-- .../adaptive_lighting/strings.json | 40 +++++++++---------- custom_components/adaptive_lighting/switch.py | 2 +- .../adaptive_lighting/translations/en.json | 4 +- 5 files changed, 26 insertions(+), 28 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 0f078263..d0f0bf2d 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -99,8 +99,6 @@ class OptionsFlowHandler(config_entries.OptionsFlow): options_schema = {} for name, default, validation in VALIDATION_TUPLES: - if name == "sleep_rgb_or_color_temp": - continue key = vol.Optional(name, default=conf.options.get(name, default)) value = to_replace.get(name, validation) options_schema[key] = value diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 596c29d4..08b47154 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -90,13 +90,11 @@ VALIDATION_TUPLES = [ DEFAULT_SLEEP_RGB_OR_COLOR_TEMP, selector.SelectSelector( selector.SelectSelectorConfig( - options=["color_temp", "rgb"], + options=["color_temp", "rgb_color"], multiple=False, mode=selector.SelectSelectorMode.DROPDOWN, ), - ) - # vol.Any(vol.Literal("color_temp"), vol.Literal("rgb")), - # cv.multi_select(["color_temp", "rgb_color"]), + ), ), (CONF_SUNRISE_TIME, NONE_STR, str), (CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int), diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index c20335bc..0d1491dd 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -20,28 +20,28 @@ "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", "data": { "lights": "lights", - "initial_transition": "initial_transition, when lights go 'off' to 'on'", - "sleep_transition": "sleep_transition, when 'sleep_state' changes", - "interval": "interval, time between switch updates in seconds", - "max_brightness": "max_brightness, in %", - "max_color_temp": "max_color_temp, in Kelvin", - "min_brightness": "min_brightness, in %", - "min_color_temp": "min_color_temp, in Kelvin", - "only_once": "only_once, only adapt the lights when turning them on", - "prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible", - "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.", - "sleep_brightness": "sleep_brightness, in %", + "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (seconds)", + "sleep_transition": "sleep_transition: When 'sleep_state' changes. (seconds)", + "interval": "interval: Time between switch updates. (seconds)", + "max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)", + "max_color_temp": "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)", + "min_brightness": "min_brightness: Lowest brightness of lights during a cycle. (%)", + "min_color_temp": "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)", + "only_once": "only_once: Only adapt the lights when turning them on.", + "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.", + "separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).", + "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)", "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, use 'rgb' or 'color_temp'", - "sleep_color_temp": "sleep_color_temp, in Kelvin", "sleep_rgb_color": "sleep_rgb_color, in RGB", - "sunrise_offset": "sunrise_offset, in +/- seconds", - "sunrise_time": "sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location)", - "sunset_offset": "sunset_offset, in +/- seconds", - "sunset_time": "sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location)", - "take_over_control": "take_over_control, if anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", - "detect_non_ha_changes": "detect_non_ha_changes, detects all >5% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", - "transition": "transition, in seconds", - "adapt_delay": "Wait time between light turn on, and Adaptive Lights applying changes to the light state. May avoid flickering." + "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)", + "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- seconds)", + "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- seconds)", + "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", + "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", + "transition": "Transition time when applying a change to the lights (seconds)", + "adapt_delay": "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering." } } }, diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 95c52e55..1108dd80 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1077,7 +1077,7 @@ class SunLightSettings: min_brightness: int min_color_temp: int sleep_brightness: int - sleep_rgb_or_color_temp: Literal["color_temp", "rgb"] + sleep_rgb_or_color_temp: Literal["color_temp", "rgb_color"] sleep_color_temp: int sleep_rgb_color: tuple[int, int, int] sunrise_offset: datetime.timedelta | None diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index ecc6eff8..189d68e6 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -32,6 +32,8 @@ "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.", "separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).", "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, use 'rgb' or 'color_temp'", + "sleep_rgb_color": "sleep_rgb_color, in RGB", "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)", "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- seconds)", "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", @@ -40,7 +42,7 @@ "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", "transition": "Transition time when applying a change to the lights (seconds)", - "adapt_delay": "Wait time between light turn on, and Adaptive Lights applying changes to the light state. May avoid flickering." + "adapt_delay": "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering." } } }, From 8965e6becf02cf18c9483f49aa420164cdb99bef Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 31 Aug 2022 22:53:27 -0700 Subject: [PATCH 0401/1077] Ugly first implementation --- custom_components/adaptive_lighting/switch.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 1108dd80..0e166abe 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -816,6 +816,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): color_temp_mired = self._settings["color_temp_mired"] color_temp_mired = max(min(color_temp_mired, max_mireds), min_mireds) service_data[ATTR_COLOR_TEMP] = color_temp_mired + if ( + self.sleep_mode_switch.is_on + and self._sun_light_settings.sleep_rgb_or_color_temp == "rgb_color" + ): + # Special case: if we're in sleep mode and the user has chosen to use RGB color + # in sleep mode, we use this + if "color" not in features: + raise ValueError( + "sleep_rgb_or_color_temp is set to 'rgb_color' however it is " + "not supported by the light." + ) + service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"] elif "color" in features and adapt_color: service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"] From 4a6c966a5353094af80eccee73cd923e20976159 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 31 Aug 2022 22:54:37 -0700 Subject: [PATCH 0402/1077] Change order --- custom_components/adaptive_lighting/const.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 08b47154..b800015b 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -79,12 +79,6 @@ VALIDATION_TUPLES = [ (CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP, int_between(1000, 10000)), (CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP, int_between(1000, 10000)), (CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)), - (CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP, int_between(1000, 10000)), - ( - CONF_SLEEP_RGB_COLOR, - DEFAULT_SLEEP_RGB_COLOR, - selector.ColorRGBSelector(selector.ColorRGBSelectorConfig()), - ), ( CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP, @@ -96,6 +90,12 @@ VALIDATION_TUPLES = [ ), ), ), + (CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP, int_between(1000, 10000)), + ( + CONF_SLEEP_RGB_COLOR, + DEFAULT_SLEEP_RGB_COLOR, + selector.ColorRGBSelector(selector.ColorRGBSelectorConfig()), + ), (CONF_SUNRISE_TIME, NONE_STR, str), (CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int), (CONF_SUNSET_TIME, NONE_STR, str), From c78d1bec752f807d8726d2941e0336d42402c705 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 31 Aug 2022 23:08:53 -0700 Subject: [PATCH 0403/1077] Fix setting RGB light in sleep --- custom_components/adaptive_lighting/switch.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 0e166abe..e3688a62 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -806,29 +806,24 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): brightness = round(255 * self._settings["brightness_pct"] / 100) service_data[ATTR_BRIGHTNESS] = brightness + sleep_rgb = ( + self.sleep_mode_switch.is_on + and self._sun_light_settings.sleep_rgb_or_color_temp == "rgb_color" + ) if ( "color_temp" in features and adapt_color and not (prefer_rgb_color and "color" in features) + and not (sleep_rgb and "color" in features) ): + _LOGGER.debug("%s: Setting color_temp of light %s", self._name, light) attributes = self.hass.states.get(light).attributes min_mireds, max_mireds = attributes["min_mireds"], attributes["max_mireds"] color_temp_mired = self._settings["color_temp_mired"] color_temp_mired = max(min(color_temp_mired, max_mireds), min_mireds) service_data[ATTR_COLOR_TEMP] = color_temp_mired - if ( - self.sleep_mode_switch.is_on - and self._sun_light_settings.sleep_rgb_or_color_temp == "rgb_color" - ): - # Special case: if we're in sleep mode and the user has chosen to use RGB color - # in sleep mode, we use this - if "color" not in features: - raise ValueError( - "sleep_rgb_or_color_temp is set to 'rgb_color' however it is " - "not supported by the light." - ) - service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"] elif "color" in features and adapt_color: + _LOGGER.debug("%s: Setting rgb_color of light %s", self._name, light) service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"] context = context or self.create_context("adapt_lights") From f685f5b58b1c0da7f9550f1eb6b29ef271db2e6d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 31 Aug 2022 23:17:13 -0700 Subject: [PATCH 0404/1077] Update README --- README.md | 80 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 2fe5fd06..aa955ee2 100644 --- a/README.md +++ b/README.md @@ -47,30 +47,32 @@ adaptive_lighting: ``` ### Options -| option | description | required | default | type | -|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|-----------|---------| -| name | The name to use when displaying this switch. | False | default | string | -| lights | List of light entities for Adaptive Lighting to control (may be empty). | False | list | [] | -| prefer_rgb_color | Whether to use RGB color adjustment instead of native light color temperature. | False | False | boolean | -| initial_transition | How long the first transition is when the lights go from `off` to `on`. | False | 1 | time | -| sleep_transition | How long the transition is when when "sleep mode" is toggled | False | 1 | time | -| transition | How long the transition is when the lights change, in seconds. | False | 45 | integer | -| interval | How often to adapt the lights, in seconds. | False | 90 | integer | -| min_brightness | The minimum percent of brightness to set the lights to. | False | 1 | integer | -| max_brightness | The maximum percent of brightness to set the lights to. | False | 100 | integer | -| min_color_temp | The warmest color temperature to set the lights to, in Kelvin. | False | 2000 | integer | -| max_color_temp | The coldest color temperature to set the lights to, in Kelvin. | False | 5500 | integer | -| sleep_brightness | Brightness of lights while the sleep mode is enabled. | False | 1 | integer | -| sleep_color_temp | Color temperature of lights while the sleep mode is enabled. | False | 1000 | integer | -| sunrise_time | Override the sunrise time with a fixed time. | False | time | | -| sunrise_offset | Change the sunrise time with a positive or negative offset. | False | 0 | time | -| sunset_time | Override the sunset time with a fixed time. | False | time | | -| sunset_offset | Change the sunset time with a positive or negative offset. | False | 0 | time | -| only_once | Whether to keep adapting the lights (false) or to only adapt the lights as soon as they are turned on (true). | False | False | boolean | -| take_over_control | If another source calls `light.turn_on` while the lights are on and being adapted, disable Adaptive Lighting. | False | True | boolean | -| detect_non_ha_changes | Whether to detect state changes and stop adapting lights, even not from `light.turn_on`. Needs `take_over_control` to be enabled. Note that by enabling this option, it calls 'homeassistant.update_entity' every 'interval'! | False | False | boolean | -| separate_turn_on_commands | Whether to use separate `light.turn_on` calls for color and brightness, needed for some types of lights | False | False | boolean | -| adapt_delay | Wait time in seconds between light turn on, and Adaptive Lights applying changes to the light state. May avoid flickering. | False | 0 | integer | +| option | description | required | default | type | +| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------- | ------- | +| name | The name to use when displaying this switch. | False | default | string | +| lights | List of light entities for Adaptive Lighting to control (may be empty). | False | list | [] | +| prefer_rgb_color | Whether to use RGB color adjustment instead of native light color temperature. | False | False | boolean | +| initial_transition | How long the first transition is when the lights go from `off` to `on`. | False | 1 | time | +| sleep_transition | How long the transition is when when "sleep mode" is toggled | False | 1 | time | +| transition | How long the transition is when the lights change, in seconds. | False | 45 | integer | +| interval | How often to adapt the lights, in seconds. | False | 90 | integer | +| min_brightness | The minimum percent of brightness to set the lights to. | False | 1 | integer | +| max_brightness | The maximum percent of brightness to set the lights to. | False | 100 | integer | +| min_color_temp | The warmest color temperature to set the lights to, in Kelvin. | False | 2000 | integer | +| max_color_temp | The coldest color temperature to set the lights to, in Kelvin. | False | 5500 | integer | +| sleep_brightness | Brightness of lights while the sleep mode is enabled. | False | 1 | integer | +| sleep_rgb_or_color_temp | Use either 'rgb_color' or 'color_temp' when in sleep mode. | False | 'color_temp' | string | +| sleep_rgb_color | List of three numbers between 0-255, indicating the RGB color in sleep mode (only used when sleep_rgb_or_color_temp is 'rgb_color'). | False | `[255, 56, 0]` | list | +| sleep_color_temp | Color temperature of lights while the sleep mode is enabled (only used when sleep_rgb_or_color_temp is 'color_temp'). | False | 1000 | integer | +| sunrise_time | Override the sunrise time with a fixed time. | False | time | | +| sunrise_offset | Change the sunrise time with a positive or negative offset. | False | 0 | time | +| sunset_time | Override the sunset time with a fixed time. | False | time | | +| sunset_offset | Change the sunset time with a positive or negative offset. | False | 0 | time | +| only_once | Whether to keep adapting the lights (false) or to only adapt the lights as soon as they are turned on (true). | False | False | boolean | +| take_over_control | If another source calls `light.turn_on` while the lights are on and being adapted, disable Adaptive Lighting. | False | True | boolean | +| detect_non_ha_changes | Whether to detect state changes and stop adapting lights, even not from `light.turn_on`. Needs `take_over_control` to be enabled. Note that by enabling this option, it calls 'homeassistant.update_entity' every 'interval'! | False | False | boolean | +| separate_turn_on_commands | Whether to use separate `light.turn_on` calls for color and brightness, needed for some types of lights | False | False | boolean | +| adapt_delay | Wait time in seconds between light turn on, and Adaptive Lights applying changes to the light state. May avoid flickering. | False | 0 | integer | Full example: @@ -101,25 +103,25 @@ adaptive_lighting: ### Services -`adaptive_lighting.apply` applies Adaptive Lighting settings to lights on demand. +`adaptive_lighting.**apply**` applies Adaptive Lighting settings to lights on demand. -| Service data attribute | Optional | Description | -|---------------------------|----------|-------------------------------------------------------------------------| -| `entity_id` | no | The `entity_id` of the switch with the settings to apply. | -| `lights` | no | A light (or list of lights) to apply the settings to. | -| `transition` | yes | The number of seconds for the transition. | -| `adapt_brightness` | yes | Whether to change the brightness of the light or not. | -| `adapt_color` | yes | Whether to adapt the color on supporting lights. | -| `prefer_rgb_color` | yes | Whether to prefer RGB color adjustment over of native light color temperature when possible. | -| `turn_on_lights` | yes | Whether to turn on lights that are currently off. | +| Service data attribute | Optional | Description | +| ---------------------- | -------- | -------------------------------------------------------------------------------------------- | +| `entity_id` | no | The `entity_id` of the switch with the settings to apply. | +| `lights` | no | A light (or list of lights) to apply the settings to. | +| `transition` | yes | The number of seconds for the transition. | +| `adapt_brightness` | yes | Whether to change the brightness of the light or not. | +| `adapt_color` | yes | Whether to adapt the color on supporting lights. | +| `prefer_rgb_color` | yes | Whether to prefer RGB color adjustment over of native light color temperature when possible. | +| `turn_on_lights` | yes | Whether to turn on lights that are currently off. | `adaptive_lighting.set_manual_control` can mark (or unmark) whether a light is "manually controlled", meaning that when a light has `manual_control`, the light is not adapted. -| Service data attribute | Optional | Description | -|------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------| -| `entity_id` | no | The `entity_id` of the switch in which to (un)mark the light as being "manually controlled". | -| `lights` | yes | entity_id(s) of lights, if not specified, all lights in the switch are selected. | -| `manual_control` | yes | Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true | +| Service data attribute | Optional | Description | +| ---------------------- | -------- | --------------------------------------------------------------------------------------------------- | +| `entity_id` | no | The `entity_id` of the switch in which to (un)mark the light as being "manually controlled". | +| `lights` | yes | entity_id(s) of lights, if not specified, all lights in the switch are selected. | +| `manual_control` | yes | Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true | ## Automation examples From 9ed588816ce946781cbb7ae19c734cd1f836da8c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 31 Aug 2022 23:17:38 -0700 Subject: [PATCH 0405/1077] Fix translations --- custom_components/adaptive_lighting/strings.json | 2 +- custom_components/adaptive_lighting/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 0d1491dd..0755cca8 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -31,7 +31,7 @@ "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.", "separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).", "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, use 'rgb' or 'color_temp'", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'", "sleep_rgb_color": "sleep_rgb_color, in RGB", "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)", "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- seconds)", diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 189d68e6..d4070a30 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -32,7 +32,7 @@ "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.", "separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).", "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, use 'rgb' or 'color_temp'", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'", "sleep_rgb_color": "sleep_rgb_color, in RGB", "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)", "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- seconds)", From 07a0dec20381f25f0079678af3fcb97c8755f6ae Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 31 Aug 2022 23:19:40 -0700 Subject: [PATCH 0406/1077] Bump to 1.1.0 --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 529820f3..5b852cb4 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -6,7 +6,7 @@ "config_flow": true, "dependencies": [], "codeowners": ["@basnijholt", "@RubenKelevra"], - "version": "1.0.20", + "version": "1.1.0", "requirements": [], "iot_class": "calculated" } From 0efb467a2964aa12721cf61ee9bd0f23da81c15a Mon Sep 17 00:00:00 2001 From: awashingmachine Date: Thu, 1 Sep 2022 12:10:27 +0200 Subject: [PATCH 0407/1077] Update it.json --- .../adaptive_lighting/translations/it.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/it.json b/custom_components/adaptive_lighting/translations/it.json index b00b663b..c2263631 100644 --- a/custom_components/adaptive_lighting/translations/it.json +++ b/custom_components/adaptive_lighting/translations/it.json @@ -3,7 +3,7 @@ "config": { "step": { "user": { - "title": "Scegli un nome per l'istanza Illuminazione Adattiva", + "title": "Scegli un nome per l'istanza di Illuminazione Adattiva", "description": "Scegli un nome per questa istanza. Puoi eseguire più istanze di Illuminazione adattiva, ognuna delle quali può contenere più luci!", "data": { "name": "Nome" @@ -11,7 +11,7 @@ } }, "abort": { - "already_configured": "Questo dispositivo è già configurato" + "already_configured": "Questo dispositivo è già stato configurato" } }, "options": { @@ -21,17 +21,17 @@ "description": "Tutte le opzioni per il componente Illuminazione Adattiva. I nomi delle opzioni corrispondono con le impostazioni YAML. Non sono mostrate opzioni se hai la voce adaptive-lighting definita nella tua configurazione YAML.", "data": { "lights": "luci", - "initial_transition": "initial_transition: Quando le luci vanno da 'off' a 'on'. (secondi)", + "initial_transition": "initial_transition: Quando le luci vengono accese (off -> on). (secondi)", "sleep_transition": "sleep_transition: Quando 'sleep_state' cambia. (secondi)", "interval": "interval: Tempo tra i cambiamenti dello switch. (secondi)", - "max_brightness": "max_brightness: Illuminazione massima delle luci durante un ciclo. (%)", + "max_brightness": "max_brightness: Luminosità massima delle luci durante un ciclo. (%)", "max_color_temp": "max_color_temp: Gradazione più fredda del ciclo di temperatura del colore. (Kelvin)", - "min_brightness": "min_brightness: Illuminazione minima delle luci durante un ciclo. (%)", - "min_color_temp": "min_color_temp, Gradazione più calda del ciclo di temperatura del colore. (Kelvin)", + "min_brightness": "min_brightness: Luminosità minima delle luci durante un ciclo. (%)", + "min_color_temp": "min_color_temp: Gradazione più calda del ciclo di temperatura del colore. (Kelvin)", "only_once": "only_once: Adatta le luci solo quando vengono accese.", "prefer_rgb_color": "prefer_rgb_color: Usa 'rgb_color' al posto di 'color_temp' quando possibile.", "separate_turn_on_commands": "separate_turn_on_commands: Separa i comandi per ogni attributo (color, brightness, etc.) in 'light.turn_on' (richiesto per alcune luci).", - "sleep_brightness": "sleep_brightness, Impostazione della luminosità per la modalità notturna. (%)", + "sleep_brightness": "sleep_brightness: Impostazione della luminosità per la modalità notturna. (%)", "sleep_color_temp": "sleep_color_temp: Impostazione della temperatura colore per la modalità notturna. (Kelvin)", "sunrise_offset": "sunrise_offset: Imposta quanto anticipare(-) o ritardare(+) l'alba nel ciclo (+/- secondi)", "sunrise_time": "sunrise_time: Imposta manualmente l'ora dell'alba, se 'None', usa l'ora effettiva dell'alba alla tua posizione (HH:MM:SS)", @@ -39,8 +39,8 @@ "sunset_time": "sunset_time: Imposta manualmente l'ora del tramonto, se 'None', usa l'ora effettiva del tramonto alla tua posizione (HH:MM:SS)", "take_over_control": "take_over_control: Se viene chiamato il servizio 'lights.turn_on' (non da Illuminazione Adattiva) quando una luce è già accesa, interrompi l'adattamento della luce finquando essa o l'interruttore non vengono riaccesi (off -> on.)", "detect_non_ha_changes": "detect_non_ha_changes: rileva tutti i cambiamenti >10% applicati alle luci (anche fuori da HA), richiede che 'take_over_control' sia abilitato (chiama 'homeassistant.update_entity' ad ogni 'intervallo'!)", - "transition": "Tempo di transizione quando viene applicato un cambiamento alle luci (secondi)", - "adapt_delay": "Tempo di attesa tra l'accensione della luce, e Illuminazione Adattiva che applica i cambiamenti allo stato della luce. Potrebbe evitare lo sfarfallio." + "transition": "Tempo di transizione quando viene applicata una modifica alle luci (secondi)", + "adapt_delay": "Tempo di attesa tra l'accensione della luce, e Illuminazione Adattiva che applica le modifiche allo stato della luce. Potrebbe evitare sfarfallii." } } }, From 24c2800d67202a7f3ca75915cea44316eae08987 Mon Sep 17 00:00:00 2001 From: danaues Date: Fri, 28 Oct 2022 02:27:37 +0000 Subject: [PATCH 0408/1077] fix missing configured light entries in options --- custom_components/adaptive_lighting/config_flow.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index d0f0bf2d..e30ccc63 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -14,7 +14,7 @@ from .const import ( # pylint: disable=unused-import NONE_STR, VALIDATION_TUPLES, ) -from .switch import _supported_features +from .switch import _supported_features, validate _LOGGER = logging.getLogger(__name__) @@ -82,6 +82,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Handle options flow.""" conf = self.config_entry + data = validate(conf) if conf.source == config_entries.SOURCE_IMPORT: return self.async_show_form(step_id="init", data_schema=None) errors = {} @@ -95,6 +96,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): for light in self.hass.states.async_entity_ids("light") if _supported_features(self.hass, light) ] + for configured_light in data[CONF_LIGHTS]: + if not configured_light in all_lights: + all_lights.append(configured_light) to_replace = {CONF_LIGHTS: cv.multi_select(sorted(all_lights))} options_schema = {} From 45f2085457df86a3fa39a65c5f65ce7c85b0bc71 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Oct 2022 21:57:49 -0700 Subject: [PATCH 0409/1077] Fix pre-commit issues --- custom_components/adaptive_lighting/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index e30ccc63..c3326998 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -61,12 +61,12 @@ def validate_options(user_input, errors): This is an extra validation step because the validators in `EXTRA_VALIDATION` cannot be serialized to json. """ - for key, (validate, _) in EXTRA_VALIDATION.items(): + for key, (_validate, _) in EXTRA_VALIDATION.items(): # these are unserializable validators value = user_input.get(key) try: if value is not None and value != NONE_STR: - validate(value) + _validate(value) except vol.Invalid: _LOGGER.exception("Configuration option %s=%s is incorrect", key, value) errors["base"] = "option_error" @@ -97,7 +97,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if _supported_features(self.hass, light) ] for configured_light in data[CONF_LIGHTS]: - if not configured_light in all_lights: + if configured_light not in all_lights: all_lights.append(configured_light) to_replace = {CONF_LIGHTS: cv.multi_select(sorted(all_lights))} From 14019695a41738928668bb7c481fec485f1615ff Mon Sep 17 00:00:00 2001 From: danaues Date: Sat, 29 Oct 2022 23:45:29 +0000 Subject: [PATCH 0410/1077] add error logging and display error to user --- custom_components/adaptive_lighting/config_flow.py | 2 ++ custom_components/adaptive_lighting/strings.json | 3 ++- custom_components/adaptive_lighting/translations/da.json | 3 ++- custom_components/adaptive_lighting/translations/de.json | 3 ++- custom_components/adaptive_lighting/translations/en.json | 3 ++- custom_components/adaptive_lighting/translations/et.json | 3 ++- custom_components/adaptive_lighting/translations/fr.json | 3 ++- custom_components/adaptive_lighting/translations/it.json | 3 ++- custom_components/adaptive_lighting/translations/nb.json | 3 ++- custom_components/adaptive_lighting/translations/pl.json | 3 ++- custom_components/adaptive_lighting/translations/pt-BR.json | 3 ++- custom_components/adaptive_lighting/translations/ru.json | 3 ++- custom_components/adaptive_lighting/translations/sv.json | 3 ++- custom_components/adaptive_lighting/translations/uk.json | 3 ++- 14 files changed, 28 insertions(+), 13 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index c3326998..4a814190 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -98,6 +98,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ] for configured_light in data[CONF_LIGHTS]: if configured_light not in all_lights: + errors = {"lights":"entity_missing"} + _LOGGER.error("%s: light entity %s is configured, but was not found", data[CONF_NAME], configured_light) all_lights.append(configured_light) to_replace = {CONF_LIGHTS: cv.multi_select(sorted(all_lights))} diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 0755cca8..b82f875e 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -46,7 +46,8 @@ } }, "error": { - "option_error": "Invalid option" + "option_error": "Invalid option", + "entity_missing": "One or more selected light entities are missing from Home Assistant" } } } diff --git a/custom_components/adaptive_lighting/translations/da.json b/custom_components/adaptive_lighting/translations/da.json index 2a881e5d..5b63c115 100644 --- a/custom_components/adaptive_lighting/translations/da.json +++ b/custom_components/adaptive_lighting/translations/da.json @@ -43,7 +43,8 @@ } }, "error": { - "option_error": "Ugyldig indstilling" + "option_error": "Ugyldig indstilling", + "entity_missing": "Et udvalgt lys blev ikke fundet " } } } diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json index 24c1d07e..c1da2ccd 100644 --- a/custom_components/adaptive_lighting/translations/de.json +++ b/custom_components/adaptive_lighting/translations/de.json @@ -43,7 +43,8 @@ } }, "error": { - "option_error": "Fehlerhafte Option" + "option_error": "Fehlerhafte Option", + "entity_missing": "Ein ausgewähltes Licht wurde nicht gefunden" } } } diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index d4070a30..7da48c8f 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -47,7 +47,8 @@ } }, "error": { - "option_error": "Invalid option" + "option_error": "Invalid option", + "entity_missing": "One or more selected light entities are missing from Home Assistant" } } } diff --git a/custom_components/adaptive_lighting/translations/et.json b/custom_components/adaptive_lighting/translations/et.json index 7c9af5d2..b2599f2a 100644 --- a/custom_components/adaptive_lighting/translations/et.json +++ b/custom_components/adaptive_lighting/translations/et.json @@ -43,7 +43,8 @@ } }, "error": { - "option_error": "Vigane suvand" + "option_error": "Vigane suvand", + "entity_missing": "Valitud valgust ei leitud" } } } diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json index 966967ce..a41d84a0 100644 --- a/custom_components/adaptive_lighting/translations/fr.json +++ b/custom_components/adaptive_lighting/translations/fr.json @@ -44,7 +44,8 @@ } }, "error": { - "option_error": "Option non valide" + "option_error": "Option non valide", + "entity_missing": "Une lumière sélectionnée n’a pas été trouvée" } } } diff --git a/custom_components/adaptive_lighting/translations/it.json b/custom_components/adaptive_lighting/translations/it.json index c2263631..c1ae6163 100644 --- a/custom_components/adaptive_lighting/translations/it.json +++ b/custom_components/adaptive_lighting/translations/it.json @@ -45,7 +45,8 @@ } }, "error": { - "option_error": "Opzione non valida" + "option_error": "Opzione non valida", + "entity_missing": "Non è stata trovata una luce selezionata" } } } diff --git a/custom_components/adaptive_lighting/translations/nb.json b/custom_components/adaptive_lighting/translations/nb.json index 2abeae9b..7cfba678 100644 --- a/custom_components/adaptive_lighting/translations/nb.json +++ b/custom_components/adaptive_lighting/translations/nb.json @@ -43,7 +43,8 @@ } }, "error":{ - "option_error":"En eller flere valgte innstillinger er ugyldige" + "option_error":"En eller flere valgte innstillinger er ugyldige", + "entity_missing": "Et utvalgt lys ble ikke funnet" } } } diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json index 07cb8d79..80cc8fdd 100644 --- a/custom_components/adaptive_lighting/translations/pl.json +++ b/custom_components/adaptive_lighting/translations/pl.json @@ -44,7 +44,8 @@ } }, "error": { - "option_error": "Błędne opcje" + "option_error": "Błędne opcje", + "entity_missing": "Nie znaleziono wybranego światła" } } } diff --git a/custom_components/adaptive_lighting/translations/pt-BR.json b/custom_components/adaptive_lighting/translations/pt-BR.json index 87d74547..43eacb01 100644 --- a/custom_components/adaptive_lighting/translations/pt-BR.json +++ b/custom_components/adaptive_lighting/translations/pt-BR.json @@ -44,7 +44,8 @@ } }, "error": { - "option_error": "Opção inválida" + "option_error": "Opção inválida", + "entity_missing": "Uma luz selecionada não foi encontrada" } } } diff --git a/custom_components/adaptive_lighting/translations/ru.json b/custom_components/adaptive_lighting/translations/ru.json index 1f18a68a..2e160f9e 100644 --- a/custom_components/adaptive_lighting/translations/ru.json +++ b/custom_components/adaptive_lighting/translations/ru.json @@ -45,7 +45,8 @@ } }, "error": { - "option_error": "Ошибка в настройках!" + "option_error": "Ошибка в настройках!", + "entity_missing": "Выбранный индикатор не найден" } } } diff --git a/custom_components/adaptive_lighting/translations/sv.json b/custom_components/adaptive_lighting/translations/sv.json index 8c9c4bf7..2239ad3b 100644 --- a/custom_components/adaptive_lighting/translations/sv.json +++ b/custom_components/adaptive_lighting/translations/sv.json @@ -46,7 +46,8 @@ } }, "error": { - "option_error": "Ogiltlig inställning" + "option_error": "Ogiltlig inställning", + "entity_missing": "Ett valt ljus hittades inte" } } } diff --git a/custom_components/adaptive_lighting/translations/uk.json b/custom_components/adaptive_lighting/translations/uk.json index c71d5e63..90265f2e 100644 --- a/custom_components/adaptive_lighting/translations/uk.json +++ b/custom_components/adaptive_lighting/translations/uk.json @@ -43,7 +43,8 @@ } }, "error": { - "option_error": "Хибна опція" + "option_error": "Хибна опція", + "entity_missing": "Вибраного світла в домашньому помічнику не знайшли" } } } From c24ac4e615c837503af87caa6dbcaa1b179b8128 Mon Sep 17 00:00:00 2001 From: danaues Date: Sat, 29 Oct 2022 23:58:03 +0000 Subject: [PATCH 0411/1077] linting, sub string for constant --- custom_components/adaptive_lighting/config_flow.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 4a814190..10ba5d86 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -98,8 +98,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ] for configured_light in data[CONF_LIGHTS]: if configured_light not in all_lights: - errors = {"lights":"entity_missing"} - _LOGGER.error("%s: light entity %s is configured, but was not found", data[CONF_NAME], configured_light) + errors = {CONF_LIGHTS: "entity_missing"} + _LOGGER.error( + "%s: light entity %s is configured, but was not found", + data[CONF_NAME], + configured_light, + ) all_lights.append(configured_light) to_replace = {CONF_LIGHTS: cv.multi_select(sorted(all_lights))} From fb656296199288273610fc441c5115e1d8e1f6f5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Nov 2022 12:30:20 -0800 Subject: [PATCH 0412/1077] Add components.stream as dependency --- test_dependencies.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test_dependencies.py b/test_dependencies.py index 384abe84..532c88f5 100644 --- a/test_dependencies.py +++ b/test_dependencies.py @@ -23,6 +23,7 @@ required = [ "components.mqtt", "components.zeroconf", "components.http", + "components.stream", ] to_install = [] for r in required: From 70899dfe22f205b82e6f01a30cf56aaafaa5c3b5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Nov 2022 15:11:17 -0800 Subject: [PATCH 0413/1077] Remove area_id from entity_registry.async_get_or_create, closes #356 --- tests/test_switch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_switch.py b/tests/test_switch.py index 4739855e..312cf7c6 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -878,7 +878,10 @@ async def test_area(hass): area_registry.async_create("test_area") entity = entity_registry.async_get(hass).async_get_or_create( - LIGHT_DOMAIN, "demo", light.unique_id, area_id="test_area" + LIGHT_DOMAIN, "demo", light.unique_id + ) + entity = entity_registry.async_get(hass).async_update_entity( + entity.entity_id, area_id="test_area" ) _LOGGER.debug("test_area entity: %s", entity) await hass.services.async_call( From d0010740ca24c5935f8fa936e4c85c360caa3b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Beye?= Date: Tue, 8 Nov 2022 17:37:57 +0100 Subject: [PATCH 0414/1077] Add max_sunrise_time and min_sunset_time overrides --- README.md | 4 +++- custom_components/adaptive_lighting/const.py | 6 ++++++ .../adaptive_lighting/strings.json | 2 ++ custom_components/adaptive_lighting/switch.py | 21 ++++++++++++++++++- .../adaptive_lighting/translations/en.json | 2 ++ 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aa955ee2..5c03341b 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ adaptive_lighting: ### Options | option | description | required | default | type | -| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------- | ------- | +|---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -------- | -------------- | ------- | | name | The name to use when displaying this switch. | False | default | string | | lights | List of light entities for Adaptive Lighting to control (may be empty). | False | list | [] | | prefer_rgb_color | Whether to use RGB color adjustment instead of native light color temperature. | False | False | boolean | @@ -65,8 +65,10 @@ adaptive_lighting: | sleep_rgb_color | List of three numbers between 0-255, indicating the RGB color in sleep mode (only used when sleep_rgb_or_color_temp is 'rgb_color'). | False | `[255, 56, 0]` | list | | sleep_color_temp | Color temperature of lights while the sleep mode is enabled (only used when sleep_rgb_or_color_temp is 'color_temp'). | False | 1000 | integer | | sunrise_time | Override the sunrise time with a fixed time. | False | time | | +| max_sunrise_time | Make the virtual sun always rise at at most a specific time while still allowing for even earlier times based on the real sun | False | time | | | sunrise_offset | Change the sunrise time with a positive or negative offset. | False | 0 | time | | sunset_time | Override the sunset time with a fixed time. | False | time | | +| min_sunset_time | Make the virtual sun always set at at least a specific time while still allowing for even later times based on the real sun | False | time | | | sunset_offset | Change the sunset time with a positive or negative offset. | False | 0 | time | | only_once | Whether to keep adapting the lights (false) or to only adapt the lights as soon as they are turned on (true). | False | False | boolean | | take_over_control | If another source calls `light.turn_on` while the lights are on and being adapted, disable Adaptive Lighting. | False | True | boolean | diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index b800015b..36c84a2d 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -39,8 +39,10 @@ CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = ( ) CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 CONF_SUNRISE_TIME = "sunrise_time" +CONF_MAX_SUNRISE_TIME = "max_sunrise_time" CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 CONF_SUNSET_TIME = "sunset_time" +CONF_MIN_SUNSET_TIME = "min_sunset_time" CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 45 @@ -97,8 +99,10 @@ VALIDATION_TUPLES = [ selector.ColorRGBSelector(selector.ColorRGBSelectorConfig()), ), (CONF_SUNRISE_TIME, NONE_STR, str), + (CONF_MAX_SUNRISE_TIME, NONE_STR, str), (CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int), (CONF_SUNSET_TIME, NONE_STR, str), + (CONF_MIN_SUNSET_TIME, NONE_STR, str), (CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int), (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), (CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool), @@ -122,8 +126,10 @@ EXTRA_VALIDATION = { CONF_INTERVAL: (cv.time_period, timedelta_as_int), CONF_SUNRISE_OFFSET: (cv.time_period, timedelta_as_int), CONF_SUNRISE_TIME: (cv.time, str), + CONF_MAX_SUNRISE_TIME: (cv.time, str), CONF_SUNSET_OFFSET: (cv.time_period, timedelta_as_int), CONF_SUNSET_TIME: (cv.time, str), + CONF_MIN_SUNSET_TIME: (cv.time, str), } diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index b82f875e..8d4cb44c 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -36,8 +36,10 @@ "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)", "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- seconds)", "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "max_sunrise_time": "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- seconds)", "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "min_sunset_time": "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", "transition": "Transition time when applying a change to the lights (seconds)", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index e3688a62..435f49e5 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -118,8 +118,10 @@ from .const import ( CONF_SLEEP_TRANSITION, CONF_SUNRISE_OFFSET, CONF_SUNRISE_TIME, + CONF_MAX_SUNRISE_TIME, CONF_SUNSET_OFFSET, CONF_SUNSET_TIME, + CONF_MIN_SUNSET_TIME, CONF_TAKE_OVER_CONTROL, CONF_TRANSITION, CONF_TURN_ON_LIGHTS, @@ -593,8 +595,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): sleep_rgb_or_color_temp=data[CONF_SLEEP_RGB_OR_COLOR_TEMP], sunrise_offset=data[CONF_SUNRISE_OFFSET], sunrise_time=data[CONF_SUNRISE_TIME], + max_sunrise_time=data[CONF_MAX_SUNRISE_TIME], sunset_offset=data[CONF_SUNSET_OFFSET], sunset_time=data[CONF_SUNSET_TIME], + min_sunset_time=data[CONF_MIN_SUNSET_TIME], time_zone=self.hass.config.time_zone, transition=data[CONF_TRANSITION], ) @@ -1089,8 +1093,10 @@ class SunLightSettings: sleep_rgb_color: tuple[int, int, int] sunrise_offset: datetime.timedelta | None sunrise_time: datetime.time | None + max_sunrise_time: datetime.time | None sunset_offset: datetime.timedelta | None sunset_time: datetime.time | None + min_sunset_time: datetime.time | None time_zone: datetime.tzinfo transition: int @@ -1135,7 +1141,20 @@ class SunLightSettings: else _replace_time(date, "sunset") ) + self.sunset_offset - if self.sunrise_time is None and self.sunset_time is None: + if self.max_sunrise_time is not None: + max_sunrise = _replace_time(date, "max_sunrise") + if max_sunrise < sunrise: + sunrise = max_sunrise + + if self.min_sunset_time is not None: + min_sunset = _replace_time(date, "min_sunset") + if min_sunset > sunset: + sunset = min_sunset + + if ( + self.sunrise_time is None and self.sunset_time is None and + self.max_sunrise_time is None and self.min_sunset_time is None + ): try: # Astral v1 solar_noon = location.solar_noon(date, local=False) diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 7da48c8f..e4920cdc 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -37,8 +37,10 @@ "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)", "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- seconds)", "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "max_sunrise_time": "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- seconds)", "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "min_sunset_time": "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", "transition": "Transition time when applying a change to the lights (seconds)", From 60b1be43db741350c1e8d14b24901bfaf5b321fc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 09:11:32 -0800 Subject: [PATCH 0415/1077] Run pre-commit filters --- custom_components/adaptive_lighting/switch.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 435f49e5..ff100a44 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -106,8 +106,10 @@ from .const import ( CONF_MANUAL_CONTROL, CONF_MAX_BRIGHTNESS, CONF_MAX_COLOR_TEMP, + CONF_MAX_SUNRISE_TIME, CONF_MIN_BRIGHTNESS, CONF_MIN_COLOR_TEMP, + CONF_MIN_SUNSET_TIME, CONF_ONLY_ONCE, CONF_PREFER_RGB_COLOR, CONF_SEPARATE_TURN_ON_COMMANDS, @@ -118,10 +120,8 @@ from .const import ( CONF_SLEEP_TRANSITION, CONF_SUNRISE_OFFSET, CONF_SUNRISE_TIME, - CONF_MAX_SUNRISE_TIME, CONF_SUNSET_OFFSET, CONF_SUNSET_TIME, - CONF_MIN_SUNSET_TIME, CONF_TAKE_OVER_CONTROL, CONF_TRANSITION, CONF_TURN_ON_LIGHTS, @@ -1152,8 +1152,10 @@ class SunLightSettings: sunset = min_sunset if ( - self.sunrise_time is None and self.sunset_time is None and - self.max_sunrise_time is None and self.min_sunset_time is None + self.sunrise_time is None + and self.sunset_time is None + and self.max_sunrise_time is None + and self.min_sunset_time is None ): try: # Astral v1 From 327c5e16acc4f4f6d72afdd2daa9e2ad46107956 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 09:27:52 -0800 Subject: [PATCH 0416/1077] Add contributors to README --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5c03341b..19424c22 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,13 @@ These graphs were generated using the values calculated by the Adaptive Lighting ##### Brightness: ![cl_brightness|690x130](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/8/58ebd994b62a8b1abfb3497a5288d923ff4e2330.PNG) -# Maintainers +## Contributors -- @basnijholt -- @RubenKelevra + + + + + + + + From 89af672fa3075772cdb702141f3d8d4256a7ad1e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 09:59:12 -0800 Subject: [PATCH 0417/1077] Add badge --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 19424c22..ff1323e6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) + +[![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors-) + ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) # Adaptive Lighting component for Home Assistant From 0ee8f024910cdd1bc87bcff54bc105722f344ac2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 09:59:21 -0800 Subject: [PATCH 0418/1077] add .all-contributorsrc --- .all-contributorsrc | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .all-contributorsrc diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 00000000..c87ea184 --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,15 @@ +{ + "projectName": "adaptive-lighting", + "projectOwner": "basnijholt", + "repoType": "github", + "repoHost": "https://github.com", + "files": [ + "README.md" + ], + "imageSize": 100, + "commit": true, + "commitConvention": "angular", + "contributors": [], + "contributorsPerLine": 7, + "linkToUsage": true +} From e3f962b8a23c0540a166d4ec2a456d5c3f77c540 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 09:59:36 -0800 Subject: [PATCH 0419/1077] docs: add @basnijholt as a contributor --- .all-contributorsrc | 12 +++++++++++- README.md | 18 +++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index c87ea184..1eaab3bd 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -9,7 +9,17 @@ "imageSize": 100, "commit": true, "commitConvention": "angular", - "contributors": [], + "contributors": [ + { + "login": "basnijholt", + "name": "Bas Nijholt", + "avatar_url": "https://avatars.githubusercontent.com/u/6897215?v=4", + "profile": "http://www.nijho.lt/", + "contributions": [ + "code" + ] + } + ], "contributorsPerLine": 7, "linkToUsage": true } diff --git a/README.md b/README.md index ff1323e6..e2ce6f01 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -204,6 +204,22 @@ These graphs were generated using the values calculated by the Adaptive Lighting + + + + + + + + + + + +
Bas Nijholt
Bas Nijholt

💻
+ + Add your contributions + +
From 05e08b1981505c0e01c5fb68c4bd6da422d4e478 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 09:59:41 -0800 Subject: [PATCH 0420/1077] docs: update @basnijholt as a contributor --- .all-contributorsrc | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 1eaab3bd..ccabf58b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -16,7 +16,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/6897215?v=4", "profile": "http://www.nijho.lt/", "contributions": [ - "code" + "code", + "maintenance" ] } ], diff --git a/README.md b/README.md index e2ce6f01..bc0e4398 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting - + From fd41d4305fad96033a69b4e80d9c20fe7019a7f7 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 09:59:42 -0800 Subject: [PATCH 0421/1077] docs: update @basnijholt as a contributor --- .all-contributorsrc | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index ccabf58b..48297f5e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -17,7 +17,8 @@ "profile": "http://www.nijho.lt/", "contributions": [ "code", - "maintenance" + "maintenance", + "bug" ] } ], diff --git a/README.md b/README.md index bc0e4398..612e6964 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting
Bas Nijholt
Bas Nijholt

💻
Bas Nijholt
Bas Nijholt

💻 🚧
- + From 64b0600657804c07c36481287453b1104b9f8fce Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 09:59:48 -0800 Subject: [PATCH 0422/1077] docs: add @wrt54g as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 48297f5e..a481dbbb 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -20,6 +20,15 @@ "maintenance", "bug" ] + }, + { + "login": "wrt54g", + "name": "Sven Serlier", + "avatar_url": "https://avatars.githubusercontent.com/u/85389871?v=4", + "profile": "https://github.com/wrt54g", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 612e6964..4238f10f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -208,6 +208,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 527a114b9dabb2ad75ea8e1cbff548e3838a6625 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 09:59:49 -0800 Subject: [PATCH 0423/1077] docs: add @willpuckett as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index a481dbbb..12d67627 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -29,6 +29,15 @@ "contributions": [ "doc" ] + }, + { + "login": "willpuckett", + "name": "Will Puckett", + "avatar_url": "https://avatars.githubusercontent.com/u/12959477?v=4", + "profile": "https://github.com/willpuckett", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 4238f10f..b6a0d4b0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -209,6 +209,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 14ea94d03daae18273b977028f68185bffee8d13 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 09:59:51 -0800 Subject: [PATCH 0424/1077] docs: add @vapescherov as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 12d67627..ba6eeb85 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -38,6 +38,15 @@ "contributions": [ "doc" ] + }, + { + "login": "vapescherov", + "name": "vapescherov", + "avatar_url": "https://avatars.githubusercontent.com/u/9620482?v=4", + "profile": "https://github.com/vapescherov", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index b6a0d4b0..1a3393ae 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -210,6 +210,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 648f86f5dd63904f792ac20416c734ffb8abf05b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 09:59:52 -0800 Subject: [PATCH 0425/1077] docs: add @travisp as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index ba6eeb85..0d5d8f1c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -47,6 +47,15 @@ "contributions": [ "code" ] + }, + { + "login": "travisp", + "name": "Travis Pew", + "avatar_url": "https://avatars.githubusercontent.com/u/165698?v=4", + "profile": "https://github.com/travisp", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 1a3393ae..ce8d34d6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -211,6 +211,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 8d7ba5aba24dc83cf1f2b40b19dfa06f373cab31 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 09:59:53 -0800 Subject: [PATCH 0426/1077] docs: add @sindrebroch as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 0d5d8f1c..63497060 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -56,6 +56,15 @@ "contributions": [ "doc" ] + }, + { + "login": "sindrebroch", + "name": "Sindre Broch", + "avatar_url": "https://avatars.githubusercontent.com/u/10772085?v=4", + "profile": "https://github.com/sindrebroch", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index ce8d34d6..09d05d17 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-6-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -212,6 +212,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 00e9ceaddd734defd30d7f5bafa2ff4497cf57b7 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 09:59:55 -0800 Subject: [PATCH 0427/1077] docs: add @Shulyaka as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 63497060..302836d6 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -65,6 +65,15 @@ "contributions": [ "doc" ] + }, + { + "login": "Shulyaka", + "name": "Denis Shulyaka", + "avatar_url": "https://avatars.githubusercontent.com/u/2741408?v=4", + "profile": "https://github.com/Shulyaka", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 09d05d17..31906831 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-6-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -213,6 +213,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 10d0fb543f125e131cc2f16e6fde3b1dddc6d744 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 09:59:56 -0800 Subject: [PATCH 0428/1077] docs: add @RubenKelevra as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 302836d6..4533448f 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -74,6 +74,15 @@ "contributions": [ "code" ] + }, + { + "login": "RubenKelevra", + "name": "@RubenKelevra", + "avatar_url": "https://avatars.githubusercontent.com/u/614929?v=4", + "profile": "https://github.com/RubenKelevra", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 31906831..c3cdc644 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -215,6 +215,9 @@ These graphs were generated using the values calculated by the Adaptive Lighting + + + From 8bcf9396b5dd4119b73dfeb298cbdbae8484262b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 09:59:57 -0800 Subject: [PATCH 0429/1077] docs: update @RubenKelevra as a contributor --- .all-contributorsrc | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 4533448f..90b1af5c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -81,7 +81,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/614929?v=4", "profile": "https://github.com/RubenKelevra", "contributions": [ - "doc" + "doc", + "code" ] } ], diff --git a/README.md b/README.md index c3cdc644..ddf9469b 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting - + From 8c40615af7eaba40a196f68ea0433285c4b3c805 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 09:59:59 -0800 Subject: [PATCH 0430/1077] docs: add @robert as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 90b1af5c..203ad97d 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -84,6 +84,15 @@ "doc", "code" ] + }, + { + "login": "robert", + "name": "Rob Heaton", + "avatar_url": "https://avatars.githubusercontent.com/u/1565857?v=4", + "profile": "https://robertheaton.com/", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index ddf9469b..d64fabc2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-9-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -217,6 +217,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 9b8562065ed063b8b4b9248ffb22d465cad5c401 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:00 -0800 Subject: [PATCH 0431/1077] docs: add @Repsionu as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 203ad97d..e42dab62 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -93,6 +93,15 @@ "contributions": [ "code" ] + }, + { + "login": "Repsionu", + "name": "Jüri Rebane", + "avatar_url": "https://avatars.githubusercontent.com/u/46962963?v=4", + "profile": "https://github.com/Repsionu", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index d64fabc2..387f7c59 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-9-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -218,6 +218,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From ae23e1509baee28ac26e3f80704899f225030390 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:01 -0800 Subject: [PATCH 0432/1077] docs: add @quantumlemur as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index e42dab62..702adad6 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -102,6 +102,15 @@ "contributions": [ "translation" ] + }, + { + "login": "quantumlemur", + "name": "quantumlemur", + "avatar_url": "https://avatars.githubusercontent.com/u/229782?v=4", + "profile": "https://github.com/quantumlemur", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 387f7c59..e2942a3d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-11-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -219,6 +219,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 7034ab2c74923d5bd80daf9726c52a440ae50fae Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:03 -0800 Subject: [PATCH 0433/1077] docs: add @Oekn5w as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 702adad6..f03896ce 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -111,6 +111,15 @@ "contributions": [ "code" ] + }, + { + "login": "Oekn5w", + "name": "Michael Kirsch", + "avatar_url": "https://avatars.githubusercontent.com/u/38046255?v=4", + "profile": "https://github.com/Oekn5w", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index e2942a3d..51968a9f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-11-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-12-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -220,6 +220,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 6de3b22e116ef6badc13f54c0ea47e464b5fd3a3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:04 -0800 Subject: [PATCH 0434/1077] docs: add @Nicholaiii as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index f03896ce..3ebf7a8b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -120,6 +120,15 @@ "contributions": [ "code" ] + }, + { + "login": "Nicholaiii", + "name": "Nicholai Nissen", + "avatar_url": "https://avatars.githubusercontent.com/u/7280931?v=4", + "profile": "https://nicholai.dev/", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 51968a9f..96bc69af 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-12-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-13-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -221,6 +221,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 504f2a25f21dc8d4571404418e5375bfe3cb1a5b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:06 -0800 Subject: [PATCH 0435/1077] docs: add @myhrmans as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 3ebf7a8b..84dc5264 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -129,6 +129,15 @@ "contributions": [ "translation" ] + }, + { + "login": "myhrmans", + "name": "Martin Myhrman", + "avatar_url": "https://avatars.githubusercontent.com/u/14261388?v=4", + "profile": "https://github.com/myhrmans", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 96bc69af..cb8ea5e3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-13-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-14-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -222,6 +222,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 6f87c4c0787859c691cc6dfdbe2cdee0b3137a54 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:07 -0800 Subject: [PATCH 0436/1077] docs: add @mpeterson as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 84dc5264..0e2200ad 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -138,6 +138,15 @@ "contributions": [ "translation" ] + }, + { + "login": "mpeterson", + "name": "Michel Peterson", + "avatar_url": "https://avatars.githubusercontent.com/u/11870?v=4", + "profile": "https://github.com/mpeterson", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index cb8ea5e3..cdb8e2f2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-14-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-15-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -224,6 +224,9 @@ These graphs were generated using the values calculated by the Adaptive Lighting + + + From 86f3da3f9a68900220bb36464ac7690da422826e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:08 -0800 Subject: [PATCH 0437/1077] docs: add @matt as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 0e2200ad..b830c486 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -147,6 +147,15 @@ "contributions": [ "code" ] + }, + { + "login": "matt", + "name": "Matthew Mohrman", + "avatar_url": "https://avatars.githubusercontent.com/u/2709?v=4", + "profile": "https://github.com/matt", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index cdb8e2f2..810febdf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-15-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-16-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -226,6 +226,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 7d4a7855249fbe2286ce866263587e9daac7876c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:10 -0800 Subject: [PATCH 0438/1077] docs: add @MangoScango as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index b830c486..3cfbc654 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -156,6 +156,15 @@ "contributions": [ "code" ] + }, + { + "login": "MangoScango", + "name": "MangoScango", + "avatar_url": "https://avatars.githubusercontent.com/u/7623678?v=4", + "profile": "https://github.com/MangoScango", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 810febdf..4863fd1f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-16-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-17-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -227,6 +227,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From e329417f88c374705bbf97832c0c3931154ce879 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:11 -0800 Subject: [PATCH 0439/1077] docs: add @Lynilia as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 3cfbc654..21d3c029 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -165,6 +165,15 @@ "contributions": [ "code" ] + }, + { + "login": "Lynilia", + "name": "Lynilia", + "avatar_url": "https://avatars.githubusercontent.com/u/89228568?v=4", + "profile": "https://github.com/Lynilia", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 4863fd1f..196cbc68 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-17-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-18-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -228,6 +228,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From aa9b0542fd339a75591831feb8a4f18630df7af5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:13 -0800 Subject: [PATCH 0440/1077] docs: add @LukaszP2 as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 21d3c029..9e5264ca 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -174,6 +174,15 @@ "contributions": [ "translation" ] + }, + { + "login": "LukaszP2", + "name": "LukaszP2", + "avatar_url": "https://avatars.githubusercontent.com/u/44735995?v=4", + "profile": "https://github.com/LukaszP2", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 196cbc68..99df4451 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-18-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-19-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -229,6 +229,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From d2c38111666b3685acf5c4d0a4570fb91bd17eec Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:14 -0800 Subject: [PATCH 0441/1077] docs: add @jowgn as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 9e5264ca..7d1d1eab 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -183,6 +183,15 @@ "contributions": [ "translation" ] + }, + { + "login": "jowgn", + "name": "Joscha Wagner", + "avatar_url": "https://avatars.githubusercontent.com/u/24966042?v=4", + "profile": "https://github.com/jowgn", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 99df4451..4d1c46ad 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-19-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-20-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -230,6 +230,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 418f5570b7cb9189c69cbae7b31ecaa27b209053 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:15 -0800 Subject: [PATCH 0442/1077] docs: add @josecarlosfernandez as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 7d1d1eab..c8c10bed 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -192,6 +192,15 @@ "contributions": [ "translation" ] + }, + { + "login": "josecarlosfernandez", + "name": "skdzzz", + "avatar_url": "https://avatars.githubusercontent.com/u/624242?v=4", + "profile": "https://github.com/josecarlosfernandez", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 4d1c46ad..0b43082b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-20-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-21-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -231,6 +231,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 2cf3ebc299fba9c3efeccbaa82056e73c3a6d0ca Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:17 -0800 Subject: [PATCH 0443/1077] docs: add @itssimon as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index c8c10bed..042b51cc 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -201,6 +201,15 @@ "contributions": [ "translation" ] + }, + { + "login": "itssimon", + "name": "Simon Gurcke", + "avatar_url": "https://avatars.githubusercontent.com/u/1176585?v=4", + "profile": "https://github.com/itssimon", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 0b43082b..9b9d28c9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-21-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-22-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -233,6 +233,9 @@ These graphs were generated using the values calculated by the Adaptive Lighting + + + From b9c86591d6114e42fbbaf9e50c40d4c575d67468 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:19 -0800 Subject: [PATCH 0444/1077] docs: add @Hypfer as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 042b51cc..8dbc5272 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -210,6 +210,15 @@ "contributions": [ "code" ] + }, + { + "login": "Hypfer", + "name": "Sören Beye", + "avatar_url": "https://avatars.githubusercontent.com/u/974410?v=4", + "profile": "http://hypfer.de/", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 9b9d28c9..ab84b8b4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-22-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-23-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -235,6 +235,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 553e2741185ece86dd83d7b0c26fdda7cbac7677 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:20 -0800 Subject: [PATCH 0445/1077] docs: add @hudsonbrendon as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 8dbc5272..64f38888 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -219,6 +219,15 @@ "contributions": [ "code" ] + }, + { + "login": "hudsonbrendon", + "name": "Hudson Brendon", + "avatar_url": "https://avatars.githubusercontent.com/u/5201888?v=4", + "profile": "http://medium.com/@hudsonbrendon", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index ab84b8b4..4fc18bcb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-23-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-24-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -236,6 +236,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From f0b6790b542d4043c91f1def68f357a257a20b08 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:22 -0800 Subject: [PATCH 0446/1077] docs: add @gvssr as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 64f38888..80deb079 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -228,6 +228,15 @@ "contributions": [ "translation" ] + }, + { + "login": "gvssr", + "name": "Gabriel Visser", + "avatar_url": "https://avatars.githubusercontent.com/u/61377476?v=4", + "profile": "https://github.com/gvssr", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 4fc18bcb..1e2b42b4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-24-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-25-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -237,6 +237,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 5ed39db275e6d7b17d5064750b4ca0593a20af62 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:24 -0800 Subject: [PATCH 0447/1077] docs: add @glebsterx as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 80deb079..63dd2bb2 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -237,6 +237,15 @@ "contributions": [ "doc" ] + }, + { + "login": "glebsterx", + "name": "Gleb", + "avatar_url": "https://avatars.githubusercontent.com/u/8779304?v=4", + "profile": "https://github.com/glebsterx", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 1e2b42b4..31595c87 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-25-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-26-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -238,6 +238,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 5d848593326d14e84274c6e37e3a17b7f5b31d8d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:25 -0800 Subject: [PATCH 0448/1077] docs: add @ghost as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 63dd2bb2..7ecd385a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -246,6 +246,15 @@ "contributions": [ "translation" ] + }, + { + "login": "ghost", + "name": "Deleted user", + "avatar_url": "https://avatars.githubusercontent.com/u/10137?v=4", + "profile": "https://github.com/ghost", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 31595c87..f664f5e2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-26-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-27-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -239,6 +239,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 9063dedb37a53784726dd622424b7bba54e06637 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:27 -0800 Subject: [PATCH 0449/1077] docs: add @Djelibeybi as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 7ecd385a..f3c2d948 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -255,6 +255,15 @@ "contributions": [ "translation" ] + }, + { + "login": "Djelibeybi", + "name": "Avi Miller", + "avatar_url": "https://avatars.githubusercontent.com/u/103232?v=4", + "profile": "https://omg.dje.li/", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index f664f5e2..8ca54581 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-27-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-28-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -240,6 +240,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 93f3232587839a2f917057cd2263f6a7c17721be Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:28 -0800 Subject: [PATCH 0450/1077] docs: update @Djelibeybi as a contributor --- .all-contributorsrc | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index f3c2d948..440b4c2b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -262,7 +262,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/103232?v=4", "profile": "https://omg.dje.li/", "contributions": [ - "doc" + "doc", + "code" ] } ], diff --git a/README.md b/README.md index 8ca54581..11bd0c2f 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting - + From 58fe0888fc6b0743cd9e936b8b0e27faefddcbb7 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:29 -0800 Subject: [PATCH 0451/1077] docs: add @denysdovhan as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 440b4c2b..7f903a5b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -265,6 +265,15 @@ "doc", "code" ] + }, + { + "login": "denysdovhan", + "name": "Denys Dovhan", + "avatar_url": "https://avatars.githubusercontent.com/u/3459374?v=4", + "profile": "https://github.com/denysdovhan", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 11bd0c2f..9b0e8857 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-28-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-29-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -242,6 +242,9 @@ These graphs were generated using the values calculated by the Adaptive Lighting + + + From 4573261c7bd0eeedf9d62e614abb69a1e69da5a1 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:31 -0800 Subject: [PATCH 0452/1077] docs: add @Davst as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 7f903a5b..d823fb4c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -274,6 +274,15 @@ "contributions": [ "translation" ] + }, + { + "login": "Davst", + "name": "David Stenbeck", + "avatar_url": "https://avatars.githubusercontent.com/u/3330933?v=4", + "profile": "http://davidstenbeck.com/", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 9b0e8857..bbbca215 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-29-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-30-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -244,6 +244,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From a0bca638000fcee217ea186497acbe38fc8d7f3e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:32 -0800 Subject: [PATCH 0453/1077] docs: add @danaues as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index d823fb4c..c0d1ef30 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -283,6 +283,15 @@ "contributions": [ "doc" ] + }, + { + "login": "danaues", + "name": "Kevin Addeman", + "avatar_url": "https://avatars.githubusercontent.com/u/24459240?v=4", + "profile": "https://github.com/danaues", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index bbbca215..fa7e292a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-30-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-31-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -245,6 +245,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 0c74ee81b008773ecbd35a870e8b710d414be8a2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:34 -0800 Subject: [PATCH 0454/1077] docs: add @covid10 as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index c0d1ef30..8d4b521e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -292,6 +292,15 @@ "contributions": [ "code" ] + }, + { + "login": "covid10", + "name": "covid10", + "avatar_url": "https://avatars.githubusercontent.com/u/71146231?v=4", + "profile": "https://github.com/covid10", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index fa7e292a..e97d3cca 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-31-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-32-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -246,6 +246,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From b6808125a25f94ec7be5dbd408f9ffdb4e8aa22c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:35 -0800 Subject: [PATCH 0455/1077] docs: update @covid10 as a contributor --- .all-contributorsrc | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 8d4b521e..7f85bb97 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -299,7 +299,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/71146231?v=4", "profile": "https://github.com/covid10", "contributions": [ - "translation" + "translation", + "code" ] } ], diff --git a/README.md b/README.md index e97d3cca..84e46dc2 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting - + From e30910ad73547ded46b46eb11badb219ced28490 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:37 -0800 Subject: [PATCH 0456/1077] docs: add @chishm as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 7f85bb97..a94ffa79 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -302,6 +302,15 @@ "translation", "code" ] + }, + { + "login": "chishm", + "name": "Michael Chisholm", + "avatar_url": "https://avatars.githubusercontent.com/u/18148723?v=4", + "profile": "https://github.com/chishm", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 84e46dc2..10114d54 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-32-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-33-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -247,6 +247,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From ed7fbe856daec02ef61f2ed4f8f0ba33d7d24909 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:38 -0800 Subject: [PATCH 0457/1077] docs: add @blueshiftlabs as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index a94ffa79..a88e3419 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -311,6 +311,15 @@ "contributions": [ "code" ] + }, + { + "login": "blueshiftlabs", + "name": "Justin Paupore", + "avatar_url": "https://avatars.githubusercontent.com/u/1445520?v=4", + "profile": "https://github.com/blueshiftlabs", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 10114d54..b088d302 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-33-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-34-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -248,6 +248,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From a341a9169a5e724e4b6df52f6ea3964f429e877e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:40 -0800 Subject: [PATCH 0458/1077] docs: add @bedaes as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index a88e3419..a3bf6389 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -320,6 +320,15 @@ "contributions": [ "code" ] + }, + { + "login": "bedaes", + "name": "bedaes", + "avatar_url": "https://avatars.githubusercontent.com/u/8410205?v=4", + "profile": "https://github.com/bedaes", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index b088d302..dea29665 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-34-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-35-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -249,6 +249,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From fd88bbe5638670dcc2940f651201514f55927d11 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:00:41 -0800 Subject: [PATCH 0459/1077] docs: add @awashingmachine as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index a3bf6389..d13ac3b9 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -329,6 +329,15 @@ "contributions": [ "code" ] + }, + { + "login": "awashingmachine", + "name": "awashingmachine", + "avatar_url": "https://avatars.githubusercontent.com/u/79043726?v=4", + "profile": "https://github.com/awashingmachine", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index dea29665..1c8f152f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) -[![All Contributors](https://img.shields.io/badge/all_contributors-35-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-36-orange.svg?style=flat-square)](#contributors-) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) @@ -251,6 +251,9 @@ These graphs were generated using the values calculated by the Adaptive Lighting + + + From 125bc431730d6654ca8a13344bcd4711e4a1fb04 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:20:40 -0800 Subject: [PATCH 0460/1077] move badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1c8f152f..c71f4438 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) +![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) [![All Contributors](https://img.shields.io/badge/all_contributors-36-orange.svg?style=flat-square)](#contributors-) -![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) # Adaptive Lighting component for Home Assistant From bd1bad3f5330b8de699e71744e53bced2db846e5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:21:09 -0800 Subject: [PATCH 0461/1077] docs: add @claytonjn as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index d13ac3b9..b4e28113 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -338,6 +338,15 @@ "contributions": [ "translation" ] + }, + { + "login": "claytonjn", + "name": "Clayton Nummer", + "avatar_url": "https://avatars.githubusercontent.com/u/3850252?v=4", + "profile": "https://github.com/claytonjn", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index c71f4438..0396abe4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-36-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-37-orange.svg?style=flat-square)](#contributors-) # Adaptive Lighting component for Home Assistant @@ -253,6 +253,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 1d489455f0537ddd1a8a38840b9be67eb1150735 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 10:38:31 -0800 Subject: [PATCH 0462/1077] Bump to 1.2.0 in manifest.json --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 5b852cb4..8ca55595 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -6,7 +6,7 @@ "config_flow": true, "dependencies": [], "codeowners": ["@basnijholt", "@RubenKelevra"], - "version": "1.1.0", + "version": "1.2.0", "requirements": [], "iot_class": "calculated" } From fee8654e9f38ae1d460ce0911e9390d2f95b01c2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 16:47:52 -0800 Subject: [PATCH 0463/1077] Fix typos in en.json and strings.json --- custom_components/adaptive_lighting/strings.json | 2 +- custom_components/adaptive_lighting/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 8d4cb44c..754cb9bc 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -39,7 +39,7 @@ "max_sunrise_time": "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- seconds)", "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", - "min_sunset_time": "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "min_sunset_time": "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", "transition": "Transition time when applying a change to the lights (seconds)", diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index e4920cdc..1318a0bb 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -40,7 +40,7 @@ "max_sunrise_time": "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- seconds)", "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", - "min_sunset_time": "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "min_sunset_time": "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", "transition": "Transition time when applying a change to the lights (seconds)", From 015b46a3fd52ba5144a5c99111c5a0a133f55f34 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Nov 2022 18:55:37 -0800 Subject: [PATCH 0464/1077] Fix time defaults in table --- README.md | 56 +++++++++++++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 0396abe4..3f5afae5 100644 --- a/README.md +++ b/README.md @@ -50,34 +50,34 @@ adaptive_lighting: ``` ### Options -| option | description | required | default | type | -|---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -------- | -------------- | ------- | -| name | The name to use when displaying this switch. | False | default | string | -| lights | List of light entities for Adaptive Lighting to control (may be empty). | False | list | [] | -| prefer_rgb_color | Whether to use RGB color adjustment instead of native light color temperature. | False | False | boolean | -| initial_transition | How long the first transition is when the lights go from `off` to `on`. | False | 1 | time | -| sleep_transition | How long the transition is when when "sleep mode" is toggled | False | 1 | time | -| transition | How long the transition is when the lights change, in seconds. | False | 45 | integer | -| interval | How often to adapt the lights, in seconds. | False | 90 | integer | -| min_brightness | The minimum percent of brightness to set the lights to. | False | 1 | integer | -| max_brightness | The maximum percent of brightness to set the lights to. | False | 100 | integer | -| min_color_temp | The warmest color temperature to set the lights to, in Kelvin. | False | 2000 | integer | -| max_color_temp | The coldest color temperature to set the lights to, in Kelvin. | False | 5500 | integer | -| sleep_brightness | Brightness of lights while the sleep mode is enabled. | False | 1 | integer | -| sleep_rgb_or_color_temp | Use either 'rgb_color' or 'color_temp' when in sleep mode. | False | 'color_temp' | string | -| sleep_rgb_color | List of three numbers between 0-255, indicating the RGB color in sleep mode (only used when sleep_rgb_or_color_temp is 'rgb_color'). | False | `[255, 56, 0]` | list | -| sleep_color_temp | Color temperature of lights while the sleep mode is enabled (only used when sleep_rgb_or_color_temp is 'color_temp'). | False | 1000 | integer | -| sunrise_time | Override the sunrise time with a fixed time. | False | time | | -| max_sunrise_time | Make the virtual sun always rise at at most a specific time while still allowing for even earlier times based on the real sun | False | time | | -| sunrise_offset | Change the sunrise time with a positive or negative offset. | False | 0 | time | -| sunset_time | Override the sunset time with a fixed time. | False | time | | -| min_sunset_time | Make the virtual sun always set at at least a specific time while still allowing for even later times based on the real sun | False | time | | -| sunset_offset | Change the sunset time with a positive or negative offset. | False | 0 | time | -| only_once | Whether to keep adapting the lights (false) or to only adapt the lights as soon as they are turned on (true). | False | False | boolean | -| take_over_control | If another source calls `light.turn_on` while the lights are on and being adapted, disable Adaptive Lighting. | False | True | boolean | -| detect_non_ha_changes | Whether to detect state changes and stop adapting lights, even not from `light.turn_on`. Needs `take_over_control` to be enabled. Note that by enabling this option, it calls 'homeassistant.update_entity' every 'interval'! | False | False | boolean | -| separate_turn_on_commands | Whether to use separate `light.turn_on` calls for color and brightness, needed for some types of lights | False | False | boolean | -| adapt_delay | Wait time in seconds between light turn on, and Adaptive Lights applying changes to the light state. May avoid flickering. | False | 0 | integer | +| option | description | required | default | type | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------- | ------- | +| `name` | The name to use when displaying this switch. | False | default | string | +| `lights` | List of light entities for Adaptive Lighting to control (may be empty). | False | list | [] | +| `prefer_rgb_color` | Whether to use RGB color adjustment instead of native light color temperature. | False | False | boolean | +| `initial_transition` | How long the first transition is when the lights go from `off` to `on`. | False | 1 | time | +| `sleep_transition` | How long the transition is when when "sleep mode" is toggled | False | 1 | time | +| `transition` | How long the transition is when the lights change, in seconds. | False | 45 | integer | +| `interval` | How often to adapt the lights, in seconds. | False | 90 | integer | +| `min_brightness` | The minimum percent of brightness to set the lights to. | False | 1 | integer | +| `max_brightness` | The maximum percent of brightness to set the lights to. | False | 100 | integer | +| `min_color_temp` | The warmest color temperature to set the lights to, in Kelvin. | False | 2000 | integer | +| `max_color_temp` | The coldest color temperature to set the lights to, in Kelvin. | False | 5500 | integer | +| `sleep_brightness` | Brightness of lights while the sleep mode is enabled. | False | 1 | integer | +| `sleep_rgb_or_color_temp` | Use either 'rgb_color' or 'color_temp' when in sleep mode. | False | 'color_temp' | string | +| `sleep_rgb_color` | List of three numbers between 0-255, indicating the RGB color in sleep mode (only used when sleep_rgb_or_color_temp is 'rgb_color'). | False | `[255, 56, 0]` | list | +| `sleep_color_temp` | Color temperature of lights while the sleep mode is enabled (only used when sleep_rgb_or_color_temp is 'color_temp'). | False | 1000 | integer | +| `sunrise_time` | Override the sunrise time with a fixed time. | False | None | time | +| `max_sunrise_time` | Make the virtual sun always rise at at most a specific time while still allowing for even earlier times based on the real sun | False | None | time | +| `sunrise_offset` | Change the sunrise time with a positive or negative offset. | False | 0 | time | +| `sunset_time` | Override the sunset time with a fixed time. | False | None | time | +| `min_sunset_time` | Make the virtual sun always set at at least a specific time while still allowing for even later times based on the real sun | False | None | time | +| `sunset_offset` | Change the sunset time with a positive or negative offset. | False | 0 | time | +| `only_once` | Whether to keep adapting the lights (false) or to only adapt the lights as soon as they are turned on (true). | False | False | boolean | +| `take_over_control` | If another source calls `light.turn_on` while the lights are on and being adapted, disable Adaptive Lighting. | False | True | boolean | +| `detect_non_ha_changes` | Whether to detect state changes and stop adapting lights, even not from `light.turn_on`. Needs `take_over_control` to be enabled. Note that by enabling this option, it calls 'homeassistant.update_entity' every 'interval'! | False | False | boolean | +| `separate_turn_on_commands` | Whether to use separate `light.turn_on` calls for color and brightness, needed for some types of lights | False | False | boolean | +| `adapt_delay` | Wait time in seconds between light turn on, and Adaptive Lights applying changes to the light state. May avoid flickering. | False | 0 | integer | Full example: From 1e87750ba0dbb99cf05dfcf13229a6c37728ee1a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 9 Nov 2022 19:09:14 -0800 Subject: [PATCH 0465/1077] docs: add @robert-crandall as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index b4e28113..34ffda95 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -347,6 +347,15 @@ "contributions": [ "code" ] + }, + { + "login": "robert-crandall", + "name": "Robert Crandall", + "avatar_url": "https://avatars.githubusercontent.com/u/86014438?v=4", + "profile": "https://github.com/robert-crandall", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 3f5afae5..9103ff71 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-37-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-38-orange.svg?style=flat-square)](#contributors-) # Adaptive Lighting component for Home Assistant @@ -254,6 +254,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 4740844f1dd62025701d6ac3427c7348e29a336b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 9 Nov 2022 19:09:24 -0800 Subject: [PATCH 0466/1077] docs: add @matt-forster as a contributor --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 34ffda95..0f9020f2 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -356,6 +356,15 @@ "contributions": [ "code" ] + }, + { + "login": "matt-forster", + "name": "Matt Forster", + "avatar_url": "https://avatars.githubusercontent.com/u/3375444?v=4", + "profile": "https://mattforster.ca/", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 9103ff71..c645ac01 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-38-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-39-orange.svg?style=flat-square)](#contributors-) # Adaptive Lighting component for Home Assistant @@ -255,6 +255,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From b873ea67b1c4cdefcd77ea3fb663140cd665b605 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 9 Nov 2022 19:10:14 -0800 Subject: [PATCH 0467/1077] Remove incorrectly added contributors --- .all-contributorsrc | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 0f9020f2..f8912e34 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -85,15 +85,6 @@ "code" ] }, - { - "login": "robert", - "name": "Rob Heaton", - "avatar_url": "https://avatars.githubusercontent.com/u/1565857?v=4", - "profile": "https://robertheaton.com/", - "contributions": [ - "code" - ] - }, { "login": "Repsionu", "name": "Jüri Rebane", @@ -148,15 +139,6 @@ "code" ] }, - { - "login": "matt", - "name": "Matthew Mohrman", - "avatar_url": "https://avatars.githubusercontent.com/u/2709?v=4", - "profile": "https://github.com/matt", - "contributions": [ - "code" - ] - }, { "login": "MangoScango", "name": "MangoScango", From 48fa920e377cb3a8d4caa549c9f0735a05b7bb44 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 9 Nov 2022 19:10:38 -0800 Subject: [PATCH 0468/1077] Regenerate README --- README.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c645ac01..94535180 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-39-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-37-orange.svg?style=flat-square)](#contributors-) # Adaptive Lighting component for Home Assistant @@ -217,43 +217,41 @@ These graphs were generated using the values calculated by the Adaptive Lighting - + - - - - + + - - + + - - + + From e9984ef4e1163220702081d8da5d51a2f2e0f9f6 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 13 Nov 2022 12:13:00 -0800 Subject: [PATCH 0469/1077] Change title, better description in HACS --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 94535180..561fd002 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![All Contributors](https://img.shields.io/badge/all_contributors-37-orange.svg?style=flat-square)](#contributors-) -# Adaptive Lighting component for Home Assistant +# Adaptive Lighting - automatically adapt the brightness and color of lights based on the sun position and take over manual control ![](https://github.com/home-assistant/brands/raw/b4a168b9af282ef916e120d31091ecd5e3c35e66/core_integrations/adaptive_lighting/icon.png) From 9ed1f596eb506fab82184976d546752ac791d311 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 13 Nov 2022 12:33:42 -0800 Subject: [PATCH 0470/1077] Change title for better view in HACS --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 561fd002..62f36851 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![All Contributors](https://img.shields.io/badge/all_contributors-37-orange.svg?style=flat-square)](#contributors-) -# Adaptive Lighting - automatically adapt the brightness and color of lights based on the sun position and take over manual control +# Automatically adapt the brightness and color of lights based on the sun position and take over manual control ![](https://github.com/home-assistant/brands/raw/b4a168b9af282ef916e120d31091ecd5e3c35e66/core_integrations/adaptive_lighting/icon.png) From 54ddb8c4ae8ea3714adf4c0ac5c55b654b64d37b Mon Sep 17 00:00:00 2001 From: Mark Niemeyer <64665067+Mark-Niemeyer@users.noreply.github.com> Date: Sun, 27 Nov 2022 17:18:48 +0100 Subject: [PATCH 0471/1077] pre-commit: switch flake8 repo from gitlab to github --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4394d4bb..5a742db3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,8 +7,8 @@ repos: - id: end-of-file-fixer - id: mixed-line-ending args: ["--fix=lf"] - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 + - repo: https://github.com/pycqa/flake8 + rev: 5.0.4 hooks: - id: flake8 - repo: https://github.com/ambv/black From 040d576948b97c57a5b71cf808813a18840f9dff Mon Sep 17 00:00:00 2001 From: Mark Niemeyer <64665067+Mark-Niemeyer@users.noreply.github.com> Date: Sun, 27 Nov 2022 17:30:28 +0100 Subject: [PATCH 0472/1077] update german translation --- .../adaptive_lighting/translations/de.json | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json index c1da2ccd..a68df8e4 100644 --- a/custom_components/adaptive_lighting/translations/de.json +++ b/custom_components/adaptive_lighting/translations/de.json @@ -22,6 +22,7 @@ "data": { "lights": "Lichter", "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt", + "sleep_transition": "sleep_transition: Wenn 'sleep_state' sich ändert. (Sekunden)", "interval": "interval, Zeit zwischen Updates des Switches", "max_brightness": "max_brightness, maximale Helligkeit in %", "max_color_temp": "max_color_temp, maximale Farbtemperatur in Kelvin", @@ -29,16 +30,21 @@ "min_color_temp": "min_color_temp, minimale Farbtemperatur in Kelvin", "only_once": "only_once, passe die Lichter nur beim Einschalten an", "prefer_rgb_color": "prefer_rgb_color, nutze 'rgb_color' vor 'color_temp', wenn möglich", - "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.", + "separate_turn_on_commands": "separate_turn_on_commands, für jedes Attribut (Farbe, Helligkeit usw.) in 'light.turn_on' werden separate Befehle gesendet. Wird für manche Leuchtmittel benötigt.", "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %", - "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperaturin Kelvin", - "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- seconds", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, nutze 'rgb_color' oder 'color_temp'", + "sleep_rgb_color": "sleep_rgb_color, in RGB", + "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperatur in Kelvin", + "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- Sekunden", "sunrise_time": "sunrise_time, Sonnenaufgangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenaufgangs an deiner Position verwendet)", - "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- seconds", + "max_sunrise_time": "max_sunrise_time: Manuelles Überschreiben der max. sunrise_time. Falls 'None', wird die tatsächliche sunrise_time an deiner Position verwendet (HH:MM:SS)", + "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- Sekunden", "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)", - "take_over_control": "take_over_control, wenn irgendetwas während ein Licht an ist außer Adaptive Lighting den Service 'light.turn_on' aufruft, stoppe die Anpassung des Lichtes (oder des Schalters) bis dieser wieder von off -> on geschaltet wird.", + "min_sunset_time": "min_sunset_time: Manuelles Überschreiben der min. sunset_time. Falls 'None', wird die tatsächliche sunset_time an deiner Position verwendet (HH:MM:SS)", + "take_over_control": "take_over_control, wenn irgendetwas während ein Licht an ist außer Adaptive Lighting den Service 'light.turn_on' aufruft, stoppe die Anpassung des Lichtes (oder des Schalters) bis dieser wieder von off -> on geschaltet wird.", "detect_non_ha_changes": "detect_non_ha_changes, entdeckt alle Änderungen über 10% am Licht (auch außerhalb von HA gemacht), 'take_over_control' muss aktiviert sein (ruft 'homeassistant.update_entity' jede 'interval' auf!)", - "transition": "transition, Wechselzeit in Sekunden" + "transition": "transition, Wechselzeit in Sekunden", + "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden." } } }, From 8275ccd36cf383c578df8162de7bd6777ec5bca7 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 27 Nov 2022 17:07:33 +0000 Subject: [PATCH 0473/1077] docs: update README.md --- README.md | 77 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 62f36851..1792507a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-37-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-38-orange.svg?style=flat-square)](#contributors-) # Automatically adapt the brightness and color of lights based on the sun position and take over manual control @@ -207,53 +207,54 @@ These graphs were generated using the values calculated by the Adaptive Lighting
Bas Nijholt
Bas Nijholt

💻 🚧
Bas Nijholt
Bas Nijholt

💻 🚧 🐛
Bas Nijholt
Bas Nijholt

💻 🚧 🐛
Sven Serlier
Sven Serlier

📖
Bas Nijholt
Bas Nijholt

💻 🚧 🐛
Sven Serlier
Sven Serlier

📖
Will Puckett
Will Puckett

📖
Bas Nijholt
Bas Nijholt

💻 🚧 🐛
Sven Serlier
Sven Serlier

📖
Will Puckett
Will Puckett

📖
vapescherov
vapescherov

💻
Sven Serlier
Sven Serlier

📖
Will Puckett
Will Puckett

📖
vapescherov
vapescherov

💻
Travis Pew
Travis Pew

📖
Will Puckett
Will Puckett

📖
vapescherov
vapescherov

💻
Travis Pew
Travis Pew

📖
Sindre Broch
Sindre Broch

📖
vapescherov
vapescherov

💻
Travis Pew
Travis Pew

📖
Sindre Broch
Sindre Broch

📖
Denis Shulyaka
Denis Shulyaka

💻
Sindre Broch
Sindre Broch

📖
Denis Shulyaka
Denis Shulyaka

💻
@RubenKelevra
@RubenKelevra

📖
Denis Shulyaka
Denis Shulyaka

💻
@RubenKelevra
@RubenKelevra

📖
@RubenKelevra
@RubenKelevra

📖 💻
@RubenKelevra
@RubenKelevra

📖 💻
Rob Heaton
Rob Heaton

💻
@RubenKelevra
@RubenKelevra

📖 💻
Rob Heaton
Rob Heaton

💻
Jüri Rebane
Jüri Rebane

🌍
@RubenKelevra
@RubenKelevra

📖 💻
Rob Heaton
Rob Heaton

💻
Jüri Rebane
Jüri Rebane

🌍
quantumlemur
quantumlemur

💻
Rob Heaton
Rob Heaton

💻
Jüri Rebane
Jüri Rebane

🌍
quantumlemur
quantumlemur

💻
Michael Kirsch
Michael Kirsch

💻
Jüri Rebane
Jüri Rebane

🌍
quantumlemur
quantumlemur

💻
Michael Kirsch
Michael Kirsch

💻
Nicholai Nissen
Nicholai Nissen

🌍
quantumlemur
quantumlemur

💻
Michael Kirsch
Michael Kirsch

💻
Nicholai Nissen
Nicholai Nissen

🌍
Martin Myhrman
Martin Myhrman

🌍
Nicholai Nissen
Nicholai Nissen

🌍
Martin Myhrman
Martin Myhrman

🌍
Michel Peterson
Michel Peterson

💻
Michel Peterson
Michel Peterson

💻
Matthew Mohrman
Matthew Mohrman

💻
Michel Peterson
Michel Peterson

💻
Matthew Mohrman
Matthew Mohrman

💻
MangoScango
MangoScango

💻
Michel Peterson
Michel Peterson

💻
Matthew Mohrman
Matthew Mohrman

💻
MangoScango
MangoScango

💻
Lynilia
Lynilia

🌍
Matthew Mohrman
Matthew Mohrman

💻
MangoScango
MangoScango

💻
Lynilia
Lynilia

🌍
LukaszP2
LukaszP2

🌍
MangoScango
MangoScango

💻
Lynilia
Lynilia

🌍
LukaszP2
LukaszP2

🌍
Joscha Wagner
Joscha Wagner

🌍
Lynilia
Lynilia

🌍
LukaszP2
LukaszP2

🌍
Joscha Wagner
Joscha Wagner

🌍
skdzzz
skdzzz

🌍
Joscha Wagner
Joscha Wagner

🌍
skdzzz
skdzzz

🌍
Simon Gurcke
Simon Gurcke

💻
Simon Gurcke
Simon Gurcke

💻
Sören Beye
Sören Beye

💻
Simon Gurcke
Simon Gurcke

💻
Sören Beye
Sören Beye

💻
Hudson Brendon
Hudson Brendon

🌍
Simon Gurcke
Simon Gurcke

💻
Sören Beye
Sören Beye

💻
Hudson Brendon
Hudson Brendon

🌍
Gabriel Visser
Gabriel Visser

📖
Sören Beye
Sören Beye

💻
Hudson Brendon
Hudson Brendon

🌍
Gabriel Visser
Gabriel Visser

📖
Gleb
Gleb

🌍
Hudson Brendon
Hudson Brendon

🌍
Gabriel Visser
Gabriel Visser

📖
Gleb
Gleb

🌍
Deleted user
Deleted user

🌍
Gabriel Visser
Gabriel Visser

📖
Gleb
Gleb

🌍
Deleted user
Deleted user

🌍
Avi Miller
Avi Miller

📖
Gabriel Visser
Gabriel Visser

📖
Gleb
Gleb

🌍
Deleted user
Deleted user

🌍
Avi Miller
Avi Miller

📖
Avi Miller
Avi Miller

📖 💻
Deleted user
Deleted user

🌍
Avi Miller
Avi Miller

📖 💻
Denys Dovhan
Denys Dovhan

🌍
Denys Dovhan
Denys Dovhan

🌍
David Stenbeck
David Stenbeck

📖
Denys Dovhan
Denys Dovhan

🌍
David Stenbeck
David Stenbeck

📖
Kevin Addeman
Kevin Addeman

💻
Denys Dovhan
Denys Dovhan

🌍
David Stenbeck
David Stenbeck

📖
Kevin Addeman
Kevin Addeman

💻
covid10
covid10

🌍
Denys Dovhan
Denys Dovhan

🌍
David Stenbeck
David Stenbeck

📖
Kevin Addeman
Kevin Addeman

💻
covid10
covid10

🌍
covid10
covid10

🌍 💻
David Stenbeck
David Stenbeck

📖
Kevin Addeman
Kevin Addeman

💻
covid10
covid10

🌍 💻
Michael Chisholm
Michael Chisholm

💻
Kevin Addeman
Kevin Addeman

💻
covid10
covid10

🌍 💻
Michael Chisholm
Michael Chisholm

💻
Justin Paupore
Justin Paupore

💻
covid10
covid10

🌍 💻
Michael Chisholm
Michael Chisholm

💻
Justin Paupore
Justin Paupore

💻
bedaes
bedaes

💻
Justin Paupore
Justin Paupore

💻
bedaes
bedaes

💻
awashingmachine
awashingmachine

🌍
awashingmachine
awashingmachine

🌍
Clayton Nummer
Clayton Nummer

💻
awashingmachine
awashingmachine

🌍
Clayton Nummer
Clayton Nummer

💻
Robert Crandall
Robert Crandall

💻
awashingmachine
awashingmachine

🌍
Clayton Nummer
Clayton Nummer

💻
Robert Crandall
Robert Crandall

💻
Matt Forster
Matt Forster

💻
@RubenKelevra
@RubenKelevra

📖 💻
Rob Heaton
Rob Heaton

💻
Jüri Rebane
Jüri Rebane

🌍
quantumlemur
quantumlemur

💻
Michael Kirsch
Michael Kirsch

💻
Nicholai Nissen
Nicholai Nissen

🌍
Martin Myhrman
Martin Myhrman

🌍
Michel Peterson
Michel Peterson

💻
Michel Peterson
Michel Peterson

💻
Matthew Mohrman
Matthew Mohrman

💻
MangoScango
MangoScango

💻
Lynilia
Lynilia

🌍
LukaszP2
LukaszP2

🌍
Joscha Wagner
Joscha Wagner

🌍
skdzzz
skdzzz

🌍
Simon Gurcke
Simon Gurcke

💻
Sören Beye
Sören Beye

💻
Hudson Brendon
Hudson Brendon

🌍
Gabriel Visser
Gabriel Visser

📖
Gleb
Gleb

🌍
Deleted user
Deleted user

🌍
Avi Miller
Avi Miller

📖 💻
Denys Dovhan
Denys Dovhan

🌍
David Stenbeck
David Stenbeck

📖
Kevin Addeman
Kevin Addeman

💻
covid10
covid10

🌍 💻
Michael Chisholm
Michael Chisholm

💻
Justin Paupore
Justin Paupore

💻
bedaes
bedaes

💻
awashingmachine
awashingmachine

🌍
Clayton Nummer
Clayton Nummer

💻
Robert Crandall
Robert Crandall

💻
Matt Forster
Matt Forster

💻
- - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - + + + From 3077bdf098eee6cdea3c512941672206d07bbdd3 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 27 Nov 2022 17:07:34 +0000 Subject: [PATCH 0474/1077] docs: update .all-contributorsrc --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index f8912e34..b699908c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -347,6 +347,15 @@ "contributions": [ "code" ] + }, + { + "login": "Mark-Niemeyer", + "name": "Mark Niemeyer", + "avatar_url": "https://avatars.githubusercontent.com/u/64665067?v=4", + "profile": "https://www.dfki.de/en/web/about-us/employee/person/maho10", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, From d7e1b5cf4dda48cf4db0885d8f1e26c52bc6e157 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 27 Nov 2022 17:09:00 +0000 Subject: [PATCH 0475/1077] docs: update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1792507a..65bf5e35 100644 --- a/README.md +++ b/README.md @@ -254,7 +254,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting - + From 75fde28acad0ea750aee1f2ecaba053d98f3449b Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 27 Nov 2022 17:09:01 +0000 Subject: [PATCH 0476/1077] docs: update .all-contributorsrc --- .all-contributorsrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index b699908c..90550145 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -354,7 +354,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/64665067?v=4", "profile": "https://www.dfki.de/en/web/about-us/employee/person/maho10", "contributions": [ - "translation" + "translation", + "code" ] } ], From ff6494240d0b3c05b08af66346f880cadca27b54 Mon Sep 17 00:00:00 2001 From: Mark Niemeyer <64665067+Mark-Niemeyer@users.noreply.github.com> Date: Sun, 27 Nov 2022 16:47:50 +0100 Subject: [PATCH 0477/1077] add a delay between sending of commands when using separate_turn_on_commands --- custom_components/adaptive_lighting/const.py | 2 ++ custom_components/adaptive_lighting/strings.json | 1 + custom_components/adaptive_lighting/switch.py | 4 +++- custom_components/adaptive_lighting/translations/de.json | 1 + custom_components/adaptive_lighting/translations/en.json | 1 + 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 36c84a2d..62c71fa2 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -62,6 +62,7 @@ CONF_TURN_ON_LIGHTS = "turn_on_lights" CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0 TURNING_OFF_DELAY = 5 +CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY = "send_split_delay", 0 def int_between(min_int, max_int): @@ -108,6 +109,7 @@ VALIDATION_TUPLES = [ (CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool), (CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool), (CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool), + (CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY, int_between(0, 10000)), (CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, int_between(0, 10000)), ] diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 754cb9bc..74099754 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -30,6 +30,7 @@ "only_once": "only_once: Only adapt the lights when turning them on.", "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.", "separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).", + "send_split_delay": "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly.", "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)", "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'", "sleep_rgb_color": "sleep_rgb_color, in RGB", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index ff100a44..500154be 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -112,6 +112,7 @@ from .const import ( CONF_MIN_SUNSET_TIME, CONF_ONLY_ONCE, CONF_PREFER_RGB_COLOR, + CONF_SEND_SPLIT_DELAY, CONF_SEPARATE_TURN_ON_COMMANDS, CONF_SLEEP_BRIGHTNESS, CONF_SLEEP_COLOR_TEMP, @@ -574,6 +575,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._take_over_control = data[CONF_TAKE_OVER_CONTROL] self._transition = data[CONF_TRANSITION] self._adapt_delay = data[CONF_ADAPT_DELAY] + self._send_split_delay = data[CONF_SEND_SPLIT_DELAY] _loc = get_astral_location(self.hass) if isinstance(_loc, tuple): # Astral v2.2 @@ -872,7 +874,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if len(service_datas) == 2: transition = service_datas[0].get(ATTR_TRANSITION) if transition is not None: - await asyncio.sleep(transition) + await asyncio.sleep(transition + self._send_split_delay / 1000.0) await turn_on(service_datas[1]) async def _update_attrs_and_maybe_adapt_lights( diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json index a68df8e4..cc427138 100644 --- a/custom_components/adaptive_lighting/translations/de.json +++ b/custom_components/adaptive_lighting/translations/de.json @@ -31,6 +31,7 @@ "only_once": "only_once, passe die Lichter nur beim Einschalten an", "prefer_rgb_color": "prefer_rgb_color, nutze 'rgb_color' vor 'color_temp', wenn möglich", "separate_turn_on_commands": "separate_turn_on_commands, für jedes Attribut (Farbe, Helligkeit usw.) in 'light.turn_on' werden separate Befehle gesendet. Wird für manche Leuchtmittel benötigt.", + "send_split_delay": "send_split_delay: Wartezeit zwischen dem Senden der Befehle (Millisekunden), wenn separate_turn_on_commands genutzt wird. Kann helfen, wenn die Leuchtmittel die separaten Befehle nicht korrekt umsetzen.", "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %", "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, nutze 'rgb_color' oder 'color_temp'", "sleep_rgb_color": "sleep_rgb_color, in RGB", diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 1318a0bb..be556ec0 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -31,6 +31,7 @@ "only_once": "only_once: Only adapt the lights when turning them on.", "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.", "separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).", + "send_split_delay": "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly.", "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)", "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'", "sleep_rgb_color": "sleep_rgb_color, in RGB", From bf82beab8287f682c324d9c39af6d0a32f23c702 Mon Sep 17 00:00:00 2001 From: Mark Niemeyer <64665067+Mark-Niemeyer@users.noreply.github.com> Date: Sun, 27 Nov 2022 19:26:47 +0100 Subject: [PATCH 0478/1077] add send_split_delay to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 65bf5e35..6eef09af 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ adaptive_lighting: | `take_over_control` | If another source calls `light.turn_on` while the lights are on and being adapted, disable Adaptive Lighting. | False | True | boolean | | `detect_non_ha_changes` | Whether to detect state changes and stop adapting lights, even not from `light.turn_on`. Needs `take_over_control` to be enabled. Note that by enabling this option, it calls 'homeassistant.update_entity' every 'interval'! | False | False | boolean | | `separate_turn_on_commands` | Whether to use separate `light.turn_on` calls for color and brightness, needed for some types of lights | False | False | boolean | +| `send_split_delay` | Wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly. | False | 0 | integer | | `adapt_delay` | Wait time in seconds between light turn on, and Adaptive Lights applying changes to the light state. May avoid flickering. | False | 0 | integer | Full example: From 97bb2cf3debcba39cbfab76e27357dba531681da Mon Sep 17 00:00:00 2001 From: Mark Niemeyer <64665067+Mark-Niemeyer@users.noreply.github.com> Date: Sun, 27 Nov 2022 19:29:10 +0100 Subject: [PATCH 0479/1077] use send_split_delay also when no transition is used --- custom_components/adaptive_lighting/switch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 500154be..e2dee95f 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -874,7 +874,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if len(service_datas) == 2: transition = service_datas[0].get(ATTR_TRANSITION) if transition is not None: - await asyncio.sleep(transition + self._send_split_delay / 1000.0) + await asyncio.sleep(transition) + await asyncio.sleep(self._send_split_delay / 1000.0) await turn_on(service_datas[1]) async def _update_attrs_and_maybe_adapt_lights( From 5c4ce3cae6d3cb510f78a66c024cb8d56ba50d60 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 7 Dec 2022 12:46:28 -0800 Subject: [PATCH 0480/1077] bump to version 1.3.0 --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 8ca55595..150915f3 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -6,7 +6,7 @@ "config_flow": true, "dependencies": [], "codeowners": ["@basnijholt", "@RubenKelevra"], - "version": "1.2.0", + "version": "1.3.0", "requirements": [], "iot_class": "calculated" } From b910b565d430ad29bbdfeef630e18594f10ce8e1 Mon Sep 17 00:00:00 2001 From: Elliott Plack Date: Thu, 8 Dec 2022 09:22:14 -0500 Subject: [PATCH 0481/1077] Updated optional status for lights in service While implementing `adaptive_lighting.**apply**` in automations, I've noticed that listing the lights isn't required. For instance, if I have an adaptive lighting entity called `adaptive_lighting.office`, I can condition a motion sensor trigger with the following action successfully: service: adaptive_lighting.apply data: entity_id: switch.adaptive_lighting_office turn_on_lights: true --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6eef09af..38718679 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ adaptive_lighting: | Service data attribute | Optional | Description | | ---------------------- | -------- | -------------------------------------------------------------------------------------------- | | `entity_id` | no | The `entity_id` of the switch with the settings to apply. | -| `lights` | no | A light (or list of lights) to apply the settings to. | +| `lights` | yes | A light (or list of lights) to apply the settings to. | | `transition` | yes | The number of seconds for the transition. | | `adapt_brightness` | yes | Whether to change the brightness of the light or not. | | `adapt_color` | yes | Whether to adapt the color on supporting lights. | From 185b4041bf324f69898d54bb3457e9e07879b1c4 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 8 Dec 2022 10:48:44 -0800 Subject: [PATCH 0482/1077] docs: add talllguy as a contributor for doc (#397) * docs: update README.md * docs: update .all-contributorsrc Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 90550145..da79451b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -357,6 +357,15 @@ "translation", "code" ] + }, + { + "login": "talllguy", + "name": "Elliott Plack", + "avatar_url": "https://avatars.githubusercontent.com/u/1827881?v=4", + "profile": "https://www.linkedin.com/in/elliottplack/", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 38718679..d5bd8675 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-38-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-39-orange.svg?style=flat-square)](#contributors-) # Automatically adapt the brightness and color of lights based on the sun position and take over manual control @@ -256,6 +256,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 8ed8531610f7a2226138ed1f39a37ae795885122 Mon Sep 17 00:00:00 2001 From: ngommers <82467671+ngommers@users.noreply.github.com> Date: Sun, 11 Dec 2022 18:35:27 +0100 Subject: [PATCH 0483/1077] Create nl.json (#399) add dutch language --- .../adaptive_lighting/translations/nl.json | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/nl.json diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json new file mode 100644 index 00000000..a0b6b082 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/nl.json @@ -0,0 +1,57 @@ +{ + "title": "Adaptieve verlichting", + "config": { + "step": { + "user": { + "title": "Kies een naam voor de adaptieve verlichting integratie", + "description": "Kies een naam voor deze integratie. U kunt verschillende integratie van Adaptieve verlichting uitvoeren, elk van deze kan meerdere lichten bevatten!", + "data": { + "name": "Naam" + } + } + }, + "abort": { + "already_configured": "Dit apparaat is al geconfigureerd" + } + }, + "options": { + "step": { + "init": { + "title": "Adaptieve verlichting instellingen", + "description": "Alle instellingen voor een Adaptieve verlichting component. De optienamen komen overeen met de YAML-instellingen. Er worden geen opties weergegeven als u het item adaptive_lighting hebt gedefinieerd in uw YAML-configuratie.", + "data": { + "lights": "Lichten", + "initial_transition": "initial_transition: Wanneer lichten van 'uit' naar 'aan' gaan. (seconden)", + "sleep_transition": "sleep_transition: Wanneer 'sleep_state' verandert. (seconden)", + "interval": "interval: Tijd tussen switch-updates. (seconden)", + "max_brightness": "max_brightness: Hoogste helderheid van lichten tijdens een cyclus. (%)", + "max_color_temp": "max_color_temp: Koudste tint van de kleurtemperatuurcyclus. (kelvin)", + "min_brightness": "min_brightness: Laagste helderheid van lichten tijdens een cyclus. (%)", + "min_color_temp": "min_color_temp, Warmste tint van de kleurtemperatuurcyclus. (kelvin)", + "only_once": "only_once: pas de verlichting alleen aan wanneer u ze aanzet.", + "prefer_rgb_color": "prefer_rgb_color: Gebruik waar mogelijk 'rgb_color' in plaats van 'color_temp'.", + "separate_turn_on_commands": "separate_turn_on_commands: Scheid de commando's voor elk attribuut (kleur, helderheid, enz.) in 'light.turn_on' (vereist voor sommige lampen).", + "send_split_delay": "send_split_delay: wacht tussen commando's (milliseconden), wanneer separate_turn_on_commands wordt gebruikt. Kan ervoor zorgen dat beide commando's correct door de lamp worden afgehandeld.", + "sleep_brightness": "sleep_brightness, helderheidsinstelling voor slaapstand. (%)", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, gebruik 'rgb_color' of 'color_temp'", + "sleep_rgb_color": "sleep_rgb_color, in RGB", + "sleep_color_temp": "sleep_color_temp: Kleurtemperatuurinstelling voor slaapstand. (kelvin)", + "sunrise_offset": "sunrise_offset: Hoe lang voor(-) of na(+) zonsopgang uitvoeren (+/- seconden)", + "sunrise_time": "sunrise_time: Handmatige wijziging van de zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", + "max_sunrise_time": "max_sunrise_time: handmatige aanpassing van de maximale zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", + "sunset_offset": "sunset_offset: Hoe lang voor(-) of na(+) zonsondergang uitvoeren (+/- seconden)", + "sunset_time": "sunset_time: handmatige onderdrukking van de zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", + "min_sunset_time": "min_sunset_time: handmatige onderdrukking van de minimale zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsondergangstijd op uw locatie gebruikt (UU:MM:SS)", + "take_over_control": "take_over_control: Als iets anders dan Adaptive Lighting 'light.turn_on' roept wanneer een lamp al aan is, stop dan met het aanpassen van het licht totdat het (of de schakelaar) uit -> aan gaat.", + "detect_non_ha_changes": "detect_non_ha_changes: detecteert alle >10% wijzigingen aan de lichten (ook buiten HA), vereist dat 'take_over_control' is ingeschakeld (heet 'homeassistant.update_entity' elke 'interval'!)", + "transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)", + "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen." + } + } + }, + "error": { + "option_error": "Ongeldige optie", + "entity_missing": "Een of meer geselecteerde lichtentiteiten ontbreken in Home Assistant" + } + } +} From 4ef577ff7cf2327b123c7c69405fb3d9a065edbc Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 11 Dec 2022 09:35:55 -0800 Subject: [PATCH 0484/1077] docs: add ngommers as a contributor for translation (#401) * docs: update README.md * docs: update .all-contributorsrc Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index da79451b..47c07f44 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -366,6 +366,15 @@ "contributions": [ "doc" ] + }, + { + "login": "ngommers", + "name": "ngommers", + "avatar_url": "https://avatars.githubusercontent.com/u/82467671?v=4", + "profile": "https://github.com/ngommers", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index d5bd8675..1f2da91d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-39-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-40-orange.svg?style=flat-square)](#contributors-) # Automatically adapt the brightness and color of lights based on the sun position and take over manual control @@ -257,6 +257,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 1ef55ca180e0b2b6cd9310088e8ede661771fb99 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 11 Dec 2022 16:54:57 -0800 Subject: [PATCH 0485/1077] Use Kelvin instead of Mired, default since core=2022.11 (#375) * Use Kelvin instead of Mired, default since core=2022.11 * Fix attributes * Use ATTR_COLOR_TEMP_KELVIN in tests * use kelvin in tests * no duplicate * Round to nearest 5 --- custom_components/adaptive_lighting/switch.py | 40 ++++++++--------- tests/test_switch.py | 45 ++++++++++++++----- 2 files changed, 53 insertions(+), 32 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index e2dee95f..acd483a2 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -21,7 +21,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS_STEP_PCT, ATTR_COLOR_NAME, - ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, ATTR_KELVIN, ATTR_RGB_COLOR, @@ -85,7 +85,6 @@ from homeassistant.helpers.template import area_entities from homeassistant.util import slugify from homeassistant.util.color import ( color_RGB_to_xy, - color_temperature_kelvin_to_mired, color_temperature_to_rgb, color_xy_to_hs, ) @@ -155,12 +154,12 @@ SCAN_INTERVAL = timedelta(seconds=10) # Consider it a significant change when attribute changes more than BRIGHTNESS_CHANGE = 25 # ≈10% of total range -COLOR_TEMP_CHANGE = 20 # ≈5% of total range +COLOR_TEMP_CHANGE = 100 # ≈3% of total range (2000-6500) RGB_REDMEAN_CHANGE = 80 # ≈10% of total range COLOR_ATTRS = { # Should ATTR_PROFILE be in here? ATTR_COLOR_NAME, - ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, ATTR_KELVIN, ATTR_RGB_COLOR, @@ -228,7 +227,7 @@ def _split_service_data(service_data, adapt_brightness, adapt_color): if adapt_brightness: service_data_brightness = service_data.copy() service_data_brightness.pop(ATTR_RGB_COLOR, None) - service_data_brightness.pop(ATTR_COLOR_TEMP, None) + service_data_brightness.pop(ATTR_COLOR_TEMP_KELVIN, None) service_datas.append(service_data_brightness) if not service_datas: # neither adapt_brightness nor adapt_color @@ -491,11 +490,11 @@ def _attributes_have_changed( if ( adapt_color - and ATTR_COLOR_TEMP in old_attributes - and ATTR_COLOR_TEMP in new_attributes + and ATTR_COLOR_TEMP_KELVIN in old_attributes + and ATTR_COLOR_TEMP_KELVIN in new_attributes ): - last_color_temp = old_attributes[ATTR_COLOR_TEMP] - current_color_temp = new_attributes[ATTR_COLOR_TEMP] + last_color_temp = old_attributes[ATTR_COLOR_TEMP_KELVIN] + current_color_temp = new_attributes[ATTR_COLOR_TEMP_KELVIN] if abs(current_color_temp - last_color_temp) > COLOR_TEMP_CHANGE: _LOGGER.debug( "Color temperature of '%s' significantly changed from %s to %s with" @@ -530,7 +529,8 @@ def _attributes_have_changed( ATTR_RGB_COLOR in old_attributes and ATTR_RGB_COLOR not in new_attributes ) switched_to_rgb_color = ( - ATTR_COLOR_TEMP in old_attributes and ATTR_COLOR_TEMP not in new_attributes + ATTR_COLOR_TEMP_KELVIN in old_attributes + and ATTR_COLOR_TEMP_KELVIN not in new_attributes ) if switched_color_temp or switched_to_rgb_color: # Light switched from RGB mode to color_temp or visa versa @@ -824,10 +824,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ): _LOGGER.debug("%s: Setting color_temp of light %s", self._name, light) attributes = self.hass.states.get(light).attributes - min_mireds, max_mireds = attributes["min_mireds"], attributes["max_mireds"] - color_temp_mired = self._settings["color_temp_mired"] - color_temp_mired = max(min(color_temp_mired, max_mireds), min_mireds) - service_data[ATTR_COLOR_TEMP] = color_temp_mired + min_kelvin = attributes["min_color_temp_kelvin"] + max_kelvin = attributes["max_color_temp_kelvin"] + color_temp_kelvin = self._settings["color_temp_kelvin"] + color_temp_kelvin = max(min(color_temp_kelvin, max_kelvin), min_kelvin) + service_data[ATTR_COLOR_TEMP_KELVIN] = color_temp_kelvin elif "color" in features and adapt_color: _LOGGER.debug("%s: Setting rgb_color of light %s", self._name, light) service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"] @@ -1229,16 +1230,17 @@ class SunLightSettings: percent = 1 + percent return (delta_brightness * percent) + self.min_brightness - def calc_color_temp_kelvin(self, percent: float) -> float: + def calc_color_temp_kelvin(self, percent: float) -> int: """Calculate the color temperature in Kelvin.""" if percent > 0: delta = self.max_color_temp - self.min_color_temp - return (delta * percent) + self.min_color_temp + ct = (delta * percent) + self.min_color_temp + return 5 * round(ct / 5) # round to nearest 5 return self.min_color_temp def get_settings( self, is_sleep, transition - ) -> dict[str, float | tuple[float, float] | tuple[float, float, float]]: + ) -> dict[str, float | int | tuple[float, float] | tuple[float, float, float]]: """Get all light settings. Calculating all values takes <0.5ms. @@ -1257,13 +1259,11 @@ class SunLightSettings: rgb_color: tuple[float, float, float] = color_temperature_to_rgb( color_temp_kelvin ) - color_temp_mired: float = color_temperature_kelvin_to_mired(color_temp_kelvin) xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color) hs_color: tuple[float, float] = color_xy_to_hs(*xy_color) return { "brightness_pct": brightness_pct, "color_temp_kelvin": color_temp_kelvin, - "color_temp_mired": color_temp_mired, "rgb_color": rgb_color, "xy_color": xy_color, "hs_color": hs_color, @@ -1397,7 +1397,7 @@ class TurnOnOffListener: # settings the light will be later *or* the second event might indicate a # final state. The latter case happens for example when a light was # called with a color_temp outside of its range (and HA reports the - # incorrect 'min_mireds' and 'max_mireds', which happens e.g., for + # incorrect 'min_kelvin' and 'max_kelvin', which happens e.g., for # Philips Hue White GU10 Bluetooth lights). old_state: list[State] | None = self.last_state_change.get(entity_id) if ( diff --git a/tests/test_switch.py b/tests/test_switch.py index 312cf7c6..59c9245d 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -41,7 +41,7 @@ from homeassistant.components.demo.light import DemoLight from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, - ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN @@ -62,6 +62,7 @@ from homeassistant.const import ( from homeassistant.core import Context, State from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component +from homeassistant.util.color import color_temperature_mired_to_kelvin import homeassistant.util.dt as dt_util import pytest @@ -338,7 +339,10 @@ async def test_light_settings(hass): state.entity_id ] assert state.attributes[ATTR_BRIGHTNESS] == last_service_data[ATTR_BRIGHTNESS] - assert state.attributes[ATTR_COLOR_TEMP] == last_service_data[ATTR_COLOR_TEMP] + assert ( + state.attributes[ATTR_COLOR_TEMP_KELVIN] + == last_service_data[ATTR_COLOR_TEMP_KELVIN] + ) # Turn off "sleep mode" await hass.services.async_call( @@ -371,7 +375,10 @@ async def test_light_settings(hass): last_service_data = switch.turn_on_off_listener.last_service_data[ state.entity_id ] - assert state.attributes[ATTR_COLOR_TEMP] == last_service_data[ATTR_COLOR_TEMP] + assert ( + state.attributes[ATTR_COLOR_TEMP_KELVIN] + == last_service_data[ATTR_COLOR_TEMP_KELVIN] + ) # At sunset the brightness should be max and color_temp at the smallest value light_states = await patch_time_and_get_updated_states(sunset) @@ -481,7 +488,9 @@ async def test_manual_control(hass): return (light._brightness + 100) % 255 def increased_color_temp(): - return max((light._ct + 100) % light.max_mireds, light.min_mireds) + return max( + (light._ct + 100) % light.max_color_temp_kelvin, light.min_color_temp_kelvin + ) # Nothing is manually controlled await update() @@ -520,7 +529,13 @@ async def test_manual_control(hass): await switch.adapt_brightness_switch.async_turn_off() await turn_light(True, brightness=increased_brightness()) assert not manual_control[ENTITY_LIGHT] - await turn_light(True, color_temp=(light._ct + 100) % 500) + mired_range = (light.min_color_temp_kelvin, light.max_color_temp_kelvin) + kelvin_range = ( + color_temperature_mired_to_kelvin(mired_range[1]), + color_temperature_mired_to_kelvin(mired_range[0]), + ) + ptp_kelvin = kelvin_range[1] - kelvin_range[0] + await turn_light(True, color_temp_kelvin=(light._ct + 100) % ptp_kelvin) assert manual_control[ENTITY_LIGHT] await switch.adapt_brightness_switch.async_turn_on() # turn on again @@ -573,7 +588,9 @@ async def test_apply_service(hass): return (light._brightness + 100) % 255 def increased_color_temp(): - return max((light._ct + 100) % light.max_mireds, light.min_mireds) + return max( + (light._ct + 100) % light.max_color_temp_kelvin, light.min_color_temp_kelvin + ) async def change_light(): await hass.services.async_call( @@ -582,7 +599,7 @@ async def test_apply_service(hass): { ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: increased_brightness(), - ATTR_COLOR_TEMP: increased_color_temp(), + ATTR_COLOR_TEMP_KELVIN: increased_color_temp(), }, blocking=True, ) @@ -613,7 +630,7 @@ async def test_apply_service(hass): await apply(adapt_color=True, adapt_brightness=False) new_state = hass.states.get(entity_id).attributes assert old_state[ATTR_BRIGHTNESS] == new_state[ATTR_BRIGHTNESS] - assert old_state[ATTR_COLOR_TEMP] != new_state[ATTR_COLOR_TEMP] + assert old_state[ATTR_COLOR_TEMP_KELVIN] != new_state[ATTR_COLOR_TEMP_KELVIN] # Test only changing brightness await change_light() @@ -621,7 +638,7 @@ async def test_apply_service(hass): await apply(adapt_color=False, adapt_brightness=True) new_state = hass.states.get(entity_id).attributes assert old_state[ATTR_BRIGHTNESS] != new_state[ATTR_BRIGHTNESS] - assert old_state[ATTR_COLOR_TEMP] == new_state[ATTR_COLOR_TEMP] + assert old_state[ATTR_COLOR_TEMP_KELVIN] == new_state[ATTR_COLOR_TEMP_KELVIN] async def test_switch_off_on_off(hass): @@ -733,11 +750,15 @@ def test_is_our_context(): def test_attributes_have_changed(): """Test _attributes_have_changed function.""" - attributes_1 = {ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (0, 0, 0), ATTR_COLOR_TEMP: 100} + attributes_1 = { + ATTR_BRIGHTNESS: 1, + ATTR_RGB_COLOR: (0, 0, 0), + ATTR_COLOR_TEMP_KELVIN: 100, + } attributes_2 = { ATTR_BRIGHTNESS: 100, ATTR_RGB_COLOR: (255, 0, 0), - ATTR_COLOR_TEMP: 300, + ATTR_COLOR_TEMP_KELVIN: 300, } kwargs = dict( light="light.test", @@ -756,7 +777,7 @@ def test_attributes_have_changed(): ) # Switch from rgb_color to color_temp assert _attributes_have_changed( - old_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP: 100}, + old_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 100}, new_attributes={ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (0, 0, 0)}, **kwargs, ) From dc8fa60e75732fd4450e843552ffcc1d6dfa9175 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 11 Dec 2022 16:56:14 -0800 Subject: [PATCH 0486/1077] Bump to 1.4.0 --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 150915f3..93345a88 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -6,7 +6,7 @@ "config_flow": true, "dependencies": [], "codeowners": ["@basnijholt", "@RubenKelevra"], - "version": "1.3.0", + "version": "1.4.0", "requirements": [], "iot_class": "calculated" } From ce4503c9c6c8323802c7c6c350496e8fa883e042 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 11 Dec 2022 17:23:08 -0800 Subject: [PATCH 0487/1077] Also trigger manual control when using "color_temp" in mired format (#402) --- custom_components/adaptive_lighting/manifest.json | 2 +- custom_components/adaptive_lighting/switch.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 93345a88..d6580942 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -6,7 +6,7 @@ "config_flow": true, "dependencies": [], "codeowners": ["@basnijholt", "@RubenKelevra"], - "version": "1.4.0", + "version": "1.4.1", "requirements": [], "iot_class": "calculated" } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index acd483a2..ac1acc96 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -21,6 +21,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS_STEP_PCT, ATTR_COLOR_NAME, + ATTR_COLOR_TEMP, ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, ATTR_KELVIN, @@ -159,6 +160,7 @@ RGB_REDMEAN_CHANGE = 80 # ≈10% of total range COLOR_ATTRS = { # Should ATTR_PROFILE be in here? ATTR_COLOR_NAME, + ATTR_COLOR_TEMP, ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, ATTR_KELVIN, From 7ee7dfe570a2f80f7eeb6dbbbce9c458778e6bd7 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 12 Jan 2023 12:42:33 -0800 Subject: [PATCH 0488/1077] Rename ci.yaml to pytest.yaml --- .github/workflows/{ci.yaml => pytest.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{ci.yaml => pytest.yaml} (100%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/pytest.yaml similarity index 100% rename from .github/workflows/ci.yaml rename to .github/workflows/pytest.yaml From 6482fc54c27cddc6357b30871a77cc6e9a16a750 Mon Sep 17 00:00:00 2001 From: Andrew Berry Date: Sun, 12 Feb 2023 17:28:37 -0500 Subject: [PATCH 0489/1077] Add troubleshooting steps for lights (#427) * Fix skipping heading level * Add troubleshooting for when lights misbehave * Rephrase Zigbee groups troubleshooting section Co-authored-by: Andrew Berry --------- Co-authored-by: Bas Nijholt --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1f2da91d..b1d44f8f 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,7 @@ See the documentation of the PR at https://deploy-preview-14877--home-assistant- This integration was originally based of the great work of @claytonjn https://github.com/claytonjn/hass-circadian_lighting, but has been 100% rewritten and extended with new features. # Having problems? + Please enable debug logging by putting this in `configuration.yaml`: ```yaml logger: @@ -187,17 +188,59 @@ logger: ``` and after the problem occurs please create an issue with the log (`/config/home-assistant.log`). +## Lights are not responding or turning on by themselves -### Graphs! +This addon sends many more commands to lights compared to what humans would typically send. If the network used to send light commands is not healthy: + +- Manual commands like turning lights on or off may feel laggy. +- Lights may not respond to commands at all. +- Home Assistant may think a light is on, when it's actually off. Adaptive Lights will send it's regular adjustments causing the light to turn on after it's turned off. + +What's important is that many bugs that seem to be caused by this integration are really due to other unrelated issues. Fixing those will make your Home Assistant experience much better. Consider this integration a great stress test of your Home Assistant setup! + +### Wifi networks + +Make sure bulbs have a solid connection to your Wifi network. In general, if the signal is less than -70dBm, the connection is weak and may drop messages. + +### Zigbee, Z-Wave, and other mesh networks + +These types of mesh networks usually need powered devices that act as routers (that repeat messages) back to the central coordinator (the radio connected to Home Assistant). Most Philips lights are routers, but Ikea, Sengled, and generic Tuya bulbs often are not. If devices become unavailable or miss responding to commands, Adaptive Lighting will only make things worse. Use reporting tools such as network maps (available in ZHA, zigbee2mqtt, deCONZ, and ZWaveJS UI) to check your network. Smart plugs are often a cost-effective way to add additional routers to your network. + +For most Zigbee networks, groups are **absolutely required for good performance**. For example, imagine you want to use Adaptive Lighting in a hallway with 6 bulbs. If you add each individual bulb in the Adaptive Lighting configuration, then six individual commands will be sent to adjust them, which can eventually overwhelm a network. Instead, create a group in your Zigbee software (but _not_ a regular Home Assistant group), and add the one group to the Adaptive Lighting configuration. This will send only a single broadcast command to adjust the bulbs, giving much better response times and keeping the bulbs adjusting in sync with each other. + +A good rule to follow is that if you always control lights together (like bulbs in a ceiling fixture), then they should be in a Zigbee group. Then, only expose the group (and not individual bulbs) in Home Assistant Dashboards and external systems like Google Home or Apple HomeKit. + +### Light colors are not matching + +Bulbs made by different manufacturers or of different models may have different specifications for the color temperatures they support. For example you have two Adaptive Lighting configurations: + +- The first configuration has only Philips Hue White Ambiance bulbs. +- The second has the a few of the same model of White Ambiance bulbs as well as a few Sengled bulbs. + +Even with identical settings, the Philips Hue bulbs may appear to have different color temperatures set at the same time. + +To avoid this: + +1. Only put bulbs of the same make and model in a single Adaptive Lighting configuration. +2. Move where bulbs are installed so you can't see different light temperatures at the same time. + +### Bulb-specific issues + +Some bulbs have buggy behaviour with long light transition commands. + +- [Sengled Z01-A19NAE26](https://www.zigbee2mqtt.io/devices/Z01-A19NAE26.html#sengled-z01-a19nae26): If Adaptive lighting sends a long transition time (like the default 45 seconds), and the bulb is turned off in that time, it will turn itself back on after 10 seconds or so to continue the transition command. Since the bulb is turning itself on, there will be no obvious trigger in Home Assistant or other logs showing what caused the light to turn on. Fix this by setting a much shorter transition time such as 1 second. +- As well, the same bulbs peform poorly when in typical enclosed "dome" style ceiling lights. When hot, their performance becomes marginal at best. While most LEDs (even non-smart ones) say in the small print that they do not support working in enclosed fixtures, in practice more expensive bulbs like Philips Hue perform better. Fix this by moving suspect bulbs to open-air fixtures. + +## Graphs! These graphs were generated using the values calculated by the Adaptive Lighting sensor/switch(es). -##### Sun Position: +#### Sun Position: ![cl_percent|690x131](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/6/5/657ff98beb65a94598edeb4bdfd939095db1a22c.PNG) -##### Color Temperature: +#### Color Temperature: ![cl_color_temp|690x129](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/9/59e84263cbecd8e428cb08777a0413672c48dfcd.PNG) -##### Brightness: +#### Brightness: ![cl_brightness|690x130](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/8/58ebd994b62a8b1abfb3497a5288d923ff4e2330.PNG) ## Contributors From 53455f15a7d5e743bce4e1d4f5316e51dfc0e709 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 12 Feb 2023 14:28:56 -0800 Subject: [PATCH 0490/1077] docs: add deviantintegral as a contributor for doc (#428) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .all-contributorsrc | 9 +++++ README.md | 83 +++++++++++++++++++++++---------------------- 2 files changed, 51 insertions(+), 41 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 47c07f44..c8aa5b80 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -375,6 +375,15 @@ "contributions": [ "translation" ] + }, + { + "login": "deviantintegral", + "name": "Andrew Berry", + "avatar_url": "https://avatars.githubusercontent.com/u/255023?v=4", + "profile": "https://github.com/deviantintegral", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index b1d44f8f..f90fb30e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-40-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-41-orange.svg?style=flat-square)](#contributors-) # Automatically adapt the brightness and color of lights based on the sun position and take over manual control @@ -251,56 +251,57 @@ These graphs were generated using the values calculated by the Adaptive Lighting
Bas Nijholt
Bas Nijholt

💻 🚧 🐛
Sven Serlier
Sven Serlier

📖
Will Puckett
Will Puckett

📖
vapescherov
vapescherov

💻
Travis Pew
Travis Pew

📖
Sindre Broch
Sindre Broch

📖
Denis Shulyaka
Denis Shulyaka

💻
Bas Nijholt
Bas Nijholt

💻 🚧 🐛
Sven Serlier
Sven Serlier

📖
Will Puckett
Will Puckett

📖
vapescherov
vapescherov

💻
Travis Pew
Travis Pew

📖
Sindre Broch
Sindre Broch

📖
Denis Shulyaka
Denis Shulyaka

💻
@RubenKelevra
@RubenKelevra

📖 💻
Jüri Rebane
Jüri Rebane

🌍
quantumlemur
quantumlemur

💻
Michael Kirsch
Michael Kirsch

💻
Nicholai Nissen
Nicholai Nissen

🌍
Martin Myhrman
Martin Myhrman

🌍
Michel Peterson
Michel Peterson

💻
@RubenKelevra
@RubenKelevra

📖 💻
Jüri Rebane
Jüri Rebane

🌍
quantumlemur
quantumlemur

💻
Michael Kirsch
Michael Kirsch

💻
Nicholai Nissen
Nicholai Nissen

🌍
Martin Myhrman
Martin Myhrman

🌍
Michel Peterson
Michel Peterson

💻
MangoScango
MangoScango

💻
Lynilia
Lynilia

🌍
LukaszP2
LukaszP2

🌍
Joscha Wagner
Joscha Wagner

🌍
skdzzz
skdzzz

🌍
Simon Gurcke
Simon Gurcke

💻
Sören Beye
Sören Beye

💻
MangoScango
MangoScango

💻
Lynilia
Lynilia

🌍
LukaszP2
LukaszP2

🌍
Joscha Wagner
Joscha Wagner

🌍
skdzzz
skdzzz

🌍
Simon Gurcke
Simon Gurcke

💻
Sören Beye
Sören Beye

💻
Hudson Brendon
Hudson Brendon

🌍
Gabriel Visser
Gabriel Visser

📖
Gleb
Gleb

🌍
Deleted user
Deleted user

🌍
Avi Miller
Avi Miller

📖 💻
Denys Dovhan
Denys Dovhan

🌍
David Stenbeck
David Stenbeck

📖
Hudson Brendon
Hudson Brendon

🌍
Gabriel Visser
Gabriel Visser

📖
Gleb
Gleb

🌍
Deleted user
Deleted user

🌍
Avi Miller
Avi Miller

📖 💻
Denys Dovhan
Denys Dovhan

🌍
David Stenbeck
David Stenbeck

📖
Kevin Addeman
Kevin Addeman

💻
covid10
covid10

🌍 💻
Michael Chisholm
Michael Chisholm

💻
Justin Paupore
Justin Paupore

💻
bedaes
bedaes

💻
awashingmachine
awashingmachine

🌍
Clayton Nummer
Clayton Nummer

💻
Kevin Addeman
Kevin Addeman

💻
covid10
covid10

🌍 💻
Michael Chisholm
Michael Chisholm

💻
Justin Paupore
Justin Paupore

💻
bedaes
bedaes

💻
awashingmachine
awashingmachine

🌍
Clayton Nummer
Clayton Nummer

💻
Robert Crandall
Robert Crandall

💻
Matt Forster
Matt Forster

💻
Robert Crandall
Robert Crandall

💻
Matt Forster
Matt Forster

💻
Mark Niemeyer
Mark Niemeyer

🌍
Robert Crandall
Robert Crandall

💻
Matt Forster
Matt Forster

💻
Mark Niemeyer
Mark Niemeyer

🌍
Mark Niemeyer
Mark Niemeyer

🌍 💻
Robert Crandall
Robert Crandall

💻
Matt Forster
Matt Forster

💻
Mark Niemeyer
Mark Niemeyer

🌍 💻
Elliott Plack
Elliott Plack

📖
Matt Forster
Matt Forster

💻
Mark Niemeyer
Mark Niemeyer

🌍 💻
Elliott Plack
Elliott Plack

📖
ngommers
ngommers

🌍
- - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - + + + + + + From 738e7957c6e1b35b83179e0de026ea7c8ad73fbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Valigura?= Date: Sun, 12 Feb 2023 23:33:21 +0100 Subject: [PATCH 0491/1077] Add files via upload (#422) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Please add a Czech translation. Translated with love in collaboration with Jitka Kravcová. Co-authored-by: Bas Nijholt --- .../adaptive_lighting/translations/cs.json | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/cs.json diff --git a/custom_components/adaptive_lighting/translations/cs.json b/custom_components/adaptive_lighting/translations/cs.json new file mode 100644 index 00000000..58f8fead --- /dev/null +++ b/custom_components/adaptive_lighting/translations/cs.json @@ -0,0 +1,57 @@ +{ + "title": "Adaptivní osvětlení", + "config": { + "step": { + "user": { + "title": "Vyberte název instance Adaptivního osvětlení", + "description": "Vyberte název pro tuto instanci. Můžete spustit několik instancí Adaptivního osvětlení, každá z nich může obsahovat více světel!", + "data": { + "name": "Název" + } + } + }, + "abort": { + "already_configured": "Toto zařízení je již nakonfigurováno" + } + }, + "options": { + "step": { + "init": { + "title": "Nastavení adaptivního osvětlení", + "description": "Všechna nastavení komponenty Adaptivního osvětlení. Názvy možností odpovídají nastavení YAML. Pokud máte v konfiguraci YAML definovánu položku 'adaptive_lighting', nezobrazí se žádné možnosti.", + "data": { + "lights": "osvětlení", + "initial_transition": "initial_transition: Prodlení pro změnu z 'vypnuto' do 'zapnuto' (sekundy)", + "sleep_transition": "sleep_transition: Prodleva pro přepnutí do „režimu spánku“ (sekundy)", + "interval": "interval: Prodleva pro změny osvětlení (v sekundách)", + "max_brightness": "max_brightness: Nejvyšší jas osvětlení během cyklu. (%)", + "max_color_temp": "max_color_temp: Nejchladnější odstín cyklu teploty barev. (Kelvin)", + "min_brightness": "min_brightness: Nejnižší jas osvětlení během cyklu. (%)", + "min_color_temp": "min_color_temp, Nejteplejší odstín cyklu teploty barev. (Kelvin)", + "only_once": "only_once: Přizpůsobení osvětlení pouze při rozsvícení.", + "prefer_rgb_color": "prefer_rgb_color: Upřednostněte použití 'rgb_color' před 'color_temp'.", + "separate_turn_on_commands": "separate_turn_on_commands: Oddělení příkazů pro každý atribut (barva, jas, atd.) v atributu 'light.turn_on' (vyžadováno pro některá světla).", + "send_split_delay": "send_split_delay: prodleva mezi příkazy (milisekundy), když je použit atribut 'separate_turn_on_commands'. Může zajistit správné zpracování obou příkazů.", + "sleep_brightness": "sleep_brightness, Nastavení jasu pro režim spánku. (%)", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, použijte 'rgb_color' nebo 'color_temp'", + "sleep_rgb_color": "sleep_rgb_color, v RGB", + "sleep_color_temp": "sleep_color_temp: Nastavení teploty barev pro režim spánku. (v Kelvinech)", + "sunrise_offset": "sunrise_offset: Jak dlouho před (-) nebo po (+) definovat bod cyklu východu slunce (+/- v sekundách)", + "sunrise_time": "sunrise_time: Manuální přepsání času východu slunce, pokud je „None“, použije se skutečný čas východu slunce ve vaší lokalitě (HH:MM:SS)", + "max_sunrise_time": "max_sunrise_time: Ruční přepsání nejpozdějšího času východu slunce, pokud je „None“, použije se skutečný čas východu slunce vaší lokality (HH:MM:SS)", + "sunset_offset": "sunset_offset: Jak dlouho před (-) nebo po (+) definovat bod cyklu západu slunce (+/- v sekundách)", + "sunset_time": "sunset_time: Ruční přepsání času západu slunce, pokud je „None“, použije se skutečný čas západu slunce vaší lokality (HH:MM:SS)", + "min_sunset_time": "min_sunset_time: Ruční přepsání nejdřívějšího času západu slunce, pokud je „None“, použije se skutečný čas západu slunce vaší lokality (HH:MM:SS)", + "take_over_control": "take_over_control: Je-li volán 'light.turn_on' z jiného zdroje, než Adaptivním osvětlením, když je světlo již rozsvíceno, přestaňte toto světlo ovládat, dokud není vypnuto -> zapnuto (nebo i vypínačem).", + "detect_non_ha_changes": "detect_non_ha_changes: detekuje všechny změny >10% provedených pro osvětlení (také mimo HA), vyžaduje povolení atributu 'take_over_control' (každý 'interval' spouští 'homeassistant.update_entity'!)", + "transition": "transition: doba přechodu při změně osvětlení (sekundy)", + "adapt_delay": "adapt_delay: prodleva mezi zapnutím světla ( sekundy) a projevem změny v Adaptivní osvětlení. Může předcházet blikání." + } + } + }, + "error": { + "option_error": "Neplatná možnost", + "entity_missing": "V aplikaci Home Assistant chybí jedna nebo více vybraných entit osvětlení" + } + } +} From c3f26a36857fa90a03603e3442de7527a2cf2a2f Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 12 Feb 2023 14:34:42 -0800 Subject: [PATCH 0492/1077] docs: add brebtatv as a contributor for translation (#431) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index c8aa5b80..f8fc295f 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -384,6 +384,15 @@ "contributions": [ "doc" ] + }, + { + "login": "brebtatv", + "name": "Tomáš Valigura", + "avatar_url": "https://avatars.githubusercontent.com/u/10747062?v=4", + "profile": "https://github.com/brebtatv", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index f90fb30e..fa4425e0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-41-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-42-orange.svg?style=flat-square)](#contributors-) # Automatically adapt the brightness and color of lights based on the sun position and take over manual control @@ -302,6 +302,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 7fefa7de96c1070450a58a21ad615160ff5d665c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 12 Feb 2023 14:37:40 -0800 Subject: [PATCH 0493/1077] Update pre-commit filters (#432) --- .pre-commit-config.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5a742db3..2f8938a7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: check-added-large-files - id: trailing-whitespace @@ -8,19 +8,19 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - repo: https://github.com/pycqa/flake8 - rev: 5.0.4 + rev: 6.0.0 hooks: - id: flake8 - - repo: https://github.com/ambv/black - rev: 22.6.0 + - repo: https://github.com/psf/black + rev: 23.1.0 hooks: - id: black - repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 + rev: v3.3.1 hooks: - id: pyupgrade args: ["--py39-plus"] - - repo: https://github.com/timothycrosley/isort - rev: 5.10.1 + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 hooks: - id: isort From 393885a019429718fd567084e301a67d0ee91f22 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 12 Feb 2023 14:40:38 -0800 Subject: [PATCH 0494/1077] Do not test on Python 3.9, which Home Assistant no longer supports (#429) --- .github/workflows/pytest.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 84769d86..afa2eb21 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -13,7 +13,7 @@ jobs: timeout-minutes: 60 strategy: matrix: - python-version: ["3.9", "3.10"] + python-version: ["3.10"] steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 From 231e7c197d25d6472d278e38326c51b1efb85f30 Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Sat, 25 Mar 2023 14:43:51 -0500 Subject: [PATCH 0495/1077] Show config options on the switch as attributes with `include_config_in_attributes` option (#445) * Switch attributes now show the config settings. `sunset_time`, `sunrise_time`, `max_sunrise_time`, and `min_sunset_time` show 'null' when not overridden - TODO * fixed basnijholt's requested issues * Run pre-commit * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * 'configuration' attr holds list of config options returned the `=` from basnijholt's review. * Small style changes * Add include_config_in_attributes to en.json --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- README.md | 1 + custom_components/adaptive_lighting/const.py | 5 ++++ .../adaptive_lighting/strings.json | 1 + custom_components/adaptive_lighting/switch.py | 28 +++++++++++++++---- .../adaptive_lighting/translations/en.json | 1 + 5 files changed, 31 insertions(+), 5 deletions(-) mode change 100755 => 100644 custom_components/adaptive_lighting/switch.py diff --git a/README.md b/README.md index fa4425e0..5b2d148c 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ adaptive_lighting: | option | description | required | default | type | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------- | ------- | | `name` | The name to use when displaying this switch. | False | default | string | +| `include_config_in_attributes` | When set to `true`, will list all of the below options as attributes on the switch in Home Assistant. | False | False | boolean | | `lights` | List of light entities for Adaptive Lighting to control (may be empty). | False | list | [] | | `prefer_rgb_color` | Whether to use RGB color adjustment instead of native light color temperature. | False | False | boolean | | `initial_transition` | How long the first transition is when the lights go from `off` to `on`. | False | 1 | time | diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 62c71fa2..e9d6f01c 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -17,6 +17,10 @@ CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = ( "detect_non_ha_changes", False, ) +CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES = ( + "include_config_in_attributes", + False, +) CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION = "sleep_transition", 1 CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 @@ -73,6 +77,7 @@ def int_between(min_int, max_int): VALIDATION_TUPLES = [ (CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), (CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool), + (CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES, bool), (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), (CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION, VALID_TRANSITION), (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 74099754..18b5a438 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -21,6 +21,7 @@ "data": { "lights": "lights", "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (seconds)", + "include_config_in_attributes": "include_config_in_attributes: All config options will be listed as attributes under the adaptive-lighting switch this integration creates. (default: false)", "sleep_transition": "sleep_transition: When 'sleep_state' changes. (seconds)", "interval": "interval: Time between switch updates. (seconds)", "max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py old mode 100755 new mode 100644 index ac1acc96..5c228ade --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -100,6 +100,7 @@ from .const import ( ATTR_TURN_ON_OFF_LISTENER, CONF_ADAPT_DELAY, CONF_DETECT_NON_HA_CHANGES, + CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, CONF_INITIAL_TRANSITION, CONF_INTERVAL, CONF_LIGHTS, @@ -568,6 +569,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._lights = data[CONF_LIGHTS] self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] + self._include_config_in_attributes = data[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES] self._initial_transition = data[CONF_INITIAL_TRANSITION] self._sleep_transition = data[CONF_SLEEP_TRANSITION] self._interval = data[CONF_INTERVAL] @@ -623,6 +625,16 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set in self._update_attrs_and_maybe_adapt_lights self._settings: dict[str, Any] = {} + self._config: dict[str, Any] = {} + if self._include_config_in_attributes: + attrdata = deepcopy(data) + for k, v in attrdata.items(): + if isinstance(v, (datetime.date, datetime.datetime)): + attrdata[k] = v.isoformat() + if isinstance(v, (datetime.timedelta)): + attrdata[k] = v.total_seconds() + self._config.update(attrdata) + # Set and unset tracker in async_turn_on and async_turn_off self.remove_listeners = [] _LOGGER.debug( @@ -715,14 +727,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return the attributes of the switch.""" + extra_state_attributes = {"configuration": self._config} if not self.is_on: - return {key: None for key in self._settings} - manual_control = [ + for key in self._settings: + extra_state_attributes[key] = None + return extra_state_attributes + extra_state_attributes["manual_control"] = [ light for light in self._lights if self.turn_on_off_listener.manual_control.get(light) ] - return dict(self._settings, manual_control=manual_control) + extra_state_attributes.update(self._settings) + return extra_state_attributes def create_context( self, which: str = "default", parent: Context | None = None @@ -895,8 +911,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context.id, ) assert self.is_on - self._settings = self._sun_light_settings.get_settings( - self.sleep_mode_switch.is_on, transition + self._settings.update( + self._sun_light_settings.get_settings( + self.sleep_mode_switch.is_on, transition + ) ) self.async_write_ha_state() if lights is None: diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index be556ec0..8b2ba140 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -22,6 +22,7 @@ "data": { "lights": "lights", "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (seconds)", + "include_config_in_attributes": "include_config_in_attributes: All config options will be listed as attributes under the adaptive-lighting switch this integration creates. (default: false)", "sleep_transition": "sleep_transition: When 'sleep_state' changes. (seconds)", "interval": "interval: Time between switch updates. (seconds)", "max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)", From 8c630c995aa24419daad981b7cec1406fd95b41f Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 25 Mar 2023 12:46:10 -0700 Subject: [PATCH 0496/1077] docs: add th3w1zard1 as a contributor for code (#463) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index f8fc295f..9eafff4d 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -393,6 +393,15 @@ "contributions": [ "translation" ] + }, + { + "login": "th3w1zard1", + "name": "Benjamin Auquite", + "avatar_url": "https://avatars.githubusercontent.com/u/2219836?v=4", + "profile": "https://github.com/th3w1zard1", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 5b2d148c..00bde008 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-42-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-43-orange.svg?style=flat-square)](#contributors-) # Automatically adapt the brightness and color of lights based on the sun position and take over manual control @@ -305,6 +305,9 @@ These graphs were generated using the values calculated by the Adaptive Lighting + + + From c8ab1df74148170b72264733e5044717edffc13e Mon Sep 17 00:00:00 2001 From: Skyler Carlson <43375685+skycarl@users.noreply.github.com> Date: Sat, 25 Mar 2023 19:11:51 -0700 Subject: [PATCH 0497/1077] Fix sunrise vs sunset typo (#458) Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/strings.json | 2 +- custom_components/adaptive_lighting/translations/en.json | 2 +- custom_components/adaptive_lighting/translations/pl.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 18b5a438..73f70a2c 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -40,7 +40,7 @@ "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", "max_sunrise_time": "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- seconds)", - "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", "min_sunset_time": "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 8b2ba140..51311d64 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -41,7 +41,7 @@ "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", "max_sunrise_time": "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- seconds)", - "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", "min_sunset_time": "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json index 80cc8fdd..99a97e3c 100644 --- a/custom_components/adaptive_lighting/translations/pl.json +++ b/custom_components/adaptive_lighting/translations/pl.json @@ -36,7 +36,7 @@ "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- sekund)", "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- sekund)", - "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", "transition": "Transition time when applying a change to the lights (sekund)" From 5552f5ec89e562b62d49bb6bb5bdd624a4ef6f2d Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 25 Mar 2023 19:12:47 -0700 Subject: [PATCH 0498/1077] docs: add skycarl as a contributor for doc (#464) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 9eafff4d..4de99a92 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -402,6 +402,15 @@ "contributions": [ "code" ] + }, + { + "login": "skycarl", + "name": "Skyler Carlson", + "avatar_url": "https://avatars.githubusercontent.com/u/43375685?v=4", + "profile": "https://github.com/skycarl", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 00bde008..45ca3235 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-43-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-44-orange.svg?style=flat-square)](#contributors-) # Automatically adapt the brightness and color of lights based on the sun position and take over manual control @@ -307,6 +307,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 08ff04141e791ac5061c569658a80672c69b3374 Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Sat, 25 Mar 2023 22:22:05 -0500 Subject: [PATCH 0499/1077] Feature requests: Switch now optional for service calls | New default icons | Reload `.yaml` w/o restart | `adapt_delay` ms (#459) * added #274 * attempt getSwitchFromLightId() * Update switch.py * changed from register_entity_service to hass.services.async_register * Update switch.py * Update switch.py * Update switch.py * Update switch.py * Update switch.py * Update switch.py * Update services.yaml * Update switch.py * test * test builds ready * target selector may not be possible with current syntax priority is backwards-compatibility as I know most people will smash that update button * expanded light groups * test builds ready * add feature #104 * Update custom_components/adaptive_lighting/switch.py Co-authored-by: Chris * Might as well type both. No reason not to. Co-authored-by: Chris * Might as well type both. No reason not to. Co-authored-by: Chris * Reformatted debug messages. Reimported ServiceCall as suggested * Multiple switches allowed again in services. Apparently this was possible before. With this change, the `lights` argument must not be passed with multiple switches, or the integration has no way of knowing what the user wants to do. Integration did not make this check in prior versions. Also reformatted the debug messages. Removed `automerge.yaml` (my apologies) * Reload config without restart. You can now reload any changes to the yaml file without restarting your home assistant. Should show a 'reload' button in your integrations page or you can call the homeassistant reload integration service call. See https://community.home-assistant.io/t/how-to-allow-custom-compontent-for-yaml-configuration-reloading/391190/4 * Use snake_case for function name 'parseServiceArgs' * Slight rephrase * Small style changes * Rephrased debug messages. Removed `integration_entities` as we rewrote the code from that function already in parse_service_args. Reference function now above parse_service_args. * removed: `these_switches = data = None` * Factor out _find_switch_with_lights * Rename function and add log statement * Remove pylint marker * Handle multiple switches found * Small changes * Rename _parse_service_args to _get_switches_from_service_call * Rephrase log messages * Add type hint --------- Co-authored-by: Chris Co-authored-by: Bas Nijholt --- .../adaptive_lighting/__init__.py | 3 + custom_components/adaptive_lighting/const.py | 7 +- .../adaptive_lighting/services.yaml | 43 ++- custom_components/adaptive_lighting/switch.py | 301 +++++++++++++----- 4 files changed, 265 insertions(+), 89 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 33881c75..dc928a6b 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -6,6 +6,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import async_setup_reload_service import voluptuous as vol from .const import ( @@ -37,6 +38,8 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: dict[str, Any]): """Import integration from config.""" + # This will reload any changes the user made to any YAML configurations. + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) if DOMAIN in config: for entry in config[DOMAIN]: diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index e9d6f01c..e1f13499 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -5,7 +5,10 @@ from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv import voluptuous as vol -ICON = "mdi:theme-light-dark" +ICON_MAIN = "mdi:theme-light-dark" +ICON_BRIGHTNESS = "mdi:brightness-4" +ICON_COLOR_TEMP = "mdi:sun-thermometer" +ICON_SLEEP = "mdi:sleep" DOMAIN = "adaptive_lighting" SUN_EVENT_NOON = "solar_noon" @@ -115,7 +118,7 @@ VALIDATION_TUPLES = [ (CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool), (CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool), (CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY, int_between(0, 10000)), - (CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, int_between(0, 10000)), + (CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, cv.positive_float), ] diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 8f449a77..05e706e9 100755 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -2,35 +2,66 @@ apply: description: Applies the current Adaptive Lighting settings to lights. fields: entity_id: - description: entity_id of the Adaptive Lighting switch. + description: entity_id of the Adaptive Lighting switch. If not specified, the current Adaptive Lighting switch will be used. example: switch.adaptive_lighting_default + selector: + entity: + integration: adaptive_lighting + domain: switch + multiple: false lights: - description: "entity_id(s) of lights, default: lights of the switch" + description: entity_id(s) of lights, if not specified, all lights in the switch are selected. example: light.bedroom_ceiling + selector: + entity: + domain: light + multiple: true transition: description: Transition of the lights. example: 10 + selector: + text: adapt_brightness: description: "Adapt the 'brightness', default: true" example: true + selector: + boolean: adapt_color: description: "Adapt the color_temp/color_rgb, default: true" example: true + selector: + boolean: prefer_rgb_color: description: "Prefer to use color_rgb over color_temp if possible, default: false" example: false + selector: + boolean: turn_on_lights: description: "Turn on the lights that are off, default: false" example: false + selector: + boolean: set_manual_control: description: Mark whether a light is 'manually controlled'. fields: entity_id: - description: entity_id of the Adaptive Lighting switch. + description: entity_id of the Adaptive Lighting switch. If not specified, the current Adaptive Lighting switch will be used. example: switch.adaptive_lighting_default - manual_control: - description: "Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true" - example: true + selector: + entity: + integration: adaptive_lighting + domain: switch + multiple: false lights: description: entity_id(s) of lights, if not specified, all lights in the switch are selected. example: light.bedroom_ceiling + selector: + entity: + domain: light + multiple: true + manual_control: + description: "Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true" + example: true + default: true + selector: + boolean: diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 5c228ade..0b1d00de 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -21,10 +21,8 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS_STEP_PCT, ATTR_COLOR_NAME, - ATTR_COLOR_TEMP, ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, - ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, @@ -74,7 +72,7 @@ from homeassistant.core import ( State, callback, ) -from homeassistant.helpers import entity_platform +from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( async_track_state_change_event, @@ -129,7 +127,10 @@ from .const import ( CONF_TURN_ON_LIGHTS, DOMAIN, EXTRA_VALIDATION, - ICON, + ICON_BRIGHTNESS, + ICON_COLOR_TEMP, + ICON_MAIN, + ICON_SLEEP, SERVICE_APPLY, SERVICE_SET_MANUAL_CONTROL, SLEEP_MODE_SWITCH, @@ -161,10 +162,8 @@ RGB_REDMEAN_CHANGE = 80 # ≈10% of total range COLOR_ATTRS = { # Should ATTR_PROFILE be in here? ATTR_COLOR_NAME, - ATTR_COLOR_TEMP, ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, - ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_XY_COLOR, } @@ -238,57 +237,116 @@ def _split_service_data(service_data, adapt_brightness, adapt_color): return service_datas -async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): - """Handle the entity service apply.""" - hass = switch.hass - data = service_call.data - all_lights = data[CONF_LIGHTS] - if not all_lights: - all_lights = switch._lights - all_lights = _expand_light_groups(hass, all_lights) - switch.turn_on_off_listener.lights.update(all_lights) - _LOGGER.debug( - "Called 'adaptive_lighting.apply' service with '%s'", - data, - ) - for light in all_lights: - if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light): - await switch._adapt_light( # pylint: disable=protected-access - light, - data[CONF_TRANSITION], - data[ATTR_ADAPT_BRIGHTNESS], - data[ATTR_ADAPT_COLOR], - data[CONF_PREFER_RGB_COLOR], - force=True, - context=switch.create_context("service", parent=service_call.context), - ) +def _find_switch_with_any_of_lights( + hass: HomeAssistant, + lights: list[str], + service_call: ServiceCall, +) -> AdaptiveSwitch: + """Find the switch that controls the lights in 'lights'.""" + config_entries = hass.config_entries.async_entries(DOMAIN) + data = hass.data[DOMAIN] + switches = {} + for config in config_entries: + # this check is necessary as there seems to always be an extra config + # entry that doesn't contain any data. I believe this happens when the + # integration exists, but is disabled by the user in HASS. + if config.entry_id in data: + switch = data[config.entry_id]["instance"] + all_check_lights = _expand_light_groups(hass, lights) + switch._expand_light_groups() + if set(switch._lights) & set(all_check_lights): + switches[config.entry_id] = switch + if len(switches) == 1: + return next(iter(switches.values())) -async def handle_set_manual_control(switch: AdaptiveSwitch, service_call: ServiceCall): - """Set or unset lights as 'manually controlled'.""" - lights = service_call.data[CONF_LIGHTS] - if not lights: - all_lights = switch._lights # pylint: disable=protected-access + if len(switches) > 1: + _LOGGER.error( + "Invalid service data: Light(s) %s found in multiple switch configs (%s)." + " You must pass a switch under 'entity_id'. See the README for" + " details. Got %s", + lights, + list(switches.keys()), + service_call.data, + ) + raise ValueError( + "adaptive-lighting: Light(s) %s found in multiple switch configs.", + lights, + ) else: - all_lights = _expand_light_groups(switch.hass, lights) + _LOGGER.error( + "Invalid service data: Light was not found in any of your switch's configs." + " You must either include the light(s) that is/are in the integration config, or" + " pass a switch under 'entity_id'. See the README for details. Got %s", + service_call.data, + ) + raise ValueError( + "adaptive-lighting: Light(s) %s not found in any switch's configuration.", + lights, + ) + + +# For documentation on this function, see integration_entities() from HomeAssistant Core: +# https://github.com/home-assistant/core/blob/dev/homeassistant/helpers/template.py#L1109 +def _get_switches_from_service_call( + hass: HomeAssistant, service_call: ServiceCall +) -> list[AdaptiveSwitch]: _LOGGER.debug( - "Called 'adaptive_lighting.set_manual_control' service with '%s'", + "Function '_get_switches_from_service_call' called with service data:\n'%s'", service_call.data, ) - if service_call.data[CONF_MANUAL_CONTROL]: - for light in all_lights: - switch.turn_on_off_listener.manual_control[light] = True - _fire_manual_control_event(switch, light, service_call.context) - else: - switch.turn_on_off_listener.reset(*all_lights) - # pylint: disable=protected-access - if switch.is_on: - await switch._update_attrs_and_maybe_adapt_lights( - all_lights, - transition=switch._initial_transition, - force=True, - context=switch.create_context("service", parent=service_call.context), + data = service_call.data + lights = data[CONF_LIGHTS] + switch_entity_ids: list[str] | None = data.get("entity_id") + if not lights and not switch_entity_ids: + _LOGGER.debug( + "If you intended to adapt every single light on every single switch, please inform the" + " developers at https://github.com/basnijholt/adaptive-lighting of your use case." + " Currently, you must pass either an adaptive-lighting switch or the lights to" + " an `adaptive_lighting` service call." + ) + _LOGGER.error( + "Invalid service data passed to adaptive-lighting service call -" + " you must pass either a switch or a light's entity ID. Service data:\n%s", + service_call.data, + ) + raise ValueError( + "adaptive-lighting: No switch or light was passed to service call." + ) + + if switch_entity_ids is not None: + if len(switch_entity_ids) > 1 and lights: + _LOGGER.error( + "Invalid service data: cannot pass multiple switch entities while also passing" + " lights. Service data received: %s", + service_call.data, ) + raise ValueError( + "adaptive-lighting: Multiple switches were passed with lights argument" + ) + switches = [] + ent_reg = entity_registry.async_get(hass) + for entity_id in switch_entity_ids: + ent_entry = ent_reg.async_get(entity_id) + config_id = ent_entry.config_entry_id + switches.append(hass.data[DOMAIN][config_id]["instance"]) + return switches + + if lights: + switch = _find_switch_with_any_of_lights(hass, lights, service_call) + _LOGGER.debug( + "Switch '%s' found for lights '%s'", + switch.entity_id, + lights, + ) + return [switch] + + _LOGGER.error( + "Invalid service data passed to adaptive-lighting service call -" + " entities were not found in the integration. Service data:\n%s", + service_call.data, + ) + raise ValueError("adaptive-lighting: User sent incorrect data to service call") @callback @@ -320,10 +378,15 @@ async def async_setup_entry( if ATTR_TURN_ON_OFF_LISTENER not in data: data[ATTR_TURN_ON_OFF_LISTENER] = TurnOnOffListener(hass) turn_on_off_listener = data[ATTR_TURN_ON_OFF_LISTENER] - - sleep_mode_switch = SimpleSwitch("Sleep Mode", False, hass, config_entry) - adapt_color_switch = SimpleSwitch("Adapt Color", True, hass, config_entry) - adapt_brightness_switch = SimpleSwitch("Adapt Brightness", True, hass, config_entry) + sleep_mode_switch = SimpleSwitch( + "Sleep Mode", False, hass, config_entry, ICON_SLEEP + ) + adapt_color_switch = SimpleSwitch( + "Adapt Color", True, hass, config_entry, ICON_COLOR_TEMP + ) + adapt_brightness_switch = SimpleSwitch( + "Adapt Brightness", True, hass, config_entry, ICON_BRIGHTNESS + ) switch = AdaptiveSwitch( hass, config_entry, @@ -333,6 +396,9 @@ async def async_setup_entry( adapt_brightness_switch, ) + # save our switch instance, allows us to make switch's entity_id optional in service calls. + hass.data[DOMAIN][config_entry.entry_id]["instance"] = switch + data[config_entry.entry_id][SLEEP_MODE_SWITCH] = sleep_mode_switch data[config_entry.entry_id][ADAPT_COLOR_SWITCH] = adapt_color_switch data[config_entry.entry_id][ADAPT_BRIGHTNESS_SWITCH] = adapt_brightness_switch @@ -343,33 +409,101 @@ async def async_setup_entry( update_before_add=True, ) + @callback + async def handle_apply(service_call: ServiceCall): + """Handle the entity service apply.""" + data = service_call.data + _LOGGER.debug( + "Called 'adaptive_lighting.apply' service with '%s'", + data, + ) + these_switches = _get_switches_from_service_call(hass, service_call) + lights = data[CONF_LIGHTS] + for this_switch in these_switches: + if not lights: + all_lights = this_switch._lights # pylint: disable=protected-access + else: + all_lights = _expand_light_groups(this_switch.hass, lights) + this_switch.turn_on_off_listener.lights.update(all_lights) + for light in all_lights: + if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light): + await this_switch._adapt_light( # pylint: disable=protected-access + light, + data[CONF_TRANSITION], + data[ATTR_ADAPT_BRIGHTNESS], + data[ATTR_ADAPT_COLOR], + data[CONF_PREFER_RGB_COLOR], + force=True, + context=this_switch.create_context( + "service", parent=service_call.context + ), + ) + + @callback + async def handle_set_manual_control(service_call: ServiceCall): + """Set or unset lights as 'manually controlled'.""" + data = service_call.data + _LOGGER.debug( + "Called 'adaptive_lighting.set_manual_control' service with '%s'", + data, + ) + these_switches = _get_switches_from_service_call(hass, service_call) + lights = data[CONF_LIGHTS] + for this_switch in these_switches: + if not lights: + all_lights = this_switch._lights # pylint: disable=protected-access + else: + all_lights = _expand_light_groups(this_switch.hass, lights) + if service_call.data[CONF_MANUAL_CONTROL]: + for light in all_lights: + this_switch.turn_on_off_listener.manual_control[light] = True + _fire_manual_control_event(this_switch, light, service_call.context) + else: + this_switch.turn_on_off_listener.reset(*all_lights) + # pylint: disable=protected-access + if this_switch.is_on: + await this_switch._update_attrs_and_maybe_adapt_lights( + all_lights, + transition=this_switch._initial_transition, + force=True, + context=this_switch.create_context( + "service", parent=service_call.context + ), + ) + # Register `apply` service - platform = entity_platform.current_platform.get() - platform.async_register_entity_service( - SERVICE_APPLY, - { - vol.Optional( - CONF_LIGHTS, default=[] - ): cv.entity_ids, # pylint: disable=protected-access - vol.Optional( - CONF_TRANSITION, - default=switch._initial_transition, # pylint: disable=protected-access - ): VALID_TRANSITION, - vol.Optional(ATTR_ADAPT_BRIGHTNESS, default=True): cv.boolean, - vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean, - vol.Optional(CONF_PREFER_RGB_COLOR, default=False): cv.boolean, - vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean, - }, - handle_apply, + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_APPLY, + service_func=handle_apply, + schema=vol.Schema( + { + vol.Optional("entity_id"): cv.entity_ids, + vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, + vol.Optional( + CONF_TRANSITION, + default=switch._initial_transition, # pylint: disable=protected-access + ): VALID_TRANSITION, + vol.Optional(ATTR_ADAPT_BRIGHTNESS, default=True): cv.boolean, + vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean, + vol.Optional(CONF_PREFER_RGB_COLOR, default=False): cv.boolean, + vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean, + } + ), ) - platform.async_register_entity_service( - SERVICE_SET_MANUAL_CONTROL, - { - vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, - vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean, - }, - handle_set_manual_control, + # Register `set_manual_control` service + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_SET_MANUAL_CONTROL, + service_func=handle_set_manual_control, + schema=vol.Schema( + { + vol.Optional("entity_id"): cv.entity_ids, + vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, + vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean, + } + ), ) @@ -610,7 +744,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) # Set other attributes - self._icon = ICON + self._icon = ICON_MAIN self._state = None # Tracks 'off' → 'on' state changes @@ -1046,12 +1180,17 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" def __init__( - self, which: str, initial_state: bool, hass: HomeAssistant, config_entry + self, + which: str, + initial_state: bool, + hass: HomeAssistant, + config_entry: ConfigEntry, + icon: str, ): """Initialize the Adaptive Lighting switch.""" self.hass = hass data = validate(config_entry) - self._icon = ICON + self._icon = icon self._state = None self._which = which name = data[CONF_NAME] From 97e2e27a9b2348036384e15dc93f01ee63aa4393 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 25 Mar 2023 20:28:22 -0700 Subject: [PATCH 0500/1077] docs: add firstof9 as a contributor for code (#465) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 4de99a92..756c16a2 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -411,6 +411,15 @@ "contributions": [ "doc" ] + }, + { + "login": "firstof9", + "name": "Chris", + "avatar_url": "https://avatars.githubusercontent.com/u/1105672?v=4", + "profile": "https://github.com/firstof9", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 45ca3235..2afd9666 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-44-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-45-orange.svg?style=flat-square)](#contributors-) # Automatically adapt the brightness and color of lights based on the sun position and take over manual control @@ -308,6 +308,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 66e65e2fe7146e526174634dada3552da77ba30c Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 25 Mar 2023 20:41:56 -0700 Subject: [PATCH 0501/1077] docs: add raman325 as a contributor for code (#466) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 756c16a2..063b429c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -420,6 +420,15 @@ "contributions": [ "code" ] + }, + { + "login": "raman325", + "name": "Raman Gupta", + "avatar_url": "https://avatars.githubusercontent.com/u/7243222?v=4", + "profile": "https://github.com/raman325", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 2afd9666..89232f79 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-45-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-46-orange.svg?style=flat-square)](#contributors-) # Automatically adapt the brightness and color of lights based on the sun position and take over manual control @@ -309,6 +309,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 06dcdbb4a484350aac8244695fd7c75abcafbaa8 Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Sun, 26 Mar 2023 17:04:13 -0500 Subject: [PATCH 0502/1077] Fix the broken tests (#467) * Update test_dependencies.py pytest reversion todo in later commit * increase verbosity for analysis * "instance" was created in the basic features PR. * Can't stand the deprecation warnings. * add testing steps to workflow action in logs. Help the next person running into this issue again. * wording in new messages. * Manifest keys should be sorted: domain, name, then alphabetical order * return basnijholt's original settings. replaced `-v` with `-qq` again in `pytest.yaml`, returned `branches: [master]` in both yaml files. * Forgot a comma * Update version in manifest to 1.6.0 * @th3w1zard1 is a code owner! * Update .gitignore * Revert "Update .gitignore" This reverts commit 172841d356173a4025f244d5a2f17a64197f7009. --- .github/workflows/hassfest.yaml | 2 +- .github/workflows/pytest.yaml | 18 ++++++++++++++---- .../adaptive_lighting/manifest.json | 10 +++++----- test_dependencies.py | 1 + tests/test_switch.py | 4 +++- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml index 157d5415..cc16d185 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/hassfest.yaml @@ -11,5 +11,5 @@ jobs: validate_hassfest: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v2" + - uses: "actions/checkout@v3.0.2" - uses: home-assistant/actions/hassfest@master diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index afa2eb21..60d9577f 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -27,12 +27,22 @@ jobs: uses: actions/setup-python@v4.1.0 with: python-version: ${{ matrix.python-version }} + - name: Click here for troubleshooting steps if tests break again. + run: | + echo "::notice::### If tests fail, try these debug steps: ###" + echo "::notice::### 1. Replace '-qq' from .github/workflow/pytest.yaml. with '-v' for extra verbosity. ###" + echo "::notice::### 2. Push or run action again. ###" + echo "::notice::### 3. Check for any log messages in github actions resembling the following using CTRL+F ### + echo "::notice::### 4. ERROR:homeassistant.setup:Setup failed for 'component': Unable to import component: No module named ''module'' ###" + echo "::notice::### 5. add 'component'.'module' (without the '') from the above log into the 'required' list inside of 'test_dependencies.py' ###" + echo "::notice::### 6. Try again! If more issues persist they should be easily solvable by reading the verbose logs now. ###" - name: Install dependencies run: | - pip install -r core/requirements.txt - pip install -r core/requirements_test.txt - pip install -e core/ - pip install $(python test_dependencies.py) + echo "::warning::### WARNING! Deprecation warnings muted with option '--use-pep517' please address this at some point in pytest.yaml. ###" + pip install -r core/requirements.txt --use-pep517 + pip install -r core/requirements_test.txt --use-pep517 + pip install -e core/ --use-pep517 + pip install $(python test_dependencies.py) --use-pep517 - name: Run pytest timeout-minutes: 60 run: | diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index d6580942..3fcd3b99 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -1,12 +1,12 @@ { "domain": "adaptive_lighting", "name": "Adaptive Lighting", - "documentation": "https://github.com/basnijholt/adaptive-lighting#readme", - "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", + "codeowners": ["@basnijholt", "@RubenKelevra", "@th3w1zard1"], "config_flow": true, "dependencies": [], - "codeowners": ["@basnijholt", "@RubenKelevra"], - "version": "1.4.1", + "documentation": "https://github.com/basnijholt/adaptive-lighting#readme", + "iot_class": "calculated", + "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": [], - "iot_class": "calculated" + "version": "1.6.0" } diff --git a/test_dependencies.py b/test_dependencies.py index 532c88f5..a4dc0e89 100644 --- a/test_dependencies.py +++ b/test_dependencies.py @@ -24,6 +24,7 @@ required = [ "components.zeroconf", "components.http", "components.stream", + "components.conversation", ] to_install = [] for r in required: diff --git a/tests/test_switch.py b/tests/test_switch.py index 59c9245d..321894cf 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -223,7 +223,9 @@ async def test_adaptive_lighting_switches(hass): assert ADAPT_COLOR_SWITCH in data assert ADAPT_BRIGHTNESS_SWITCH in data assert UNDO_UPDATE_LISTENER in data - assert len(data.keys()) == 5 + assert "instance" in data + + assert len(data.keys()) == 6 @pytest.mark.parametrize("lat,long,timezone", LAT_LONG_TZS) From 7486110e3e71932a1e5bda60a2be68443bb21da9 Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Sun, 26 Mar 2023 17:16:00 -0500 Subject: [PATCH 0503/1077] manually edited files for new commit. (#468) Co-authored-by: Bas Nijholt From 1b08e8e6badf6518b7d3ef67084548f01376564a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 26 Mar 2023 18:18:18 -0700 Subject: [PATCH 0504/1077] Add testing Dockerfile (#470) * Add testing Dockerfile * Add comments * Format table * Add tests/README.md * Rephrase * add note in dockerfile --- .github/workflows/docker-build.yml | 26 ++++++++++++++ Dockerfile | 58 ++++++++++++++++++++++++++++++ tests/README.md | 20 +++++++++++ 3 files changed, 104 insertions(+) create mode 100644 .github/workflows/docker-build.yml create mode 100644 Dockerfile create mode 100644 tests/README.md diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 00000000..5abf1029 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,26 @@ +name: docker + +on: + push: + branches: + - "master" + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v4 + with: + push: true + platforms: linux/amd64,linux/arm64,linux/arm/v7 + tags: ${{ secrets.DOCKERHUB_USERNAME }}/home-assistant-streamdeck-yaml:latest diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..e45da74c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,58 @@ +# See tests/README.md for instructions on how to run the tests. + +# tl;dr: +# Run the following command in the adaptive-lighting repo folder to run the tests: +# docker run -v $(pwd):/app basnijholt/adaptive-lighting:latest + +# Optionally build the image yourself with: +# docker build -t basnijholt/adaptive-lighting:latest . + +FROM python:3.11-buster + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Clone home-assistant/core +RUN git clone https://github.com/home-assistant/core.git /core + +# Install home-assistant/core dependencies +RUN pip3 install -r /core/requirements.txt --use-pep517 && \ + pip3 install -r /core/requirements_test.txt --use-pep517 && \ + pip3 install -e /core/ --use-pep517 + +# Clone the Adaptive Lighting repository +RUN git clone https://github.com/basnijholt/adaptive-lighting.git /app + +# Setup symlinks in core +RUN ln -s /app/custom_components/adaptive_lighting /core/homeassistant/components/adaptive_lighting && \ + ln -s /app/tests /core/tests/components/adaptive_lighting && \ + # For test_dependencies.py + ln -s /core /app/core + +# Install dependencies of components that Adaptive Lighting depends on +RUN pip3 install $(python3 /app/test_dependencies.py) --use-pep517 + +WORKDIR /core + +CMD ["python3", \ + # Enable Python development mode + "-X", "dev", \ + # Run pytest + "-m", "pytest", \ + # Verbose output + "-vvv", \ + # Set a timeout of 9 seconds per test + "--timeout=9", \ + # Print the 10 slowest tests + "--durations=10", \ + # Measure code coverage for the 'homeassistant' package + "--cov='homeassistant'", \ + # Generate an XML report of the code coverage + "--cov-report=xml", \ + # Print a count of test results in the console + "-o", "console_output_style=count", \ + # Run tests in the 'tests/components/adaptive_lighting' directory + "tests/components/adaptive_lighting" \ + ] diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..3176f04e --- /dev/null +++ b/tests/README.md @@ -0,0 +1,20 @@ +# Developer notes for the tests directory + +To run the tests, check out the [CI configuration](../.github/workflows/pytest.yml) to see how they are executed in the CI pipeline. +Alternatively, you can use the provided Docker image to run the tests locally. + +To run the tests using the Docker image, navigate to the `adaptive-lighting` repo folder and execute the following command: + +```bash +docker run -v $(pwd):/app basnijholt/adaptive-lighting:latest +``` + +This command will download the Docker image from [the adaptive-lighting Docker Hub repo]((https://hub.docker.com/r/basnijholt/adaptive-lighting)) and run the tests. + +If you prefer to build the image yourself, use the following command: + +```bash +docker build -t basnijholt/adaptive-lighting:latest --no-cache . +``` + +This might be necessary if the image on Docker Hub is outdated or if the [`test_dependencies.py`](../test_dependencies.py) file is updated. From 36154ca606c0792d9ff0989a278991aec3a34f4e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 26 Mar 2023 18:21:00 -0700 Subject: [PATCH 0505/1077] Format the Markdown table in README.md (#471) --- README.md | 60 +++++++++++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 89232f79..23ff9287 100644 --- a/README.md +++ b/README.md @@ -50,36 +50,36 @@ adaptive_lighting: ``` ### Options -| option | description | required | default | type | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------- | ------- | -| `name` | The name to use when displaying this switch. | False | default | string | -| `include_config_in_attributes` | When set to `true`, will list all of the below options as attributes on the switch in Home Assistant. | False | False | boolean | -| `lights` | List of light entities for Adaptive Lighting to control (may be empty). | False | list | [] | -| `prefer_rgb_color` | Whether to use RGB color adjustment instead of native light color temperature. | False | False | boolean | -| `initial_transition` | How long the first transition is when the lights go from `off` to `on`. | False | 1 | time | -| `sleep_transition` | How long the transition is when when "sleep mode" is toggled | False | 1 | time | -| `transition` | How long the transition is when the lights change, in seconds. | False | 45 | integer | -| `interval` | How often to adapt the lights, in seconds. | False | 90 | integer | -| `min_brightness` | The minimum percent of brightness to set the lights to. | False | 1 | integer | -| `max_brightness` | The maximum percent of brightness to set the lights to. | False | 100 | integer | -| `min_color_temp` | The warmest color temperature to set the lights to, in Kelvin. | False | 2000 | integer | -| `max_color_temp` | The coldest color temperature to set the lights to, in Kelvin. | False | 5500 | integer | -| `sleep_brightness` | Brightness of lights while the sleep mode is enabled. | False | 1 | integer | -| `sleep_rgb_or_color_temp` | Use either 'rgb_color' or 'color_temp' when in sleep mode. | False | 'color_temp' | string | -| `sleep_rgb_color` | List of three numbers between 0-255, indicating the RGB color in sleep mode (only used when sleep_rgb_or_color_temp is 'rgb_color'). | False | `[255, 56, 0]` | list | -| `sleep_color_temp` | Color temperature of lights while the sleep mode is enabled (only used when sleep_rgb_or_color_temp is 'color_temp'). | False | 1000 | integer | -| `sunrise_time` | Override the sunrise time with a fixed time. | False | None | time | -| `max_sunrise_time` | Make the virtual sun always rise at at most a specific time while still allowing for even earlier times based on the real sun | False | None | time | -| `sunrise_offset` | Change the sunrise time with a positive or negative offset. | False | 0 | time | -| `sunset_time` | Override the sunset time with a fixed time. | False | None | time | -| `min_sunset_time` | Make the virtual sun always set at at least a specific time while still allowing for even later times based on the real sun | False | None | time | -| `sunset_offset` | Change the sunset time with a positive or negative offset. | False | 0 | time | -| `only_once` | Whether to keep adapting the lights (false) or to only adapt the lights as soon as they are turned on (true). | False | False | boolean | -| `take_over_control` | If another source calls `light.turn_on` while the lights are on and being adapted, disable Adaptive Lighting. | False | True | boolean | -| `detect_non_ha_changes` | Whether to detect state changes and stop adapting lights, even not from `light.turn_on`. Needs `take_over_control` to be enabled. Note that by enabling this option, it calls 'homeassistant.update_entity' every 'interval'! | False | False | boolean | -| `separate_turn_on_commands` | Whether to use separate `light.turn_on` calls for color and brightness, needed for some types of lights | False | False | boolean | -| `send_split_delay` | Wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly. | False | 0 | integer | -| `adapt_delay` | Wait time in seconds between light turn on, and Adaptive Lights applying changes to the light state. May avoid flickering. | False | 0 | integer | +| option | description | required | default | type | +| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------- | ------- | +| `name` | The name to use when displaying this switch. | False | default | string | +| `include_config_in_attributes` | When set to `true`, will list all of the below options as attributes on the switch in Home Assistant. | False | False | boolean | +| `lights` | List of light entities for Adaptive Lighting to control (may be empty). | False | list | [] | +| `prefer_rgb_color` | Whether to use RGB color adjustment instead of native light color temperature. | False | False | boolean | +| `initial_transition` | How long the first transition is when the lights go from `off` to `on`. | False | 1 | time | +| `sleep_transition` | How long the transition is when when "sleep mode" is toggled | False | 1 | time | +| `transition` | How long the transition is when the lights change, in seconds. | False | 45 | integer | +| `interval` | How often to adapt the lights, in seconds. | False | 90 | integer | +| `min_brightness` | The minimum percent of brightness to set the lights to. | False | 1 | integer | +| `max_brightness` | The maximum percent of brightness to set the lights to. | False | 100 | integer | +| `min_color_temp` | The warmest color temperature to set the lights to, in Kelvin. | False | 2000 | integer | +| `max_color_temp` | The coldest color temperature to set the lights to, in Kelvin. | False | 5500 | integer | +| `sleep_brightness` | Brightness of lights while the sleep mode is enabled. | False | 1 | integer | +| `sleep_rgb_or_color_temp` | Use either 'rgb_color' or 'color_temp' when in sleep mode. | False | 'color_temp' | string | +| `sleep_rgb_color` | List of three numbers between 0-255, indicating the RGB color in sleep mode (only used when sleep_rgb_or_color_temp is 'rgb_color'). | False | `[255, 56, 0]` | list | +| `sleep_color_temp` | Color temperature of lights while the sleep mode is enabled (only used when sleep_rgb_or_color_temp is 'color_temp'). | False | 1000 | integer | +| `sunrise_time` | Override the sunrise time with a fixed time. | False | None | time | +| `max_sunrise_time` | Make the virtual sun always rise at at most a specific time while still allowing for even earlier times based on the real sun | False | None | time | +| `sunrise_offset` | Change the sunrise time with a positive or negative offset. | False | 0 | time | +| `sunset_time` | Override the sunset time with a fixed time. | False | None | time | +| `min_sunset_time` | Make the virtual sun always set at at least a specific time while still allowing for even later times based on the real sun | False | None | time | +| `sunset_offset` | Change the sunset time with a positive or negative offset. | False | 0 | time | +| `only_once` | Whether to keep adapting the lights (false) or to only adapt the lights as soon as they are turned on (true). | False | False | boolean | +| `take_over_control` | If another source calls `light.turn_on` while the lights are on and being adapted, disable Adaptive Lighting. | False | True | boolean | +| `detect_non_ha_changes` | Whether to detect state changes and stop adapting lights, even not from `light.turn_on`. Needs `take_over_control` to be enabled. Note that by enabling this option, it calls 'homeassistant.update_entity' every 'interval'! | False | False | boolean | +| `separate_turn_on_commands` | Whether to use separate `light.turn_on` calls for color and brightness, needed for some types of lights | False | False | boolean | +| `send_split_delay` | Wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly. | False | 0 | integer | +| `adapt_delay` | Wait time in seconds between light turn on, and Adaptive Lights applying changes to the light state. May avoid flickering. | False | 0 | integer | Full example: From 87af937f41a002591886bcedab86d21a9e5e245a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 26 Mar 2023 18:38:20 -0700 Subject: [PATCH 0506/1077] Set clone depth=1 to speedup the cloning of home-assistant/core (#472) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e45da74c..48a24fe2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ RUN apt-get update && \ && rm -rf /var/lib/apt/lists/* # Clone home-assistant/core -RUN git clone https://github.com/home-assistant/core.git /core +RUN git clone --depth 1 https://github.com/home-assistant/core.git /core # Install home-assistant/core dependencies RUN pip3 install -r /core/requirements.txt --use-pep517 && \ From a523cccb0412d960c2b76a44d1e020bc7823141d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 26 Mar 2023 18:46:36 -0700 Subject: [PATCH 0507/1077] Add command to print pytest with colors in Docker (#473) --- tests/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/README.md b/tests/README.md index 3176f04e..f879529d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -9,6 +9,12 @@ To run the tests using the Docker image, navigate to the `adaptive-lighting` rep docker run -v $(pwd):/app basnijholt/adaptive-lighting:latest ``` +and to show the logs with colors use: + +```bash +docker run -v $(pwd):/app -e 'PYTEST_ADDOPTS="--color=yes"' basnijholt/adaptive-lighting:latest +``` + This command will download the Docker image from [the adaptive-lighting Docker Hub repo]((https://hub.docker.com/r/basnijholt/adaptive-lighting)) and run the tests. If you prefer to build the image yourself, use the following command: From 8b3a9d67559f1094869aca209bcbf03dcb11acbc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 26 Mar 2023 18:50:49 -0700 Subject: [PATCH 0508/1077] Try installing extra deps such that bcrypt and cryptography can be installed in arm/v7 (#474) --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 48a24fe2..745a8e4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ FROM python:3.11-buster RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y \ git \ + build-essential libssl-dev libffi-dev python3-dev \ && rm -rf /var/lib/apt/lists/* # Clone home-assistant/core From f408f330c14674fc20b1adfb8aa31f0b6b1b4f1c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 26 Mar 2023 19:14:19 -0700 Subject: [PATCH 0509/1077] Disable Docker image builds on ARM for now (#475) --- .github/workflows/docker-build.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 5abf1029..f374ea55 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -4,6 +4,7 @@ on: push: branches: - "master" + pull_request: jobs: docker: @@ -21,6 +22,8 @@ jobs: - name: Build and push uses: docker/build-push-action@v4 with: - push: true - platforms: linux/amd64,linux/arm64,linux/arm/v7 + # Only push on the master branch + push: ${{ github.ref == 'refs/heads/master' }} + # TODO: fix builds on linux/arm64,linux/arm/v7 + platforms: linux/amd64 tags: ${{ secrets.DOCKERHUB_USERNAME }}/home-assistant-streamdeck-yaml:latest From 1fb364949e539d1c5d14732ce267b10b340e2a3c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 26 Mar 2023 19:39:57 -0700 Subject: [PATCH 0510/1077] Fix name of Docker image (#478) --- .github/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index f374ea55..e69c88ef 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -26,4 +26,4 @@ jobs: push: ${{ github.ref == 'refs/heads/master' }} # TODO: fix builds on linux/arm64,linux/arm/v7 platforms: linux/amd64 - tags: ${{ secrets.DOCKERHUB_USERNAME }}/home-assistant-streamdeck-yaml:latest + tags: ${{ secrets.DOCKERHUB_USERNAME }}/adaptive-lighting:latest From aa282c83b2064586ab4cee912027a9580fd8e0d0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 26 Mar 2023 19:50:40 -0700 Subject: [PATCH 0511/1077] Fix Markdown URL (#479) --- tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index f879529d..01b47047 100644 --- a/tests/README.md +++ b/tests/README.md @@ -15,7 +15,7 @@ and to show the logs with colors use: docker run -v $(pwd):/app -e 'PYTEST_ADDOPTS="--color=yes"' basnijholt/adaptive-lighting:latest ``` -This command will download the Docker image from [the adaptive-lighting Docker Hub repo]((https://hub.docker.com/r/basnijholt/adaptive-lighting)) and run the tests. +This command will download the Docker image from [the adaptive-lighting Docker Hub repo](https://hub.docker.com/r/basnijholt/adaptive-lighting) and run the tests. If you prefer to build the image yourself, use the following command: From 491aca696c0c12ded9bd8d8226e8bd3d46eeb7a5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 26 Mar 2023 20:00:12 -0700 Subject: [PATCH 0512/1077] Add linux/arm64 to build platform of Docker image (#476) * Add linux/arm64 to build platform of Docker image * Fix url --- .github/workflows/docker-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index e69c88ef..cf1866fb 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -24,6 +24,6 @@ jobs: with: # Only push on the master branch push: ${{ github.ref == 'refs/heads/master' }} - # TODO: fix builds on linux/arm64,linux/arm/v7 - platforms: linux/amd64 + # TODO: fix builds on linux/arm/v7 + platforms: linux/amd64,linux/arm64 tags: ${{ secrets.DOCKERHUB_USERNAME }}/adaptive-lighting:latest From 85461ea8569daf595657c75974c06753d93033e5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 26 Mar 2023 21:37:21 -0700 Subject: [PATCH 0513/1077] Always color the logs and print coverage report (#481) --- Dockerfile | 6 ++++++ tests/README.md | 6 ------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 745a8e4d..1fe5a1ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,6 +52,12 @@ CMD ["python3", \ "--cov='homeassistant'", \ # Generate an XML report of the code coverage "--cov-report=xml", \ + # Generate an HTML report of the code coverage + "--cov-report=html", \ + # Print a summary of the code coverage in the console + "--cov-report=term", \ + # Print logs in color + "--color=yes", \ # Print a count of test results in the console "-o", "console_output_style=count", \ # Run tests in the 'tests/components/adaptive_lighting' directory diff --git a/tests/README.md b/tests/README.md index 01b47047..2034ed70 100644 --- a/tests/README.md +++ b/tests/README.md @@ -9,12 +9,6 @@ To run the tests using the Docker image, navigate to the `adaptive-lighting` rep docker run -v $(pwd):/app basnijholt/adaptive-lighting:latest ``` -and to show the logs with colors use: - -```bash -docker run -v $(pwd):/app -e 'PYTEST_ADDOPTS="--color=yes"' basnijholt/adaptive-lighting:latest -``` - This command will download the Docker image from [the adaptive-lighting Docker Hub repo](https://hub.docker.com/r/basnijholt/adaptive-lighting) and run the tests. If you prefer to build the image yourself, use the following command: From eb43e1755e0dd24d8b598a3e8da83cad24d5d67a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 26 Mar 2023 22:07:07 -0700 Subject: [PATCH 0514/1077] Use ENTRYPOINT and CMD (#482) --- Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1fe5a1ff..45522da8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,7 +37,7 @@ RUN pip3 install $(python3 /app/test_dependencies.py) --use-pep517 WORKDIR /core -CMD ["python3", \ +ENTRYPOINT ["python3", \ # Enable Python development mode "-X", "dev", \ # Run pytest @@ -59,7 +59,7 @@ CMD ["python3", \ # Print logs in color "--color=yes", \ # Print a count of test results in the console - "-o", "console_output_style=count", \ - # Run tests in the 'tests/components/adaptive_lighting' directory - "tests/components/adaptive_lighting" \ - ] + "-o", "console_output_style=count"] + +# Run tests in the 'tests/components/adaptive_lighting' directory +CMD ["tests/components/adaptive_lighting"] From 01274964410dd2c65ce76b5d10c5aab7ec30f5c4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 26 Mar 2023 22:47:50 -0700 Subject: [PATCH 0515/1077] Add example of how to pass command line args (#483) --- tests/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/README.md b/tests/README.md index 2034ed70..adedcc4c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -18,3 +18,13 @@ docker build -t basnijholt/adaptive-lighting:latest --no-cache . ``` This might be necessary if the image on Docker Hub is outdated or if the [`test_dependencies.py`](../test_dependencies.py) file is updated. + +## Passing arguments to pytest + +You can pass arguments to pytest by appending them to the command: + +For example, to run the tests with a custom log format, use the following command (this also gets rid of the captured stderr output): + +```bash +docker run -v $(pwd):/app basnijholt/adaptive-lighting:latest --show-capture=log --log-format="%(asctime)s %(levelname)-8s %(name)s:%(filename)s:%(lineno)s %(message)s" --log-date-format="%H:%M:%S" tests/components/adaptive_lighting/ +``` From c05e0cc244d59a03706beba34df8091c2995d25e Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Sun, 26 Mar 2023 22:47:50 -0700 Subject: [PATCH 0516/1077] Change attributes in AdaptiveSwitch via service call (#443) --- README.md | 55 ++++++ .../adaptive_lighting/__init__.py | 3 + custom_components/adaptive_lighting/const.py | 2 + .../adaptive_lighting/services.yaml | 178 ++++++++++++++++++ custom_components/adaptive_lighting/switch.py | 151 ++++++++++++--- 5 files changed, 366 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 23ff9287..e67772e6 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,14 @@ adaptive_lighting: | `lights` | yes | entity_id(s) of lights, if not specified, all lights in the switch are selected. | | `manual_control` | yes | Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true | +`adaptive_lighting.change_switch_settings` (new in 1.7.0) Change any of the above configuration options of Adaptive Lighting (such as `sunrise_time` or `prefer_rgb_color`) with a service call directly from your script/automation. + +| DISALLOWED service data | Description | +| ------------------------- | --------------------------------------------------------------------------------------------------- | +| `entity_id` | You cannot change the switch's unique_id, it's already been registered | +| `lights` | See above, you may call adaptive_lighting.apply() with your lights or create a new config instead | +| `name` | See above. You can already rename your switch's display name in Home Assistant's UI. | +| `interval` | Nope. The interval is only used once when the config loads. A config change and restart is required | ## Automation examples @@ -172,6 +180,53 @@ Toggle multiple Adaptive Lighting switches to "sleep mode" using an `input_boole - switch.adaptive_lighting_sleep_mode_bedroom ``` +Set your sunrise and sunset time based on your alarm. The below script sets sunset_time exactly 12 hours after the custom sunrise time. + +``` +iphone_carly_wakeup: + alias: iPhone Carly Wakeup + sequence: + - condition: state + entity_id: input_boolean.carly_iphone_wakeup + state: 'off' + - service: input_datetime.set_datetime + target: + entity_id: input_datetime.carly_iphone_wakeup + data: + time: '{{ now().strftime("%H:%M:%S") }}' + - service: input_boolean.turn_on + target: + entity_id: input_boolean.carly_iphone_wakeup + - repeat: + count: '{{ (states.switch | map(attribute=''entity_id'') | select(">","switch.adaptive_lighting_al_") | + select("<", "switch.adaptive_lighting_al_z") | join(",")).split(",")|length + }}' + sequence: + - service: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_al_den_ceilingfan_lights + sunrise_time: '{{ now().strftime("%H:%M:%S") }}' + sunset_time: '{{ (as_timestamp(now()) + 12*60*60) | timestamp_custom("%H:%M:%S") + }}' + - service: script.turn_on + target: + entity_id: script.run_wakeup_routine + - service: input_boolean.turn_off + target: + entity_id: + - input_boolean.carly_iphone_winddown + - input_boolean.carly_iphone_bedtime + - service: input_datetime.set_datetime + target: + entity_id: input_datetime.wakeup_time + data: + time: '{{ now().strftime("%H:%M:%S") }}' + - service: script.adaptive_lighting_disable_sleep_mode + mode: queued + icon: mdi:weather-sunset + max: 10 +``` + # Other See the documentation of the PR at https://deploy-preview-14877--home-assistant-docs.netlify.app/integrations/adaptive_lighting/ and [this video on Reddit](https://www.reddit.com/r/homeassistant/comments/jabhso/ha_has_it_before_apple_has_even_finished_it_i/) to see how to add the integration and set the options. diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index dc928a6b..78aa8a26 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -41,6 +41,9 @@ async def async_setup(hass: HomeAssistant, config: dict[str, Any]): # This will reload any changes the user made to any YAML configurations. await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + # quickly populate data for change_switch_settings before actually loading integration. + # update_services_yaml() + if DOMAIN in config: for entry in config[DOMAIN]: hass.async_create_task( diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index e1f13499..f4b751e5 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -66,6 +66,8 @@ SERVICE_SET_MANUAL_CONTROL = "set_manual_control" CONF_MANUAL_CONTROL = "manual_control" SERVICE_APPLY = "apply" CONF_TURN_ON_LIGHTS = "turn_on_lights" +SERVICE_CHANGE_SWITCH_SETTINGS = "change_switch_settings" +CONF_USE_DEFAULTS = "use_defaults" CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0 TURNING_OFF_DELAY = 5 diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 05e706e9..8524dd64 100755 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -41,6 +41,7 @@ apply: example: false selector: boolean: + set_manual_control: description: Mark whether a light is 'manually controlled'. fields: @@ -65,3 +66,180 @@ set_manual_control: default: true selector: boolean: + +change_switch_settings: + description: "Change any settings you'd like in the switch. All options here are the same as in the config flow." + fields: + entity_id: + description: "entity_id of the Adaptive Lighting switch." + required: true + selector: + entity: + domain: switch + use_defaults: + description: "(default: 'current' for current settings) You can set this to 'factory', 'configuration', or 'current' to reset the variables not being set with this service call. 'current' leaves them as is, 'configuration' resets to whatever already initializes at startup, 'factory' resets to the default values listed in the documentation." + example: "current" + required: false + default: "current" + selector: + select: + options: + - "current" + - "configuration" + - "factory" + turn_on_lights: + description: "Turn on the lights that are off, default: false" + example: false + required: false + selector: + boolean: + initial_transition: + description: "initial_transition: When lights turn 'off' to 'on'. (seconds)" + example: 1 + required: false + selector: + text: + sleep_transition: + description: "sleep_transition: When 'sleep_state' changes. (seconds)" + example: 1 + required: false + selector: + text: + max_brightness: + description: "max_brightness: Highest brightness of lights during a cycle. (%)" + required: false + example: 100 + selector: + text: + max_color_temp: + description: "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)" + required: false + example: 5500 + selector: + text: + min_brightness: + description: "min_brightness: Lowest brightness of lights during a cycle. (%)" + required: false + example: 1 + selector: + text: + min_color_temp: + description: "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)" + required: false + example: 2000 + selector: + text: + only_once: + description: "only_once: Only adapt the lights when turning them on." + example: false + required: false + selector: + boolean: + prefer_rgb_color: + description: "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible." + required: false + example: false + selector: + boolean: + separate_turn_on_commands: + description: "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights)." + required: false + example: false + selector: + boolean: + send_split_delay: + description: "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly." + required: false + example: 0 + selector: + boolean: + sleep_brightness: + description: "sleep_brightness, Brightness setting for Sleep Mode. (%)" + required: false + example: 1 + selector: + text: + sleep_rgb_or_color_temp: + description: "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'" + required: false + example: "color_temp" + selector: + select: + options: + - "rgb_color" + - "color_temp" + sleep_rgb_color: + description: "sleep_rgb_color, in RGB" + required: false + selector: + color_rgb: + sleep_color_temp: + description: "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)" + required: false + example: 1000 + selector: + text: + sunrise_offset: + description: sunrise_offset, in +/- seconds (integer) + required: false + example: 0 + selector: + number: + min: 0 + max: 86300 + sunrise_time: + description: sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location) + required: false + example: "" + selector: + time: + sunset_offset: + description: sunset_offset, in +/- seconds (integer) + required: false + example: "" + selector: + number: + min: 0 + max: 86300 + sunset_time: + description: sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location) + example: "" + required: false + selector: + time: + max_sunrise_time: + description: "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)" + example: "" + required: false + selector: + time: + min_sunset_time: + description: "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)" + example: "" + required: false + selector: + time: + take_over_control: + description: "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on." + required: false + example: true + selector: + boolean: + detect_non_ha_changes: + description: "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)" + required: false + example: false + selector: + boolean: + transition: + description: "Transition time when applying a change to the lights (seconds)" + required: false + example: 45 + selector: + text: + adapt_delay: + description: "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering." + required: false + example: 0 + selector: + text: diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 0b1d00de..dba0160b 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -72,7 +72,7 @@ from homeassistant.core import ( State, callback, ) -from homeassistant.helpers import entity_registry +from homeassistant.helpers import entity_platform, entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( async_track_state_change_event, @@ -125,6 +125,7 @@ from .const import ( CONF_TAKE_OVER_CONTROL, CONF_TRANSITION, CONF_TURN_ON_LIGHTS, + CONF_USE_DEFAULTS, DOMAIN, EXTRA_VALIDATION, ICON_BRIGHTNESS, @@ -132,6 +133,7 @@ from .const import ( ICON_MAIN, ICON_SLEEP, SERVICE_APPLY, + SERVICE_CHANGE_SWITCH_SETTINGS, SERVICE_SET_MANUAL_CONTROL, SLEEP_MODE_SWITCH, SUN_EVENT_MIDNIGHT, @@ -187,6 +189,11 @@ def _int_to_bytes(i: int, signed: bool = False) -> bytes: return i.to_bytes((bits + 7) // 8, "little", signed=signed) +def int_between(min_int, max_int): + """Return an integer between 'min_int' and 'max_int'.""" + return vol.All(vol.Coerce(int), vol.Range(min=min_int, max=max_int)) + + def _short_hash(string: str, length: int = 4) -> str: """Create a hash of 'string' with length 'length'.""" str_hash_bytes = _int_to_bytes(hash(string), signed=True) @@ -349,6 +356,48 @@ def _get_switches_from_service_call( raise ValueError("adaptive-lighting: User sent incorrect data to service call") +async def handle_change_switch_settings( + switch: AdaptiveSwitch, service_call: ServiceCall +): + """Allows HASS to change config values via a service call.""" + data = service_call.data + + which = data.get(CONF_USE_DEFAULTS, "current") + if which == "current": # use whatever we're already using. + defaults = switch._current_settings # pylint: disable=protected-access + # not needed since validate() does this part for us + elif which == "factory": # use actual defaults listed in the documentation + defaults = {key: default for key, default, _ in VALIDATION_TUPLES} + elif ( + which == "configuration" + ): # use whatever's in the config flow or configuration.yaml + defaults = switch._config_backup # pylint: disable=protected-access + else: + defaults = None + + switch._set_changeable_settings( + data=data, + defaults=defaults, + ) + + _LOGGER.debug( + "Called 'adaptive_lighting.change_switch_settings' service with '%s'", + data, + ) + + all_lights = switch._lights # pylint: disable=protected-access + switch.turn_on_off_listener.reset(*all_lights, reset_manual_control=False) + # pylint: disable=protected-access + if switch.is_on: + await switch._update_attrs_and_maybe_adapt_lights( + all_lights, + transition=switch._initial_transition, + force=True, + context=switch.create_context("service", parent=service_call.context), + ) + # pylint: enable=protected-access + + @callback def _fire_manual_control_event( switch: AdaptiveSwitch, light: str, context: Context, is_async=True @@ -506,13 +555,38 @@ async def async_setup_entry( ), ) + args = {vol.Optional(CONF_USE_DEFAULTS, default="current"): cv.string} + for k, _, valid in VALIDATION_TUPLES: + # Modifying these after initialization isn't possible (yet) + if k != CONF_INTERVAL and k != CONF_NAME and k != CONF_LIGHTS: + args[vol.Optional(k)] = valid + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + SERVICE_CHANGE_SWITCH_SETTINGS, + args, + handle_change_switch_settings, + ) -def validate(config_entry: ConfigEntry): + +def validate(config_entry: ConfigEntry, **kwargs): """Get the options and data from the config_entry and add defaults.""" - defaults = {key: default for key, default, _ in VALIDATION_TUPLES} - data = deepcopy(defaults) - data.update(config_entry.options) # come from options flow - data.update(config_entry.data) # all yaml settings come from data + # defaults and data will exist only if this is called from change_switch_settings + defaults = kwargs.get("defaults") + service_data = kwargs.get("data") + if defaults is None: + defaults = {key: default for key, default, _ in VALIDATION_TUPLES} + if config_entry is not None: + # Is this deepcopy necessary? + # We're already creating a new array for the defaults variable... + # afterwards defaults is never used again. + data = deepcopy(defaults) + data.update(config_entry.options) # come from options flow + data.update(config_entry.data) # all yaml settings come from data + else: + # no idea how to clear original data from memory + # hopefully it does it automatically, otherwise TODO. + data = deepcopy(defaults) + data.update(service_data) data = {key: replace_none_str(value) for key, value in data.items()} for key, (validate_value, _) in EXTRA_VALIDATION.items(): value = data.get(key) @@ -682,31 +756,25 @@ def _attributes_have_changed( class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" - def __init__( + def _set_changeable_settings( self, - hass, - config_entry: ConfigEntry, - turn_on_off_listener: TurnOnOffListener, - sleep_mode_switch: SimpleSwitch, - adapt_color_switch: SimpleSwitch, - adapt_brightness_switch: SimpleSwitch, + data: dict, + defaults: dict, ): - """Initialize the Adaptive Lighting switch.""" - self.hass = hass - self.turn_on_off_listener = turn_on_off_listener - self.sleep_mode_switch = sleep_mode_switch - self.adapt_color_switch = adapt_color_switch - self.adapt_brightness_switch = adapt_brightness_switch + # Should only contain the settings we want the users to be able to change during runtime. + data = validate( + config_entry=None, + data=data, + defaults=defaults, + ) - data = validate(config_entry) - self._name = data[CONF_NAME] - self._lights = data[CONF_LIGHTS] + # backup data for use in change_switch_settings "current" CONF_USE_DEFAULTS + self._current_settings = data self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] self._include_config_in_attributes = data[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES] self._initial_transition = data[CONF_INITIAL_TRANSITION] self._sleep_transition = data[CONF_SLEEP_TRANSITION] - self._interval = data[CONF_INTERVAL] self._only_once = data[CONF_ONLY_ONCE] self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR] self._separate_turn_on_commands = data[CONF_SEPARATE_TURN_ON_COMMANDS] @@ -742,6 +810,43 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): time_zone=self.hass.config.time_zone, transition=data[CONF_TRANSITION], ) + _LOGGER.debug( + "%s: Set switch settings for lights '%s'. now using data: '%s'", + self._name, + self._lights, + data, + ) + + def __init__( + self, + hass, + config_entry: ConfigEntry, + turn_on_off_listener: TurnOnOffListener, + sleep_mode_switch: SimpleSwitch, + adapt_color_switch: SimpleSwitch, + adapt_brightness_switch: SimpleSwitch, + ): + """Initialize the Adaptive Lighting switch.""" + # Set attributes we DON'T want users modifying + # during runtime here. + self.hass = hass + self.turn_on_off_listener = turn_on_off_listener + self.sleep_mode_switch = sleep_mode_switch + self.adapt_color_switch = adapt_color_switch + self.adapt_brightness_switch = adapt_brightness_switch + + data = validate(config_entry) + + self._name = data[CONF_NAME] + self._interval = data[CONF_INTERVAL] + self._lights = data[CONF_LIGHTS] + + # backup data for use in change_switch_settings "configuration" CONF_USE_DEFAULTS + self._config_backup = deepcopy(data) + self._set_changeable_settings( + data=data, + defaults=None, + ) # Set other attributes self._icon = ICON_MAIN From 1ca3bf0f6ad3c1daf403bae56cc6384b3770ebb6 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 27 Mar 2023 18:56:54 -0700 Subject: [PATCH 0517/1077] Retroactive review changes for #443 (#485) * Retroactive review changes for #443 * Move up __init__ * Add test for test_change_switch_settings_service * Add docs * Add test for defaults * Remove unused function * Add skip var * move pylint marker * Refactor validate * Update README.md --------- Co-authored-by: Benjamin Auquite --- README.md | 95 +++++----- .../adaptive_lighting/__init__.py | 3 - .../adaptive_lighting/manifest.json | 2 +- custom_components/adaptive_lighting/switch.py | 171 +++++++++--------- tests/test_switch.py | 56 ++++++ 5 files changed, 191 insertions(+), 136 deletions(-) diff --git a/README.md b/README.md index e67772e6..c4e3c66e 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ adaptive_lighting: ### Services -`adaptive_lighting.**apply**` applies Adaptive Lighting settings to lights on demand. +`adaptive_lighting.apply` applies Adaptive Lighting settings to lights on demand. | Service data attribute | Optional | Description | | ---------------------- | -------- | -------------------------------------------------------------------------------------------- | @@ -132,16 +132,23 @@ adaptive_lighting: `adaptive_lighting.change_switch_settings` (new in 1.7.0) Change any of the above configuration options of Adaptive Lighting (such as `sunrise_time` or `prefer_rgb_color`) with a service call directly from your script/automation. -| DISALLOWED service data | Description | -| ------------------------- | --------------------------------------------------------------------------------------------------- | -| `entity_id` | You cannot change the switch's unique_id, it's already been registered | -| `lights` | See above, you may call adaptive_lighting.apply() with your lights or create a new config instead | -| `name` | See above. You can already rename your switch's display name in Home Assistant's UI. | -| `interval` | Nope. The interval is only used once when the config loads. A config change and restart is required | +| Service data attribute | Description | +| ------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `use_defaults` | (default: 'current' for current settings) You can set this to 'factory', 'configuration', or 'current' to reset the variables not being set with this service call. 'current' leaves them as is, 'configuration' resets to whatever already initializes at startup, 'factory' resets to the default values listed in the documentation. | +| all other keys except the ones in the table below | See above, you may call `adaptive_lighting.apply` with your lights or create a new config instead | + + +| **DISALLOWED** service data | Description | +| --------------------------- | ------------------------------------------------------------------------------------------------- | +| `entity_id` | You cannot change the switch's `entity_id`, it's already been registered | +| `lights` | See above, you may call `adaptive_lighting.apply` with your lights or create a new config instead | +| `name` | See above. You can already rename your switch's display name in Home Assistant's UI. | +| `interval` | The interval is only used once when the config loads. A config change and restart is required | ## Automation examples Reset the `manual_control` status of a light after an hour. + ```yaml - alias: "Adaptive lighting: reset manual_control after 1 hour" mode: parallel @@ -186,42 +193,46 @@ Set your sunrise and sunset time based on your alarm. The below script sets suns iphone_carly_wakeup: alias: iPhone Carly Wakeup sequence: - - condition: state - entity_id: input_boolean.carly_iphone_wakeup - state: 'off' - - service: input_datetime.set_datetime - target: - entity_id: input_datetime.carly_iphone_wakeup - data: - time: '{{ now().strftime("%H:%M:%S") }}' - - service: input_boolean.turn_on - target: + - condition: state entity_id: input_boolean.carly_iphone_wakeup - - repeat: - count: '{{ (states.switch | map(attribute=''entity_id'') | select(">","switch.adaptive_lighting_al_") | - select("<", "switch.adaptive_lighting_al_z") | join(",")).split(",")|length - }}' - sequence: - - service: adaptive_lighting.change_switch_settings - data: - entity_id: switch.adaptive_lighting_al_den_ceilingfan_lights - sunrise_time: '{{ now().strftime("%H:%M:%S") }}' - sunset_time: '{{ (as_timestamp(now()) + 12*60*60) | timestamp_custom("%H:%M:%S") - }}' - - service: script.turn_on - target: - entity_id: script.run_wakeup_routine - - service: input_boolean.turn_off - target: - entity_id: - - input_boolean.carly_iphone_winddown - - input_boolean.carly_iphone_bedtime - - service: input_datetime.set_datetime - target: - entity_id: input_datetime.wakeup_time - data: - time: '{{ now().strftime("%H:%M:%S") }}' - - service: script.adaptive_lighting_disable_sleep_mode + state: "off" + - service: input_datetime.set_datetime + target: + entity_id: input_datetime.carly_iphone_wakeup + data: + time: '{{ now().strftime("%H:%M:%S") }}' + - service: input_boolean.turn_on + target: + entity_id: input_boolean.carly_iphone_wakeup + - repeat: + count: > + {{ (states.switch + | map(attribute="entity_id") + | select(">","switch.adaptive_lighting_al_") + | select("<", "switch.adaptive_lighting_al_z") + | join(",") + ).split(",") | length }} + sequence: + - service: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_al_den_ceilingfan_lights + sunrise_time: '{{ now().strftime("%H:%M:%S") }}' + sunset_time: > + {{ (as_timestamp(now()) + 12*60*60) | timestamp_custom("%H:%M:%S") }} + - service: script.turn_on + target: + entity_id: script.run_wakeup_routine + - service: input_boolean.turn_off + target: + entity_id: + - input_boolean.carly_iphone_winddown + - input_boolean.carly_iphone_bedtime + - service: input_datetime.set_datetime + target: + entity_id: input_datetime.wakeup_time + data: + time: '{{ now().strftime("%H:%M:%S") }}' + - service: script.adaptive_lighting_disable_sleep_mode mode: queued icon: mdi:weather-sunset max: 10 diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 78aa8a26..dc928a6b 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -41,9 +41,6 @@ async def async_setup(hass: HomeAssistant, config: dict[str, Any]): # This will reload any changes the user made to any YAML configurations. await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - # quickly populate data for change_switch_settings before actually loading integration. - # update_services_yaml() - if DOMAIN in config: for entry in config[DOMAIN]: hass.async_create_task( diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 3fcd3b99..8c4233f9 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": [], - "version": "1.6.0" + "version": "1.7.0" } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index dba0160b..895b4908 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -189,11 +189,6 @@ def _int_to_bytes(i: int, signed: bool = False) -> bytes: return i.to_bytes((bits + 7) // 8, "little", signed=signed) -def int_between(min_int, max_int): - """Return an integer between 'min_int' and 'max_int'.""" - return vol.All(vol.Coerce(int), vol.Range(min=min_int, max=max_int)) - - def _short_hash(string: str, length: int = 4) -> str: """Create a hash of 'string' with length 'length'.""" str_hash_bytes = _int_to_bytes(hash(string), signed=True) @@ -365,12 +360,10 @@ async def handle_change_switch_settings( which = data.get(CONF_USE_DEFAULTS, "current") if which == "current": # use whatever we're already using. defaults = switch._current_settings # pylint: disable=protected-access - # not needed since validate() does this part for us elif which == "factory": # use actual defaults listed in the documentation defaults = {key: default for key, default, _ in VALIDATION_TUPLES} - elif ( - which == "configuration" - ): # use whatever's in the config flow or configuration.yaml + elif which == "configuration": + # use whatever's in the config flow or configuration.yaml defaults = switch._config_backup # pylint: disable=protected-access else: defaults = None @@ -387,15 +380,13 @@ async def handle_change_switch_settings( all_lights = switch._lights # pylint: disable=protected-access switch.turn_on_off_listener.reset(*all_lights, reset_manual_control=False) - # pylint: disable=protected-access if switch.is_on: - await switch._update_attrs_and_maybe_adapt_lights( + await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access all_lights, transition=switch._initial_transition, force=True, context=switch.create_context("service", parent=service_call.context), ) - # pylint: enable=protected-access @callback @@ -509,8 +500,8 @@ async def async_setup_entry( _fire_manual_control_event(this_switch, light, service_call.context) else: this_switch.turn_on_off_listener.reset(*all_lights) - # pylint: disable=protected-access if this_switch.is_on: + # pylint: disable=protected-access await this_switch._update_attrs_and_maybe_adapt_lights( all_lights, transition=this_switch._initial_transition, @@ -556,9 +547,10 @@ async def async_setup_entry( ) args = {vol.Optional(CONF_USE_DEFAULTS, default="current"): cv.string} + # Modifying these after init isn't possible + skip = (CONF_INTERVAL, CONF_NAME, CONF_LIGHTS) for k, _, valid in VALIDATION_TUPLES: - # Modifying these after initialization isn't possible (yet) - if k != CONF_INTERVAL and k != CONF_NAME and k != CONF_LIGHTS: + if k not in skip: args[vol.Optional(k)] = valid platform = entity_platform.current_platform.get() platform.async_register_entity_service( @@ -568,24 +560,24 @@ async def async_setup_entry( ) -def validate(config_entry: ConfigEntry, **kwargs): +def validate( + config_entry: ConfigEntry, + service_data: dict[str, Any] | None = None, + defaults: dict[str, Any] | None = None, +): """Get the options and data from the config_entry and add defaults.""" - # defaults and data will exist only if this is called from change_switch_settings - defaults = kwargs.get("defaults") - service_data = kwargs.get("data") if defaults is None: - defaults = {key: default for key, default, _ in VALIDATION_TUPLES} + data = {key: default for key, default, _ in VALIDATION_TUPLES} + else: + data = defaults + if config_entry is not None: - # Is this deepcopy necessary? - # We're already creating a new array for the defaults variable... - # afterwards defaults is never used again. - data = deepcopy(defaults) + assert service_data is None + assert defaults is None data.update(config_entry.options) # come from options flow data.update(config_entry.data) # all yaml settings come from data else: - # no idea how to clear original data from memory - # hopefully it does it automatically, otherwise TODO. - data = deepcopy(defaults) + assert service_data is not None data.update(service_data) data = {key: replace_none_str(value) for key, value in data.items()} for key, (validate_value, _) in EXTRA_VALIDATION.items(): @@ -756,67 +748,6 @@ def _attributes_have_changed( class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" - def _set_changeable_settings( - self, - data: dict, - defaults: dict, - ): - # Should only contain the settings we want the users to be able to change during runtime. - data = validate( - config_entry=None, - data=data, - defaults=defaults, - ) - - # backup data for use in change_switch_settings "current" CONF_USE_DEFAULTS - self._current_settings = data - - self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] - self._include_config_in_attributes = data[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES] - self._initial_transition = data[CONF_INITIAL_TRANSITION] - self._sleep_transition = data[CONF_SLEEP_TRANSITION] - self._only_once = data[CONF_ONLY_ONCE] - self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR] - self._separate_turn_on_commands = data[CONF_SEPARATE_TURN_ON_COMMANDS] - self._take_over_control = data[CONF_TAKE_OVER_CONTROL] - self._transition = data[CONF_TRANSITION] - self._adapt_delay = data[CONF_ADAPT_DELAY] - self._send_split_delay = data[CONF_SEND_SPLIT_DELAY] - _loc = get_astral_location(self.hass) - if isinstance(_loc, tuple): - # Astral v2.2 - location, _ = _loc - else: - # Astral v1 - location = _loc - - self._sun_light_settings = SunLightSettings( - name=self._name, - astral_location=location, - max_brightness=data[CONF_MAX_BRIGHTNESS], - max_color_temp=data[CONF_MAX_COLOR_TEMP], - min_brightness=data[CONF_MIN_BRIGHTNESS], - min_color_temp=data[CONF_MIN_COLOR_TEMP], - sleep_brightness=data[CONF_SLEEP_BRIGHTNESS], - sleep_color_temp=data[CONF_SLEEP_COLOR_TEMP], - sleep_rgb_color=data[CONF_SLEEP_RGB_COLOR], - sleep_rgb_or_color_temp=data[CONF_SLEEP_RGB_OR_COLOR_TEMP], - sunrise_offset=data[CONF_SUNRISE_OFFSET], - sunrise_time=data[CONF_SUNRISE_TIME], - max_sunrise_time=data[CONF_MAX_SUNRISE_TIME], - sunset_offset=data[CONF_SUNSET_OFFSET], - sunset_time=data[CONF_SUNSET_TIME], - min_sunset_time=data[CONF_MIN_SUNSET_TIME], - time_zone=self.hass.config.time_zone, - transition=data[CONF_TRANSITION], - ) - _LOGGER.debug( - "%s: Set switch settings for lights '%s'. now using data: '%s'", - self._name, - self._lights, - data, - ) - def __init__( self, hass, @@ -827,8 +758,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): adapt_brightness_switch: SimpleSwitch, ): """Initialize the Adaptive Lighting switch.""" - # Set attributes we DON'T want users modifying - # during runtime here. + # Set attributes that can't be modified during runtime self.hass = hass self.turn_on_off_listener = turn_on_off_listener self.sleep_mode_switch = sleep_mode_switch @@ -887,6 +817,67 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): data, ) + def _set_changeable_settings( + self, + data: dict, + defaults: dict, + ): + # Only pass settings users can change during runtime + data = validate( + config_entry=None, + service_data=data, + defaults=defaults, + ) + + # backup data for use in change_switch_settings "current" CONF_USE_DEFAULTS + self._current_settings = data + + self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] + self._include_config_in_attributes = data[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES] + self._initial_transition = data[CONF_INITIAL_TRANSITION] + self._sleep_transition = data[CONF_SLEEP_TRANSITION] + self._only_once = data[CONF_ONLY_ONCE] + self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR] + self._separate_turn_on_commands = data[CONF_SEPARATE_TURN_ON_COMMANDS] + self._take_over_control = data[CONF_TAKE_OVER_CONTROL] + self._transition = data[CONF_TRANSITION] + self._adapt_delay = data[CONF_ADAPT_DELAY] + self._send_split_delay = data[CONF_SEND_SPLIT_DELAY] + _loc = get_astral_location(self.hass) + if isinstance(_loc, tuple): + # Astral v2.2 + location, _ = _loc + else: + # Astral v1 + location = _loc + + self._sun_light_settings = SunLightSettings( + name=self._name, + astral_location=location, + max_brightness=data[CONF_MAX_BRIGHTNESS], + max_color_temp=data[CONF_MAX_COLOR_TEMP], + min_brightness=data[CONF_MIN_BRIGHTNESS], + min_color_temp=data[CONF_MIN_COLOR_TEMP], + sleep_brightness=data[CONF_SLEEP_BRIGHTNESS], + sleep_color_temp=data[CONF_SLEEP_COLOR_TEMP], + sleep_rgb_color=data[CONF_SLEEP_RGB_COLOR], + sleep_rgb_or_color_temp=data[CONF_SLEEP_RGB_OR_COLOR_TEMP], + sunrise_offset=data[CONF_SUNRISE_OFFSET], + sunrise_time=data[CONF_SUNRISE_TIME], + max_sunrise_time=data[CONF_MAX_SUNRISE_TIME], + sunset_offset=data[CONF_SUNSET_OFFSET], + sunset_time=data[CONF_SUNSET_TIME], + min_sunset_time=data[CONF_MIN_SUNSET_TIME], + time_zone=self.hass.config.time_zone, + transition=data[CONF_TRANSITION], + ) + _LOGGER.debug( + "%s: Set switch settings for lights '%s'. now using data: '%s'", + self._name, + self._lights, + data, + ) + @property def name(self): """Return the name of the device if any.""" diff --git a/tests/test_switch.py b/tests/test_switch.py index 321894cf..8584bb1f 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -13,6 +13,7 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_DETECT_NON_HA_CHANGES, CONF_INITIAL_TRANSITION, CONF_MANUAL_CONTROL, + CONF_MAX_BRIGHTNESS, CONF_MIN_COLOR_TEMP, CONF_PREFER_RGB_COLOR, CONF_SEPARATE_TURN_ON_COMMANDS, @@ -21,12 +22,14 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_SUNSET_TIME, CONF_TRANSITION, CONF_TURN_ON_LIGHTS, + CONF_USE_DEFAULTS, DEFAULT_MAX_BRIGHTNESS, DEFAULT_NAME, DEFAULT_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_COLOR_TEMP, DOMAIN, SERVICE_APPLY, + SERVICE_CHANGE_SWITCH_SETTINGS, SERVICE_SET_MANUAL_CONTROL, SLEEP_MODE_SWITCH, UNDO_UPDATE_LISTENER, @@ -65,6 +68,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util.color import color_temperature_mired_to_kelvin import homeassistant.util.dt as dt_util import pytest +import voluptuous.error from tests.common import MockConfigEntry, mock_area_registry from tests.components.demo.test_light import ENTITY_LIGHT @@ -928,3 +932,55 @@ async def test_area(hass): switch.turn_on_off_listener.last_service_data, ) assert light.entity_id not in switch.turn_on_off_listener.last_service_data + + +async def test_change_switch_settings_service(hass): + """Test adaptive_lighting.change_switch_settings service.""" + switch, (_, _, light) = await setup_lights_and_switch(hass) + entity_id = light.entity_id + assert entity_id not in switch._lights + + async def change_switch_settings(**kwargs): + await hass.services.async_call( + DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + { + ATTR_ENTITY_ID: ENTITY_SWITCH, + **kwargs, + }, + blocking=True, + ) + await hass.async_block_till_done() + + # Test changing sunrise offset + assert switch._sun_light_settings.sunrise_offset.total_seconds() == 0 + await change_switch_settings(**{CONF_SUNRISE_OFFSET: 10}) + assert switch._sun_light_settings.sunrise_offset.total_seconds() == 10 + + # Test changing max brightness + assert switch._sun_light_settings.max_brightness == 100 + await change_switch_settings(**{CONF_MAX_BRIGHTNESS: 50}) + assert switch._sun_light_settings.max_brightness == 50 + + # Test changing to illegal max brightness + with pytest.raises( + voluptuous.error.MultipleInvalid, + match="value must be at most 100 for dictionary", + ): + await change_switch_settings(**{CONF_MAX_BRIGHTNESS: 5000}) + + # Change CONF_MIN_COLOR_TEMP, the factory default is 2000, but setup_lights_and_switch + # sets it to 2500 + assert switch._sun_light_settings.min_color_temp == 2500 + + # testing with "factory" should change it to 2000 + await change_switch_settings(**{CONF_USE_DEFAULTS: "factory"}) + assert switch._sun_light_settings.min_color_temp == 2000 + + # testing with "current" should not change things + await change_switch_settings(**{CONF_USE_DEFAULTS: "current"}) + assert switch._sun_light_settings.min_color_temp == 2000 + + # testing with "configuration" should revert back to 2500 + await change_switch_settings(**{CONF_USE_DEFAULTS: "configuration"}) + assert switch._sun_light_settings.min_color_temp == 2500 From 2de4b7415b71c5fc9b7e5098f146ca4f15a815ed Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 29 Mar 2023 21:47:57 -0700 Subject: [PATCH 0518/1077] Refactor find_switch_for_lights and more small refactors (#488) * Refactor find_switch_for_lights * Refactor * style * renames --- custom_components/adaptive_lighting/switch.py | 153 ++++++++---------- 1 file changed, 65 insertions(+), 88 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 895b4908..ef8b42aa 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -239,52 +239,50 @@ def _split_service_data(service_data, adapt_brightness, adapt_color): return service_datas -def _find_switch_with_any_of_lights( - hass: HomeAssistant, - lights: list[str], - service_call: ServiceCall, -) -> AdaptiveSwitch: - """Find the switch that controls the lights in 'lights'.""" +def _get_switches_with_lights( + hass: HomeAssistant, lights: list[str] +) -> list[AdaptiveSwitch]: + """Get all switches that control at least one of the lights passed.""" config_entries = hass.config_entries.async_entries(DOMAIN) data = hass.data[DOMAIN] - switches = {} + switches = [] for config in config_entries: - # this check is necessary as there seems to always be an extra config - # entry that doesn't contain any data. I believe this happens when the - # integration exists, but is disabled by the user in HASS. - if config.entry_id in data: - switch = data[config.entry_id]["instance"] - all_check_lights = _expand_light_groups(hass, lights) - switch._expand_light_groups() - if set(switch._lights) & set(all_check_lights): - switches[config.entry_id] = switch + entry = data.get(config.entry_id) + if entry is None: # entry might be disabled and therefore missing + continue + switch = data[config.entry_id]["instance"] + all_check_lights = _expand_light_groups(hass, lights) + switch._expand_light_groups() + # Check if any of the lights are in the switch's lights + if set(switch._lights) & set(all_check_lights): + switches.append(switch) + return switches + +def find_switch_for_lights( + hass: HomeAssistant, + lights: list[str], + is_on: bool = False, +) -> AdaptiveSwitch: + """Find the switch that controls the lights in 'lights'.""" + switches = _get_switches_with_lights(hass, lights, is_on) if len(switches) == 1: - return next(iter(switches.values())) - - if len(switches) > 1: - _LOGGER.error( - "Invalid service data: Light(s) %s found in multiple switch configs (%s)." - " You must pass a switch under 'entity_id'. See the README for" - " details. Got %s", - lights, - list(switches.keys()), - service_call.data, - ) + return switches[0] + elif len(switches) > 1: + on_switches = [s for s in switches if s.is_on] + if len(on_switches) == 1: + # Of the multiple switches, only one is on + return on_switches[0] raise ValueError( - "adaptive-lighting: Light(s) %s found in multiple switch configs.", - lights, + f"find_switch_for_lights: Light(s) {lights} found in multiple switch configs" + f" ({[s.entity_id for s in switches]}). You must pass a switch under" + f" 'entity_id'." ) else: - _LOGGER.error( - "Invalid service data: Light was not found in any of your switch's configs." - " You must either include the light(s) that is/are in the integration config, or" - " pass a switch under 'entity_id'. See the README for details. Got %s", - service_call.data, - ) raise ValueError( - "adaptive-lighting: Light(s) %s not found in any switch's configuration.", - lights, + f"find_switch_for_lights: Light(s) {lights} not found in any switch's" + f" configuration. You must either include the light(s) that is/are" + f" in the integration config, or pass a switch under 'entity_id'." ) @@ -293,38 +291,24 @@ def _find_switch_with_any_of_lights( def _get_switches_from_service_call( hass: HomeAssistant, service_call: ServiceCall ) -> list[AdaptiveSwitch]: - _LOGGER.debug( - "Function '_get_switches_from_service_call' called with service data:\n'%s'", - service_call.data, - ) data = service_call.data lights = data[CONF_LIGHTS] switch_entity_ids: list[str] | None = data.get("entity_id") + if not lights and not switch_entity_ids: - _LOGGER.debug( - "If you intended to adapt every single light on every single switch, please inform the" - " developers at https://github.com/basnijholt/adaptive-lighting of your use case." - " Currently, you must pass either an adaptive-lighting switch or the lights to" - " an `adaptive_lighting` service call." - ) - _LOGGER.error( - "Invalid service data passed to adaptive-lighting service call -" - " you must pass either a switch or a light's entity ID. Service data:\n%s", - service_call.data, - ) raise ValueError( - "adaptive-lighting: No switch or light was passed to service call." + "adaptive-lighting: Neither a switch nor a light was provided in the service call." + " If you intend to adapt all lights on all switches, please inform the developers at" + " https://github.com/basnijholt/adaptive-lighting about your use case." + " Currently, you must pass either an adaptive-lighting switch or the lights to an" + " `adaptive_lighting` service call." ) if switch_entity_ids is not None: if len(switch_entity_ids) > 1 and lights: - _LOGGER.error( - "Invalid service data: cannot pass multiple switch entities while also passing" - " lights. Service data received: %s", - service_call.data, - ) raise ValueError( - "adaptive-lighting: Multiple switches were passed with lights argument" + f"adaptive-lighting: Cannot pass multiple switches with lights argument." + f" Invalid service data received: {service_call.data}" ) switches = [] ent_reg = entity_registry.async_get(hass) @@ -335,20 +319,13 @@ def _get_switches_from_service_call( return switches if lights: - switch = _find_switch_with_any_of_lights(hass, lights, service_call) - _LOGGER.debug( - "Switch '%s' found for lights '%s'", - switch.entity_id, - lights, - ) + switch = find_switch_for_lights(hass, lights, service_call) return [switch] - _LOGGER.error( - "Invalid service data passed to adaptive-lighting service call -" - " entities were not found in the integration. Service data:\n%s", - service_call.data, + raise ValueError( + f"adaptive-lighting: Incorrect data provided in service call." + f" Entities not found in the integration. Service data: {service_call.data}" ) - raise ValueError("adaptive-lighting: User sent incorrect data to service call") async def handle_change_switch_settings( @@ -457,24 +434,24 @@ async def async_setup_entry( "Called 'adaptive_lighting.apply' service with '%s'", data, ) - these_switches = _get_switches_from_service_call(hass, service_call) + switches = _get_switches_from_service_call(hass, service_call) lights = data[CONF_LIGHTS] - for this_switch in these_switches: + for switch in switches: if not lights: - all_lights = this_switch._lights # pylint: disable=protected-access + all_lights = switch._lights # pylint: disable=protected-access else: - all_lights = _expand_light_groups(this_switch.hass, lights) - this_switch.turn_on_off_listener.lights.update(all_lights) + all_lights = _expand_light_groups(switch.hass, lights) + switch.turn_on_off_listener.lights.update(all_lights) for light in all_lights: if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light): - await this_switch._adapt_light( # pylint: disable=protected-access + await switch._adapt_light( # pylint: disable=protected-access light, data[CONF_TRANSITION], data[ATTR_ADAPT_BRIGHTNESS], data[ATTR_ADAPT_COLOR], data[CONF_PREFER_RGB_COLOR], force=True, - context=this_switch.create_context( + context=switch.create_context( "service", parent=service_call.context ), ) @@ -487,26 +464,26 @@ async def async_setup_entry( "Called 'adaptive_lighting.set_manual_control' service with '%s'", data, ) - these_switches = _get_switches_from_service_call(hass, service_call) + switches = _get_switches_from_service_call(hass, service_call) lights = data[CONF_LIGHTS] - for this_switch in these_switches: + for switch in switches: if not lights: - all_lights = this_switch._lights # pylint: disable=protected-access + all_lights = switch._lights # pylint: disable=protected-access else: - all_lights = _expand_light_groups(this_switch.hass, lights) + all_lights = _expand_light_groups(switch.hass, lights) if service_call.data[CONF_MANUAL_CONTROL]: for light in all_lights: - this_switch.turn_on_off_listener.manual_control[light] = True - _fire_manual_control_event(this_switch, light, service_call.context) + switch.turn_on_off_listener.manual_control[light] = True + _fire_manual_control_event(switch, light, service_call.context) else: - this_switch.turn_on_off_listener.reset(*all_lights) - if this_switch.is_on: + switch.turn_on_off_listener.reset(*all_lights) + if switch.is_on: # pylint: disable=protected-access - await this_switch._update_attrs_and_maybe_adapt_lights( + await switch._update_attrs_and_maybe_adapt_lights( all_lights, - transition=this_switch._initial_transition, + transition=switch._initial_transition, force=True, - context=this_switch.create_context( + context=switch.create_context( "service", parent=service_call.context ), ) From 26c19525cd40e27ac5129fb689da6162cfe6293c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 30 Mar 2023 13:58:29 -0700 Subject: [PATCH 0519/1077] Rewrite the README (#492) * Rewrite the README * More changes * More * Rewrites * Test collapse * test * Fix * backticks * Rewrite options * emoji headings * chore(docs): update TOC --------- Co-authored-by: basnijholt --- .github/workflows/toc.yaml | 10 ++ README.md | 275 +++++++++++++++++++++---------------- 2 files changed, 167 insertions(+), 118 deletions(-) create mode 100644 .github/workflows/toc.yaml diff --git a/.github/workflows/toc.yaml b/.github/workflows/toc.yaml new file mode 100644 index 00000000..28dac912 --- /dev/null +++ b/.github/workflows/toc.yaml @@ -0,0 +1,10 @@ +on: push +name: TOC Generator +jobs: + generateTOC: + name: TOC Generator + runs-on: ubuntu-latest + steps: + - uses: technote-space/toc-generator@v4 + with: + TOC_TITLE: "" diff --git a/README.md b/README.md index c4e3c66e..ac7897e0 100644 --- a/README.md +++ b/README.md @@ -4,43 +4,67 @@ [![All Contributors](https://img.shields.io/badge/all_contributors-46-orange.svg?style=flat-square)](#contributors-) -# Automatically adapt the brightness and color of lights based on the sun position and take over manual control +# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 ![](https://github.com/home-assistant/brands/raw/b4a168b9af282ef916e120d31091ecd5e3c35e66/core_integrations/adaptive_lighting/icon.png) -_Try out this code by adding https://github.com/basnijholt/adaptive-lighting to your custom repos in [HACS (Home Assistant Community Store)](https://hacs.xyz/) and install it!_ +Adaptive Lighting is a custom component for Home Assistant that intelligently adjusts the brightness and color of your lights 💡 based on the sun's position, while still allowing for manual control. Try it out now by finding it in HACS (Home Assistant Community Store) and installing it! +By automatically adapting the settings of your lights throughout the day, Adaptive Lighting helps maintain your natural circadian rhythm 😴, which can lead to improved sleep, mood, and overall well-being. Experience cooler color temperatures at noon, gradually transitioning to warmer colors at sunset and sunrise. -The `adaptive_lighting` platform changes the settings of your lights throughout the day. -It uses the position of the sun to calculate the color temperature and brightness that is most fitting for that time of the day. -Scientific research has shown that this helps to maintain your natural circadian rhythm (your biological clock) and might lead to improved sleep, mood, and general well-being. +In addition to its regular mode, Adaptive Lighting also offers a "sleep mode" 🌜 which sets your lights to minimal brightness and a very warm color, perfect for winding down at night. -In practical terms, this means that after the sun sets, the brightness of your lights will decrease to a certain minimum brightness, while the color temperature will be at its coolest color temperature at noon, after which it will decrease and reach its warmest color at sunset. -Around sunrise, the opposite will happen. +[[ToC](#books-table-of-contents)] -Additionally, the integration provides a way to define and set your lights in "sleep mode". -When "sleep mode" is enabled, the lights will be at a minimal brightness and have a very warm color. +## :bulb: Features -The integration creates 4 switches (in this example the component's name is `"living_room"`): -1. `switch.adaptive_lighting_living_room`, which turns the Adaptive Lighting integration on or off. It has several attributes that show the current light settings. -2. `switch.adaptive_lighting_sleep_mode_living_room`, which when activated, turns on "sleep mode" (you can set a specific `sleep_brightness` and `sleep_color_temp`). -3. `switch.adaptive_lighting_adapt_brightness_living_room`, which sets whether the integration should adapt the brightness of the lights (if supported by the light). -4. `switch.adaptive_lighting_adapt_color_living_room`, which sets whether the integration should adapt the color of the lights (if supported by the light). +Adaptive Lighting provides four switches (using "living_room" as an example component name): -## Taking back control +- `switch.adaptive_lighting_living_room`: Turn Adaptive Lighting on or off and view current light settings through its attributes. +- `switch.adaptive_lighting_sleep_mode_living_room`: Activate "sleep mode" 😴 and set custom sleep_brightness and sleep_color_temp. +- `switch.adaptive_lighting_adapt_brightness_living_room`: Enable or disable brightness adaptation 🔆 for supported lights. +- `switch.adaptive_lighting_adapt_color_living_room`: Enable or disable color adaptation 🌈 for supported lights. -Although having your lights automatically adapt is great most of the time, there might be times at which you want to set the lights to a different color/brightness and keep it that way. -For this purpose, the integration (when `take_over_control` is enabled) automatically detects whether someone (e.g., person toggling the light switch) or something (automation) changes the lights. -If this happens *and* the light is already on, the light that was changed gets marked as "manually controlled" and the Adaptive Lighting component will stop adapting that light until it turns off and on again (or if you use the service call `adaptive_lighting.set_manual_control`). -This mechanism works by listening to all `light.turn_on` calls that change the color or brightness and by noting that the component did not make the call. -Additionally, there is an option to detect all state changes (when `detect_non_ha_changes` is enabled), so also changes to the lights that were not made by a `light.turn_on` call (e.g., through an app or via something outside of Home Assistant.) -It does this by comparing a light's state to Adaptive Lighting's previously used settings. -Whenever a light gets marked as "manually controlled", an `adaptive_lighting.manual_control` event is fired, such that one can use this information in automations. +### :control_knobs: Regain Manual Control -## Configuration +Adaptive Lighting is designed to automatically detect when you or another source (e.g., automation) manually changes light settings 🕹️. +When this occurs, the affected light is marked as "manually controlled," and Adaptive Lighting will not make further adjustments until the light is turned off and back on or reset using the `adaptive_lighting.set_manual_control` service call. +This feature is available when take_over_control is enabled. -This integration is both fully configurable through YAML _and_ the frontend. (**Configuration** -> **Integrations** -> **Adaptive Lighting**, **Adaptive Lighting** -> **Options**) -Here, the options in the frontend and in YAML have the same names. +Additionally, enabling detect_non_ha_changes allows Adaptive Lighting to detect all state changes, including those made outside of Home Assistant, by comparing the light's state to its previously used settings. +The `adaptive_lighting.manual_control` event is fired when a light is marked as "manually controlled," allowing for integration with automations 🤖. + +## :books: Table of Contents + + + + + - [:gear: Configuration](#gear-configuration) + - [:memo: Options](#memo-options) + - [:hammer_and_wrench: Services](#hammer_and_wrench-services) + - [`adaptive_lighting.apply`](#adaptive_lightingapply) + - [`adaptive_lighting.set_manual_control`](#adaptive_lightingset_manual_control) + - [`adaptive_lighting.change_switch_settings`](#adaptive_lightingchange_switch_settings) + - [:robot: Automation examples](#robot-automation-examples) +- [Additional Information](#additional-information) +- [Troubleshooting](#troubleshooting) + - [:exclamation: Common Problems & Solutions](#exclamation-common-problems--solutions) + - [:bulb: Lights Not Responding or Turning On by Themselves](#bulb-lights-not-responding-or-turning-on-by-themselves) + - [:signal_strength: WiFi Networks](#signal_strength-wifi-networks) + - [:spider_web: Zigbee, Z-Wave, and Other Mesh Networks](#spider_web-zigbee-z-wave-and-other-mesh-networks) + - [:rainbow: Light Colors Not Matching](#rainbow-light-colors-not-matching) + - [:bulb: Bulb-Specific Issues](#bulb-bulb-specific-issues) + - [:bar_chart: Graphs!](#bar_chart-graphs) + - [:sunny: Sun Position](#sunny-sun-position) + - [:thermometer: Color Temperature](#thermometer-color-temperature) + - [:high_brightness: Brightness](#high_brightness-brightness) + - [:busts_in_silhouette: Contributors](#busts_in_silhouette-contributors) + + + +## :gear: Configuration + +Adaptive Lighting supports configuration through both YAML and the frontend (**Configuration** -> **Integrations** -> **Adaptive Lighting**, **Adaptive Lighting** -> **Options**), with identical option names in both methods. ```yaml # Example configuration.yaml entry @@ -49,37 +73,40 @@ adaptive_lighting: - light.living_room_lights ``` -### Options -| option | description | required | default | type | -| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------- | ------- | -| `name` | The name to use when displaying this switch. | False | default | string | -| `include_config_in_attributes` | When set to `true`, will list all of the below options as attributes on the switch in Home Assistant. | False | False | boolean | -| `lights` | List of light entities for Adaptive Lighting to control (may be empty). | False | list | [] | -| `prefer_rgb_color` | Whether to use RGB color adjustment instead of native light color temperature. | False | False | boolean | -| `initial_transition` | How long the first transition is when the lights go from `off` to `on`. | False | 1 | time | -| `sleep_transition` | How long the transition is when when "sleep mode" is toggled | False | 1 | time | -| `transition` | How long the transition is when the lights change, in seconds. | False | 45 | integer | -| `interval` | How often to adapt the lights, in seconds. | False | 90 | integer | -| `min_brightness` | The minimum percent of brightness to set the lights to. | False | 1 | integer | -| `max_brightness` | The maximum percent of brightness to set the lights to. | False | 100 | integer | -| `min_color_temp` | The warmest color temperature to set the lights to, in Kelvin. | False | 2000 | integer | -| `max_color_temp` | The coldest color temperature to set the lights to, in Kelvin. | False | 5500 | integer | -| `sleep_brightness` | Brightness of lights while the sleep mode is enabled. | False | 1 | integer | -| `sleep_rgb_or_color_temp` | Use either 'rgb_color' or 'color_temp' when in sleep mode. | False | 'color_temp' | string | -| `sleep_rgb_color` | List of three numbers between 0-255, indicating the RGB color in sleep mode (only used when sleep_rgb_or_color_temp is 'rgb_color'). | False | `[255, 56, 0]` | list | -| `sleep_color_temp` | Color temperature of lights while the sleep mode is enabled (only used when sleep_rgb_or_color_temp is 'color_temp'). | False | 1000 | integer | -| `sunrise_time` | Override the sunrise time with a fixed time. | False | None | time | -| `max_sunrise_time` | Make the virtual sun always rise at at most a specific time while still allowing for even earlier times based on the real sun | False | None | time | -| `sunrise_offset` | Change the sunrise time with a positive or negative offset. | False | 0 | time | -| `sunset_time` | Override the sunset time with a fixed time. | False | None | time | -| `min_sunset_time` | Make the virtual sun always set at at least a specific time while still allowing for even later times based on the real sun | False | None | time | -| `sunset_offset` | Change the sunset time with a positive or negative offset. | False | 0 | time | -| `only_once` | Whether to keep adapting the lights (false) or to only adapt the lights as soon as they are turned on (true). | False | False | boolean | -| `take_over_control` | If another source calls `light.turn_on` while the lights are on and being adapted, disable Adaptive Lighting. | False | True | boolean | -| `detect_non_ha_changes` | Whether to detect state changes and stop adapting lights, even not from `light.turn_on`. Needs `take_over_control` to be enabled. Note that by enabling this option, it calls 'homeassistant.update_entity' every 'interval'! | False | False | boolean | -| `separate_turn_on_commands` | Whether to use separate `light.turn_on` calls for color and brightness, needed for some types of lights | False | False | boolean | -| `send_split_delay` | Wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly. | False | 0 | integer | -| `adapt_delay` | Wait time in seconds between light turn on, and Adaptive Lights applying changes to the light state. May avoid flickering. | False | 0 | integer | +Transform your home's atmosphere with Adaptive Lighting 🏠, and experience the benefits of intelligent, sun-synchronized lighting today! + +### :memo: Options + +| Option | Description | Required | Default | Type | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------- | --------- | +| `name` | Display name for this switch. | ❌ | `default` | `string` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. | ❌ | `False` | `boolean` | +| `lights` | List of light entities to be controlled by Adaptive Lighting (may be empty). 🌟 | ❌ | `list` | `list` | +| `prefer_rgb_color` | Use RGB color adjustment instead of native light color temperature. 🌈 | ❌ | `False` | `boolean` | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on`. ⏲️ | ❌ | `1` | `time` | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled. 😴 | ❌ | `1` | `time` | +| `transition` | Duration of transition when lights change, in seconds. | ❌ | `45` | `integer` | +| `interval` | Frequency to adapt the lights, in seconds. | ❌ | `90` | `integer` | +| `min_brightness` | Minimum brightness percentage. 💡 | ❌ | `1` | `integer` | +| `max_brightness` | Maximum brightness percentage. 💡 | ❌ | `100` | `integer` | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | ❌ | `2000` | `integer` | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | ❌ | `5500` | `integer` | +| `sleep_brightness` | Brightness of lights in sleep mode. 😴 | ❌ | `1` | `integer` | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. | ❌ | `'color_temp'` | `string` | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is `"rgb_color"`). 🌈 | ❌ | `[255, 56, 0]` | `list` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`). 😴 | ❌ | `1000` | `integer` | +| `sunrise_time` | Set a fixed time for sunrise. 🌅 | ❌ | `None` | `time` | +| `max_sunrise_time` | Set the latest virtual sunrise time, allowing for earlier real sunrises. 🌅 | ❌ | `None` | `time` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset. ⏰ | ❌ | `0` | `time` | +| `sunset_time` | Set a fixed time for sunset. 🌇 | ❌ | `None` | `time` | +| `min_sunset_time` | Set the earliest virtual sunset time, allowing for later real sunsets. 🌇 | ❌ | `None` | `time` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset. ⏰ | ❌ | `0` | `time` | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | ❌ | `False` | `boolean` | +| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | ❌ | `True` | `boolean` | +| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | ❌ | `False` | `boolean` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | ❌ | `False` | `boolean` | +| `send_split_delay` | Wait time (milliseconds) between commands when using `separate_turn_on_commands`. Helps ensure correct handling. ⏲️ | ❌ | `0` | `integer` | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Helps avoid flickering. ⏲️ | ❌ | `0` | `integer` | Full example: @@ -108,46 +135,54 @@ adaptive_lighting: ``` -### Services +### :hammer_and_wrench: Services + +#### `adaptive_lighting.apply` `adaptive_lighting.apply` applies Adaptive Lighting settings to lights on demand. -| Service data attribute | Optional | Description | +| Service data attribute | Required | Description | | ---------------------- | -------- | -------------------------------------------------------------------------------------------- | -| `entity_id` | no | The `entity_id` of the switch with the settings to apply. | -| `lights` | yes | A light (or list of lights) to apply the settings to. | -| `transition` | yes | The number of seconds for the transition. | -| `adapt_brightness` | yes | Whether to change the brightness of the light or not. | -| `adapt_color` | yes | Whether to adapt the color on supporting lights. | -| `prefer_rgb_color` | yes | Whether to prefer RGB color adjustment over of native light color temperature when possible. | -| `turn_on_lights` | yes | Whether to turn on lights that are currently off. | +| `entity_id` | ✅ | The `entity_id` of the switch with the settings to apply. | +| `lights` | ❌ | A light (or list of lights) to apply the settings to. | +| `transition` | ❌ | The number of seconds for the transition. | +| `adapt_brightness` | ❌ | Whether to change the brightness of the light or not. | +| `adapt_color` | ❌ | Whether to adapt the color on supporting lights. | +| `prefer_rgb_color` | ❌ | Whether to prefer RGB color adjustment over of native light color temperature when possible. | +| `turn_on_lights` | ❌ | Whether to turn on lights that are currently off. | + +#### `adaptive_lighting.set_manual_control` `adaptive_lighting.set_manual_control` can mark (or unmark) whether a light is "manually controlled", meaning that when a light has `manual_control`, the light is not adapted. -| Service data attribute | Optional | Description | +| Service data attribute | Required | Description | | ---------------------- | -------- | --------------------------------------------------------------------------------------------------- | -| `entity_id` | no | The `entity_id` of the switch in which to (un)mark the light as being "manually controlled". | -| `lights` | yes | entity_id(s) of lights, if not specified, all lights in the switch are selected. | -| `manual_control` | yes | Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true | +| `entity_id` | ✅ | The `entity_id` of the switch in which to (un)mark the light as being "manually controlled". | +| `lights` | ❌ | entity_id(s) of lights, if not specified, all lights in the switch are selected. | +| `manual_control` | ❌ | Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true | + +#### `adaptive_lighting.change_switch_settings` `adaptive_lighting.change_switch_settings` (new in 1.7.0) Change any of the above configuration options of Adaptive Lighting (such as `sunrise_time` or `prefer_rgb_color`) with a service call directly from your script/automation. -| Service data attribute | Description | -| ------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| `use_defaults` | (default: 'current' for current settings) You can set this to 'factory', 'configuration', or 'current' to reset the variables not being set with this service call. 'current' leaves them as is, 'configuration' resets to whatever already initializes at startup, 'factory' resets to the default values listed in the documentation. | -| all other keys except the ones in the table below | See above, you may call `adaptive_lighting.apply` with your lights or create a new config instead | +| Service data attribute | Required | Description | +| --------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `use_defaults` | ❌ | (default: `current` for current settings) Choose from `factory`, `configuration`, or `current` to reset variables not being set with this service call. `current` leaves them as they are, `configuration` resets to initial startup values, `factory` resets to default values listed in the documentation. | +| **all other keys** (except the ones in the table below ⚠️) | ❌ | See the table below for disallowed keys. | +The following keys are disallowed: -| **DISALLOWED** service data | Description | -| --------------------------- | ------------------------------------------------------------------------------------------------- | -| `entity_id` | You cannot change the switch's `entity_id`, it's already been registered | -| `lights` | See above, you may call `adaptive_lighting.apply` with your lights or create a new config instead | -| `name` | See above. You can already rename your switch's display name in Home Assistant's UI. | -| `interval` | The interval is only used once when the config loads. A config change and restart is required | +| **DISALLOWED** service data | Description | +| --------------------------- | ----------------------------------------------------------------------------------------------- | +| `entity_id` | You cannot change the switch's `entity_id`, as it has already been registered. | +| `lights` | You may call `adaptive_lighting.apply` with your lights or create a new config instead. | +| `name` | You can rename your switch's display name in Home Assistant's UI. | +| `interval` | The interval is used only once when the config loads. A config change and restart are required. | -## Automation examples +## :robot: Automation examples -Reset the `manual_control` status of a light after an hour. +
+Reset the manual_control status of a light after an hour. ```yaml - alias: "Adaptive lighting: reset manual_control after 1 hour" @@ -169,7 +204,10 @@ Reset the `manual_control` status of a light after an hour. manual_control: false ``` -Toggle multiple Adaptive Lighting switches to "sleep mode" using an `input_boolean.sleep_mode`. +
+ +
+Toggle multiple Adaptive Lighting switches to "sleep mode" using an input_boolean.sleep_mode. ```yaml - alias: "Adaptive lighting: toggle 'sleep mode'" @@ -189,7 +227,7 @@ Toggle multiple Adaptive Lighting switches to "sleep mode" using an `input_boole Set your sunrise and sunset time based on your alarm. The below script sets sunset_time exactly 12 hours after the custom sunrise time. -``` +```yaml iphone_carly_wakeup: alias: iPhone Carly Wakeup sequence: @@ -238,79 +276,80 @@ iphone_carly_wakeup: max: 10 ``` -# Other +
-See the documentation of the PR at https://deploy-preview-14877--home-assistant-docs.netlify.app/integrations/adaptive_lighting/ and [this video on Reddit](https://www.reddit.com/r/homeassistant/comments/jabhso/ha_has_it_before_apple_has_even_finished_it_i/) to see how to add the integration and set the options. +# Additional Information -This integration was originally based of the great work of @claytonjn https://github.com/claytonjn/hass-circadian_lighting, but has been 100% rewritten and extended with new features. +For more details on adding the integration and setting options, refer to the [documentation of the PR](https://deploy-preview-14877--home-assistant-docs.netlify.app/integrations/adaptive_lighting/) and [this video tutorial on Reddit](https://www.reddit.com/r/homeassistant/comments/jabhso/ha_has_it_before_apple_has_even_finished_it_i/). -# Having problems? +Adaptive Lighting was initially inspired by @claytonjn's [hass-circadian\_lighting](https://github.com/claytonjn/hass-circadian_lighting), but has since been entirely rewritten and expanded with new features. + +# Troubleshooting + +Encountering issues? Enable debug logging in your `configuration.yaml`: -Please enable debug logging by putting this in `configuration.yaml`: ```yaml logger: default: warning logs: custom_components.adaptive_lighting: debug ``` -and after the problem occurs please create an issue with the log (`/config/home-assistant.log`). -## Lights are not responding or turning on by themselves +After the issue occurs, create a new issue report with the log (`/config/home-assistant.log`). -This addon sends many more commands to lights compared to what humans would typically send. If the network used to send light commands is not healthy: +## :exclamation: Common Problems & Solutions -- Manual commands like turning lights on or off may feel laggy. -- Lights may not respond to commands at all. -- Home Assistant may think a light is on, when it's actually off. Adaptive Lights will send it's regular adjustments causing the light to turn on after it's turned off. +### :bulb: Lights Not Responding or Turning On by Themselves -What's important is that many bugs that seem to be caused by this integration are really due to other unrelated issues. Fixing those will make your Home Assistant experience much better. Consider this integration a great stress test of your Home Assistant setup! +Adaptive Lighting sends more commands to lights than a typical human user would. If your light control network is unhealthy, you may experience: -### Wifi networks +- Laggy manual commands (e.g., turning lights on or off). +- Unresponsive lights. +- Home Assistant reporting incorrect light states, causing Adaptive Lighting to inadvertently turn lights back on. -Make sure bulbs have a solid connection to your Wifi network. In general, if the signal is less than -70dBm, the connection is weak and may drop messages. +Most issues that appear to be caused by Adaptive Lighting are actually due to unrelated problems. Addressing these issues will significantly improve your Home Assistant experience. -### Zigbee, Z-Wave, and other mesh networks +#### :signal_strength: WiFi Networks -These types of mesh networks usually need powered devices that act as routers (that repeat messages) back to the central coordinator (the radio connected to Home Assistant). Most Philips lights are routers, but Ikea, Sengled, and generic Tuya bulbs often are not. If devices become unavailable or miss responding to commands, Adaptive Lighting will only make things worse. Use reporting tools such as network maps (available in ZHA, zigbee2mqtt, deCONZ, and ZWaveJS UI) to check your network. Smart plugs are often a cost-effective way to add additional routers to your network. +Ensure your light bulbs have a strong WiFi connection. If the signal strength is less than -70dBm, the connection may be weak and prone to dropping messages. -For most Zigbee networks, groups are **absolutely required for good performance**. For example, imagine you want to use Adaptive Lighting in a hallway with 6 bulbs. If you add each individual bulb in the Adaptive Lighting configuration, then six individual commands will be sent to adjust them, which can eventually overwhelm a network. Instead, create a group in your Zigbee software (but _not_ a regular Home Assistant group), and add the one group to the Adaptive Lighting configuration. This will send only a single broadcast command to adjust the bulbs, giving much better response times and keeping the bulbs adjusting in sync with each other. +#### :spider_web: Zigbee, Z-Wave, and Other Mesh Networks -A good rule to follow is that if you always control lights together (like bulbs in a ceiling fixture), then they should be in a Zigbee group. Then, only expose the group (and not individual bulbs) in Home Assistant Dashboards and external systems like Google Home or Apple HomeKit. +Mesh networks typically require powered devices to act as routers, relaying messages back to the central coordinator (the radio connected to Home Assistant). Philips lights usually function as routers, while Ikea, Sengled, and generic Tuya bulbs often do not. If devices become unresponsive or fail to respond to commands, Adaptive Lighting can exacerbate the issue. Use network maps (available in ZHA, zigbee2mqtt, deCONZ, and ZWaveJS UI) to evaluate your network health. Smart plugs can be an affordable way to add more routers to your network. -### Light colors are not matching +For most Zigbee networks, **using groups is essential for optimal performance**. For example, if you want to use Adaptive Lighting in a hallway with six bulbs, adding each bulb individually to the Adaptive Lighting configuration could overwhelm the network with commands. Instead, create a group in your Zigbee software (not a regular Home Assistant group) and add that single group to the Adaptive Lighting configuration. This sends a single broadcast command to adjust all bulbs, improving response times and keeping the bulbs in sync. -Bulbs made by different manufacturers or of different models may have different specifications for the color temperatures they support. For example you have two Adaptive Lighting configurations: +As a rule of thumb, if you always control lights together (e.g., bulbs in a ceiling fixture), they should be in a Zigbee group. Expose only the group (not individual bulbs) in Home Assistant Dashboards and external systems like Google Home or Apple HomeKit. -- The first configuration has only Philips Hue White Ambiance bulbs. -- The second has the a few of the same model of White Ambiance bulbs as well as a few Sengled bulbs. +### :rainbow: Light Colors Not Matching -Even with identical settings, the Philips Hue bulbs may appear to have different color temperatures set at the same time. +Bulbs from different manufacturers or models may have varying color temperature specifications. For instance, if you have two Adaptive Lighting configurations—one with only Philips Hue White Ambiance bulbs and another with a mix of Philips Hue White Ambiance and Sengled bulbs—the Philips Hue bulbs may appear to have different color temperatures despite having identical settings. -To avoid this: +To resolve this: -1. Only put bulbs of the same make and model in a single Adaptive Lighting configuration. -2. Move where bulbs are installed so you can't see different light temperatures at the same time. +1. Include only bulbs of the same make and model in a single Adaptive Lighting configuration. +2. Rearrange bulbs so that different color temperatures are not visible simultaneously. -### Bulb-specific issues +### :bulb: Bulb-Specific Issues -Some bulbs have buggy behaviour with long light transition commands. +Certain bulbs may have issues with long light transition commands: -- [Sengled Z01-A19NAE26](https://www.zigbee2mqtt.io/devices/Z01-A19NAE26.html#sengled-z01-a19nae26): If Adaptive lighting sends a long transition time (like the default 45 seconds), and the bulb is turned off in that time, it will turn itself back on after 10 seconds or so to continue the transition command. Since the bulb is turning itself on, there will be no obvious trigger in Home Assistant or other logs showing what caused the light to turn on. Fix this by setting a much shorter transition time such as 1 second. -- As well, the same bulbs peform poorly when in typical enclosed "dome" style ceiling lights. When hot, their performance becomes marginal at best. While most LEDs (even non-smart ones) say in the small print that they do not support working in enclosed fixtures, in practice more expensive bulbs like Philips Hue perform better. Fix this by moving suspect bulbs to open-air fixtures. +- [Sengled Z01-A19NAE26](https://www.zigbee2mqtt.io/devices/Z01-A19NAE26.html#sengled-z01-a19nae26): If Adaptive Lighting sends a long transition time (like the default 45 seconds), and the bulb is turned off during that time, it may turn back on after approximately 10 seconds to continue the transition command. Since the bulb is turning itself on, there will be no obvious trigger in Home Assistant or other logs indicating the cause of the light turning on. To fix this, set a much shorter transition time, such as 1 second. +- Additionally, these bulbs may perform poorly in enclosed "dome" style ceiling lights, particularly when hot. While most LEDs (even non-smart ones) state in the fine print that they do not support working in enclosed fixtures, in practice, more expensive bulbs like Philips Hue generally perform better. To resolve this issue, move the problematic bulbs to open-air fixtures. -## Graphs! +## :bar_chart: Graphs! These graphs were generated using the values calculated by the Adaptive Lighting sensor/switch(es). -#### Sun Position: +#### :sunny: Sun Position ![cl_percent|690x131](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/6/5/657ff98beb65a94598edeb4bdfd939095db1a22c.PNG) -#### Color Temperature: +#### :thermometer: Color Temperature ![cl_color_temp|690x129](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/9/59e84263cbecd8e428cb08777a0413672c48dfcd.PNG) -#### Brightness: +#### :high_brightness: Brightness ![cl_brightness|690x130](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/8/58ebd994b62a8b1abfb3497a5288d923ff4e2330.PNG) -## Contributors +## :busts_in_silhouette: Contributors From ec558e5616dd659ed30915643f4f55ea3896fe87 Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Sat, 1 Apr 2023 18:35:01 -0500 Subject: [PATCH 0520/1077] Move `include_config_in_attributes` --- custom_components/adaptive_lighting/switch.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index ef8b42aa..c94ab32a 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -771,16 +771,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set in self._update_attrs_and_maybe_adapt_lights self._settings: dict[str, Any] = {} - self._config: dict[str, Any] = {} - if self._include_config_in_attributes: - attrdata = deepcopy(data) - for k, v in attrdata.items(): - if isinstance(v, (datetime.date, datetime.datetime)): - attrdata[k] = v.isoformat() - if isinstance(v, (datetime.timedelta)): - attrdata[k] = v.total_seconds() - self._config.update(attrdata) - # Set and unset tracker in async_turn_on and async_turn_off self.remove_listeners = [] _LOGGER.debug( @@ -811,6 +801,16 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] self._include_config_in_attributes = data[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES] + self._config: dict[str, Any] = {} + if self._include_config_in_attributes: + attrdata = deepcopy(data) + for k, v in attrdata.items(): + if isinstance(v, (datetime.date, datetime.datetime)): + attrdata[k] = v.isoformat() + if isinstance(v, (datetime.timedelta)): + attrdata[k] = v.total_seconds() + self._config.update(attrdata) + self._initial_transition = data[CONF_INITIAL_TRANSITION] self._sleep_transition = data[CONF_SLEEP_TRANSITION] self._only_once = data[CONF_ONLY_ONCE] From cc8ce29a8a096251888f2e69536a1c7363b43376 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 1 Apr 2023 23:43:20 -0700 Subject: [PATCH 0521/1077] Auto-generate the configuration options documentation (#498) * Autogenerate the documentation and strings * Line limit * Split * Happy pre-commit * Split up workflows * Generate table function * raise * Add gen script * chore(docs): update TOC * Add gen script * path * Run - name: Install Home Assistant * chore(docs): update TOC * add copyright * Fix branch name * Run on PRs * dev * remove steps * use matrix * Run script * chore(docs): update TOC * Try fixing CI * fix * test * refactor * chore(docs): update TOC * fix * update * use v3 * rename * Fix input * fix pytest * fix path * paths * Change to github.head_ref * More backticks * Add extra text * chore(docs): update TOC * backticks * Update README.md --------- Co-authored-by: basnijholt Co-authored-by: github-actions[bot] --- .github/update-readme.py | 195 ++++++++++++++++++ .github/workflows/docker-build.yml | 8 +- .../workflows/install_dependencies/action.yml | 36 ++++ .github/workflows/pytest.yaml | 24 +-- .github/workflows/update-readme.yml | 44 ++++ README.md | 74 ++++--- custom_components/adaptive_lighting/const.py | 157 +++++++++++++- 7 files changed, 486 insertions(+), 52 deletions(-) create mode 100644 .github/update-readme.py create mode 100644 .github/workflows/install_dependencies/action.yml create mode 100644 .github/workflows/update-readme.yml diff --git a/.github/update-readme.py b/.github/update-readme.py new file mode 100644 index 00000000..ebeb9401 --- /dev/null +++ b/.github/update-readme.py @@ -0,0 +1,195 @@ +# Copyright (c) 2023, Bas Nijholt +# All rights reserved. +# When using this code, please cite the original source. +# and include the LICENSE file in your project. +"""Automatically update Markdown files with code block output. + +Add code blocks between and in your Markdown file. +The output will be inserted between and . + +Example: +------- +``` + + + + +This will be replaced by the output of the code block above. + + +``` +""" +from __future__ import annotations + +import contextlib +import io +from pathlib import Path + + +def md_comment(text: str) -> str: + """Format a string as a Markdown comment.""" + return f"" + + +MARKERS = { + "warning": md_comment("THIS CONTENT IS AUTOMATICALLY GENERATED"), + "start_code": md_comment("START_CODE"), + "end_code": md_comment("END_CODE"), + "start_output": md_comment("START_OUTPUT"), + "end_output": md_comment("END_OUTPUT"), +} + + +def remove_md_comment(commented_text: str) -> str: + """Remove Markdown comment tags from a string.""" + if not (commented_text.startswith("")): + raise ValueError("Invalid Markdown comment format") + return commented_text[5:-4] + + +def execute_code_block(code: list[str]) -> list[str]: + """Execute a code block and return its output as a list of strings.""" + f = io.StringIO() + with contextlib.redirect_stdout(f): + exec("\n".join(code)) # noqa: S102 + return f.getvalue().split("\n") + + +def process_markdown(content: list[str]) -> list[str]: + """Executes code blocks in a list of Markdown-formatted strings and returns the modified list. + + Parameters + ---------- + content + A list of Markdown-formatted strings. + + Returns + ------- + list[str] + A modified list of Markdown-formatted strings with code block output inserted. + """ + assert isinstance(content, list), "Input must be a list" + new_lines = [] + code = [] + in_code_block = in_output_block = False + output = None + + for line in content: + if MARKERS["start_code"] in line: + in_code_block = True + elif MARKERS["start_output"] in line: + in_output_block = True + new_lines.extend([line, MARKERS["warning"]] + output) + output = None + elif MARKERS["end_output"] in line: + in_output_block = False + elif in_code_block: + if MARKERS["end_code"] in line: + in_code_block = False + output = execute_code_block(code) + code = [] + else: + code.append(remove_md_comment(line)) + + if not in_output_block: + new_lines.append(line) + + return new_lines + + +def update_markdown_file(filepath: Path) -> None: + """Rewrite a Markdown file by executing and updating code blocks.""" + with filepath.open() as f: + original_lines = [line.rstrip("\n") for line in f.readlines()] + + new_lines = process_markdown(original_lines) + updated_content = "\n".join(new_lines).rstrip() + "\n" + + with filepath.open("w") as f: + f.write(updated_content) + + +def test_process_markdown(): + def assert_process(input_lines, expected_output): + output = process_markdown(input_lines) + assert output == expected_output, f"Expected {expected_output}, got {output}" + + # Test case 1: Single code block + input_lines = [ + "Some text", + MARKERS["start_code"], + md_comment("print('Hello, world!')"), + MARKERS["end_code"], + MARKERS["start_output"], + "This content will be replaced", + MARKERS["end_output"], + "More text", + ] + expected_output = [ + "Some text", + MARKERS["start_code"], + md_comment("print('Hello, world!')"), + MARKERS["end_code"], + MARKERS["start_output"], + MARKERS["warning"], + "Hello, world!", + "", + MARKERS["end_output"], + "More text", + ] + assert_process(input_lines, expected_output) + + # Test case 2: Two code blocks + input_lines = [ + "Some text", + MARKERS["start_code"], + md_comment("print('Hello, world!')"), + MARKERS["end_code"], + MARKERS["start_output"], + "This content will be replaced", + MARKERS["end_output"], + "More text", + MARKERS["start_code"], + md_comment("print('Hello again!')"), + MARKERS["end_code"], + MARKERS["start_output"], + "This content will also be replaced", + MARKERS["end_output"], + ] + expected_output = [ + "Some text", + MARKERS["start_code"], + md_comment("print('Hello, world!')"), + MARKERS["end_code"], + MARKERS["start_output"], + MARKERS["warning"], + "Hello, world!", + "", + MARKERS["end_output"], + "More text", + MARKERS["start_code"], + md_comment("print('Hello again!')"), + MARKERS["end_code"], + MARKERS["start_output"], + MARKERS["warning"], + "Hello again!", + "", + MARKERS["end_output"], + ] + assert_process(input_lines, expected_output) + + # Test case 3: No code blocks + input_lines = [ + "Some text", + "More text", + ] + expected_output = [ + "Some text", + "More text", + ] + assert_process(input_lines, expected_output) + + +if __name__ == "__main__": + test_process_markdown() + update_markdown_file(Path(__file__).parent.parent / "README.md") diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index cf1866fb..7326175a 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -9,6 +9,11 @@ on: jobs: docker: runs-on: ubuntu-latest + strategy: + matrix: + platform: + - linux/amd64 + - linux/arm64 steps: - name: Set up QEMU uses: docker/setup-qemu-action@v2 @@ -24,6 +29,5 @@ jobs: with: # Only push on the master branch push: ${{ github.ref == 'refs/heads/master' }} - # TODO: fix builds on linux/arm/v7 - platforms: linux/amd64,linux/arm64 + platforms: ${{ matrix.platform }} tags: ${{ secrets.DOCKERHUB_USERNAME }}/adaptive-lighting:latest diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml new file mode 100644 index 00000000..75820072 --- /dev/null +++ b/.github/workflows/install_dependencies/action.yml @@ -0,0 +1,36 @@ +name: 'Install Dependencies' +description: 'Install Home Assistant and test dependencies' +inputs: + python_version: + description: 'Python version' + required: true + default: '3.10' + +runs: + using: "composite" + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3 + with: + repository: ${{ github.repository }} + ref: ${{ github.ref }} + persist-credentials: false + fetch-depth: 0 + - name: Check out code from GitHub + uses: actions/checkout@v3 + with: + repository: home-assistant/core + path: core + - name: Set up Python ${{ inputs.python_version }} + id: python + uses: actions/setup-python@v4.1.0 + with: + python-version: ${{ inputs.python_version }} + - name: Install dependencies + shell: bash + run: | + echo "::warning::### WARNING! Deprecation warnings muted with option '--use-pep517' please address this at some point in pytest.yaml. ###" + pip install -r core/requirements.txt --use-pep517 + pip install -r core/requirements_test.txt --use-pep517 + pip install -e core/ --use-pep517 + pip install $(python test_dependencies.py) --use-pep517 diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 60d9577f..47d4df8a 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -6,7 +6,6 @@ on: pull_request: jobs: - pytest: name: Run pytest runs-on: ubuntu-20.04 @@ -16,17 +15,13 @@ jobs: python-version: ["3.10"] steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 - - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3 + + - name: Install Home Assistant + uses: ./.github/workflows/install_dependencies with: - repository: home-assistant/core - path: core - - name: Set up Python ${{ matrix.python-version }} - id: python - uses: actions/setup-python@v4.1.0 - with: - python-version: ${{ matrix.python-version }} + python_version: ${{ matrix.python-version }} + - name: Click here for troubleshooting steps if tests break again. run: | echo "::notice::### If tests fail, try these debug steps: ###" @@ -36,13 +31,6 @@ jobs: echo "::notice::### 4. ERROR:homeassistant.setup:Setup failed for 'component': Unable to import component: No module named ''module'' ###" echo "::notice::### 5. add 'component'.'module' (without the '') from the above log into the 'required' list inside of 'test_dependencies.py' ###" echo "::notice::### 6. Try again! If more issues persist they should be easily solvable by reading the verbose logs now. ###" - - name: Install dependencies - run: | - echo "::warning::### WARNING! Deprecation warnings muted with option '--use-pep517' please address this at some point in pytest.yaml. ###" - pip install -r core/requirements.txt --use-pep517 - pip install -r core/requirements_test.txt --use-pep517 - pip install -e core/ --use-pep517 - pip install $(python test_dependencies.py) --use-pep517 - name: Run pytest timeout-minutes: 60 run: | diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml new file mode 100644 index 00000000..36a994a9 --- /dev/null +++ b/.github/workflows/update-readme.yml @@ -0,0 +1,44 @@ +name: Update README.md + +on: + push: + branches: + - master + paths: + - ".github/update-readme.py" + - "README.md" + - ".github/workflows/update-readme.yml" + - "custom_components/adaptive_lighting/const.py" + pull_request: + +jobs: + update_readme: + runs-on: ubuntu-latest + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3 + + - name: Install Home Assistant + uses: ./.github/workflows/install_dependencies + with: + python_version: "3.10" + + - name: Install pandas and tabulate + run: | + pip install pandas tabulate + + - name: Run update-readme.py + run: python ./.github/update-readme.py + + - name: Commit updated README.md + run: | + git add README.md + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git diff --quiet && git diff --staged --quiet || git commit -m "Update README.md" + + - name: Push changes + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: ${{ github.head_ref }} diff --git a/README.md b/README.md index ac7897e0..7cad61a3 100644 --- a/README.md +++ b/README.md @@ -77,36 +77,50 @@ Transform your home's atmosphere with Adaptive Lighting 🏠, and experience the ### :memo: Options -| Option | Description | Required | Default | Type | -| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------- | --------- | -| `name` | Display name for this switch. | ❌ | `default` | `string` | -| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. | ❌ | `False` | `boolean` | -| `lights` | List of light entities to be controlled by Adaptive Lighting (may be empty). 🌟 | ❌ | `list` | `list` | -| `prefer_rgb_color` | Use RGB color adjustment instead of native light color temperature. 🌈 | ❌ | `False` | `boolean` | -| `initial_transition` | Duration of the first transition when lights turn from `off` to `on`. ⏲️ | ❌ | `1` | `time` | -| `sleep_transition` | Duration of transition when "sleep mode" is toggled. 😴 | ❌ | `1` | `time` | -| `transition` | Duration of transition when lights change, in seconds. | ❌ | `45` | `integer` | -| `interval` | Frequency to adapt the lights, in seconds. | ❌ | `90` | `integer` | -| `min_brightness` | Minimum brightness percentage. 💡 | ❌ | `1` | `integer` | -| `max_brightness` | Maximum brightness percentage. 💡 | ❌ | `100` | `integer` | -| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | ❌ | `2000` | `integer` | -| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | ❌ | `5500` | `integer` | -| `sleep_brightness` | Brightness of lights in sleep mode. 😴 | ❌ | `1` | `integer` | -| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. | ❌ | `'color_temp'` | `string` | -| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is `"rgb_color"`). 🌈 | ❌ | `[255, 56, 0]` | `list` | -| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`). 😴 | ❌ | `1000` | `integer` | -| `sunrise_time` | Set a fixed time for sunrise. 🌅 | ❌ | `None` | `time` | -| `max_sunrise_time` | Set the latest virtual sunrise time, allowing for earlier real sunrises. 🌅 | ❌ | `None` | `time` | -| `sunrise_offset` | Adjust sunrise time with a positive or negative offset. ⏰ | ❌ | `0` | `time` | -| `sunset_time` | Set a fixed time for sunset. 🌇 | ❌ | `None` | `time` | -| `min_sunset_time` | Set the earliest virtual sunset time, allowing for later real sunsets. 🌇 | ❌ | `None` | `time` | -| `sunset_offset` | Adjust sunset time with a positive or negative offset. ⏰ | ❌ | `0` | `time` | -| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | ❌ | `False` | `boolean` | -| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | ❌ | `True` | `boolean` | -| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | ❌ | `False` | `boolean` | -| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | ❌ | `False` | `boolean` | -| `send_split_delay` | Wait time (milliseconds) between commands when using `separate_turn_on_commands`. Helps ensure correct handling. ⏲️ | ❌ | `0` | `integer` | -| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Helps avoid flickering. ⏲️ | ❌ | `0` | `integer` | +All of the configuration options are listed below, along with their default values. +The YAML and frontend configuration methods support all of the options listed below. + + + + + + + + + + + +| Variable name | Description | Default | Type | +|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------| +| `lights` | List of light entities to be controlled by Adaptive Lighting (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `prefer_rgb_color` | Use RGB color adjustment instead of native light color temperature. 🌈 | `False` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on`. ⏲️ | `1` | `float` 0-6553 | +| `sleep_transition` | Duration of transition when 'sleep mode' is toggled. 😴 | `1` | `float` 0-6553 | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `sleep_brightness` | Brightness of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `'rgb_color'` or `'color_temp'` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`). 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is 'rgb_color'). 🌈 | `[255, 56, 0]` | RGB color | +| `sunrise_time` | Set a fixed time for sunrise. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time, allowing for earlier real sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time, allowing for later real sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset. ⏰ | `0` | `int` | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | +| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | +| `send_split_delay` | Wait time (milliseconds) between commands when using `separate_turn_on_commands`. Helps ensure correct handling. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Helps avoid flickering. ⏲️ | `0` | `float > 0` | + + Full example: diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index f4b751e5..421eadcc 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -14,44 +14,144 @@ DOMAIN = "adaptive_lighting" SUN_EVENT_NOON = "solar_noon" SUN_EVENT_MIDNIGHT = "solar_midnight" +DOCS = {} + + CONF_NAME, DEFAULT_NAME = "name", "default" +DOCS[CONF_NAME] = "Display name for this switch. 📝" + CONF_LIGHTS, DEFAULT_LIGHTS = "lights", [] +DOCS[CONF_LIGHTS] = ( + "List of light entities to be controlled by Adaptive " "Lighting (may be empty). 🌟" +) + CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = ( "detect_non_ha_changes", False, ) +DOCS[CONF_DETECT_NON_HA_CHANGES] = ( + "Detect non-`light.turn_on` state changes and stop adapting lights. " + "Requires `take_over_control`. 🕵️" +) + CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES = ( "include_config_in_attributes", False, ) +DOCS[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES] = ( + "Show all options as attributes on the switch in " + "Home Assistant when set to `true`. 📝" +) + CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 +DOCS[CONF_INITIAL_TRANSITION] = ( + "Duration of the first transition when lights turn " "from `off` to `on`. ⏲️" +) + CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION = "sleep_transition", 1 +DOCS[CONF_SLEEP_TRANSITION] = "Duration of transition when 'sleep mode' is toggled. 😴" + CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 +DOCS[CONF_INTERVAL] = "Frequency to adapt the lights, in seconds. 🔄" + CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100 +DOCS[CONF_MAX_BRIGHTNESS] = "Maximum brightness percentage. 💡" + CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP = "max_color_temp", 5500 +DOCS[CONF_MAX_COLOR_TEMP] = "Coldest color temperature in Kelvin. ❄️" + CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS = "min_brightness", 1 +DOCS[CONF_MIN_BRIGHTNESS] = "Minimum brightness percentage. 💡" + CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP = "min_color_temp", 2000 +DOCS[CONF_MIN_COLOR_TEMP] = "Warmest color temperature in Kelvin. 🔥" + CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE = "only_once", False +DOCS[CONF_ONLY_ONCE] = ( + "Adapt lights only when they are turned on (`true`) or keep adapting them " + "(`false`). 🔄" +) + CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False +DOCS[ + CONF_PREFER_RGB_COLOR +] = "Use RGB color adjustment instead of native light color temperature. 🌈" + CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS = ( "separate_turn_on_commands", False, ) +DOCS[CONF_SEPARATE_TURN_ON_COMMANDS] = ( + "Use separate `light.turn_on` calls for color and brightness, needed for " + "some light types. 🔀" +) + CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1 +DOCS[CONF_SLEEP_BRIGHTNESS] = "Brightness of lights in sleep mode. 😴" + CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP = "sleep_color_temp", 1000 +DOCS[CONF_SLEEP_COLOR_TEMP] = ( + "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is " + "`color_temp`). 😴" +) + CONF_SLEEP_RGB_COLOR, DEFAULT_SLEEP_RGB_COLOR = "sleep_rgb_color", [255, 56, 0] +DOCS[CONF_SLEEP_RGB_COLOR] = ( + "RGB color in sleep mode (used when " "`sleep_rgb_or_color_temp` is 'rgb_color'). 🌈" +) + CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = ( "sleep_rgb_or_color_temp", "color_temp", ) +DOCS[ + CONF_SLEEP_RGB_OR_COLOR_TEMP +] = "Use either `'rgb_color'` or `'color_temp'` in sleep mode. 🌙" + CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 +DOCS[CONF_SUNRISE_OFFSET] = "Adjust sunrise time with a positive or negative offset. ⏰" + CONF_SUNRISE_TIME = "sunrise_time" +DOCS[CONF_SUNRISE_TIME] = "Set a fixed time for sunrise. 🌅" + CONF_MAX_SUNRISE_TIME = "max_sunrise_time" +DOCS[CONF_MAX_SUNRISE_TIME] = ( + "Set the latest virtual sunrise time, allowing" " for earlier real sunrises. 🌅" +) + CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 +DOCS[CONF_SUNSET_OFFSET] = "Adjust sunset time with a positive or negative offset. ⏰" + CONF_SUNSET_TIME = "sunset_time" +DOCS[CONF_SUNSET_TIME] = "Set a fixed time for sunset. 🌇" + CONF_MIN_SUNSET_TIME = "min_sunset_time" +DOCS[CONF_MIN_SUNSET_TIME] = ( + "Set the earliest virtual sunset time, allowing" " for later real sunsets. 🌇" +) + CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True +DOCS[CONF_TAKE_OVER_CONTROL] = ( + "Disable Adaptive Lighting if another source calls `light.turn_on` while lights " + "are on and being adapted. Note that this calls `homeassistant.update_entity` " + "every `interval`! 🔒" +) + CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 45 +DOCS[CONF_TRANSITION] = "Duration of transition when lights change, in seconds. 🕑" + +CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0 +DOCS[CONF_ADAPT_DELAY] = ( + "Wait time (seconds) between light turn on and Adaptive Lighting applying " + "changes. Helps avoid flickering. ⏲️" +) + +CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY = "send_split_delay", 0 +DOCS[CONF_SEND_SPLIT_DELAY] = ( + "Wait time (milliseconds) between commands when using `separate_turn_on_commands`. " + "Helps ensure correct handling. ⏲️" +) + SLEEP_MODE_SWITCH = "sleep_mode_switch" ADAPT_COLOR_SWITCH = "adapt_color_switch" @@ -69,9 +169,8 @@ CONF_TURN_ON_LIGHTS = "turn_on_lights" SERVICE_CHANGE_SWITCH_SETTINGS = "change_switch_settings" CONF_USE_DEFAULTS = "use_defaults" -CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0 + TURNING_OFF_DELAY = 5 -CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY = "send_split_delay", 0 def int_between(min_int, max_int): @@ -169,3 +268,57 @@ _DOMAIN_SCHEMA = vol.Schema( for key, default, validation in _yaml_validation_tuples } ) + + +def _format_voluptuous_instance(instance): + coerce_type = None + min_val = None + max_val = None + + for validator in instance.validators: + if isinstance(validator, vol.Coerce): + coerce_type = validator.type.__name__ + elif isinstance(validator, (vol.Clamp, vol.Range)): + min_val = validator.min + max_val = validator.max + + if min_val is not None and max_val is not None: + return f"`{coerce_type}` {min_val}-{max_val}" + elif min_val is not None: + return f"`{coerce_type} > {min_val}`" + elif max_val is not None: + return f"`{coerce_type} < {max_val}`" + else: + return f"`{coerce_type}`" + + +def generate_markdown_table(): + import pandas as pd + + rows = [] + for k, default, type_ in VALIDATION_TUPLES: + description = DOCS[k] + if type_ == cv.entity_ids: + type_ = "list of `entity_id`s" + elif type_ in (bool, int, float, str): + type_ = f"`{type_.__name__}`" + elif isinstance(type_, vol.All): + type_ = _format_voluptuous_instance(type_) + elif isinstance(type_, vol.In): + type_ = f"one of `{type_.container}`" + elif isinstance(type_, selector.SelectSelector): + type_ = f"one of `{type_.config['options']}`" + elif isinstance(type_, selector.ColorRGBSelector): + type_ = "RGB color" + else: + raise ValueError(f"Unknown type: {type_}") + row = { + "Variable name": f"`{k}`", + "Description": description, + "Default": f"`{default}`", + "Type": type_, + } + rows.append(row) + + df = pd.DataFrame(rows) + return df.to_markdown(index=False) From 8108bd0a4b5d0813751f72399aa17f7dd12fd45f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 2 Apr 2023 18:56:36 -0700 Subject: [PATCH 0522/1077] Use markdown-code-runner instead of packaged solution (#505) * Use markdown-code-runner instead of packaged solution * add debug --- .github/update-readme.py | 195 ---------------------------- .github/workflows/update-readme.yml | 9 +- README.md | 2 +- 3 files changed, 5 insertions(+), 201 deletions(-) delete mode 100644 .github/update-readme.py diff --git a/.github/update-readme.py b/.github/update-readme.py deleted file mode 100644 index ebeb9401..00000000 --- a/.github/update-readme.py +++ /dev/null @@ -1,195 +0,0 @@ -# Copyright (c) 2023, Bas Nijholt -# All rights reserved. -# When using this code, please cite the original source. -# and include the LICENSE file in your project. -"""Automatically update Markdown files with code block output. - -Add code blocks between and in your Markdown file. -The output will be inserted between and . - -Example: -------- -``` - - - - -This will be replaced by the output of the code block above. - - -``` -""" -from __future__ import annotations - -import contextlib -import io -from pathlib import Path - - -def md_comment(text: str) -> str: - """Format a string as a Markdown comment.""" - return f"" - - -MARKERS = { - "warning": md_comment("THIS CONTENT IS AUTOMATICALLY GENERATED"), - "start_code": md_comment("START_CODE"), - "end_code": md_comment("END_CODE"), - "start_output": md_comment("START_OUTPUT"), - "end_output": md_comment("END_OUTPUT"), -} - - -def remove_md_comment(commented_text: str) -> str: - """Remove Markdown comment tags from a string.""" - if not (commented_text.startswith("")): - raise ValueError("Invalid Markdown comment format") - return commented_text[5:-4] - - -def execute_code_block(code: list[str]) -> list[str]: - """Execute a code block and return its output as a list of strings.""" - f = io.StringIO() - with contextlib.redirect_stdout(f): - exec("\n".join(code)) # noqa: S102 - return f.getvalue().split("\n") - - -def process_markdown(content: list[str]) -> list[str]: - """Executes code blocks in a list of Markdown-formatted strings and returns the modified list. - - Parameters - ---------- - content - A list of Markdown-formatted strings. - - Returns - ------- - list[str] - A modified list of Markdown-formatted strings with code block output inserted. - """ - assert isinstance(content, list), "Input must be a list" - new_lines = [] - code = [] - in_code_block = in_output_block = False - output = None - - for line in content: - if MARKERS["start_code"] in line: - in_code_block = True - elif MARKERS["start_output"] in line: - in_output_block = True - new_lines.extend([line, MARKERS["warning"]] + output) - output = None - elif MARKERS["end_output"] in line: - in_output_block = False - elif in_code_block: - if MARKERS["end_code"] in line: - in_code_block = False - output = execute_code_block(code) - code = [] - else: - code.append(remove_md_comment(line)) - - if not in_output_block: - new_lines.append(line) - - return new_lines - - -def update_markdown_file(filepath: Path) -> None: - """Rewrite a Markdown file by executing and updating code blocks.""" - with filepath.open() as f: - original_lines = [line.rstrip("\n") for line in f.readlines()] - - new_lines = process_markdown(original_lines) - updated_content = "\n".join(new_lines).rstrip() + "\n" - - with filepath.open("w") as f: - f.write(updated_content) - - -def test_process_markdown(): - def assert_process(input_lines, expected_output): - output = process_markdown(input_lines) - assert output == expected_output, f"Expected {expected_output}, got {output}" - - # Test case 1: Single code block - input_lines = [ - "Some text", - MARKERS["start_code"], - md_comment("print('Hello, world!')"), - MARKERS["end_code"], - MARKERS["start_output"], - "This content will be replaced", - MARKERS["end_output"], - "More text", - ] - expected_output = [ - "Some text", - MARKERS["start_code"], - md_comment("print('Hello, world!')"), - MARKERS["end_code"], - MARKERS["start_output"], - MARKERS["warning"], - "Hello, world!", - "", - MARKERS["end_output"], - "More text", - ] - assert_process(input_lines, expected_output) - - # Test case 2: Two code blocks - input_lines = [ - "Some text", - MARKERS["start_code"], - md_comment("print('Hello, world!')"), - MARKERS["end_code"], - MARKERS["start_output"], - "This content will be replaced", - MARKERS["end_output"], - "More text", - MARKERS["start_code"], - md_comment("print('Hello again!')"), - MARKERS["end_code"], - MARKERS["start_output"], - "This content will also be replaced", - MARKERS["end_output"], - ] - expected_output = [ - "Some text", - MARKERS["start_code"], - md_comment("print('Hello, world!')"), - MARKERS["end_code"], - MARKERS["start_output"], - MARKERS["warning"], - "Hello, world!", - "", - MARKERS["end_output"], - "More text", - MARKERS["start_code"], - md_comment("print('Hello again!')"), - MARKERS["end_code"], - MARKERS["start_output"], - MARKERS["warning"], - "Hello again!", - "", - MARKERS["end_output"], - ] - assert_process(input_lines, expected_output) - - # Test case 3: No code blocks - input_lines = [ - "Some text", - "More text", - ] - expected_output = [ - "Some text", - "More text", - ] - assert_process(input_lines, expected_output) - - -if __name__ == "__main__": - test_process_markdown() - update_markdown_file(Path(__file__).parent.parent / "README.md") diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index 36a994a9..f2a62407 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -5,10 +5,9 @@ on: branches: - master paths: - - ".github/update-readme.py" - "README.md" - - ".github/workflows/update-readme.yml" - "custom_components/adaptive_lighting/const.py" + - "github/workflows/update-readme.yml" pull_request: jobs: @@ -25,10 +24,10 @@ jobs: - name: Install pandas and tabulate run: | - pip install pandas tabulate + pip install markdown-code-runner pandas tabulate - - name: Run update-readme.py - run: python ./.github/update-readme.py + - name: Run markdown-code-runner + run: markdown-code-runner --debug README.md - name: Commit updated README.md run: | diff --git a/README.md b/README.md index 7cad61a3..378c0bb6 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ The YAML and frontend configuration methods support all of the options listed be - + From 9e5e9a49e5e682592c0fb4170ee4a6173c8d4646 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 2 Apr 2023 18:56:56 -0700 Subject: [PATCH 0523/1077] Sync main branch to master (#507) --- .github/workflows/main-to-master-sync.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/main-to-master-sync.yml diff --git a/.github/workflows/main-to-master-sync.yml b/.github/workflows/main-to-master-sync.yml new file mode 100644 index 00000000..99cd85b3 --- /dev/null +++ b/.github/workflows/main-to-master-sync.yml @@ -0,0 +1,22 @@ +name: Sync Main to Master + +on: + push: + branches: + - main + +jobs: + sync: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + ref: main + fetch-depth: 0 + + - name: Push changes to master + run: | + git checkout -b master + git push origin +master From b081d79b8631e32881e1b26c788e0de374339f28 Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Sun, 2 Apr 2023 20:57:11 -0500 Subject: [PATCH 0524/1077] Update services.yaml (#497) Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/services.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 8524dd64..5eb49149 100755 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -87,6 +87,11 @@ change_switch_settings: - "current" - "configuration" - "factory" + include_config_in_attributes: + description: "include_config_in_attributes: All config options will be listed as attributes under the adaptive-lighting switch this integration creates. (default: false)" + required: false + selector: + boolean: turn_on_lights: description: "Turn on the lights that are off, default: false" example: false From ccf18c38792da5bdaa1cc37e2707d416f8aa9b77 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 2 Apr 2023 19:17:35 -0700 Subject: [PATCH 0525/1077] Bump to 1.8.0 (#508) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 8c4233f9..39276752 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": [], - "version": "1.7.0" + "version": "1.8.0" } From ea2a6b0173240f98e50319f8a1db26d48ee1c5b8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 2 Apr 2023 19:22:23 -0700 Subject: [PATCH 0526/1077] Add auto_reset_manual_control with async timer (#487) * Add auto_reset_manual_control with async timer * Add failing test * Debugging * Refactor find_switch_for_lights * Revert changes in is_manually_controlled * style * Fix test_manual_control * add types * fixes * dict * make all tests pass * text * Rework find_switch_for_lights * Style * Better check * revert, do in other test! * No need to log when raising * Add type hint * Suggestion https://github.com/basnijholt/adaptive-lighting/pull/488/files#r1152408541 by @th3w1zard1 * Small fixes * document new config everywhere (#496) * chore(docs): update TOC * undo change * fi * Update README.md * Update README.md * chore(docs): update TOC * Update README.md * Use markdown-code-runner * Remove * Use markdown-code-runner instead of packaged solution * fix comment * Only commit when needed --------- Co-authored-by: Benjamin Auquite Co-authored-by: basnijholt Co-authored-by: github-actions[bot] --- .github/workflows/update-readme.yml | 10 +- README.md | 7 +- custom_components/adaptive_lighting/const.py | 10 ++ .../adaptive_lighting/services.yaml | 6 + .../adaptive_lighting/strings.json | 3 +- custom_components/adaptive_lighting/switch.py | 122 +++++++++++++++++- tests/test_switch.py | 48 ++++++- 7 files changed, 195 insertions(+), 11 deletions(-) diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index f2a62407..4d4c4530 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -30,13 +30,21 @@ jobs: run: markdown-code-runner --debug README.md - name: Commit updated README.md + id: commit run: | git add README.md git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" - git diff --quiet && git diff --staged --quiet || git commit -m "Update README.md" + if git diff --quiet && git diff --staged --quiet; then + echo "No changes in README.md, skipping commit." + echo "commit_status=skipped" >> $GITHUB_ENV + else + git commit -m "Update README.md" + echo "commit_status=committed" >> $GITHUB_ENV + fi - name: Push changes + if: env.commit_status == 'committed' uses: ad-m/github-push-action@master with: github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 378c0bb6..6c03f1f7 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Adaptive Lighting provides four switches (using "living_room" as an example comp Adaptive Lighting is designed to automatically detect when you or another source (e.g., automation) manually changes light settings 🕹️. When this occurs, the affected light is marked as "manually controlled," and Adaptive Lighting will not make further adjustments until the light is turned off and back on or reset using the `adaptive_lighting.set_manual_control` service call. -This feature is available when take_over_control is enabled. +This feature is available when `take_over_control` is enabled. Additionally, enabling detect_non_ha_changes allows Adaptive Lighting to detect all state changes, including those made outside of Home Assistant, by comparing the light's state to its previously used settings. The `adaptive_lighting.manual_control` event is fired when a light is marked as "manually controlled," allowing for integration with automations 🤖. @@ -47,7 +47,7 @@ The `adaptive_lighting.manual_control` event is fired when a light is marked as - [`adaptive_lighting.change_switch_settings`](#adaptive_lightingchange_switch_settings) - [:robot: Automation examples](#robot-automation-examples) - [Additional Information](#additional-information) -- [Troubleshooting](#troubleshooting) +- [:sos: Troubleshooting](#sos-troubleshooting) - [:exclamation: Common Problems & Solutions](#exclamation-common-problems--solutions) - [:bulb: Lights Not Responding or Turning On by Themselves](#bulb-lights-not-responding-or-turning-on-by-themselves) - [:signal_strength: WiFi Networks](#signal_strength-wifi-networks) @@ -119,6 +119,7 @@ The YAML and frontend configuration methods support all of the options listed be | `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | | `send_split_delay` | Wait time (milliseconds) between commands when using `separate_turn_on_commands`. Helps ensure correct handling. ⏲️ | `0` | `int` 0-10000 | | `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Helps avoid flickering. ⏲️ | `0` | `float > 0` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-604800 | @@ -298,7 +299,7 @@ For more details on adding the integration and setting options, refer to the [do Adaptive Lighting was initially inspired by @claytonjn's [hass-circadian\_lighting](https://github.com/claytonjn/hass-circadian_lighting), but has since been entirely rewritten and expanded with new features. -# Troubleshooting +# :sos: Troubleshooting Encountering issues? Enable debug logging in your `configuration.yaml`: diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 421eadcc..eb29fa13 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -152,6 +152,11 @@ DOCS[CONF_SEND_SPLIT_DELAY] = ( "Helps ensure correct handling. ⏲️" ) +CONF_AUTORESET_CONTROL, DEFAULT_AUTORESET_CONTROL = "autoreset_control_seconds", 0 +DOCS[CONF_AUTORESET_CONTROL] = ( + "Automatically reset the manual control after a number of seconds. " + "Set to 0 to disable. ⏲️" +) SLEEP_MODE_SWITCH = "sleep_mode_switch" ADAPT_COLOR_SWITCH = "adapt_color_switch" @@ -220,6 +225,11 @@ VALIDATION_TUPLES = [ (CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool), (CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY, int_between(0, 10000)), (CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, cv.positive_float), + ( + CONF_AUTORESET_CONTROL, + DEFAULT_AUTORESET_CONTROL, + int_between(0, 7 * 24 * 60 * 60), # 7 days max + ), ] diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 5eb49149..373e796a 100755 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -248,3 +248,9 @@ change_switch_settings: example: 0 selector: text: + autoreset_control_seconds: + description: "autoreset_control_seconds: wait time (seconds) before Adaptive Lighting resets `manual_control` status of any light (default: 0)" + required: false + example: 0 + selector: + text: diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 73f70a2c..5a8ef6af 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -45,7 +45,8 @@ "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", "transition": "Transition time when applying a change to the lights (seconds)", - "adapt_delay": "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering." + "adapt_delay": "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering.", + "autoreset_control_seconds": "autoreset_control_seconds: wait time (seconds) before Adaptive Lighting resets `manual_control` status of any light (default: 0)" } } }, diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index c94ab32a..5cf17122 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -97,6 +97,7 @@ from .const import ( ATTR_ADAPT_COLOR, ATTR_TURN_ON_OFF_LISTENER, CONF_ADAPT_DELAY, + CONF_AUTORESET_CONTROL, CONF_DETECT_NON_HA_CHANGES, CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, CONF_INITIAL_TRANSITION, @@ -265,7 +266,7 @@ def find_switch_for_lights( is_on: bool = False, ) -> AdaptiveSwitch: """Find the switch that controls the lights in 'lights'.""" - switches = _get_switches_with_lights(hass, lights, is_on) + switches = _get_switches_with_lights(hass, lights) if len(switches) == 1: return switches[0] elif len(switches) > 1: @@ -330,7 +331,7 @@ def _get_switches_from_service_call( async def handle_change_switch_settings( switch: AdaptiveSwitch, service_call: ServiceCall -): +) -> None: """Allows HASS to change config values via a service call.""" data = service_call.data @@ -473,7 +474,7 @@ async def async_setup_entry( all_lights = _expand_light_groups(switch.hass, lights) if service_call.data[CONF_MANUAL_CONTROL]: for light in all_lights: - switch.turn_on_off_listener.manual_control[light] = True + switch.turn_on_off_listener.mark_as_manual_control(light) _fire_manual_control_event(switch, light, service_call.context) else: switch.turn_on_off_listener.reset(*all_lights) @@ -820,6 +821,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._transition = data[CONF_TRANSITION] self._adapt_delay = data[CONF_ADAPT_DELAY] self._send_split_delay = data[CONF_SEND_SPLIT_DELAY] + self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL] _loc = get_astral_location(self.hass) if isinstance(_loc, tuple): # Astral v2.2 @@ -893,6 +895,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): def _expand_light_groups(self) -> None: all_lights = _expand_light_groups(self.hass, self._lights) self.turn_on_off_listener.lights.update(all_lights) + self.turn_on_off_listener.set_auto_reset_manual_control_times( + all_lights, self._auto_reset_manual_control_time + ) self._lights = list(all_lights) async def _setup_listeners(self, _=None) -> None: @@ -1526,6 +1531,10 @@ class TurnOnOffListener: # Track last 'service_data' to 'light.turn_on' resulting from this integration self.last_service_data: dict[str, dict[str, Any]] = {} + # Track auto reset of manual_control + self.auto_reset_manual_control_timers: dict[str, _AsyncSingleShotTimer] = {} + self.auto_reset_manual_control_times: dict[str, float] = {} + # When a state is different `max_cnt_significant_changes` times in a row, # mark it as manually_controlled. self.max_cnt_significant_changes = 2 @@ -1537,11 +1546,71 @@ class TurnOnOffListener: EVENT_STATE_CHANGED, self.state_changed_event_listener ) + def set_auto_reset_manual_control_times(self, lights: list[str], time: float): + """Set the time after which the lights are automatically reset.""" + if time == 0: + return + for light in lights: + old_time = self.auto_reset_manual_control_times.get(light) + if (old_time is not None) and (old_time != time): + _LOGGER.info( + "Setting auto_reset_manual_control for '%s' from %s seconds to %s seconds." + " This might happen because the light is in multiple swiches" + " or because of a config change.", + light, + old_time, + time, + ) + self.auto_reset_manual_control_times[light] = time + + def mark_as_manual_control(self, light: str) -> None: + """Mark a light as manually controlled.""" + _LOGGER.debug("Marking '%s' as manually controlled.", light) + self.manual_control[light] = True + delay = self.auto_reset_manual_control_times.get(light) + timer = self.auto_reset_manual_control_timers.get(light) + if timer is not None: + if delay is None: # Timer object exists, but should not anymore + timer.cancel() + self.auto_reset_manual_control_timers.pop(light) + else: # Timer object already exists, just update the delay and restart it + timer.delay = delay + timer.start() + elif delay is not None: # Timer object does not exist, create it + + async def reset(): + self.reset(light) + switches = _get_switches_with_lights(self.hass, [light]) + for switch in switches: + if not switch.is_on: + continue + # pylint: disable=protected-access + await switch._update_attrs_and_maybe_adapt_lights( + [light], + transition=switch._initial_transition, + force=True, + context=switch.create_context("autoreset"), + ) + _LOGGER.debug( + "Auto resetting 'manual_control' status of '%s' because" + " it was not manually controlled for %s seconds.", + light, + delay, + ) + assert not self.manual_control[light] + + timer = _AsyncSingleShotTimer(delay, reset) + self.auto_reset_manual_control_timers[light] = timer + timer.start() + def reset(self, *lights, reset_manual_control=True) -> None: """Reset the 'manual_control' status of the lights.""" for light in lights: if reset_manual_control: self.manual_control[light] = False + timer = self.auto_reset_manual_control_timers.pop(light, None) + if timer is not None: + timer.cancel() self.last_state_change.pop(light, None) self.last_service_data.pop(light, None) self.cnt_significant_changes[light] = 0 @@ -1599,6 +1668,14 @@ class TurnOnOffListener: if task is not None: task.cancel() self.turn_on_event[eid] = event + timer = self.auto_reset_manual_control_timers.get(eid) + if ( + timer is not None + and timer.is_running() + and event.time_fired > timer.start_time + ): + # Restart the auto reset timer + timer.start() async def state_changed_event_listener(self, event: Event) -> None: """Track 'state_changed' events.""" @@ -1674,7 +1751,7 @@ class TurnOnOffListener: ): # Light was already on and 'light.turn_on' was not called by # the adaptive_lighting integration. - manual_control = self.manual_control[light] = True + manual_control = self.mark_as_manual_control(light) _fire_manual_control_event(switch, light, turn_on_event.context) _LOGGER.debug( "'%s' was already on and 'light.turn_on' was not called by the" @@ -1746,7 +1823,7 @@ class TurnOnOffListener: # Only mark a light as significantly changing, if changed==True # N times in a row. We do this because sometimes a state changes # happens only *after* a new update interval has already started. - self.manual_control[light] = True + self.mark_as_manual_control(light) _fire_manual_control_event(switch, light, context, is_async=False) else: if n_changes > 1: @@ -1857,3 +1934,38 @@ class TurnOnOffListener: # other 'off' → 'on' state switches resulting from polling. That # would mean we 'return True' here. return False + + +class _AsyncSingleShotTimer: + def __init__(self, delay, callback): + """Initialize the timer.""" + self.delay = delay + self.callback = callback + self.task = None + self.start_time: int | None = None + + async def _run(self): + """Run the timer. Don't call this directly, use start() instead.""" + self.start_time = dt_util.utcnow() + await asyncio.sleep(self.delay) + if self.callback: + if asyncio.iscoroutinefunction(self.callback): + await self.callback() + else: + self.callback() + + def is_running(self): + """Return whether the timer is running.""" + return self.task is not None and not self.task.done() + + def start(self): + """Start the timer.""" + if self.task is not None and not self.task.done(): + self.task.cancel() + self.task = asyncio.create_task(self._run()) + + def cancel(self): + """Cancel the timer.""" + if self.task: + self.task.cancel() + self.callback = None diff --git a/tests/test_switch.py b/tests/test_switch.py index 8584bb1f..5b74c94c 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -10,6 +10,7 @@ from homeassistant.components.adaptive_lighting.const import ( ADAPT_BRIGHTNESS_SWITCH, ADAPT_COLOR_SWITCH, ATTR_TURN_ON_OFF_LISTENER, + CONF_AUTORESET_CONTROL, CONF_DETECT_NON_HA_CHANGES, CONF_INITIAL_TRANSITION, CONF_MANUAL_CONTROL, @@ -184,7 +185,7 @@ async def setup_lights_and_switch(hass, extra_conf=None): # Setup switch lights = [ - "light.bed_light", + ENTITY_LIGHT, "light.ceiling_lights", ] assert all(hass.states.get(light) is not None for light in lights) @@ -584,6 +585,50 @@ async def test_manual_control(hass): assert all([not manual_control[eid] for eid in switch._lights]) +async def test_auto_reset_manual_control(hass): + switch, (light, *_) = await setup_lights_and_switch( + hass, {CONF_AUTORESET_CONTROL: 0.1} + ) + context = switch.create_context("test") # needs to be passed to update method + manual_control = switch.turn_on_off_listener.manual_control + + async def update(): + await switch._update_attrs_and_maybe_adapt_lights(transition=0, context=context) + await hass.async_block_till_done() + + async def turn_light(state, **kwargs): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: light.entity_id, **kwargs}, + blocking=True, + ) + await hass.async_block_till_done() + await update() + _LOGGER.debug( + "Turn light %s to state %s, to %s", light.entity_id, state, kwargs + ) + + _LOGGER.debug("Start test auto reset manual control") + await turn_light(True, brightness=1) + await turn_light(True, brightness=10) + assert manual_control[light.entity_id] + await asyncio.sleep(0.3) # Should be enough time for auto reset + await update() + assert not manual_control[light.entity_id], (light, manual_control) + + # Do a couple of quick changes and check that light is not reset + for i in range(3): + _LOGGER.debug("Quick change %s", i) + await turn_light(True, brightness=(i + 1) * 20) + await asyncio.sleep(0.05) # Less than 0.1 + assert manual_control[light.entity_id] + + await asyncio.sleep(0.3) # Wait the auto reset time + await update() + assert not manual_control[light.entity_id] + + async def test_apply_service(hass): """Test adaptive_lighting.apply service.""" switch, (_, _, light) = await setup_lights_and_switch(hass) @@ -734,6 +779,7 @@ async def test_significant_change(hass): assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] # On next update the light should be marked as manually controlled await update(force=False) + # TODO: the state should be `bool(manual_control) is True` assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] From c915bda9b94896a8016f69436a1b4b2af7cf9432 Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Mon, 3 Apr 2023 01:57:12 -0500 Subject: [PATCH 0527/1077] Autoreset_Control_Time - small changes (#515) * small changes * Update README.md * trivial comment change --------- Co-authored-by: github-actions[bot] Co-authored-by: Bas Nijholt --- README.md | 2 +- custom_components/adaptive_lighting/const.py | 2 +- custom_components/adaptive_lighting/switch.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6c03f1f7..037a6b05 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ The YAML and frontend configuration methods support all of the options listed be | `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | | `send_split_delay` | Wait time (milliseconds) between commands when using `separate_turn_on_commands`. Helps ensure correct handling. ⏲️ | `0` | `int` 0-10000 | | `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Helps avoid flickering. ⏲️ | `0` | `float > 0` | -| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-604800 | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index eb29fa13..471d593c 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -228,7 +228,7 @@ VALIDATION_TUPLES = [ ( CONF_AUTORESET_CONTROL, DEFAULT_AUTORESET_CONTROL, - int_between(0, 7 * 24 * 60 * 60), # 7 days max + int_between(0, 365 * 24 * 60 * 60), # 1 year max ), ] diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 5cf17122..6527b73d 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -822,6 +822,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._adapt_delay = data[CONF_ADAPT_DELAY] self._send_split_delay = data[CONF_SEND_SPLIT_DELAY] self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL] + self._expand_light_groups() # updates manual control timers _loc = get_astral_location(self.hass) if isinstance(_loc, tuple): # Astral v2.2 From 9caf3048f1889129d8225a90a35384721f671830 Mon Sep 17 00:00:00 2001 From: igiannakas <59056762+igiannakas@users.noreply.github.com> Date: Mon, 3 Apr 2023 08:27:07 +0100 Subject: [PATCH 0528/1077] Continue to adapt color temperature down to the sleep temperature after sunset (#87) * Update switch.py Continue to adapt color temperature down to the sleep temperature after sunset. Results in a gradually warming light during the night time rather than a fixed color temperature throughout the night time. * Run pre-commit * Merge branch 'master' into pr/87 * add config option bool `adapt_until_sleep` defaulting to `false` --------- Co-authored-by: Bas Nijholt Co-authored-by: Bas Nijholt Co-authored-by: Benjamin Auquite --- README.md | 6 ++++++ custom_components/adaptive_lighting/const.py | 10 ++++++++++ custom_components/adaptive_lighting/strings.json | 1 + custom_components/adaptive_lighting/switch.py | 10 +++++++++- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 037a6b05..c354153d 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ The `adaptive_lighting.manual_control` event is fired when a light is marked as - [:sunny: Sun Position](#sunny-sun-position) - [:thermometer: Color Temperature](#thermometer-color-temperature) - [:high_brightness: Brightness](#high_brightness-brightness) + - [While using `adapt_until_sleep: true`](#while-using-adapt_until_sleep-true) - [:busts_in_silhouette: Contributors](#busts_in_silhouette-contributors) @@ -95,6 +96,7 @@ The YAML and frontend configuration methods support all of the options listed be | `lights` | List of light entities to be controlled by Adaptive Lighting (may be empty). 🌟 | `[]` | list of `entity_id`s | | `prefer_rgb_color` | Use RGB color adjustment instead of native light color temperature. 🌈 | `False` | `bool` | | `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| `adapt_until_sleep` | When `true`, Adaptive Lighting will use the sleep settings as the minimum, and transition to these values past the sunset | `False` | `bool` | | `initial_transition` | Duration of the first transition when lights turn from `off` to `on`. ⏲️ | `1` | `float` 0-6553 | | `sleep_transition` | Duration of transition when 'sleep mode' is toggled. 😴 | `1` | `float` 0-6553 | | `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | @@ -364,6 +366,10 @@ These graphs were generated using the values calculated by the Adaptive Lighting #### :high_brightness: Brightness ![cl_brightness|690x130](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/8/58ebd994b62a8b1abfb3497a5288d923ff4e2330.PNG) +#### While using `adapt_until_sleep: true` +![image](https://user-images.githubusercontent.com/2219836/228949675-f9699624-8abc-466c-bb04-250ce0f495b8.png) + + ## :busts_in_silhouette: Contributors diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 471d593c..a5a5f2c1 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -140,6 +140,15 @@ DOCS[CONF_TAKE_OVER_CONTROL] = ( CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 45 DOCS[CONF_TRANSITION] = "Duration of transition when lights change, in seconds. 🕑" +CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP = ( + "transition_until_sleep", + False, +) +DOCS[CONF_ADAPT_UNTIL_SLEEP] = ( + "When checked, Adaptive Lighting will use the sleep settings as the minimum," + " and transition to these values past the sunset" +) + CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0 DOCS[CONF_ADAPT_DELAY] = ( "Wait time (seconds) between light turn on and Adaptive Lighting applying " @@ -190,6 +199,7 @@ VALIDATION_TUPLES = [ (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), (CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION, VALID_TRANSITION), (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), + (CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP, bool), (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), (CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS, int_between(1, 100)), (CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)), diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 5a8ef6af..2ebd96d0 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -22,6 +22,7 @@ "lights": "lights", "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (seconds)", "include_config_in_attributes": "include_config_in_attributes: All config options will be listed as attributes under the adaptive-lighting switch this integration creates. (default: false)", + "adapt_until_sleep": "adapt_until_sleep: When checked, Adaptive Lighting will use the sleep settings as the minimum, and transition to these values past the sunset (default: false)", "sleep_transition": "sleep_transition: When 'sleep_state' changes. (seconds)", "interval": "interval: Time between switch updates. (seconds)", "max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 6527b73d..75361965 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -97,6 +97,7 @@ from .const import ( ATTR_ADAPT_COLOR, ATTR_TURN_ON_OFF_LISTENER, CONF_ADAPT_DELAY, + CONF_ADAPT_UNTIL_SLEEP, CONF_AUTORESET_CONTROL, CONF_DETECT_NON_HA_CHANGES, CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, @@ -834,6 +835,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._sun_light_settings = SunLightSettings( name=self._name, astral_location=location, + adapt_until_sleep=data[CONF_ADAPT_UNTIL_SLEEP], max_brightness=data[CONF_MAX_BRIGHTNESS], max_color_temp=data[CONF_MAX_COLOR_TEMP], min_brightness=data[CONF_MIN_BRIGHTNESS], @@ -1325,6 +1327,7 @@ class SunLightSettings: name: str astral_location: astral.Location + adapt_until_sleep: bool max_brightness: int max_color_temp: int min_brightness: int @@ -1474,7 +1477,12 @@ class SunLightSettings: delta = self.max_color_temp - self.min_color_temp ct = (delta * percent) + self.min_color_temp return 5 * round(ct / 5) # round to nearest 5 - return self.min_color_temp + if percent == 0 or not self.adapt_until_sleep: + return self.min_color_temp + if self.adapt_until_sleep and percent < 0: + delta = abs(self.min_color_temp - self.sleep_color_temp) + ct = (delta * abs(1 + percent)) + self.sleep_color_temp + return 5 * round(ct / 5) # round to nearest 5 def get_settings( self, is_sleep, transition From cdc3585a66969211a08762eb05ae36ab3f070130 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 07:58:20 +0000 Subject: [PATCH 0529/1077] docs: add igiannakas as a contributor for code (#517) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 063b429c..344b3112 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -429,6 +429,15 @@ "contributions": [ "code" ] + }, + { + "login": "igiannakas", + "name": "igiannakas", + "avatar_url": "https://avatars.githubusercontent.com/u/59056762?v=4", + "profile": "https://github.com/igiannakas", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index c354153d..9ebc7397 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-46-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-47-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -436,6 +436,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting
+ From 19fcb1d6b9c1c0a0abcb0a88bc16b05839818726 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 3 Apr 2023 00:58:39 -0700 Subject: [PATCH 0530/1077] Automatically generate more service tables in the README (#509) * Automatically sync more data * Automatically generate apply table * Generate manual control * Add manual control docs * Update README.md * Move docs functions to separate file * Update code * simple * no path * link paths * Fix link * missed * cd core * Update README.md * Allow alternative docs * update readme * More special * Fx * Update README.md * Remove common descriptions * Update README.md * Update README.md * Rephrase * Update README.md --------- Co-authored-by: github-actions[bot] --- .github/workflows/pytest.yaml | 8 +- .github/workflows/update-readme.yml | 7 +- README.md | 75 ++++---- .../adaptive_lighting/_docs_helpers.py | 116 ++++++++++++ custom_components/adaptive_lighting/const.py | 142 +++++++-------- .../adaptive_lighting/services.yaml | 168 +++++++++--------- custom_components/adaptive_lighting/switch.py | 28 +-- 7 files changed, 333 insertions(+), 211 deletions(-) create mode 100644 custom_components/adaptive_lighting/_docs_helpers.py diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 47d4df8a..02bf93e3 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -31,8 +31,8 @@ jobs: echo "::notice::### 4. ERROR:homeassistant.setup:Setup failed for 'component': Unable to import component: No module named ''module'' ###" echo "::notice::### 5. add 'component'.'module' (without the '') from the above log into the 'required' list inside of 'test_dependencies.py' ###" echo "::notice::### 6. Try again! If more issues persist they should be easily solvable by reading the verbose logs now. ###" - - name: Run pytest - timeout-minutes: 60 + + - name: Link custom_components/adaptive_lighting run: | cd core @@ -46,6 +46,10 @@ jobs: ln -fs ../../../tests adaptive_lighting cd - + - name: Run pytest + timeout-minutes: 60 + run: | + cd core python3 -X dev -m pytest \ -qq \ --timeout=9 \ diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index 4d4c4530..bc31bb94 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -22,10 +22,15 @@ jobs: with: python_version: "3.10" - - name: Install pandas and tabulate + - name: Install markdown-code-runner and README code dependencies run: | pip install markdown-code-runner pandas tabulate + - name: Link custom_components/adaptive_lighting + run: | + cd core/homeassistant/components + ln -fs ../../../custom_components/adaptive_lighting adaptive_lighting + - name: Run markdown-code-runner run: markdown-code-runner --debug README.md diff --git a/README.md b/README.md index 9ebc7397..5b98de8b 100644 --- a/README.md +++ b/README.md @@ -82,11 +82,8 @@ All of the configuration options are listed below, along with their default valu The YAML and frontend configuration methods support all of the options listed below. - - - - - + + @@ -94,27 +91,27 @@ The YAML and frontend configuration methods support all of the options listed be | Variable name | Description | Default | Type | |:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------| | `lights` | List of light entities to be controlled by Adaptive Lighting (may be empty). 🌟 | `[]` | list of `entity_id`s | -| `prefer_rgb_color` | Use RGB color adjustment instead of native light color temperature. 🌈 | `False` | `bool` | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | | `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | -| `adapt_until_sleep` | When `true`, Adaptive Lighting will use the sleep settings as the minimum, and transition to these values past the sunset | `False` | `bool` | -| `initial_transition` | Duration of the first transition when lights turn from `off` to `on`. ⏲️ | `1` | `float` 0-6553 | -| `sleep_transition` | Duration of transition when 'sleep mode' is toggled. 😴 | `1` | `float` 0-6553 | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | +| `sleep_transition` | Duration of transition when 'sleep mode' is toggled in seconds. 😴 | `1` | `float` 0-6553 | | `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | | `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | | `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | | `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | | `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | | `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | -| `sleep_brightness` | Brightness of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | | `sleep_rgb_or_color_temp` | Use either `'rgb_color'` or `'color_temp'` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | -| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`). 😴 | `1000` | `int` 1000-10000 | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | | `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is 'rgb_color'). 🌈 | `[255, 56, 0]` | RGB color | -| `sunrise_time` | Set a fixed time for sunrise. 🌅 | `None` | `str` | -| `max_sunrise_time` | Set the latest virtual sunrise time, allowing for earlier real sunrises. 🌅 | `None` | `str` | -| `sunrise_offset` | Adjust sunrise time with a positive or negative offset. ⏰ | `0` | `int` | -| `sunset_time` | Set a fixed time for sunset. 🌇 | `None` | `str` | -| `min_sunset_time` | Set the earliest virtual sunset time, allowing for later real sunsets. 🌇 | `None` | `str` | -| `sunset_offset` | Adjust sunset time with a positive or negative offset. ⏰ | `0` | `int` | +| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | | `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | | `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | | `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` | @@ -158,26 +155,42 @@ adaptive_lighting: `adaptive_lighting.apply` applies Adaptive Lighting settings to lights on demand. -| Service data attribute | Required | Description | -| ---------------------- | -------- | -------------------------------------------------------------------------------------------- | -| `entity_id` | ✅ | The `entity_id` of the switch with the settings to apply. | -| `lights` | ❌ | A light (or list of lights) to apply the settings to. | -| `transition` | ❌ | The number of seconds for the transition. | -| `adapt_brightness` | ❌ | Whether to change the brightness of the light or not. | -| `adapt_color` | ❌ | Whether to adapt the color on supporting lights. | -| `prefer_rgb_color` | ❌ | Whether to prefer RGB color adjustment over of native light color temperature when possible. | -| `turn_on_lights` | ❌ | Whether to turn on lights that are currently off. | + + + + + + +| Service data attribute | Description | Required | Type | +|:-------------------------|:-------------------------------------------------------------------------------------|:-----------|:---------------------| +| `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ✅ | list of `entity_id`s | +| `lights` | A light (or list of lights) to apply the settings to. 💡 | ❌ | list of `entity_id`s | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | ❌ | `float` 0-6553 | +| `adapt_brightness` | Whether to adapt the brightness of the light. 🌞 | ❌ | bool | +| `adapt_color` | Whether to adapt the color on supporting lights. 🌈 | ❌ | bool | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | ❌ | bool | +| `turn_on_lights` | Whether to turn on lights that are currently off. 🔆 | ❌ | bool | + + #### `adaptive_lighting.set_manual_control` `adaptive_lighting.set_manual_control` can mark (or unmark) whether a light is "manually controlled", meaning that when a light has `manual_control`, the light is not adapted. -| Service data attribute | Required | Description | -| ---------------------- | -------- | --------------------------------------------------------------------------------------------------- | -| `entity_id` | ✅ | The `entity_id` of the switch in which to (un)mark the light as being "manually controlled". | -| `lights` | ❌ | entity_id(s) of lights, if not specified, all lights in the switch are selected. | -| `manual_control` | ❌ | Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true | + + + + + + +| Service data attribute | Description | Required | Type | +|:-------------------------|:-----------------------------------------------------------------------------------------------|:-----------|:---------------------| +| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s | +| `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s | +| `manual_control` | Whether to add ('true') or remove ('false') the light from the 'manual_control' list. 🔒 | ❌ | bool | + + #### `adaptive_lighting.change_switch_settings` `adaptive_lighting.change_switch_settings` (new in 1.7.0) Change any of the above configuration options of Adaptive Lighting (such as `sunrise_time` or `prefer_rgb_color`) with a service call directly from your script/automation. diff --git a/custom_components/adaptive_lighting/_docs_helpers.py b/custom_components/adaptive_lighting/_docs_helpers.py new file mode 100644 index 00000000..40afc235 --- /dev/null +++ b/custom_components/adaptive_lighting/_docs_helpers.py @@ -0,0 +1,116 @@ +from typing import Any + +from homeassistant.helpers import selector +import homeassistant.helpers.config_validation as cv +import pandas as pd +import voluptuous as vol + +from .const import ( + DOCS, + DOCS_APPLY, + DOCS_MANUAL_CONTROL, + SET_MANUAL_CONTROL_SCHEMA, + VALIDATION_TUPLES, + apply_service_schema, +) + + +def _format_voluptuous_instance(instance): + coerce_type = None + min_val = None + max_val = None + + for validator in instance.validators: + if isinstance(validator, vol.Coerce): + coerce_type = validator.type.__name__ + elif isinstance(validator, (vol.Clamp, vol.Range)): + min_val = validator.min + max_val = validator.max + + if min_val is not None and max_val is not None: + return f"`{coerce_type}` {min_val}-{max_val}" + elif min_val is not None: + return f"`{coerce_type} > {min_val}`" + elif max_val is not None: + return f"`{coerce_type} < {max_val}`" + else: + return f"`{coerce_type}`" + + +def _type_to_str(type_: Any) -> str: + """Convert a (voluptuous) type to a string.""" + if type_ == cv.entity_ids: + return "list of `entity_id`s" + elif type_ in (bool, int, float, str): + return f"`{type_.__name__}`" + elif type_ == cv.boolean: + return "bool" + elif isinstance(type_, vol.All): + return _format_voluptuous_instance(type_) + elif isinstance(type_, vol.In): + return f"one of `{type_.container}`" + elif isinstance(type_, selector.SelectSelector): + return f"one of `{type_.config['options']}`" + elif isinstance(type_, selector.ColorRGBSelector): + return "RGB color" + else: + raise ValueError(f"Unknown type: {type_}") + + +def generate_config_markdown_table(): + import pandas as pd + + rows = [] + for k, default, type_ in VALIDATION_TUPLES: + description = DOCS[k] + row = { + "Variable name": f"`{k}`", + "Description": description, + "Default": f"`{default}`", + "Type": _type_to_str(type_), + } + rows.append(row) + + df = pd.DataFrame(rows) + return df.to_markdown(index=False) + + +def _schema_to_dict(schema: vol.Schema) -> dict[str, tuple[Any, Any]]: + result = {} + for key, value in schema.schema.items(): + if isinstance(key, vol.Optional): + default_value = key.default + result[key.schema] = (default_value, value) + return result + + +def _generate_service_markdown_table( + schema: dict[str, tuple[Any, Any]], alternative_docs: dict[str, str] = None +): + schema = _schema_to_dict(schema) + rows = [] + for k, (default, type_) in schema.items(): + if alternative_docs is not None and k in alternative_docs: + description = alternative_docs[k] + else: + description = DOCS[k] + row = { + "Service data attribute": f"`{k}`", + "Description": description, + "Required": "✅" if default == vol.UNDEFINED else "❌", + "Type": _type_to_str(type_), + } + rows.append(row) + + df = pd.DataFrame(rows) + return df.to_markdown(index=False) + + +def generate_apply_markdown_table(): + return _generate_service_markdown_table(apply_service_schema(), DOCS_APPLY) + + +def generate_set_manual_control_markdown_table(): + return _generate_service_markdown_table( + SET_MANUAL_CONTROL_SCHEMA, DOCS_MANUAL_CONTROL + ) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index a5a5f2c1..bb0c07cd 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,6 +1,7 @@ """Constants for the Adaptive Lighting integration.""" from homeassistant.components.light import VALID_TRANSITION +from homeassistant.const import CONF_ENTITY_ID from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv import voluptuous as vol @@ -14,7 +15,7 @@ DOMAIN = "adaptive_lighting" SUN_EVENT_NOON = "solar_noon" SUN_EVENT_MIDNIGHT = "solar_midnight" -DOCS = {} +DOCS = {CONF_ENTITY_ID: "Entity ID of the switch. 📝"} CONF_NAME, DEFAULT_NAME = "name", "default" @@ -45,11 +46,14 @@ DOCS[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES] = ( CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 DOCS[CONF_INITIAL_TRANSITION] = ( - "Duration of the first transition when lights turn " "from `off` to `on`. ⏲️" + "Duration of the first transition when lights turn " + "from `off` to `on` in seconds. ⏲️" ) CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION = "sleep_transition", 1 -DOCS[CONF_SLEEP_TRANSITION] = "Duration of transition when 'sleep mode' is toggled. 😴" +DOCS[CONF_SLEEP_TRANSITION] = ( + "Duration of transition when 'sleep mode' is toggled " "in seconds. 😴" +) CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 DOCS[CONF_INTERVAL] = "Frequency to adapt the lights, in seconds. 🔄" @@ -73,9 +77,10 @@ DOCS[CONF_ONLY_ONCE] = ( ) CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False -DOCS[ - CONF_PREFER_RGB_COLOR -] = "Use RGB color adjustment instead of native light color temperature. 🌈" +DOCS[CONF_PREFER_RGB_COLOR] = ( + "Whether to prefer RGB color adjustment over " + "light color temperature when possible. 🌈" +) CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS = ( "separate_turn_on_commands", @@ -87,12 +92,12 @@ DOCS[CONF_SEPARATE_TURN_ON_COMMANDS] = ( ) CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1 -DOCS[CONF_SLEEP_BRIGHTNESS] = "Brightness of lights in sleep mode. 😴" +DOCS[CONF_SLEEP_BRIGHTNESS] = "Brightness percentage of lights in sleep mode. 😴" CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP = "sleep_color_temp", 1000 DOCS[CONF_SLEEP_COLOR_TEMP] = ( "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is " - "`color_temp`). 😴" + "`color_temp`) in Kelvin. 😴" ) CONF_SLEEP_RGB_COLOR, DEFAULT_SLEEP_RGB_COLOR = "sleep_rgb_color", [255, 56, 0] @@ -104,30 +109,36 @@ CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = ( "sleep_rgb_or_color_temp", "color_temp", ) -DOCS[ - CONF_SLEEP_RGB_OR_COLOR_TEMP -] = "Use either `'rgb_color'` or `'color_temp'` in sleep mode. 🌙" +DOCS[CONF_SLEEP_RGB_OR_COLOR_TEMP] = ( + "Use either `'rgb_color'` or `'color_temp'` " "in sleep mode. 🌙" +) CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 -DOCS[CONF_SUNRISE_OFFSET] = "Adjust sunrise time with a positive or negative offset. ⏰" +DOCS[CONF_SUNRISE_OFFSET] = ( + "Adjust sunrise time with a positive or negative offset " "in seconds. ⏰" +) CONF_SUNRISE_TIME = "sunrise_time" -DOCS[CONF_SUNRISE_TIME] = "Set a fixed time for sunrise. 🌅" +DOCS[CONF_SUNRISE_TIME] = "Set a fixed time (HH:MM:SS) for sunrise. 🌅" CONF_MAX_SUNRISE_TIME = "max_sunrise_time" DOCS[CONF_MAX_SUNRISE_TIME] = ( - "Set the latest virtual sunrise time, allowing" " for earlier real sunrises. 🌅" + "Set the latest virtual sunrise time (HH:MM:SS), allowing" + " for earlier real sunrises. 🌅" ) CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 -DOCS[CONF_SUNSET_OFFSET] = "Adjust sunset time with a positive or negative offset. ⏰" +DOCS[ + CONF_SUNSET_OFFSET +] = "Adjust sunset time with a positive or negative offset in seconds. ⏰" CONF_SUNSET_TIME = "sunset_time" -DOCS[CONF_SUNSET_TIME] = "Set a fixed time for sunset. 🌇" +DOCS[CONF_SUNSET_TIME] = "Set a fixed time (HH:MM:SS) for sunset. 🌇" CONF_MIN_SUNSET_TIME = "min_sunset_time" DOCS[CONF_MIN_SUNSET_TIME] = ( - "Set the earliest virtual sunset time, allowing" " for later real sunsets. 🌇" + "Set the earliest virtual sunset time (HH:MM:SS), allowing" + " for later real sunsets. 🌇" ) CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True @@ -145,8 +156,8 @@ CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP = ( False, ) DOCS[CONF_ADAPT_UNTIL_SLEEP] = ( - "When checked, Adaptive Lighting will use the sleep settings as the minimum," - " and transition to these values past the sunset" + "When enabled, Adaptive Lighting will treat sleep settings as the minimum, " + "transitioning to these values after sunset. 🌙" ) CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0 @@ -174,18 +185,36 @@ ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener" UNDO_UPDATE_LISTENER = "undo_update_listener" NONE_STR = "None" ATTR_ADAPT_COLOR = "adapt_color" +DOCS[ATTR_ADAPT_COLOR] = "Whether to adapt the color on supporting lights. 🌈" ATTR_ADAPT_BRIGHTNESS = "adapt_brightness" +DOCS[ATTR_ADAPT_BRIGHTNESS] = "Whether to adapt the brightness of the light. 🌞" SERVICE_SET_MANUAL_CONTROL = "set_manual_control" CONF_MANUAL_CONTROL = "manual_control" +DOCS[CONF_MANUAL_CONTROL] = "Whether to manually control the lights. 🔒" SERVICE_APPLY = "apply" CONF_TURN_ON_LIGHTS = "turn_on_lights" +DOCS[CONF_TURN_ON_LIGHTS] = "Whether to turn on lights that are currently off. 🔆" SERVICE_CHANGE_SWITCH_SETTINGS = "change_switch_settings" CONF_USE_DEFAULTS = "use_defaults" - +DOCS[CONF_USE_DEFAULTS] = "Whether to use default settings for the switches. ⚙️" TURNING_OFF_DELAY = 5 +DOCS_MANUAL_CONTROL = { + CONF_ENTITY_ID: "The `entity_id` of the switch in which to (un)mark the " + "light as being `manually controlled`. 📝", + CONF_LIGHTS: "entity_id(s) of lights, if not specified, all lights in the " + "switch are selected. 💡", + CONF_MANUAL_CONTROL: "Whether to add ('true') or remove ('false') the " + "light from the 'manual_control' list. 🔒", +} + +DOCS_APPLY = { + CONF_ENTITY_ID: "The `entity_id` of the switch with the settings to apply. 📝", + CONF_LIGHTS: "A light (or list of lights) to apply the settings to. 💡", +} + def int_between(min_int, max_int): """Return an integer between 'min_int' and 'max_int'.""" @@ -290,55 +319,28 @@ _DOMAIN_SCHEMA = vol.Schema( ) -def _format_voluptuous_instance(instance): - coerce_type = None - min_val = None - max_val = None - - for validator in instance.validators: - if isinstance(validator, vol.Coerce): - coerce_type = validator.type.__name__ - elif isinstance(validator, (vol.Clamp, vol.Range)): - min_val = validator.min - max_val = validator.max - - if min_val is not None and max_val is not None: - return f"`{coerce_type}` {min_val}-{max_val}" - elif min_val is not None: - return f"`{coerce_type} > {min_val}`" - elif max_val is not None: - return f"`{coerce_type} < {max_val}`" - else: - return f"`{coerce_type}`" - - -def generate_markdown_table(): - import pandas as pd - - rows = [] - for k, default, type_ in VALIDATION_TUPLES: - description = DOCS[k] - if type_ == cv.entity_ids: - type_ = "list of `entity_id`s" - elif type_ in (bool, int, float, str): - type_ = f"`{type_.__name__}`" - elif isinstance(type_, vol.All): - type_ = _format_voluptuous_instance(type_) - elif isinstance(type_, vol.In): - type_ = f"one of `{type_.container}`" - elif isinstance(type_, selector.SelectSelector): - type_ = f"one of `{type_.config['options']}`" - elif isinstance(type_, selector.ColorRGBSelector): - type_ = "RGB color" - else: - raise ValueError(f"Unknown type: {type_}") - row = { - "Variable name": f"`{k}`", - "Description": description, - "Default": f"`{default}`", - "Type": type_, +def apply_service_schema(initial_transition: int = 1): + """Return the schema for the apply service.""" + return vol.Schema( + { + vol.Optional(CONF_ENTITY_ID): cv.entity_ids, + vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, + vol.Optional( + CONF_TRANSITION, + default=initial_transition, + ): VALID_TRANSITION, + vol.Optional(ATTR_ADAPT_BRIGHTNESS, default=True): cv.boolean, + vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean, + vol.Optional(CONF_PREFER_RGB_COLOR, default=False): cv.boolean, + vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean, } - rows.append(row) + ) - df = pd.DataFrame(rows) - return df.to_markdown(index=False) + +SET_MANUAL_CONTROL_SCHEMA = vol.Schema( + { + vol.Optional(CONF_ENTITY_ID): cv.entity_ids, + vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, + vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean, + } +) diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 373e796a..89c112e3 100755 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -2,7 +2,7 @@ apply: description: Applies the current Adaptive Lighting settings to lights. fields: entity_id: - description: entity_id of the Adaptive Lighting switch. If not specified, the current Adaptive Lighting switch will be used. + description: "Entity ID of the switch. \U0001F4DD" example: switch.adaptive_lighting_default selector: entity: @@ -10,43 +10,42 @@ apply: domain: switch multiple: false lights: - description: entity_id(s) of lights, if not specified, all lights in the switch are selected. + description: "List of light entities to be controlled by Adaptive Lighting (may be empty). \U0001F31F" example: light.bedroom_ceiling selector: entity: domain: light multiple: true transition: - description: Transition of the lights. + description: "Duration of transition when lights change, in seconds. \U0001F551" example: 10 selector: - text: + text: null adapt_brightness: - description: "Adapt the 'brightness', default: true" + description: "Whether to adapt the brightness of the light. \U0001F31E" example: true selector: - boolean: + boolean: null adapt_color: - description: "Adapt the color_temp/color_rgb, default: true" + description: "Whether to adapt the color of the light. \U0001F308" example: true selector: - boolean: + boolean: null prefer_rgb_color: - description: "Prefer to use color_rgb over color_temp if possible, default: false" + description: "Use RGB color adjustment instead of native light color temperature. \U0001F308" example: false selector: - boolean: + boolean: null turn_on_lights: - description: "Turn on the lights that are off, default: false" + description: "Whether to turn on lights if they are off. \U0001F506" example: false selector: - boolean: - + boolean: null set_manual_control: description: Mark whether a light is 'manually controlled'. fields: entity_id: - description: entity_id of the Adaptive Lighting switch. If not specified, the current Adaptive Lighting switch will be used. + description: "Entity ID of the switch. \U0001F4DD" example: switch.adaptive_lighting_default selector: entity: @@ -54,138 +53,137 @@ set_manual_control: domain: switch multiple: false lights: - description: entity_id(s) of lights, if not specified, all lights in the switch are selected. + description: "List of light entities to be controlled by Adaptive Lighting (may be empty). \U0001F31F" example: light.bedroom_ceiling selector: entity: domain: light multiple: true manual_control: - description: "Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true" + description: "Whether to manually control the lights. \U0001F512" example: true default: true selector: - boolean: - + boolean: null change_switch_settings: - description: "Change any settings you'd like in the switch. All options here are the same as in the config flow." + description: Change any settings you'd like in the switch. All options here are the same as in the config flow. fields: entity_id: - description: "entity_id of the Adaptive Lighting switch." + description: "Entity ID of the switch. \U0001F4DD" required: true selector: entity: domain: switch use_defaults: - description: "(default: 'current' for current settings) You can set this to 'factory', 'configuration', or 'current' to reset the variables not being set with this service call. 'current' leaves them as is, 'configuration' resets to whatever already initializes at startup, 'factory' resets to the default values listed in the documentation." - example: "current" + description: "Whether to use default settings for the switches. \u2699\uFE0F" + example: current required: false - default: "current" + default: current selector: select: options: - - "current" - - "configuration" - - "factory" + - current + - configuration + - factory include_config_in_attributes: - description: "include_config_in_attributes: All config options will be listed as attributes under the adaptive-lighting switch this integration creates. (default: false)" + description: "Show all options as attributes on the switch in Home Assistant when set to `true`. \U0001F4DD" required: false selector: - boolean: + boolean: null turn_on_lights: - description: "Turn on the lights that are off, default: false" + description: "Whether to turn on lights if they are off. \U0001F506" example: false required: false selector: - boolean: + boolean: null initial_transition: - description: "initial_transition: When lights turn 'off' to 'on'. (seconds)" + description: "Duration of the first transition when lights turn from `off` to `on` in seconds. \u23F2\uFE0F" example: 1 required: false selector: - text: + text: null sleep_transition: - description: "sleep_transition: When 'sleep_state' changes. (seconds)" + description: "Duration of transition when 'sleep mode' is toggled in seconds. \U0001F634" example: 1 required: false selector: - text: + text: null max_brightness: - description: "max_brightness: Highest brightness of lights during a cycle. (%)" + description: "Maximum brightness percentage. \U0001F4A1" required: false example: 100 selector: - text: + text: null max_color_temp: - description: "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)" + description: "Coldest color temperature in Kelvin. \u2744\uFE0F" required: false example: 5500 selector: - text: + text: null min_brightness: - description: "min_brightness: Lowest brightness of lights during a cycle. (%)" + description: "Minimum brightness percentage. \U0001F4A1" required: false example: 1 selector: - text: + text: null min_color_temp: - description: "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)" + description: "Warmest color temperature in Kelvin. \U0001F525" required: false example: 2000 selector: - text: + text: null only_once: - description: "only_once: Only adapt the lights when turning them on." + description: "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). \U0001F504" example: false required: false selector: - boolean: + boolean: null prefer_rgb_color: - description: "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible." + description: "Use RGB color adjustment instead of native light color temperature. \U0001F308" required: false example: false selector: - boolean: + boolean: null separate_turn_on_commands: - description: "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights)." + description: "Use separate `light.turn_on` calls for color and brightness, needed for some light types. \U0001F500" required: false example: false selector: - boolean: + boolean: null send_split_delay: - description: "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly." + description: "Wait time (milliseconds) between commands when using `separate_turn_on_commands`. Helps ensure correct handling. \u23F2\uFE0F" required: false example: 0 selector: - boolean: + boolean: null sleep_brightness: - description: "sleep_brightness, Brightness setting for Sleep Mode. (%)" + description: "Brightness percentage of lights in sleep mode. \U0001F634" required: false example: 1 selector: - text: + text: null sleep_rgb_or_color_temp: - description: "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'" + description: "Use either `'rgb_color'` or `'color_temp'` in sleep mode. \U0001F319" required: false - example: "color_temp" + example: color_temp selector: select: options: - - "rgb_color" - - "color_temp" + - rgb_color + - color_temp sleep_rgb_color: - description: "sleep_rgb_color, in RGB" + description: "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is 'rgb_color'). \U0001F308" required: false selector: - color_rgb: + color_rgb: null sleep_color_temp: - description: "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)" + description: "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. \U0001F634" required: false example: 1000 selector: - text: + text: null sunrise_offset: - description: sunrise_offset, in +/- seconds (integer) + description: "Adjust sunrise time with a positive or negative offset in seconds. \u23F0" required: false example: 0 selector: @@ -193,64 +191,64 @@ change_switch_settings: min: 0 max: 86300 sunrise_time: - description: sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location) + description: "Set a fixed time (HH:MM:SS) for sunrise. \U0001F305" required: false - example: "" + example: '' selector: - time: + time: null sunset_offset: - description: sunset_offset, in +/- seconds (integer) + description: "Adjust sunset time with a positive or negative offset in seconds. \u23F0" required: false - example: "" + example: '' selector: number: min: 0 max: 86300 sunset_time: - description: sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location) - example: "" + description: "Set a fixed time (HH:MM:SS) for sunset. \U0001F307" + example: '' required: false selector: - time: + time: null max_sunrise_time: - description: "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)" - example: "" + description: "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. \U0001F305" + example: '' required: false selector: - time: + time: null min_sunset_time: - description: "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)" - example: "" + description: "Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. \U0001F307" + example: '' required: false selector: - time: + time: null take_over_control: - description: "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on." + description: "Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! \U0001F512" required: false example: true selector: - boolean: + boolean: null detect_non_ha_changes: - description: "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)" + description: "Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. \U0001F575\uFE0F" required: false example: false selector: - boolean: + boolean: null transition: - description: "Transition time when applying a change to the lights (seconds)" + description: "Duration of transition when lights change, in seconds. \U0001F551" required: false example: 45 selector: - text: + text: null adapt_delay: - description: "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering." + description: "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Helps avoid flickering. \u23F2\uFE0F" required: false example: 0 selector: - text: + text: null autoreset_control_seconds: - description: "autoreset_control_seconds: wait time (seconds) before Adaptive Lighting resets `manual_control` status of any light (default: 0)" + description: "Automatically reset the manual control after a number of seconds. Set to 0 to disable. \u23F2\uFE0F" required: false example: 0 selector: - text: + text: null diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 75361965..3e1c86db 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -39,7 +39,6 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, - VALID_TRANSITION, is_on, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN @@ -137,11 +136,13 @@ from .const import ( SERVICE_APPLY, SERVICE_CHANGE_SWITCH_SETTINGS, SERVICE_SET_MANUAL_CONTROL, + SET_MANUAL_CONTROL_SCHEMA, SLEEP_MODE_SWITCH, SUN_EVENT_MIDNIGHT, SUN_EVENT_NOON, TURNING_OFF_DELAY, VALIDATION_TUPLES, + apply_service_schema, replace_none_str, ) @@ -495,20 +496,9 @@ async def async_setup_entry( domain=DOMAIN, service=SERVICE_APPLY, service_func=handle_apply, - schema=vol.Schema( - { - vol.Optional("entity_id"): cv.entity_ids, - vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, - vol.Optional( - CONF_TRANSITION, - default=switch._initial_transition, # pylint: disable=protected-access - ): VALID_TRANSITION, - vol.Optional(ATTR_ADAPT_BRIGHTNESS, default=True): cv.boolean, - vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean, - vol.Optional(CONF_PREFER_RGB_COLOR, default=False): cv.boolean, - vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean, - } - ), + schema=apply_service_schema( + switch._initial_transition + ), # pylint: disable=protected-access ) # Register `set_manual_control` service @@ -516,13 +506,7 @@ async def async_setup_entry( domain=DOMAIN, service=SERVICE_SET_MANUAL_CONTROL, service_func=handle_set_manual_control, - schema=vol.Schema( - { - vol.Optional("entity_id"): cv.entity_ids, - vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, - vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean, - } - ), + schema=SET_MANUAL_CONTROL_SCHEMA, ) args = {vol.Optional(CONF_USE_DEFAULTS, default="current"): cv.string} From 87391e6d24752141b1559ff65c95478b405ef185 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 3 Apr 2023 01:01:58 -0700 Subject: [PATCH 0531/1077] Bump to 1.9.0 (#518) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 39276752..803069a7 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": [], - "version": "1.8.0" + "version": "1.9.0" } From c6a6cd323f701f356decbcca4a75a03500ca7f6b Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Mon, 3 Apr 2023 03:02:06 -0500 Subject: [PATCH 0532/1077] Basic community fixes PR (#460) * Fixes #423 #423 * Do not adapt lights turned on with custom payloads. * Update switch.py * Issue fixes #423, #378, #403, #449 * quickly test #274 * Revert feature requests, this branch only has fixes. Reverted FR 274 * pre-commit fix * Create automerge.yaml * test * Delete automerge.yaml My bad. * Fix #460 and #408 * see @basnijholt 's comment in #450. * @basnijholt requested changes. --------- Co-authored-by: Bas Nijholt --- .../adaptive_lighting/services.yaml | 4 ---- custom_components/adaptive_lighting/switch.py | 21 ++++++++++++++++--- tests/test_switch.py | 11 ++++++++-- 3 files changed, 27 insertions(+), 9 deletions(-) mode change 100755 => 100644 custom_components/adaptive_lighting/services.yaml diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml old mode 100755 new mode 100644 index 89c112e3..351fe5b5 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -3,7 +3,6 @@ apply: fields: entity_id: description: "Entity ID of the switch. \U0001F4DD" - example: switch.adaptive_lighting_default selector: entity: integration: adaptive_lighting @@ -11,7 +10,6 @@ apply: multiple: false lights: description: "List of light entities to be controlled by Adaptive Lighting (may be empty). \U0001F31F" - example: light.bedroom_ceiling selector: entity: domain: light @@ -46,7 +44,6 @@ set_manual_control: fields: entity_id: description: "Entity ID of the switch. \U0001F4DD" - example: switch.adaptive_lighting_default selector: entity: integration: adaptive_lighting @@ -54,7 +51,6 @@ set_manual_control: multiple: false lights: description: "List of light entities to be controlled by Adaptive Lighting (may be empty). \U0001F31F" - example: light.bedroom_ceiling selector: entity: domain: light diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 3e1c86db..4b9e1856 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -582,7 +582,7 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]: def _supported_features(hass: HomeAssistant, light: str): state = hass.states.get(light) - supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported = { key for key, value in _SUPPORT_OPTS.items() if supported_features & value } @@ -1017,7 +1017,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if prefer_rgb_color is None: prefer_rgb_color = self._prefer_rgb_color - if "transition" in features: + # Check transition == 0 to fix #378 + if "transition" in features and transition > 0: service_data[ATTR_TRANSITION] = transition # The switch might be off and not have _settings set. @@ -1064,7 +1065,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) ): return - self.turn_on_off_listener.last_service_data[light] = service_data + # See #80. Doesn't check if transitions differ but it does the job. + last_service_data = self.turn_on_off_listener.last_service_data + if light in last_service_data and last_service_data[light] == service_data: + _LOGGER.debug( + "%s: Cancelling adapt to light %s, there's no new values to set (context.id='%s')", + self._name, + light, + context.id, + ) + return + else: + self.turn_on_off_listener.last_service_data[light] = service_data async def turn_on(service_data): _LOGGER.debug( @@ -1489,11 +1501,14 @@ class SunLightSettings: rgb_color: tuple[float, float, float] = color_temperature_to_rgb( color_temp_kelvin ) + # backwards compatibility for versions < 1.3.1 - see #403 + color_temp_mired: float = math.floor(1000000 / color_temp_kelvin) xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color) hs_color: tuple[float, float] = color_xy_to_hs(*xy_color) return { "brightness_pct": brightness_pct, "color_temp_kelvin": color_temp_kelvin, + "color_temp_mired": color_temp_mired, "rgb_color": rgb_color, "xy_color": xy_color, "hs_color": hs_color, diff --git a/tests/test_switch.py b/tests/test_switch.py index 5b74c94c..212cea82 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -527,11 +527,18 @@ async def test_manual_control(hass): await turn_switch(True, entity_id) assert not manual_control[ENTITY_LIGHT] + # Check that manual control is still enabled if set while bulb is off. + # Test issue #37 + await turn_light(False) + await change_manual_control(True) + await turn_light(True) + assert manual_control[ENTITY_LIGHT] + # Check that when 'adapt_brightness' is off, changing the brightness # doesn't mark it as manually controlled but changing color_temp # does - await turn_light(False) # reset manually controlled status - await turn_light(True) + await turn_light(False) + await turn_light(True) # reset manually controlled status assert not manual_control[ENTITY_LIGHT] await switch.adapt_brightness_switch.async_turn_off() await turn_light(True, brightness=increased_brightness()) From 0958feb744d503c2a7680476e9c80dcdc44d0b75 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 3 Apr 2023 01:47:01 -0700 Subject: [PATCH 0533/1077] Undo accidental changes introduced in #509, but adds the changes from #460 (#521) --- .../adaptive_lighting/services.yaml | 168 +++++++++--------- 1 file changed, 85 insertions(+), 83 deletions(-) mode change 100644 => 100755 custom_components/adaptive_lighting/services.yaml diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml old mode 100644 new mode 100755 index 351fe5b5..c22d1f76 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -2,184 +2,186 @@ apply: description: Applies the current Adaptive Lighting settings to lights. fields: entity_id: - description: "Entity ID of the switch. \U0001F4DD" + description: entity_id of the Adaptive Lighting switch. If not specified, the current Adaptive Lighting switch will be used. selector: entity: integration: adaptive_lighting domain: switch multiple: false lights: - description: "List of light entities to be controlled by Adaptive Lighting (may be empty). \U0001F31F" + description: entity_id(s) of lights, if not specified, all lights in the switch are selected. selector: entity: domain: light multiple: true transition: - description: "Duration of transition when lights change, in seconds. \U0001F551" + description: Transition of the lights. example: 10 selector: - text: null + text: adapt_brightness: - description: "Whether to adapt the brightness of the light. \U0001F31E" + description: "Adapt the 'brightness', default: true" example: true selector: - boolean: null + boolean: adapt_color: - description: "Whether to adapt the color of the light. \U0001F308" + description: "Adapt the color_temp/color_rgb, default: true" example: true selector: - boolean: null + boolean: prefer_rgb_color: - description: "Use RGB color adjustment instead of native light color temperature. \U0001F308" + description: "Prefer to use color_rgb over color_temp if possible, default: false" example: false selector: - boolean: null + boolean: turn_on_lights: - description: "Whether to turn on lights if they are off. \U0001F506" + description: "Turn on the lights that are off, default: false" example: false selector: - boolean: null + boolean: + set_manual_control: description: Mark whether a light is 'manually controlled'. fields: entity_id: - description: "Entity ID of the switch. \U0001F4DD" + description: entity_id of the Adaptive Lighting switch. If not specified, the current Adaptive Lighting switch will be used. selector: entity: integration: adaptive_lighting domain: switch multiple: false lights: - description: "List of light entities to be controlled by Adaptive Lighting (may be empty). \U0001F31F" + description: entity_id(s) of lights, if not specified, all lights in the switch are selected. selector: entity: domain: light multiple: true manual_control: - description: "Whether to manually control the lights. \U0001F512" + description: "Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true" example: true default: true selector: - boolean: null + boolean: + change_switch_settings: - description: Change any settings you'd like in the switch. All options here are the same as in the config flow. + description: "Change any settings you'd like in the switch. All options here are the same as in the config flow." fields: entity_id: - description: "Entity ID of the switch. \U0001F4DD" + description: "entity_id of the Adaptive Lighting switch." required: true selector: entity: domain: switch use_defaults: - description: "Whether to use default settings for the switches. \u2699\uFE0F" - example: current + description: "(default: 'current' for current settings) You can set this to 'factory', 'configuration', or 'current' to reset the variables not being set with this service call. 'current' leaves them as is, 'configuration' resets to whatever already initializes at startup, 'factory' resets to the default values listed in the documentation." + example: "current" required: false - default: current + default: "current" selector: select: options: - - current - - configuration - - factory + - "current" + - "configuration" + - "factory" include_config_in_attributes: - description: "Show all options as attributes on the switch in Home Assistant when set to `true`. \U0001F4DD" + description: "include_config_in_attributes: All config options will be listed as attributes under the adaptive-lighting switch this integration creates. (default: false)" required: false selector: - boolean: null + boolean: turn_on_lights: - description: "Whether to turn on lights if they are off. \U0001F506" + description: "Turn on the lights that are off, default: false" example: false required: false selector: - boolean: null + boolean: initial_transition: - description: "Duration of the first transition when lights turn from `off` to `on` in seconds. \u23F2\uFE0F" + description: "initial_transition: When lights turn 'off' to 'on'. (seconds)" example: 1 required: false selector: - text: null + text: sleep_transition: - description: "Duration of transition when 'sleep mode' is toggled in seconds. \U0001F634" + description: "sleep_transition: When 'sleep_state' changes. (seconds)" example: 1 required: false selector: - text: null + text: max_brightness: - description: "Maximum brightness percentage. \U0001F4A1" + description: "max_brightness: Highest brightness of lights during a cycle. (%)" required: false example: 100 selector: - text: null + text: max_color_temp: - description: "Coldest color temperature in Kelvin. \u2744\uFE0F" + description: "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)" required: false example: 5500 selector: - text: null + text: min_brightness: - description: "Minimum brightness percentage. \U0001F4A1" + description: "min_brightness: Lowest brightness of lights during a cycle. (%)" required: false example: 1 selector: - text: null + text: min_color_temp: - description: "Warmest color temperature in Kelvin. \U0001F525" + description: "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)" required: false example: 2000 selector: - text: null + text: only_once: - description: "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). \U0001F504" + description: "only_once: Only adapt the lights when turning them on." example: false required: false selector: - boolean: null + boolean: prefer_rgb_color: - description: "Use RGB color adjustment instead of native light color temperature. \U0001F308" + description: "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible." required: false example: false selector: - boolean: null + boolean: separate_turn_on_commands: - description: "Use separate `light.turn_on` calls for color and brightness, needed for some light types. \U0001F500" + description: "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights)." required: false example: false selector: - boolean: null + boolean: send_split_delay: - description: "Wait time (milliseconds) between commands when using `separate_turn_on_commands`. Helps ensure correct handling. \u23F2\uFE0F" + description: "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly." required: false example: 0 selector: - boolean: null + boolean: sleep_brightness: - description: "Brightness percentage of lights in sleep mode. \U0001F634" + description: "sleep_brightness, Brightness setting for Sleep Mode. (%)" required: false example: 1 selector: - text: null + text: sleep_rgb_or_color_temp: - description: "Use either `'rgb_color'` or `'color_temp'` in sleep mode. \U0001F319" + description: "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'" required: false - example: color_temp + example: "color_temp" selector: select: options: - - rgb_color - - color_temp + - "rgb_color" + - "color_temp" sleep_rgb_color: - description: "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is 'rgb_color'). \U0001F308" + description: "sleep_rgb_color, in RGB" required: false selector: - color_rgb: null + color_rgb: sleep_color_temp: - description: "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. \U0001F634" + description: "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)" required: false example: 1000 selector: - text: null + text: sunrise_offset: - description: "Adjust sunrise time with a positive or negative offset in seconds. \u23F0" + description: sunrise_offset, in +/- seconds (integer) required: false example: 0 selector: @@ -187,64 +189,64 @@ change_switch_settings: min: 0 max: 86300 sunrise_time: - description: "Set a fixed time (HH:MM:SS) for sunrise. \U0001F305" + description: sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location) required: false - example: '' + example: "" selector: - time: null + time: sunset_offset: - description: "Adjust sunset time with a positive or negative offset in seconds. \u23F0" + description: sunset_offset, in +/- seconds (integer) required: false - example: '' + example: "" selector: number: min: 0 max: 86300 sunset_time: - description: "Set a fixed time (HH:MM:SS) for sunset. \U0001F307" - example: '' + description: sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location) + example: "" required: false selector: - time: null + time: max_sunrise_time: - description: "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. \U0001F305" - example: '' + description: "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)" + example: "" required: false selector: - time: null + time: min_sunset_time: - description: "Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. \U0001F307" - example: '' + description: "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)" + example: "" required: false selector: - time: null + time: take_over_control: - description: "Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! \U0001F512" + description: "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on." required: false example: true selector: - boolean: null + boolean: detect_non_ha_changes: - description: "Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. \U0001F575\uFE0F" + description: "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)" required: false example: false selector: - boolean: null + boolean: transition: - description: "Duration of transition when lights change, in seconds. \U0001F551" + description: "Transition time when applying a change to the lights (seconds)" required: false example: 45 selector: - text: null + text: adapt_delay: - description: "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Helps avoid flickering. \u23F2\uFE0F" + description: "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering." required: false example: 0 selector: - text: null + text: autoreset_control_seconds: - description: "Automatically reset the manual control after a number of seconds. Set to 0 to disable. \u23F2\uFE0F" + description: "autoreset_control_seconds: wait time (seconds) before Adaptive Lighting resets `manual_control` status of any light (default: 0)" required: false example: 0 selector: - text: null + text: From d768a1e825302b593c7a01f5d57256a04fd30463 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 3 Apr 2023 02:07:05 -0700 Subject: [PATCH 0534/1077] Release 1.9.1 (#522) * Bump to 1.9.1 * Add CODEOWNERS --- .github/CODEOWNERS | 1 + custom_components/adaptive_lighting/manifest.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..847961cb --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @basnijholt diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 803069a7..cb7ba6e1 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": [], - "version": "1.9.0" + "version": "1.9.1" } From 26974c8fd5db90f800592912080eca4b816f5710 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 3 Apr 2023 11:32:38 -0700 Subject: [PATCH 0535/1077] Simplify if-statement, (small #460 fix) (#526) --- custom_components/adaptive_lighting/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 4b9e1856..031396fe 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1067,7 +1067,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return # See #80. Doesn't check if transitions differ but it does the job. last_service_data = self.turn_on_off_listener.last_service_data - if light in last_service_data and last_service_data[light] == service_data: + if last_service_data.get(light) == service_data: _LOGGER.debug( "%s: Cancelling adapt to light %s, there's no new values to set (context.id='%s')", self._name, From b730c7cc9009be3b7a9a187e15286dcb4d06c6ef Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 3 Apr 2023 14:59:14 -0700 Subject: [PATCH 0536/1077] Add scripts to auto update en.json, strings.json and services.yaml (#520) * Add scripts to auto update strings.json and services.yaml * Run services * simplify * Run strings * rerun * revert * allow unicode * Add CODEOWNERS * Update CODEOWNERS * set CONF_USE_DEFAULTS docs * add field_name * Auto run scripts * Update desc * Update README.md, strings.json, and services.yaml * double quotes * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update README.md, strings.json, and services.yaml * Add newline * sync changes between en.json and strings.json * Update README.md, strings.json, and services.yaml * double quotes * fix * Update README.md, strings.json, and services.yaml * Add comments * Remove comments * shorter * Update README.md, strings.json, and services.yaml * Rephrase * Update README.md, strings.json, and services.yaml * remove key from desc --------- Co-authored-by: Benjamin Auquite Co-authored-by: github-actions[bot] Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/update-services.py | 25 +++ .github/update-strings.py | 31 ++++ .github/workflows/update-readme.yml | 14 +- README.md | 14 +- custom_components/adaptive_lighting/const.py | 26 +-- .../adaptive_lighting/services.yaml | 169 +++++++++--------- .../adaptive_lighting/strings.json | 60 +++---- .../adaptive_lighting/translations/en.json | 58 +++--- 8 files changed, 231 insertions(+), 166 deletions(-) create mode 100644 .github/update-services.py create mode 100644 .github/update-strings.py mode change 100755 => 100644 custom_components/adaptive_lighting/services.yaml diff --git a/.github/update-services.py b/.github/update-services.py new file mode 100644 index 00000000..df4c7b30 --- /dev/null +++ b/.github/update-services.py @@ -0,0 +1,25 @@ +from pathlib import Path +import sys + +import yaml + +sys.path.append(str(Path(__file__).parent.parent)) + +from custom_components.adaptive_lighting import const # noqa: E402 + +services_filename = "custom_components/adaptive_lighting/services.yaml" +with open(services_filename) as f: + services = yaml.safe_load(f) + +for service_name, dct in services.items(): + _docs = {"set_manual_control": const.DOCS_MANUAL_CONTROL, "apply": const.DOCS_APPLY} + alternative_docs = _docs.get(service_name, const.DOCS) + for field_name, field in dct["fields"].items(): + description = alternative_docs.get(field_name, const.DOCS[field_name]) + field["description"] = description + +comment = "# This file is auto-generated by .github/update-services.py." + +with open(services_filename, "w") as f: + f.write(comment + "\n") + yaml.dump(services, f, sort_keys=False, width=1000, allow_unicode=True) diff --git a/.github/update-strings.py b/.github/update-strings.py new file mode 100644 index 00000000..aabc7443 --- /dev/null +++ b/.github/update-strings.py @@ -0,0 +1,31 @@ +import json +from pathlib import Path +import sys + +sys.path.append(str(Path(__file__).parent.parent)) + +from custom_components.adaptive_lighting import const # noqa: E402 + +strings_fname = "custom_components/adaptive_lighting/strings.json" +en_fname = "custom_components/adaptive_lighting/translations/en.json" +with open(strings_fname) as f: + strings = json.load(f) + +data = {k: f"{k}: {const.DOCS[k]}" for k, _, _ in const.VALIDATION_TUPLES} +strings["options"]["step"]["init"]["data"] = data + +with open(strings_fname, "w") as f: + json.dump(strings, f, indent=2, ensure_ascii=False) + f.write("\n") + + +# Sync changes from strings.json to en.json +with open(en_fname) as f: + en = json.load(f) + +en["config"]["step"]["user"] = strings["config"]["step"]["user"] +en["options"]["step"]["init"]["data"] = data + +with open(en_fname, "w") as f: + json.dump(en, f, indent=2, ensure_ascii=False) + f.write("\n") diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index bc31bb94..bec8cb4c 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -34,17 +34,23 @@ jobs: - name: Run markdown-code-runner run: markdown-code-runner --debug README.md - - name: Commit updated README.md + - name: Run update strings.json + run: python .github/update-strings.py + + - name: Run update services.yaml + run: python .github/update-services.py + + - name: Commit updated README.md, strings.json, and services.yaml id: commit run: | - git add README.md + git add -u . git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" if git diff --quiet && git diff --staged --quiet; then - echo "No changes in README.md, skipping commit." + echo "No changes in README.md, strings.json, and services.yaml, skipping commit." echo "commit_status=skipped" >> $GITHUB_ENV else - git commit -m "Update README.md" + git commit -m "Update README.md, strings.json, and services.yaml" echo "commit_status=committed" >> $GITHUB_ENV fi diff --git a/README.md b/README.md index 5b98de8b..c7a873d0 100644 --- a/README.md +++ b/README.md @@ -90,11 +90,11 @@ The YAML and frontend configuration methods support all of the options listed be | Variable name | Description | Default | Type | |:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------| -| `lights` | List of light entities to be controlled by Adaptive Lighting (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | | `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | | `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | | `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | -| `sleep_transition` | Duration of transition when 'sleep mode' is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | | `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | | `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | | `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | @@ -103,9 +103,9 @@ The YAML and frontend configuration methods support all of the options listed be | `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | | `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | | `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | -| `sleep_rgb_or_color_temp` | Use either `'rgb_color'` or `'color_temp'` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | | `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | -| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is 'rgb_color'). 🌈 | `[255, 56, 0]` | RGB color | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | | `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | | `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` | | `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | @@ -116,8 +116,8 @@ The YAML and frontend configuration methods support all of the options listed be | `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | | `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` | | `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | -| `send_split_delay` | Wait time (milliseconds) between commands when using `separate_turn_on_commands`. Helps ensure correct handling. ⏲️ | `0` | `int` 0-10000 | -| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Helps avoid flickering. ⏲️ | `0` | `float > 0` | +| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | | `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | @@ -188,7 +188,7 @@ adaptive_lighting: |:-------------------------|:-----------------------------------------------------------------------------------------------|:-----------|:---------------------| | `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s | | `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s | -| `manual_control` | Whether to add ('true') or remove ('false') the light from the 'manual_control' list. 🔒 | ❌ | bool | +| `manual_control` | Whether to add ("true") or remove ("false") the light from the "manual_control" list. 🔒 | ❌ | bool | #### `adaptive_lighting.change_switch_settings` diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index bb0c07cd..64620927 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -22,9 +22,7 @@ CONF_NAME, DEFAULT_NAME = "name", "default" DOCS[CONF_NAME] = "Display name for this switch. 📝" CONF_LIGHTS, DEFAULT_LIGHTS = "lights", [] -DOCS[CONF_LIGHTS] = ( - "List of light entities to be controlled by Adaptive " "Lighting (may be empty). 🌟" -) +DOCS[CONF_LIGHTS] = "List of light entity_ids to be controlled (may be empty). 🌟" CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = ( "detect_non_ha_changes", @@ -52,7 +50,7 @@ DOCS[CONF_INITIAL_TRANSITION] = ( CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION = "sleep_transition", 1 DOCS[CONF_SLEEP_TRANSITION] = ( - "Duration of transition when 'sleep mode' is toggled " "in seconds. 😴" + 'Duration of transition when "sleep mode" is toggled ' "in seconds. 😴" ) CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 @@ -102,7 +100,7 @@ DOCS[CONF_SLEEP_COLOR_TEMP] = ( CONF_SLEEP_RGB_COLOR, DEFAULT_SLEEP_RGB_COLOR = "sleep_rgb_color", [255, 56, 0] DOCS[CONF_SLEEP_RGB_COLOR] = ( - "RGB color in sleep mode (used when " "`sleep_rgb_or_color_temp` is 'rgb_color'). 🌈" + "RGB color in sleep mode (used when " '`sleep_rgb_or_color_temp` is "rgb_color"). 🌈' ) CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = ( @@ -110,7 +108,7 @@ CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = ( "color_temp", ) DOCS[CONF_SLEEP_RGB_OR_COLOR_TEMP] = ( - "Use either `'rgb_color'` or `'color_temp'` " "in sleep mode. 🌙" + 'Use either `"rgb_color"` or `"color_temp"` ' "in sleep mode. 🌙" ) CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 @@ -163,13 +161,13 @@ DOCS[CONF_ADAPT_UNTIL_SLEEP] = ( CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0 DOCS[CONF_ADAPT_DELAY] = ( "Wait time (seconds) between light turn on and Adaptive Lighting applying " - "changes. Helps avoid flickering. ⏲️" + "changes. Might help to avoid flickering. ⏲️" ) CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY = "send_split_delay", 0 DOCS[CONF_SEND_SPLIT_DELAY] = ( - "Wait time (milliseconds) between commands when using `separate_turn_on_commands`. " - "Helps ensure correct handling. ⏲️" + "Delay (ms) between `separate_turn_on_commands` for lights that don't support " + "simultaneous brightness and color setting. ⏲️" ) CONF_AUTORESET_CONTROL, DEFAULT_AUTORESET_CONTROL = "autoreset_control_seconds", 0 @@ -197,7 +195,11 @@ CONF_TURN_ON_LIGHTS = "turn_on_lights" DOCS[CONF_TURN_ON_LIGHTS] = "Whether to turn on lights that are currently off. 🔆" SERVICE_CHANGE_SWITCH_SETTINGS = "change_switch_settings" CONF_USE_DEFAULTS = "use_defaults" -DOCS[CONF_USE_DEFAULTS] = "Whether to use default settings for the switches. ⚙️" +DOCS[CONF_USE_DEFAULTS] = ( + "Sets the default values not specified in this service call. Options: " + '"current" (default, retains current values), "factory" (resets to ' + 'documented defaults), or "configuration" (reverts to switch config defaults). ⚙️' +) TURNING_OFF_DELAY = 5 @@ -206,8 +208,8 @@ DOCS_MANUAL_CONTROL = { "light as being `manually controlled`. 📝", CONF_LIGHTS: "entity_id(s) of lights, if not specified, all lights in the " "switch are selected. 💡", - CONF_MANUAL_CONTROL: "Whether to add ('true') or remove ('false') the " - "light from the 'manual_control' list. 🔒", + CONF_MANUAL_CONTROL: 'Whether to add ("true") or remove ("false") the ' + 'light from the "manual_control" list. 🔒', } DOCS_APPLY = { diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml old mode 100755 new mode 100644 index c22d1f76..cd25811b --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -1,187 +1,186 @@ +# This file is auto-generated by .github/update-services.py. apply: description: Applies the current Adaptive Lighting settings to lights. fields: entity_id: - description: entity_id of the Adaptive Lighting switch. If not specified, the current Adaptive Lighting switch will be used. + description: The `entity_id` of the switch with the settings to apply. 📝 selector: entity: integration: adaptive_lighting domain: switch multiple: false lights: - description: entity_id(s) of lights, if not specified, all lights in the switch are selected. + description: A light (or list of lights) to apply the settings to. 💡 selector: entity: domain: light multiple: true transition: - description: Transition of the lights. + description: Duration of transition when lights change, in seconds. 🕑 example: 10 selector: - text: + text: null adapt_brightness: - description: "Adapt the 'brightness', default: true" + description: Whether to adapt the brightness of the light. 🌞 example: true selector: - boolean: + boolean: null adapt_color: - description: "Adapt the color_temp/color_rgb, default: true" + description: Whether to adapt the color on supporting lights. 🌈 example: true selector: - boolean: + boolean: null prefer_rgb_color: - description: "Prefer to use color_rgb over color_temp if possible, default: false" + description: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 example: false selector: - boolean: + boolean: null turn_on_lights: - description: "Turn on the lights that are off, default: false" + description: Whether to turn on lights that are currently off. 🔆 example: false selector: - boolean: - + boolean: null set_manual_control: description: Mark whether a light is 'manually controlled'. fields: entity_id: - description: entity_id of the Adaptive Lighting switch. If not specified, the current Adaptive Lighting switch will be used. + description: The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 selector: entity: integration: adaptive_lighting domain: switch multiple: false lights: - description: entity_id(s) of lights, if not specified, all lights in the switch are selected. + description: entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 selector: entity: domain: light multiple: true manual_control: - description: "Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true" + description: Whether to add ("true") or remove ("false") the light from the "manual_control" list. 🔒 example: true default: true selector: - boolean: - + boolean: null change_switch_settings: - description: "Change any settings you'd like in the switch. All options here are the same as in the config flow." + description: Change any settings you'd like in the switch. All options here are the same as in the config flow. fields: entity_id: - description: "entity_id of the Adaptive Lighting switch." + description: Entity ID of the switch. 📝 required: true selector: entity: domain: switch use_defaults: - description: "(default: 'current' for current settings) You can set this to 'factory', 'configuration', or 'current' to reset the variables not being set with this service call. 'current' leaves them as is, 'configuration' resets to whatever already initializes at startup, 'factory' resets to the default values listed in the documentation." - example: "current" + description: 'Sets the default values not specified in this service call. Options: "current" (default, retains current values), "factory" (resets to documented defaults), or "configuration" (reverts to switch config defaults). ⚙️' + example: current required: false - default: "current" + default: current selector: select: options: - - "current" - - "configuration" - - "factory" + - current + - configuration + - factory include_config_in_attributes: - description: "include_config_in_attributes: All config options will be listed as attributes under the adaptive-lighting switch this integration creates. (default: false)" + description: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 required: false selector: - boolean: + boolean: null turn_on_lights: - description: "Turn on the lights that are off, default: false" + description: Whether to turn on lights that are currently off. 🔆 example: false required: false selector: - boolean: + boolean: null initial_transition: - description: "initial_transition: When lights turn 'off' to 'on'. (seconds)" + description: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ example: 1 required: false selector: - text: + text: null sleep_transition: - description: "sleep_transition: When 'sleep_state' changes. (seconds)" + description: Duration of transition when "sleep mode" is toggled in seconds. 😴 example: 1 required: false selector: - text: + text: null max_brightness: - description: "max_brightness: Highest brightness of lights during a cycle. (%)" + description: Maximum brightness percentage. 💡 required: false example: 100 selector: - text: + text: null max_color_temp: - description: "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)" + description: Coldest color temperature in Kelvin. ❄️ required: false example: 5500 selector: - text: + text: null min_brightness: - description: "min_brightness: Lowest brightness of lights during a cycle. (%)" + description: Minimum brightness percentage. 💡 required: false example: 1 selector: - text: + text: null min_color_temp: - description: "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)" + description: Warmest color temperature in Kelvin. 🔥 required: false example: 2000 selector: - text: + text: null only_once: - description: "only_once: Only adapt the lights when turning them on." + description: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 example: false required: false selector: - boolean: + boolean: null prefer_rgb_color: - description: "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible." + description: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 required: false example: false selector: - boolean: + boolean: null separate_turn_on_commands: - description: "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights)." + description: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 required: false example: false selector: - boolean: + boolean: null send_split_delay: - description: "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly." + description: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ required: false example: 0 selector: - boolean: + boolean: null sleep_brightness: - description: "sleep_brightness, Brightness setting for Sleep Mode. (%)" + description: Brightness percentage of lights in sleep mode. 😴 required: false example: 1 selector: - text: + text: null sleep_rgb_or_color_temp: - description: "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'" + description: Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 required: false - example: "color_temp" + example: color_temp selector: select: options: - - "rgb_color" - - "color_temp" + - rgb_color + - color_temp sleep_rgb_color: - description: "sleep_rgb_color, in RGB" + description: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 required: false selector: - color_rgb: + color_rgb: null sleep_color_temp: - description: "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)" + description: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 required: false example: 1000 selector: - text: + text: null sunrise_offset: - description: sunrise_offset, in +/- seconds (integer) + description: Adjust sunrise time with a positive or negative offset in seconds. ⏰ required: false example: 0 selector: @@ -189,64 +188,64 @@ change_switch_settings: min: 0 max: 86300 sunrise_time: - description: sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location) + description: Set a fixed time (HH:MM:SS) for sunrise. 🌅 required: false - example: "" + example: '' selector: - time: + time: null sunset_offset: - description: sunset_offset, in +/- seconds (integer) + description: Adjust sunset time with a positive or negative offset in seconds. ⏰ required: false - example: "" + example: '' selector: number: min: 0 max: 86300 sunset_time: - description: sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location) - example: "" + description: Set a fixed time (HH:MM:SS) for sunset. 🌇 + example: '' required: false selector: - time: + time: null max_sunrise_time: - description: "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)" - example: "" + description: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 + example: '' required: false selector: - time: + time: null min_sunset_time: - description: "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)" - example: "" + description: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 + example: '' required: false selector: - time: + time: null take_over_control: - description: "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on." + description: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 required: false example: true selector: - boolean: + boolean: null detect_non_ha_changes: - description: "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)" + description: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ required: false example: false selector: - boolean: + boolean: null transition: - description: "Transition time when applying a change to the lights (seconds)" + description: Duration of transition when lights change, in seconds. 🕑 required: false example: 45 selector: - text: + text: null adapt_delay: - description: "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering." + description: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ required: false example: 0 selector: - text: + text: null autoreset_control_seconds: - description: "autoreset_control_seconds: wait time (seconds) before Adaptive Lighting resets `manual_control` status of any light (default: 0)" + description: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ required: false example: 0 selector: - text: + text: null diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 2ebd96d0..54af5e11 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "title": "Choose a name for the Adaptive Lighting", + "title": "Choose a name for the Adaptive Lighting instance", "description": "Every instance can contain multiple lights!", "data": { "name": "Name" @@ -19,35 +19,35 @@ "title": "Adaptive Lighting options", "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", "data": { - "lights": "lights", - "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (seconds)", - "include_config_in_attributes": "include_config_in_attributes: All config options will be listed as attributes under the adaptive-lighting switch this integration creates. (default: false)", - "adapt_until_sleep": "adapt_until_sleep: When checked, Adaptive Lighting will use the sleep settings as the minimum, and transition to these values past the sunset (default: false)", - "sleep_transition": "sleep_transition: When 'sleep_state' changes. (seconds)", - "interval": "interval: Time between switch updates. (seconds)", - "max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)", - "max_color_temp": "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)", - "min_brightness": "min_brightness: Lowest brightness of lights during a cycle. (%)", - "min_color_temp": "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)", - "only_once": "only_once: Only adapt the lights when turning them on.", - "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.", - "separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).", - "send_split_delay": "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly.", - "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'", - "sleep_rgb_color": "sleep_rgb_color, in RGB", - "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)", - "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- seconds)", - "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", - "max_sunrise_time": "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", - "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- seconds)", - "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", - "min_sunset_time": "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", - "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", - "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", - "transition": "Transition time when applying a change to the lights (seconds)", - "adapt_delay": "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering.", - "autoreset_control_seconds": "autoreset_control_seconds: wait time (seconds) before Adaptive Lighting resets `manual_control` status of any light (default: 0)" + "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟", + "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝", + "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "transition": "transition: Duration of transition when lights change, in seconds. 🕑", + "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", + "interval": "interval: Frequency to adapt the lights, in seconds. 🔄", + "min_brightness": "min_brightness: Minimum brightness percentage. 💡", + "max_brightness": "max_brightness: Maximum brightness percentage. 💡", + "min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥", + "max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️", + "sleep_brightness": "sleep_brightness: Brightness percentage of lights in sleep mode. 😴", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", + "sleep_color_temp": "sleep_color_temp: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", + "sleep_rgb_color": "sleep_rgb_color: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅", + "sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇", + "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇", + "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰", + "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", + "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", + "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", + "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", + "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️" } } }, diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 51311d64..38fb7b0e 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -4,7 +4,7 @@ "step": { "user": { "title": "Choose a name for the Adaptive Lighting instance", - "description": "Pick a name for this instance. You can run several instances of Adaptive lighting, each of these can contain multiple lights!", + "description": "Every instance can contain multiple lights!", "data": { "name": "Name" } @@ -20,33 +20,35 @@ "title": "Adaptive Lighting options", "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have the adaptive_lighting entry defined in your YAML configuration.", "data": { - "lights": "lights", - "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (seconds)", - "include_config_in_attributes": "include_config_in_attributes: All config options will be listed as attributes under the adaptive-lighting switch this integration creates. (default: false)", - "sleep_transition": "sleep_transition: When 'sleep_state' changes. (seconds)", - "interval": "interval: Time between switch updates. (seconds)", - "max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)", - "max_color_temp": "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)", - "min_brightness": "min_brightness: Lowest brightness of lights during a cycle. (%)", - "min_color_temp": "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)", - "only_once": "only_once: Only adapt the lights when turning them on.", - "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.", - "separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).", - "send_split_delay": "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly.", - "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'", - "sleep_rgb_color": "sleep_rgb_color, in RGB", - "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)", - "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- seconds)", - "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", - "max_sunrise_time": "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", - "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- seconds)", - "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", - "min_sunset_time": "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", - "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", - "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", - "transition": "Transition time when applying a change to the lights (seconds)", - "adapt_delay": "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering." + "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟", + "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝", + "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "transition": "transition: Duration of transition when lights change, in seconds. 🕑", + "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", + "interval": "interval: Frequency to adapt the lights, in seconds. 🔄", + "min_brightness": "min_brightness: Minimum brightness percentage. 💡", + "max_brightness": "max_brightness: Maximum brightness percentage. 💡", + "min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥", + "max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️", + "sleep_brightness": "sleep_brightness: Brightness percentage of lights in sleep mode. 😴", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", + "sleep_color_temp": "sleep_color_temp: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", + "sleep_rgb_color": "sleep_rgb_color: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅", + "sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇", + "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇", + "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰", + "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", + "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", + "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", + "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", + "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️" } } }, From c7f44e472ff672512e027f7e8bbe43dbd095558f Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Mon, 3 Apr 2023 19:04:06 -0500 Subject: [PATCH 0537/1077] Correctly wait for transitions (#510) * Add auto_reset_manual_control with async timer * cherry-pick wait for transition stuff * Update switch.py * not renamed in this branch yet. * Update switch.py * update tests * Update switch.py * merge related fix * cleanup * Revert "cleanup" This reverts commit 3aa2f3242b06370c0f84248d2c2c21a2556bb5ec. * Update switch.py * Update switch.py * Update switch.py * Small refactor * Move test to old position for better diffs * Revert "Small refactor" This reverts commit b986b3f77cba921cbfb07647dea1d6f876883dd4. * Update README.md * fix the test last_state_change isn't updated quick enough. * #510 changes (#516) * Change (WIP) * Update test_switch.py * Refactor * Revert "Revert "Small refactor"" This reverts commit 3731c9993676b0a831af12da25bfd8c07ff2a148. * Update README.md * Fix the test * Bump to 1.9.0 (#518) * Basic community fixes PR (#460) * Fixes #423 #423 * Do not adapt lights turned on with custom payloads. * Update switch.py * Issue fixes #423, #378, #403, #449 * quickly test #274 * Revert feature requests, this branch only has fixes. Reverted FR 274 * pre-commit fix * Create automerge.yaml * test * Delete automerge.yaml My bad. * Fix #460 and #408 * see @basnijholt 's comment in #450. * @basnijholt requested changes. --------- Co-authored-by: Bas Nijholt * Undo accidental changes introduced in #509, but adds the changes from #460 (#521) * Release 1.9.1 (#522) * Bump to 1.9.1 * Add CODEOWNERS --------- Co-authored-by: Benjamin Auquite Co-authored-by: github-actions[bot] * No need to wrap the reset * Remove unused attrs * Shorter log message * revert unrelated tests change * remove unused function * Use patch * Bump to 1.10.0 --------- Co-authored-by: Bas Nijholt Co-authored-by: github-actions[bot] Co-authored-by: Bas Nijholt --- .../adaptive_lighting/manifest.json | 2 +- custom_components/adaptive_lighting/switch.py | 261 +++++++++++------- tests/test_switch.py | 67 ++++- 3 files changed, 211 insertions(+), 119 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index cb7ba6e1..33564f23 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": [], - "version": "1.9.1" + "version": "1.10.0" } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 031396fe..52821774 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import base64 import bisect -from collections import defaultdict +from collections.abc import Callable, Coroutine from copy import deepcopy from dataclasses import dataclass import datetime @@ -802,10 +802,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._only_once = data[CONF_ONLY_ONCE] self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR] self._separate_turn_on_commands = data[CONF_SEPARATE_TURN_ON_COMMANDS] - self._take_over_control = data[CONF_TAKE_OVER_CONTROL] self._transition = data[CONF_TRANSITION] self._adapt_delay = data[CONF_ADAPT_DELAY] self._send_split_delay = data[CONF_SEND_SPLIT_DELAY] + self._take_over_control = data[CONF_TAKE_OVER_CONTROL] + self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] + if not data[CONF_TAKE_OVER_CONTROL] and data[CONF_DETECT_NON_HA_CHANGES]: + _LOGGER.warning( + "%s: Config mismatch: 'detect_non_ha_changes: true' " + "requires 'take_over_control' to be enabled. Adjusting config " + "and continuing setup with `take_over_control: true`.", + self._name, + ) + self._take_over_control = True self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL] self._expand_light_groups() # updates manual control timers _loc = get_astral_location(self.hass) @@ -1128,11 +1137,23 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) ) self.async_write_ha_state() + if lights is None: lights = self._lights - if (self._only_once and not force) or not lights: + + if not force and self._only_once: return - await self._adapt_lights(lights, transition, force, context) + + filtered_lights = [] + for light in lights: + # Don't adapt lights that haven't finished prior transitions. + if force or not self.turn_on_off_listener.transition_timers.get(light): + filtered_lights.append(light) + + if not filtered_lights: + return + + await self._adapt_lights(filtered_lights, transition, force, context) async def _adapt_lights( self, @@ -1532,8 +1553,6 @@ class TurnOnOffListener: self.sleep_tasks: dict[str, asyncio.Task] = {} # Tracks which lights are manually controlled self.manual_control: dict[str, bool] = {} - # Counts the number of times (in a row) a light had a changed state. - self.cnt_significant_changes: dict[str, int] = defaultdict(int) # Track 'state_changed' events of self.lights resulting from this integration self.last_state_change: dict[str, list[State]] = {} # Track last 'service_data' to 'light.turn_on' resulting from this integration @@ -1543,9 +1562,8 @@ class TurnOnOffListener: self.auto_reset_manual_control_timers: dict[str, _AsyncSingleShotTimer] = {} self.auto_reset_manual_control_times: dict[str, float] = {} - # When a state is different `max_cnt_significant_changes` times in a row, - # mark it as manually_controlled. - self.max_cnt_significant_changes = 2 + # Track light transitions + self.transition_timers: dict[str, _AsyncSingleShotTimer] = {} self.remove_listener = self.hass.bus.async_listen( EVENT_CALL_SERVICE, self.turn_on_off_event_listener @@ -1554,6 +1572,56 @@ class TurnOnOffListener: EVENT_STATE_CHANGED, self.state_changed_event_listener ) + def _handle_timer( + self, + light: str, + timers_dict: dict[str, _AsyncSingleShotTimer], + delay: float | None, + reset_coroutine: Callable[[], Coroutine[Any, Any, None]], + ) -> None: + timer = timers_dict.get(light) + if timer is not None: + if delay is None: # Timer object exists, but should not anymore + timer.cancel() + timers_dict.pop(light) + else: # Timer object already exists, just update the delay and restart it + timer.delay = delay + timer.start() + elif delay is not None: # Timer object does not exist, create it + timer = _AsyncSingleShotTimer(delay, reset_coroutine) + timers_dict[light] = timer + timer.start() + + def start_transition_timer(self, light: str) -> None: + """Mark a light as manually controlled.""" + _LOGGER.debug("Start transition timer for %s", light) + last_service_data = self.last_service_data + if ( + not last_service_data + or light not in last_service_data + or ATTR_TRANSITION not in last_service_data[light] + ): + return + + delay = last_service_data[light][ATTR_TRANSITION] + + async def reset(): + _LOGGER.debug( + "Transition finished for light %s", + light, + ) + switches = _get_switches_with_lights(self.hass, [light]) + for switch in switches: + if not switch.is_on: + continue + await switch._update_attrs_and_maybe_adapt_lights( + [light], + force=False, + context=switch.create_context("transit"), + ) + + self._handle_timer(light, self.transition_timers, delay, reset) + def set_auto_reset_manual_control_times(self, lights: list[str], time: float): """Set the time after which the lights are automatically reset.""" if time == 0: @@ -1576,40 +1644,28 @@ class TurnOnOffListener: _LOGGER.debug("Marking '%s' as manually controlled.", light) self.manual_control[light] = True delay = self.auto_reset_manual_control_times.get(light) - timer = self.auto_reset_manual_control_timers.get(light) - if timer is not None: - if delay is None: # Timer object exists, but should not anymore - timer.cancel() - self.auto_reset_manual_control_timers.pop(light) - else: # Timer object already exists, just update the delay and restart it - timer.delay = delay - timer.start() - elif delay is not None: # Timer object does not exist, create it - async def reset(): - self.reset(light) - switches = _get_switches_with_lights(self.hass, [light]) - for switch in switches: - if not switch.is_on: - continue - # pylint: disable=protected-access - await switch._update_attrs_and_maybe_adapt_lights( - [light], - transition=switch._initial_transition, - force=True, - context=switch.create_context("autoreset"), - ) - _LOGGER.debug( - "Auto resetting 'manual_control' status of '%s' because" - " it was not manually controlled for %s seconds.", - light, - delay, + async def reset(): + self.reset(light) + switches = _get_switches_with_lights(self.hass, [light]) + for switch in switches: + if not switch.is_on: + continue + await switch._update_attrs_and_maybe_adapt_lights( + [light], + transition=switch._initial_transition, + force=True, + context=switch.create_context("autoreset"), ) - assert not self.manual_control[light] + _LOGGER.debug( + "Auto resetting 'manual_control' status of '%s' because" + " it was not manually controlled for %s seconds.", + light, + delay, + ) + assert not self.manual_control[light] - timer = _AsyncSingleShotTimer(delay, reset) - self.auto_reset_manual_control_timers[light] = timer - timer.start() + self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset) def reset(self, *lights, reset_manual_control=True) -> None: """Reset the 'manual_control' status of the lights.""" @@ -1621,7 +1677,6 @@ class TurnOnOffListener: timer.cancel() self.last_state_change.pop(light, None) self.last_service_data.pop(light, None) - self.cnt_significant_changes[light] = 0 async def turn_on_off_event_listener(self, event: Event) -> None: """Track 'light.turn_off' and 'light.turn_on' service calls.""" @@ -1700,11 +1755,7 @@ class TurnOnOffListener: new_state.context.id, ) - if ( - new_state is not None - and new_state.state == STATE_ON - and is_our_context(new_state.context) - ): + if new_state is not None and new_state.state == STATE_ON: # It is possible to have multiple state change events with the same context. # This can happen because a `turn_on.light(brightness_pct=100, transition=30)` # event leads to an instant state change of @@ -1717,21 +1768,29 @@ class TurnOnOffListener: # incorrect 'min_kelvin' and 'max_kelvin', which happens e.g., for # Philips Hue White GU10 Bluetooth lights). old_state: list[State] | None = self.last_state_change.get(entity_id) - if ( - old_state is not None - and old_state[0].context.id == new_state.context.id - ): - # If there is already a state change event from this event (with this - # context) then append it to the already existing list. - _LOGGER.debug( - "State change event of '%s' is already in 'self.last_state_change' (%s)" - " adding this state also", - entity_id, - new_state.context.id, - ) + if is_our_context(new_state.context): + if ( + old_state is not None + and old_state[0].context.id == new_state.context.id + ): + _LOGGER.debug( + "TurnOnOffListener: State change event of '%s' is already" + " in 'self.last_state_change' (%s)" + " adding this state also", + entity_id, + new_state.context.id, + ) + self.last_state_change[entity_id].append(new_state) + else: + _LOGGER.debug( + "TurnOnOffListener: New adapt '%s' found for %s", + new_state, + entity_id, + ) + self.last_state_change[entity_id] = [new_state] + self.start_transition_timer(entity_id) + elif old_state is not None: self.last_state_change[entity_id].append(new_state) - else: - self.last_state_change[entity_id] = [new_state] def is_manually_controlled( self, @@ -1786,64 +1845,58 @@ class TurnOnOffListener: detected, we mark the light as 'manually controlled' until the light or switch is turned 'off' and 'on' again. """ - if light not in self.last_state_change: - return False - old_states: list[State] = self.last_state_change[light] - await self.hass.helpers.entity_component.async_update_entity(light) - new_state = self.hass.states.get(light) + last_service_data = self.last_service_data.get(light) + if last_service_data is None: + return compare_to = functools.partial( _attributes_have_changed, light=light, - new_attributes=new_state.attributes, adapt_brightness=adapt_brightness, adapt_color=adapt_color, context=context, ) - for index, old_state in enumerate(old_states): - changed = compare_to(old_attributes=old_state.attributes) - if not changed: - _LOGGER.debug( - "State of '%s' didn't change wrt change event nr. %s (context.id=%s)", - light, - index, - context.id, - ) - break - - last_service_data = self.last_service_data.get(light) - if changed and last_service_data is not None: - # It can happen that the state change events that are associated - # with the last 'light.turn_on' call by this integration were not - # final states. Possibly a later EVENT_STATE_CHANGED happened, where - # the correct target brightness/color was reached. - changed = compare_to(old_attributes=last_service_data) - if not changed: + # Update state and check for a manual change not done in HA. + # Ensure HASS is correctly updating your light's state with + # light.turn_on calls if any problems arise. This + # can happen e.g. using zigbee2mqtt with 'report: false' in device settings. + if switch._detect_non_ha_changes: + _LOGGER.debug( + "%s: 'detect_non_ha_changes: true', calling update_entity(%s)" + " and check if it's last adapt succeeded.", + switch._name, + light, + ) + # This update_entity probably isn't necessary now that we're checking + # if transitions finished from our last adapt. + await self.hass.helpers.entity_component.async_update_entity(light) + refreshed_state = self.hass.states.get(light) + _LOGGER.debug( + "%s: Current state of %s: %s", + switch._name, + light, + refreshed_state, + ) + changed = compare_to( + old_attributes=last_service_data, + new_attributes=refreshed_state.attributes, + ) + if changed: _LOGGER.debug( "State of '%s' didn't change wrt 'last_service_data' (context.id=%s)", light, context.id, ) - - n_changes = self.cnt_significant_changes[light] - if changed: - self.cnt_significant_changes[light] += 1 - if n_changes >= self.max_cnt_significant_changes: - # Only mark a light as significantly changing, if changed==True - # N times in a row. We do this because sometimes a state changes - # happens only *after* a new update interval has already started. self.mark_as_manual_control(light) _fire_manual_control_event(switch, light, context, is_async=False) - else: - if n_changes > 1: - _LOGGER.debug( - "State of '%s' had 'cnt_significant_changes=%s' but the state" - " changed to the expected settings now", - light, - n_changes, - ) - self.cnt_significant_changes[light] = 0 - - return changed + return True + _LOGGER.debug( + "%s: Light '%s' correctly matches our last adapt's service data, continuing..." + " context.id=%s.", + switch._name, + light, + context.id, + ) + return False async def maybe_cancel_adjusting( self, entity_id: str, off_to_on_event: Event, on_to_off_event: Event | None diff --git a/tests/test_switch.py b/tests/test_switch.py index 212cea82..9e88b332 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -56,6 +56,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_AREA_ID, ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, CONF_LIGHTS, CONF_NAME, CONF_PLATFORM, @@ -769,25 +770,63 @@ async def test_significant_change(hass): ) await hass.async_block_till_done() - switch, (bed_light_instance, *_) = await setup_lights_and_switch(hass) + async def set_brightness(val: int): + hass.states.async_set( + ENTITY_LIGHT, "on", {ATTR_BRIGHTNESS: val, ATTR_SUPPORTED_FEATURES: 1} + ) + await hass.async_block_till_done() + + switch, _ = await setup_lights_and_switch(hass) + _LOGGER.debug("Test detect_non_ha_changes:") + switch._take_over_control = True + assert switch._take_over_control + switch._detect_non_ha_changes = True + assert switch._detect_non_ha_changes + + # build last service data + await update(force=False) + + # force=True should not reset manual control. + await turn_light(True, brightness=40) + await turn_light(True, brightness=20) + await update(force=False) + assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + await update(force=True) + assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + + # turn light off then on should reset manual control. + await turn_light(False) await turn_light(True) - await update(force=True) # removes manual control assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] - # Change brightness by setting state (not using 'light.turn_on') - attributes = hass.states.get(ENTITY_LIGHT).attributes - new_attributes = attributes.copy() - new_brightness = (attributes[ATTR_BRIGHTNESS] + 100) % 255 - new_attributes[ATTR_BRIGHTNESS] = new_brightness - bed_light_instance._brightness = new_brightness + # Assert last_service_data got filled from update() + await update(force=True) assert switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None - for _ in range(switch.turn_on_off_listener.max_cnt_significant_changes): + + # Simulate a transition to 255 where the update() is already using brightness 255. + await set_brightness(240) + await set_brightness(244) + await set_brightness(247) + await set_brightness(250) + + # last_state_change should have our state changes. + # Change brightness by async_set (not using 'light.turn_on') + new_brightness = 50 + await set_brightness(new_brightness) + _LOGGER.debug("Test: Brightness set to %s", new_brightness) + + # mock homeassistant.core.HomeAssistant.helpers.entity_component.async_update_entity + # Otherwise what happens is update_entity() refreshes the state to the last call of + # light.turn_on(). This is because we are not using hass.states.async_set() to + # set the brightness of the light. We mock `async_update_ha_state` because + # `async_update_entity` calls it. + with patch("homeassistant.helpers.entity.Entity.async_update_ha_state"): + # On next update ENTITY_LIGHT should be marked as manually controlled await update(force=False) - assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] - # On next update the light should be marked as manually controlled - await update(force=False) - # TODO: the state should be `bool(manual_control) is True` - assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + assert ( + switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None + ) + assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] def test_color_difference_redmean(): From 981287edb96d04e012c5ab689bc15bb981135b89 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 17:30:07 -0700 Subject: [PATCH 0538/1077] [pre-commit.ci] pre-commit autoupdate (#531) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f8938a7..462e0dc2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.3.0 hooks: - id: black - repo: https://github.com/asottile/pyupgrade From be0735002c5814ec822ca5435d530569dafa62e4 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 21:59:14 -0700 Subject: [PATCH 0539/1077] docs: add th3w1zard1 as a contributor for bug (#534) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 344b3112..3872e09b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -400,7 +400,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/2219836?v=4", "profile": "https://github.com/th3w1zard1", "contributions": [ - "code" + "code", + "bug" ] }, { diff --git a/README.md b/README.md index c7a873d0..65fc0d74 100644 --- a/README.md +++ b/README.md @@ -445,7 +445,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting - + From e4d06476fd0eb3fa6f90c368edc30860a85d844b Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Tue, 4 Apr 2023 01:47:08 -0500 Subject: [PATCH 0540/1077] Add windows command for Docker test instructions (#536) * ( Tiny Change ) Add windows command for dockertest You said it earlier but the correct command for running the Docker image on windows is: ```bash docker run -v %cd%:/app basnijholt/adaptive-lighting:latest ``` * Update README.md --------- Co-authored-by: Bas Nijholt --- tests/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/README.md b/tests/README.md index adedcc4c..a576d518 100644 --- a/tests/README.md +++ b/tests/README.md @@ -5,10 +5,18 @@ Alternatively, you can use the provided Docker image to run the tests locally. To run the tests using the Docker image, navigate to the `adaptive-lighting` repo folder and execute the following command: +Linux or MacOS: + ```bash docker run -v $(pwd):/app basnijholt/adaptive-lighting:latest ``` +Windows: + +```bash +docker run -v %cd%:/app basnijholt/adaptive-lighting:latest +``` + This command will download the Docker image from [the adaptive-lighting Docker Hub repo](https://hub.docker.com/r/basnijholt/adaptive-lighting) and run the tests. If you prefer to build the image yourself, use the following command: From 744e43f4bf057775125de2ffc576d9902df8e530 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 4 Apr 2023 08:54:54 -0700 Subject: [PATCH 0541/1077] docs: add th3w1zard1 as a contributor for maintenance (#538) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 3872e09b..6c825dad 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -401,7 +401,8 @@ "profile": "https://github.com/th3w1zard1", "contributions": [ "code", - "bug" + "bug", + "maintenance" ] }, { diff --git a/README.md b/README.md index 65fc0d74..f013d6c5 100644 --- a/README.md +++ b/README.md @@ -445,7 +445,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting - + From 4fcf238360f9cd4528d89f3c17c86f44ac61ec3a Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Tue, 4 Apr 2023 22:52:54 -0500 Subject: [PATCH 0542/1077] Update README.md on transition_until_sleep parameter (#539) * Update README.md I believe you changed the config option's name after I posted the graph, so I renamed the config option there too. There was also a deleted user on the contributions list so I went ahead and removed that too. * chore(docs): update TOC * Update README.md --------- Co-authored-by: th3w1zard1 --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f013d6c5..9ba7cc03 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,9 @@ ![](https://github.com/home-assistant/brands/raw/b4a168b9af282ef916e120d31091ecd5e3c35e66/core_integrations/adaptive_lighting/icon.png) -Adaptive Lighting is a custom component for Home Assistant that intelligently adjusts the brightness and color of your lights 💡 based on the sun's position, while still allowing for manual control. Try it out now by finding it in HACS (Home Assistant Community Store) and installing it! +Adaptive Lighting is a custom component for [Home Assistant](https://www.home-assistant.io/) that intelligently adjusts the brightness and color of your lights 💡 based on the sun's position, while still allowing for manual control. + +Download and install directly through [HACS (Home Assistant Community Store)](https://hacs.xyz/) By automatically adapting the settings of your lights throughout the day, Adaptive Lighting helps maintain your natural circadian rhythm 😴, which can lead to improved sleep, mood, and overall well-being. Experience cooler color temperatures at noon, gradually transitioning to warmer colors at sunset and sunrise. @@ -58,7 +60,7 @@ The `adaptive_lighting.manual_control` event is fired when a light is marked as - [:sunny: Sun Position](#sunny-sun-position) - [:thermometer: Color Temperature](#thermometer-color-temperature) - [:high_brightness: Brightness](#high_brightness-brightness) - - [While using `adapt_until_sleep: true`](#while-using-adapt_until_sleep-true) + - [While using `transition_until_sleep: true`](#while-using-transition_until_sleep-true) - [:busts_in_silhouette: Contributors](#busts_in_silhouette-contributors) @@ -379,7 +381,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting #### :high_brightness: Brightness ![cl_brightness|690x130](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/8/58ebd994b62a8b1abfb3497a5288d923ff4e2330.PNG) -#### While using `adapt_until_sleep: true` +#### While using `transition_until_sleep: true` ![image](https://user-images.githubusercontent.com/2219836/228949675-f9699624-8abc-466c-bb04-250ce0f495b8.png) @@ -421,7 +423,6 @@ These graphs were generated using the values calculated by the Adaptive Lighting - From f5abf034c4653777d8c8b0e4e51b556caeb2660c Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Wed, 5 Apr 2023 11:13:57 -0500 Subject: [PATCH 0543/1077] Fix the docker tests instructions (#543) * Tested on multiple hardware Turns out windows 10 and 11 can't use `$(pwd):/app` OR `%cd%:/app` in PowerShell (which replaced cmd prompt), so I looked up the docs and made the necessary changes (again, sorry!) These changes have been tested on all terminal environments except macOS (the docs say it'll work there) * allow use of --exitfirst for faster debug * Remove install in actions --------- Co-authored-by: Bas Nijholt Co-authored-by: Bas Nijholt --- tests/README.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/README.md b/tests/README.md index a576d518..1c472541 100644 --- a/tests/README.md +++ b/tests/README.md @@ -5,17 +5,15 @@ Alternatively, you can use the provided Docker image to run the tests locally. To run the tests using the Docker image, navigate to the `adaptive-lighting` repo folder and execute the following command: -Linux or MacOS: - +Linux / MacOS / Windows PowerShell: ```bash -docker run -v $(pwd):/app basnijholt/adaptive-lighting:latest +docker run -v ${PWD}:/app basnijholt/adaptive-lighting:latest ``` -Windows: - -```bash -docker run -v %cd%:/app basnijholt/adaptive-lighting:latest -``` +- In windows command prompt, the command is: + ```bash + docker run -v %cd%:/app basnijholt/adaptive-lighting:latest + ``` This command will download the Docker image from [the adaptive-lighting Docker Hub repo](https://hub.docker.com/r/basnijholt/adaptive-lighting) and run the tests. From 03a2d9cbf67964ec3cae59f4f9d6ace32d113d99 Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Wed, 5 Apr 2023 18:39:31 -0500 Subject: [PATCH 0544/1077] Fix RGB Color Temp Swaps (#514) * cherry pick from 486 * Refactor `_add_missing_attributes` --------- Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/switch.py | 56 ++++++++++++++----- tests/test_switch.py | 27 +++++++-- 2 files changed, 64 insertions(+), 19 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 52821774..347ee4cb 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -85,6 +85,7 @@ from homeassistant.util.color import ( color_RGB_to_xy, color_temperature_to_rgb, color_xy_to_hs, + color_xy_to_RGB, ) import homeassistant.util.dt as dt_util import voluptuous as vol @@ -628,6 +629,41 @@ def color_difference_redmean( return math.sqrt(red_term + green_term + blue_term) +# All comparisons should be done with RGB since +# converting anything to color temp is inaccurate. +def _convert_attributes(attributes: dict[str, Any]) -> dict[str, Any]: + if ATTR_RGB_COLOR in attributes: + return attributes + + rgb = None + if ATTR_COLOR_TEMP_KELVIN in attributes: + rgb = color_temperature_to_rgb(attributes[ATTR_COLOR_TEMP_KELVIN]) + elif ATTR_XY_COLOR in attributes: + rgb = color_xy_to_RGB(*attributes[ATTR_XY_COLOR]) + + if rgb is not None: + attributes[ATTR_RGB_COLOR] = rgb + _LOGGER.debug(f"Converted {attributes} to rgb {rgb}") + else: + _LOGGER.debug("No suitable conversion found") + + return attributes + + +def _add_missing_attributes( + old_attributes: dict[str, Any], + new_attributes: dict[str, Any], +) -> dict[str, Any]: + if not any( + attr in old_attributes and attr in new_attributes + for attr in [ATTR_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR] + ): + old_attributes = _convert_attributes(old_attributes) + new_attributes = _convert_attributes(new_attributes) + + return old_attributes, new_attributes + + def _attributes_have_changed( light: str, old_attributes: dict[str, Any], @@ -636,6 +672,11 @@ def _attributes_have_changed( adapt_color: bool, context: Context, ) -> bool: + if adapt_color: + old_attributes, new_attributes = _add_missing_attributes( + old_attributes, new_attributes + ) + if ( adapt_brightness and ATTR_BRIGHTNESS in old_attributes @@ -690,21 +731,6 @@ def _attributes_have_changed( context.id, ) return True - - switched_color_temp = ( - ATTR_RGB_COLOR in old_attributes and ATTR_RGB_COLOR not in new_attributes - ) - switched_to_rgb_color = ( - ATTR_COLOR_TEMP_KELVIN in old_attributes - and ATTR_COLOR_TEMP_KELVIN not in new_attributes - ) - if switched_color_temp or switched_to_rgb_color: - # Light switched from RGB mode to color_temp or visa versa - _LOGGER.debug( - "'%s' switched from RGB mode to color_temp or visa versa", - light, - ) - return True return False diff --git a/tests/test_switch.py b/tests/test_switch.py index 9e88b332..db4900dc 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -47,6 +47,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR, + ATTR_XY_COLOR, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.light import SERVICE_TURN_OFF @@ -873,10 +874,28 @@ def test_attributes_have_changed(): assert _attributes_have_changed( old_attributes=attributes_1, new_attributes=attrs, **kwargs ) - # Switch from rgb_color to color_temp - assert _attributes_have_changed( - old_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 100}, - new_attributes={ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (0, 0, 0)}, + _LOGGER.debug("Test switch from color_temp to rgb_color") + assert not _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702}, + new_attributes={ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (255, 166, 87)}, + **kwargs, + ) + _LOGGER.debug("Test switch from rgb_color to color_temp") + assert not _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (255, 166, 87)}, + new_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702}, + **kwargs, + ) + _LOGGER.debug("Test switch from color_temp to color_xy") + assert not _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702}, + new_attributes={ATTR_BRIGHTNESS: 1, ATTR_XY_COLOR: (0.526, 0.387)}, + **kwargs, + ) + _LOGGER.debug("Test switch from color_xy to color_temp") + assert not _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 1, ATTR_XY_COLOR: (0.526, 0.387)}, + new_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702}, **kwargs, ) From cb967aeeb7ab7e5ee3818587da8975a5ad3439cd Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Thu, 6 Apr 2023 13:53:23 -0500 Subject: [PATCH 0545/1077] Create intentionally over-redundant `state_change` tests and fix #541 (#544) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add transition_timer test and debug * syntax error * test * Update switch.py * Revert "test" This reverts commit b8009e0a1a4c419ddb026a4545b38453158d9367. * Update test_switch.py * add `create_transition_events` to tests. nearly done * tests are done! * pop is for dictionaries * Update test_switch.py * combine the tests * pin markdown-code-runner * Pin with '==' * Update test_switch.py * pin in the correct place 😅 * Update test_switch.py * Use timer.is_running * Update test_switch.py * ensure timer is running in tests * this passes the test * Update test_switch.py * Do not create new list when not needed * Remove empty deps * Remove CONF_ULID_MAX_LENGTH (which is not configurable) * this shouldn't pass the test but it does. --------- Co-authored-by: Bas Nijholt Co-authored-by: Bas Nijholt --- .github/workflows/update-readme.yml | 2 +- custom_components/adaptive_lighting/switch.py | 64 +-- tests/test_switch.py | 396 ++++++++++++++---- 3 files changed, 346 insertions(+), 116 deletions(-) diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index bec8cb4c..1f661333 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -24,7 +24,7 @@ jobs: - name: Install markdown-code-runner and README code dependencies run: | - pip install markdown-code-runner pandas tabulate + pip install markdown-code-runner==1.0.0 pandas tabulate - name: Link custom_components/adaptive_lighting run: | diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 347ee4cb..dbf3c42b 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1102,7 +1102,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return # See #80. Doesn't check if transitions differ but it does the job. last_service_data = self.turn_on_off_listener.last_service_data - if last_service_data.get(light) == service_data: + if not force and last_service_data.get(light) == service_data: _LOGGER.debug( "%s: Cancelling adapt to light %s, there's no new values to set (context.id='%s')", self._name, @@ -1167,14 +1167,23 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if lights is None: lights = self._lights - if not force and self._only_once: - return - filtered_lights = [] - for light in lights: - # Don't adapt lights that haven't finished prior transitions. - if force or not self.turn_on_off_listener.transition_timers.get(light): - filtered_lights.append(light) + if not force: + if self._only_once: + return + for light in lights: + # Don't adapt lights that haven't finished prior transitions. + timer = self.turn_on_off_listener.transition_timers.get(light) + if timer is not None and timer.is_running(): + _LOGGER.debug( + "%s: Light '%s' is still transitioning", + self._name, + light, + ) + else: + filtered_lights.append(light) + else: + filtered_lights = lights if not filtered_lights: return @@ -1620,33 +1629,28 @@ class TurnOnOffListener: def start_transition_timer(self, light: str) -> None: """Mark a light as manually controlled.""" - _LOGGER.debug("Start transition timer for %s", light) - last_service_data = self.last_service_data - if ( - not last_service_data - or light not in last_service_data - or ATTR_TRANSITION not in last_service_data[light] - ): + last_service_data = self.last_service_data.get(light) + if not last_service_data: + _LOGGER.debug("This should not ever happen. Please report to the devs.") return - - delay = last_service_data[light][ATTR_TRANSITION] + last_transition = last_service_data.get(ATTR_TRANSITION) + if not last_transition: + _LOGGER.debug( + "No transition in last adapt for light %s, continuing...", light + ) + return + _LOGGER.debug( + "Start transition timer of %s seconds for light %s", last_transition, light + ) async def reset(): + ValueError("TEST") _LOGGER.debug( "Transition finished for light %s", light, ) - switches = _get_switches_with_lights(self.hass, [light]) - for switch in switches: - if not switch.is_on: - continue - await switch._update_attrs_and_maybe_adapt_lights( - [light], - force=False, - context=switch.create_context("transit"), - ) - self._handle_timer(light, self.transition_timers, delay, reset) + self._handle_timer(light, self.transition_timers, last_transition, reset) def set_auto_reset_manual_control_times(self, lights: list[str], time: float): """Set the time after which the lights are automatically reset.""" @@ -1769,7 +1773,7 @@ class TurnOnOffListener: async def state_changed_event_listener(self, event: Event) -> None: """Track 'state_changed' events.""" entity_id = event.data.get(ATTR_ENTITY_ID, "") - if entity_id not in self.lights or entity_id.split(".")[0] != LIGHT_DOMAIN: + if entity_id not in self.lights: return new_state = event.data.get("new_state") @@ -1814,6 +1818,10 @@ class TurnOnOffListener: entity_id, ) self.last_state_change[entity_id] = [new_state] + _LOGGER.debug( + "Last transition: %s", + self.last_service_data[entity_id].get(ATTR_TRANSITION), + ) self.start_transition_timer(entity_id) elif old_state is not None: self.last_state_change[entity_id].append(new_state) diff --git a/tests/test_switch.py b/tests/test_switch.py index db4900dc..6ba54a99 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1,9 +1,12 @@ """Tests for Adaptive Lighting switches.""" # pylint: disable=protected-access import asyncio +from copy import deepcopy import datetime import logging +from random import choices as random_choices from random import randint +import string from unittest.mock import patch from homeassistant.components.adaptive_lighting.const import ( @@ -47,6 +50,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR, + ATTR_TRANSITION, ATTR_XY_COLOR, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN @@ -61,6 +65,7 @@ from homeassistant.const import ( CONF_LIGHTS, CONF_NAME, CONF_PLATFORM, + EVENT_STATE_CHANGED, SERVICE_TURN_ON, STATE_OFF, STATE_ON, @@ -107,6 +112,11 @@ ENTITY_ADAPT_COLOR_SWITCH = f"{_SWITCH_FMT}_adapt_color_{DEFAULT_NAME}" ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE +GLOBAL_TEST_DEPENDENCIES = [ + "test_adaptive_lighting_switches", + "test_light_settings", +] + @pytest.fixture def reset_time_zone(): @@ -209,6 +219,69 @@ async def setup_lights_and_switch(hass, extra_conf=None): return switch, lights_instances +def create_random_context() -> str: + ulid_max_length = 26 # changed from 36->26 in core2023.4.0 + return Context( + id="".join( + random_choices(string.ascii_uppercase + string.digits, k=ulid_max_length) + ), + parent_id=None, + ) + + +# see https://github.com/home-assistant/core/blob/dev/homeassistant/scripts/benchmark/__init__.py +# basically just search the repo for EVENT_STATE_CHANGED look for how it's fired. +def create_transition_events( + light: str, + state: State, + last: dict | None = None, + current: dict | None = None, + total_events: int = 4, +) -> list[dict]: + assert light is not None + all_events = [] + for i in range(1, total_events): + # Build basic event data. + attributes = {} + + # The first state change always has the context from our integration. + # That one will not be in all_events. + # It's very possible it stores the parent_id though. + # If it stores the parent_id in all situations, there's a great improvement + # that could added in future updates. + + # Simulate the events the bulb would send to HASS. + last_brightness = last.get(ATTR_BRIGHTNESS) or state[ATTR_BRIGHTNESS] + current_brightness = current.get(ATTR_BRIGHTNESS) + if ( + last_brightness + and current_brightness + and last_brightness != current_brightness + ): + diff = (current_brightness - last_brightness) * (i / total_events) + attributes[ATTR_BRIGHTNESS] = last_brightness + diff + elif current_brightness: + attributes[ATTR_BRIGHTNESS] = current_brightness + current_kelvin = current.get(ATTR_COLOR_TEMP_KELVIN) + last_kelvin = last.get(ATTR_COLOR_TEMP_KELVIN) or state[ATTR_COLOR_TEMP_KELVIN] + if last_kelvin and current_kelvin and last_kelvin != current_kelvin: + diff = (current_kelvin - last_kelvin) * (i / total_events) + attributes[ATTR_COLOR_TEMP_KELVIN] = last_kelvin + diff + elif current_kelvin: + attributes[ATTR_COLOR_TEMP_KELVIN] = current_kelvin + + # Pack event + event_data = { + ATTR_ENTITY_ID: light, + "old_state": State(light, "on", attributes=last), + "new_state": State( + light, "on", attributes=attributes, context=create_random_context() + ), + } + all_events.append(event_data) + return all_events + + async def test_adaptive_lighting_switches(hass): """Test switches created for adaptive_lighting integration.""" entry, _ = await setup_switch(hass, {}) @@ -236,6 +309,7 @@ async def test_adaptive_lighting_switches(hass): @pytest.mark.parametrize("lat,long,timezone", LAT_LONG_TZS) +@pytest.mark.dependency("test_adaptive_lighting_switches") async def test_adaptive_lighting_time_zones_with_default_settings( hass, lat, long, timezone, reset_time_zone # pylint: disable=redefined-outer-name ): @@ -428,6 +502,7 @@ async def test_light_settings(hass): assert_expected_color_temp(state) +@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_turn_on_off_listener_not_tracking_untracked_lights(hass): """Test that lights that are not in a Adaptive Lighting switch aren't tracked.""" switch, _ = await setup_lights_and_switch(hass) @@ -447,6 +522,7 @@ async def test_turn_on_off_listener_not_tracking_untracked_lights(hass): assert light not in switch.turn_on_off_listener.lights +@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_manual_control(hass): """Test the 'manual control' tracking.""" switch, (light, *_) = await setup_lights_and_switch(hass) @@ -594,6 +670,7 @@ async def test_manual_control(hass): assert all([not manual_control[eid] for eid in switch._lights]) +@pytest.mark.dependency(depends=[*GLOBAL_TEST_DEPENDENCIES, "test_manual_control"]) async def test_auto_reset_manual_control(hass): switch, (light, *_) = await setup_lights_and_switch( hass, {CONF_AUTORESET_CONTROL: 0.1} @@ -638,6 +715,7 @@ async def test_auto_reset_manual_control(hass): assert not manual_control[light.entity_id] +@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_apply_service(hass): """Test adaptive_lighting.apply service.""" switch, (_, _, light) = await setup_lights_and_switch(hass) @@ -701,6 +779,9 @@ async def test_apply_service(hass): assert old_state[ATTR_COLOR_TEMP_KELVIN] == new_state[ATTR_COLOR_TEMP_KELVIN] +@pytest.mark.dependency( + depends=[*GLOBAL_TEST_DEPENDENCIES, "test_apply_service", "test_manual_control"] +) async def test_switch_off_on_off(hass): """Test switch rapid off_on_off.""" @@ -751,85 +832,7 @@ async def test_switch_off_on_off(hass): assert state == STATE_OFF -async def test_significant_change(hass): - """Test significant change.""" - - async def turn_light(state, **kwargs): - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON if state else SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs}, - blocking=True, - ) - await hass.async_block_till_done() - - async def update(force): - await switch._update_attrs_and_maybe_adapt_lights( - transition=0, - context=switch.create_context("test"), - force=force, - ) - await hass.async_block_till_done() - - async def set_brightness(val: int): - hass.states.async_set( - ENTITY_LIGHT, "on", {ATTR_BRIGHTNESS: val, ATTR_SUPPORTED_FEATURES: 1} - ) - await hass.async_block_till_done() - - switch, _ = await setup_lights_and_switch(hass) - _LOGGER.debug("Test detect_non_ha_changes:") - switch._take_over_control = True - assert switch._take_over_control - switch._detect_non_ha_changes = True - assert switch._detect_non_ha_changes - - # build last service data - await update(force=False) - - # force=True should not reset manual control. - await turn_light(True, brightness=40) - await turn_light(True, brightness=20) - await update(force=False) - assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] - await update(force=True) - assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] - - # turn light off then on should reset manual control. - await turn_light(False) - await turn_light(True) - assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] - - # Assert last_service_data got filled from update() - await update(force=True) - assert switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None - - # Simulate a transition to 255 where the update() is already using brightness 255. - await set_brightness(240) - await set_brightness(244) - await set_brightness(247) - await set_brightness(250) - - # last_state_change should have our state changes. - # Change brightness by async_set (not using 'light.turn_on') - new_brightness = 50 - await set_brightness(new_brightness) - _LOGGER.debug("Test: Brightness set to %s", new_brightness) - - # mock homeassistant.core.HomeAssistant.helpers.entity_component.async_update_entity - # Otherwise what happens is update_entity() refreshes the state to the last call of - # light.turn_on(). This is because we are not using hass.states.async_set() to - # set the brightness of the light. We mock `async_update_ha_state` because - # `async_update_entity` calls it. - with patch("homeassistant.helpers.entity.Entity.async_update_ha_state"): - # On next update ENTITY_LIGHT should be marked as manually controlled - await update(force=False) - assert ( - switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None - ) - assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] - - +@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) def test_color_difference_redmean(): """Test color_difference_redmean function.""" for _ in range(10): @@ -839,14 +842,6 @@ def test_color_difference_redmean(): color_difference_redmean((0, 0, 0), (255, 255, 255)) -def test_is_our_context(): - """Test is_our_context function.""" - context = create_context(DOMAIN, "test", 0) - assert is_our_context(context) - assert not is_our_context(None) - assert not is_our_context(Context()) - - def test_attributes_have_changed(): """Test _attributes_have_changed function.""" attributes_1 = { @@ -900,6 +895,229 @@ def test_attributes_have_changed(): ) +@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) +async def test_state_change_handlers(hass): + """ + Test TurnOnOffListener's EVENT_STATE_CHANGED listener. + ====================== + Sequence of events: + 1. Transition from sleep mode to normal. + 2. Create simulated transition events for that adapt. + 3. Fire all simulated transition events. + 4. Assert all possible problems that would result. + Also tests significant changes. + """ + switch, (light, *_) = await setup_lights_and_switch(hass) + context = switch.create_context("test") # needs to be passed to update method + + # [Config options]: + transition_used = 2 + total_events = 5 + + async def set_brightness(val: int): + # 'Unsafe' set but we know what we're doing. + hass.states.async_set( + ENTITY_LIGHT, "on", {ATTR_BRIGHTNESS: val, ATTR_SUPPORTED_FEATURES: 1} + ) + await hass.async_block_till_done() + # Call code in TurnOnOffListener + hass.bus.async_fire( + EVENT_STATE_CHANGED, + { + "new_state": { + ATTR_ENTITY_ID: ENTITY_LIGHT, + "state": "on", + ATTR_BRIGHTNESS: val, + } + }, + ) + await hass.async_block_till_done() + + async def turn_light(state, **kwargs): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs}, + blocking=True, + ) + await hass.async_block_till_done() + + async def update(force: bool = False): + await switch._update_attrs_and_maybe_adapt_lights( + force=force, transition=0, context=context + ) + await hass.async_block_till_done() + + # 1. Adapt to sleep without a transition. + # Should only be one state change. + _LOGGER.debug('test_state_change_handling: Turn on "sleep mode"') + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_SLEEP_MODE_SWITCH}, + blocking=True, + ) + await hass.async_block_till_done() + assert switch.turn_on_off_listener.last_state_change.get(ENTITY_LIGHT) + assert len(switch.turn_on_off_listener.last_state_change[ENTITY_LIGHT]) == 1 + assert not switch.turn_on_off_listener.transition_timers.get(ENTITY_LIGHT) + last_service_data = deepcopy(switch.turn_on_off_listener.last_service_data) + assert last_service_data.get(ENTITY_LIGHT) + + # 2 Adapt from sleep with a 'transition'. + await switch.sleep_mode_switch.async_turn_off() + await switch._update_attrs_and_maybe_adapt_lights( + force=False, transition=0, context=context + ) + await hass.async_block_till_done() + current_service_data = switch.turn_on_off_listener.last_service_data + assert current_service_data != last_service_data + + for light in switch._lights: + # current_service_data should have changed after the last update. + assert current_service_data.get(light) + assert last_service_data.get(light) + assert current_service_data[light] != last_service_data[light] + + # Test same context id events. + current_service_data[light][ATTR_TRANSITION] = transition_used + hass.bus.async_fire( + EVENT_STATE_CHANGED, + { + ATTR_ENTITY_ID: light, + "old_state": State(light, "on", attributes=last_service_data), + "new_state": State( + light, "on", attributes=current_service_data, context=context + ), + }, + ) + assert not switch.turn_on_off_listener.transition_timers.get(light) + + # 2.3 Refire and overwrite the original state_changed event with our 'transition' + hass.bus.async_fire( + EVENT_STATE_CHANGED, + { + ATTR_ENTITY_ID: light, + "old_state": State(light, "on", attributes=last_service_data), + "new_state": State( + light, + "on", + attributes=current_service_data, + # We need to overwrite the old context_id + context=switch.create_context("test"), + ), + }, + ) + await hass.async_block_till_done() + # Assert our transition timer was created. + assert switch.turn_on_off_listener.transition_timers.get(light) + # 2.5 Simulate a transition. There's no other way to do this in the demo. + events = create_transition_events( + light=light, + state=hass.states.get(light), + last=last_service_data[light], + current=current_service_data[light], + total_events=total_events, + ) + # 3. Fire simulated events for our TurnOnOffListener + for event in events: + _LOGGER.debug("Test EVENT_STATE_CHANGED listener") + hass.bus.async_fire(EVENT_STATE_CHANGED, event) + await hass.async_block_till_done() + # On real systems HA fires transition state changes every ~3 seconds. + # asyncio.sleep(3) + # 4. Assert the transition timer started and everything was filled. + listener = switch.turn_on_off_listener + assert listener.last_state_change.get(ENTITY_LIGHT) + assert len(listener.last_state_change[ENTITY_LIGHT]) == total_events + assert listener.transition_timers.get(ENTITY_LIGHT) + + # 5. Execute some checks during a transition + _LOGGER.debug("Test detect_non_ha_changes:") + switch._take_over_control = True + assert switch._take_over_control + switch._detect_non_ha_changes = True + assert switch._detect_non_ha_changes + await asyncio.sleep(transition_used / 3) + # Ensure the timer still exists + timer = listener.transition_timers.get(ENTITY_LIGHT) + assert timer and timer.is_running() + last_service_data = deepcopy(current_service_data) + await update() + assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + await update() + assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + timer = listener.transition_timers.get(ENTITY_LIGHT) + assert timer and timer.is_running() + # Ensure the light did not adapt during the transition. + assert last_service_data == current_service_data + + # 6. Assert everything after the transition finishes. + await asyncio.sleep(transition_used) + assert listener.last_state_change.get(ENTITY_LIGHT) + assert len(listener.last_state_change[ENTITY_LIGHT]) == total_events + # Timer should be done and reset now. + # This is the assert that I can't fix. + timer = listener.transition_timers.get(ENTITY_LIGHT) + assert not timer or not timer.is_running() + + # build last service data + await update(force=False) + + # force=True should not reset manual control. + await turn_light(True, brightness=40) + await turn_light(True, brightness=20) + await update(force=False) + assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + await update(force=True) + assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + + # turn light off then on should reset manual control. + await turn_light(False) + await turn_light(True) + assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + + # last_state_change should have our state changes. + # Change brightness by async_set (not using 'light.turn_on') + new_brightness = 50 + await set_brightness(new_brightness) + _LOGGER.debug("Test: Brightness set to %s", new_brightness) + + # mock homeassistant.core.HomeAssistant.helpers.entity_component.async_update_entity + # Otherwise what happens is update_entity() refreshes the state to the last call of + # light.turn_on(). This is because we are not using hass.states.async_set() to + # set the brightness of the light. We mock `async_update_ha_state` because + # `async_update_entity` calls it. + with patch("homeassistant.helpers.entity.Entity.async_update_ha_state"): + # On next update ENTITY_LIGHT should be marked as manually controlled + await update(force=False) + assert ( + switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None + ) + assert ( + switch.turn_on_off_listener.last_state_change.get(ENTITY_LIGHT) is not None + ) + assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + + +@pytest.mark.dependency( + depends=[ + *GLOBAL_TEST_DEPENDENCIES, + "test_manual_control", + "test_apply_service", + "test_attributes_have_changed", + "test_state_change_handling", + ] +) +@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) +def test_is_our_context(): + """Test is_our_context function.""" + context = create_context(DOMAIN, "test", 0) + assert is_our_context(context) + assert not is_our_context(None) + assert not is_our_context(Context()) + + async def test_unload_switch(hass): """Test removing Adaptive Lighting.""" entry, _ = await setup_switch(hass, {}) @@ -966,6 +1184,7 @@ async def test_turn_on_and_off_when_already_at_that_state(hass): await hass.async_block_till_done() +@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_async_update_at_interval(hass): """Test '_async_update_at_interval' method.""" _, switch = await setup_switch(hass, {}) @@ -973,6 +1192,7 @@ async def test_async_update_at_interval(hass): @pytest.mark.parametrize("separate_turn_on_commands", (True, False)) +@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_separate_turn_on_commands(hass, separate_turn_on_commands): """Test 'separate_turn_on_commands' argument.""" switch, (light, *_) = await setup_lights_and_switch( @@ -1009,6 +1229,7 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands): assert sleep_color_temp != color_temp +@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_area(hass): switch, (light, *_) = await setup_lights_and_switch(hass) @@ -1045,6 +1266,7 @@ async def test_area(hass): assert light.entity_id not in switch.turn_on_off_listener.last_service_data +@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_change_switch_settings_service(hass): """Test adaptive_lighting.change_switch_settings service.""" switch, (_, _, light) = await setup_lights_and_switch(hass) From 59877a034340793ec1c8d70d42916ed5116d174d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 6 Apr 2023 12:37:02 -0700 Subject: [PATCH 0546/1077] Make sure context_id is 26 chars and partially conform to ULID standard (#550) --- .../adaptive_lighting/manifest.json | 2 +- custom_components/adaptive_lighting/switch.py | 72 +++++++++++++++---- tests/test_switch.py | 17 ++--- 3 files changed, 63 insertions(+), 28 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 33564f23..166b3695 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://github.com/basnijholt/adaptive-lighting#readme", "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", - "requirements": [], + "requirements": ["ulid-transform"], "version": "1.10.0" } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index dbf3c42b..5b816b67 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -88,6 +88,7 @@ from homeassistant.util.color import ( color_xy_to_RGB, ) import homeassistant.util.dt as dt_util +import ulid_transform import voluptuous as vol from .const import ( @@ -182,21 +183,58 @@ BRIGHTNESS_ATTRS = { } # Keep a short domain version for the context instances (which can only be 36 chars) -_DOMAIN_SHORT = "adapt_lgt" +_DOMAIN_SHORT = "al" -def _int_to_bytes(i: int, signed: bool = False) -> bytes: - bits = i.bit_length() - if signed: - # Make room for the sign bit. - bits += 1 - return i.to_bytes((bits + 7) // 8, "little", signed=signed) +def _int_to_base36(num: int) -> str: + """ + Convert an integer to its base-36 representation using numbers and uppercase letters. + + Base-36 encoding uses digits 0-9 and uppercase letters A-Z, providing a case-insensitive + alphanumeric representation. The function takes an integer `num` as input and returns + its base-36 representation as a string. + + Parameters + ---------- + num + The integer to convert to base-36. + + Returns + ------- + str + The base-36 representation of the input integer. + + Examples + -------- + >>> num = 123456 + >>> base36_num = int_to_base36(num) + >>> print(base36_num) + '2N9' + """ + ALPHANUMERIC_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + if num == 0: + return ALPHANUMERIC_CHARS[0] + + base36_str = "" + base = len(ALPHANUMERIC_CHARS) + + while num: + num, remainder = divmod(num, base) + base36_str = ALPHANUMERIC_CHARS[remainder] + base36_str + + return base36_str def _short_hash(string: str, length: int = 4) -> str: """Create a hash of 'string' with length 'length'.""" - str_hash_bytes = _int_to_bytes(hash(string), signed=True) - return base64.b85encode(str_hash_bytes)[:length] + return base64.b32encode(string.encode()).decode("utf-8").zfill(length)[:length] + + +def _remove_vowels(input_str: str, length: int = 4) -> str: + vowels = "aeiouAEIOU" + output_str = "".join([char for char in input_str if char not in vowels]) + return output_str.zfill(length)[:length] def create_context( @@ -204,12 +242,16 @@ def create_context( ) -> Context: """Create a context that can identify this integration.""" # Use a hash for the name because otherwise the context might become - # too long (max len == 36) to fit in the database. - name_hash = _short_hash(name) + # too long (max len == 26) to fit in the database. # Pack index with base85 to maximize the number of contexts we can create - # before we exceed the 36-character limit and are forced to wrap. - index_packed = base64.b85encode(_int_to_bytes(index, signed=False)) - context_id = f"{_DOMAIN_SHORT}:{name_hash}:{which}:{index_packed}"[:36] + # before we exceed the 26-character limit and are forced to wrap. + time_stamp = ulid_transform.ulid_now()[:10] # time part of a ULID + name_hash = _short_hash(name) + which_short = _remove_vowels(which) + context_id_start = f"{time_stamp}:{_DOMAIN_SHORT}:{name_hash}:{which_short}:" + chars_left = 26 - len(context_id_start) + index_packed = _int_to_base36(index).zfill(chars_left)[-chars_left:] + context_id = context_id_start + index_packed parent_id = parent.id if parent else None return Context(id=context_id, parent_id=parent_id) @@ -218,7 +260,7 @@ def is_our_context(context: Context | None) -> bool: """Check whether this integration created 'context'.""" if context is None: return False - return context.id.startswith(_DOMAIN_SHORT) + return f":{_DOMAIN_SHORT}:" in context.id def _split_service_data(service_data, adapt_brightness, adapt_color): diff --git a/tests/test_switch.py b/tests/test_switch.py index 6ba54a99..ae722b0d 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -4,9 +4,7 @@ import asyncio from copy import deepcopy import datetime import logging -from random import choices as random_choices from random import randint -import string from unittest.mock import patch from homeassistant.components.adaptive_lighting.const import ( @@ -76,6 +74,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util.color import color_temperature_mired_to_kelvin import homeassistant.util.dt as dt_util import pytest +import ulid_transform import voluptuous.error from tests.common import MockConfigEntry, mock_area_registry @@ -118,6 +117,10 @@ GLOBAL_TEST_DEPENDENCIES = [ ] +def create_random_context() -> str: + return Context(id=ulid_transform.ulid_now(), parent_id=None) + + @pytest.fixture def reset_time_zone(): """Reset time zone.""" @@ -219,16 +222,6 @@ async def setup_lights_and_switch(hass, extra_conf=None): return switch, lights_instances -def create_random_context() -> str: - ulid_max_length = 26 # changed from 36->26 in core2023.4.0 - return Context( - id="".join( - random_choices(string.ascii_uppercase + string.digits, k=ulid_max_length) - ), - parent_id=None, - ) - - # see https://github.com/home-assistant/core/blob/dev/homeassistant/scripts/benchmark/__init__.py # basically just search the repo for EVENT_STATE_CHANGED look for how it's fired. def create_transition_events( From a01fec02113c46404d155205c9778913d640c3f0 Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Thu, 6 Apr 2023 15:29:32 -0500 Subject: [PATCH 0547/1077] Bump to 1.10.1 (#551) * Update manifest.json * undo merge mistake * Version 1.10.1 --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 166b3695..92b27dfa 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.10.0" + "version": "1.10.1" } From c0c363136bcc90190081f1f6a6eff52ec6e25c21 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 6 Apr 2023 16:36:57 -0700 Subject: [PATCH 0548/1077] Test multiple Home Assistant releases and the dev branch (#552) * Test for multiple Home Assistant versions * Install ulid-transform * remove unnecessary unsafe `async_set` from test * Skip test_state_change_handlers in <2023.4 * Revert "Skip test_state_change_handlers in <2023.4" This reverts commit 8d01b6ec4ea8b97feb8dabf8b737be604755dffd. --------- Co-authored-by: Benjamin Auquite --- .../workflows/install_dependencies/action.yml | 12 ++++++--- .github/workflows/pytest.yaml | 5 +++- .github/workflows/update-readme.yml | 2 +- tests/test_switch.py | 27 +++++-------------- 4 files changed, 21 insertions(+), 25 deletions(-) diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index 75820072..df0bee8a 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -1,10 +1,14 @@ name: 'Install Dependencies' description: 'Install Home Assistant and test dependencies' inputs: - python_version: + python-version: description: 'Python version' required: true default: '3.10' + core-version: + description: 'Home Assistant core version' + required: false + default: 'dev' runs: using: "composite" @@ -21,11 +25,12 @@ runs: with: repository: home-assistant/core path: core - - name: Set up Python ${{ inputs.python_version }} + ref: ${{ inputs.core-version }} + - name: Set up Python ${{ inputs.python-version }} id: python uses: actions/setup-python@v4.1.0 with: - python-version: ${{ inputs.python_version }} + python-version: ${{ inputs.python-version }} - name: Install dependencies shell: bash run: | @@ -33,4 +38,5 @@ runs: pip install -r core/requirements.txt --use-pep517 pip install -r core/requirements_test.txt --use-pep517 pip install -e core/ --use-pep517 + pip install ulid-transform # this is in Adaptive-lighting's manifest.json pip install $(python test_dependencies.py) --use-pep517 diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 02bf93e3..cea75602 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -11,8 +11,10 @@ jobs: runs-on: ubuntu-20.04 timeout-minutes: 60 strategy: + fail-fast: false matrix: python-version: ["3.10"] + core-version: ["2023.2.5", "2023.3.6", "2023.4.0", "dev"] steps: - name: Check out code from GitHub uses: actions/checkout@v3 @@ -20,7 +22,8 @@ jobs: - name: Install Home Assistant uses: ./.github/workflows/install_dependencies with: - python_version: ${{ matrix.python-version }} + python-version: ${{ matrix.python-version }} + core-version: ${{ matrix.core-version }} - name: Click here for troubleshooting steps if tests break again. run: | diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index 1f661333..2c2754fb 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -20,7 +20,7 @@ jobs: - name: Install Home Assistant uses: ./.github/workflows/install_dependencies with: - python_version: "3.10" + python-version: "3.10" - name: Install markdown-code-runner and README code dependencies run: | diff --git a/tests/test_switch.py b/tests/test_switch.py index ae722b0d..f6d671f4 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1070,27 +1070,14 @@ async def test_state_change_handlers(hass): await turn_light(True) assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] - # last_state_change should have our state changes. - # Change brightness by async_set (not using 'light.turn_on') - new_brightness = 50 - await set_brightness(new_brightness) - _LOGGER.debug("Test: Brightness set to %s", new_brightness) + await turn_light(True, brightness=50) + _LOGGER.debug("Test: Brightness set to %s", 50) - # mock homeassistant.core.HomeAssistant.helpers.entity_component.async_update_entity - # Otherwise what happens is update_entity() refreshes the state to the last call of - # light.turn_on(). This is because we are not using hass.states.async_set() to - # set the brightness of the light. We mock `async_update_ha_state` because - # `async_update_entity` calls it. - with patch("homeassistant.helpers.entity.Entity.async_update_ha_state"): - # On next update ENTITY_LIGHT should be marked as manually controlled - await update(force=False) - assert ( - switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None - ) - assert ( - switch.turn_on_off_listener.last_state_change.get(ENTITY_LIGHT) is not None - ) - assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + # On next update ENTITY_LIGHT should be marked as manually controlled + await update(force=False) + assert switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None + assert switch.turn_on_off_listener.last_state_change.get(ENTITY_LIGHT) is not None + assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] @pytest.mark.dependency( From 39e9d0e74fde95c9d35505a63316c8728ae895a4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 7 Apr 2023 17:40:36 -0700 Subject: [PATCH 0549/1077] Update issue templates (#557) --- .github/ISSUE_TEMPLATE/bug-report.md | 68 ++++++++++++++++++++++----- .github/ISSUE_TEMPLATE/doc.md | 7 ++- .github/ISSUE_TEMPLATE/enhancement.md | 7 ++- .github/ISSUE_TEMPLATE/feature.md | 7 ++- 4 files changed, 72 insertions(+), 17 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index c5bcb3a4..385259a6 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -1,17 +1,63 @@ --- -name: 'Bug Report' -about: 'Report a bug in adaptive-lighting.' -labels: kind/bug, need/triage +name: Bug Report +about: Report a bug in adaptive-lighting. +title: '' +labels: kind/bug, kind/feature, need/triage +assignees: '' + --- -#### Version information: +# Home Assistant Adaptive Lighting Issue Template + +## Bug Reports + +If you need help with using or configuring Adaptive Lighting, please [open a Q&A discussion thread here](https://github.com/basnijholt/adaptive-lighting/discussions/new?category=q-a) instead. + +### Before submitting a bug report, please follow these troubleshooting steps: + +Please confirm that you have completed the following steps: + +- [ ] I have updated to the [latest Adaptive Lighting version](https://github.com/basnijholt/adaptive-lighting/releases) available in [HACS](https://hacs.xyz/). +- [ ] I have reviewed the [Troubleshooting Section](https://github.com/basnijholt/adaptive-lighting#troubleshooting) in the [README](https://github.com/basnijholt/adaptive-lighting#readme). +- [ ] (If using Zigbee2MQTT) I have read the [Zigbee2MQTT troubleshooting guide](https://github.com/basnijholt/adaptive-lighting#zigbee2mqtt) in the [README](https://github.com/basnijholt/adaptive-lighting#readme). +- [ ] I have checked the [V2 Roadmap](https://github.com/basnijholt/adaptive-lighting/discussions/291) and [open issues](https://github.com/basnijholt/adaptive-lighting/issues) to ensure my issue isn't a duplicate. -#### Description: - +Please include the following information in your issue. + +*Issues missing this information may not be addressed.* + +1. **Debug logs** captured while the issue occurred. [See here for instructions on enabling debug logging](https://github.com/basnijholt/adaptive-lighting#troubleshooting): + +``` + +``` + +2. [Your Adaptive Lighting configuration](https://github.com/basnijholt/adaptive-lighting#gear-configuration): + +``` + +``` + +3. (If using Zigbee2MQTT), provide your configuration files (**remove all personal information before posting**): + - `devices.yaml` + - `groups.yaml` + - `configuration.yaml` ⚠️; **Warning** _**REMOVE ALL of the PERSONAL INFORMATION BELOW before posting**_ ⚠️; + - mqtt: `server`: + - mqtt: `user`: + - mqtt: `password`: + - advanced: `pan_id`: + - advanced: `network_key`: + - anything in `log_syslog` if you use this + - Brand and model number of problematic light(s) +``` + +``` + +4. Describe the bug and how to reproduce it: + + + +5. Steps to reproduce the behavior: diff --git a/.github/ISSUE_TEMPLATE/doc.md b/.github/ISSUE_TEMPLATE/doc.md index 98c9a008..f6458d77 100644 --- a/.github/ISSUE_TEMPLATE/doc.md +++ b/.github/ISSUE_TEMPLATE/doc.md @@ -1,7 +1,10 @@ --- -name: 'Documentation Issue' -about: 'Report missing, erroneous docs, broken links or propose new docs' +name: Documentation Issue +about: Report missing, erroneous docs, broken links or propose new docs +title: '' labels: kind/docs_issue, need/triage +assignees: '' + --- #### Location diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md index cc515a20..d25a9689 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.md +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -1,5 +1,8 @@ --- -name: 'Enhancement' -about: 'Suggest an improvement to an existing feature.' +name: Enhancement +about: Suggest an improvement to an existing feature. +title: '' labels: kind/enhancement, need/triage +assignees: '' + --- diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md index c4b787df..088b35e4 100644 --- a/.github/ISSUE_TEMPLATE/feature.md +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -1,5 +1,8 @@ --- -name: 'Feature' -about: 'Suggest a new feature' +name: Feature +about: Suggest a new feature +title: '' labels: kind/feature, need/triage +assignees: '' + --- From fe7bdd394014df77785bdf598a37e41bd3d3ae44 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 8 Apr 2023 03:39:08 -0700 Subject: [PATCH 0550/1077] Add auto_reset_time_remaining attribute (#558) * Add auto_reset_time_remaining attribute * fix attr * Add test --- custom_components/adaptive_lighting/switch.py | 13 +++++++++++++ tests/test_switch.py | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 5b816b67..884f74cd 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1014,6 +1014,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self.turn_on_off_listener.manual_control.get(light) ] extra_state_attributes.update(self._settings) + timers = self.turn_on_off_listener.auto_reset_manual_control_timers + extra_state_attributes["autoreset_time_remaining"] = { + light: time + for light in self._lights + if (timer := timers.get(light)) and (time := timer.remaining_time()) > 0 + } return extra_state_attributes def create_context( @@ -2106,3 +2112,10 @@ class _AsyncSingleShotTimer: if self.task: self.task.cancel() self.callback = None + + def remaining_time(self): + """Return the remaining time before the timer expires.""" + if self.start_time is not None: + elapsed_time = (dt_util.utcnow() - self.start_time).total_seconds() + return max(0, self.delay - elapsed_time) + return 0 diff --git a/tests/test_switch.py b/tests/test_switch.py index f6d671f4..476e83cc 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -692,9 +692,15 @@ async def test_auto_reset_manual_control(hass): await turn_light(True, brightness=1) await turn_light(True, brightness=10) assert manual_control[light.entity_id] + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] > 0 + ) await asyncio.sleep(0.3) # Should be enough time for auto reset await update() assert not manual_control[light.entity_id], (light, manual_control) + assert ( + light.entity_id not in switch.extra_state_attributes["autoreset_time_remaining"] + ) # Do a couple of quick changes and check that light is not reset for i in range(3): From e30b7debe551e58cc6a738f7d15a4bb75d9bb545 Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Sat, 8 Apr 2023 20:54:06 -0500 Subject: [PATCH 0551/1077] Refactor `_adapt_lights` into `_update_manual_control_and_maybe_adapt` (#513) * Add auto_reset_manual_control with async timer * cherry-pick wait for transition stuff * Update switch.py * merge wait_for_transition * Update switch.py * not renamed in this branch yet. * Update switch.py * update tests * Update switch.py * Update switch.py * merge related fix * cleanup * Revert "cleanup" This reverts commit 3aa2f3242b06370c0f84248d2c2c21a2556bb5ec. * 0.1 sometimes fails the test * not in this pr yet * Update switch.py * Update switch.py * Update switch.py * Update switch.py * Update README.md, strings.json, and services.yaml * slight refactor * Update switch.py * Update switch.py * Possible refactor of _update_attrs_and_maybe_adapt_lights * cleaned up * Revert "cleaned up" This reverts commit 441cb1ff5cc45eb3438f7e367e8f22cea061ba73. * Possible refactor of _update_attrs_and_maybe_adapt_lights (#537) Co-authored-by: Benjamin Auquite * Revert "Revert "cleaned up"" This reverts commit 11b268148b098af4f790a6f41c8b87b14ea0748a. * remove bad merge conflict * revert permissions * Bump to 1.11.0 * Undo unrelated test changes * 'else' instead of 'continue' --------- Co-authored-by: Bas Nijholt Co-authored-by: github-actions[bot] Co-authored-by: Bas Nijholt --- .../adaptive_lighting/manifest.json | 2 +- custom_components/adaptive_lighting/switch.py | 91 ++++++++++--------- 2 files changed, 50 insertions(+), 43 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 92b27dfa..2dec4c83 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.10.1" + "version": "1.11.0" } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 884f74cd..98747a17 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -424,6 +424,7 @@ def _fire_manual_control_event( switch.entity_id, light, ) + switch.turn_on_off_listener.mark_as_manual_control(light) fire( f"{DOMAIN}.manual_control", {ATTR_ENTITY_ID: light, SWITCH_DOMAIN: switch.entity_id}, @@ -519,7 +520,6 @@ async def async_setup_entry( all_lights = _expand_light_groups(switch.hass, lights) if service_call.data[CONF_MANUAL_CONTROL]: for light in all_lights: - switch.turn_on_off_listener.mark_as_manual_control(light) _fire_manual_control_event(switch, light, service_call.context) else: switch.turn_on_off_listener.reset(*all_lights) @@ -1088,9 +1088,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if lock is not None and lock.locked(): _LOGGER.debug("%s: '%s' is locked", self._name, light) return - service_data = {ATTR_ENTITY_ID: light} - features = _supported_features(self.hass, light) - if transition is None: transition = self._transition if adapt_brightness is None: @@ -1100,15 +1097,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if prefer_rgb_color is None: prefer_rgb_color = self._prefer_rgb_color - # Check transition == 0 to fix #378 - if "transition" in features and transition > 0: - service_data[ATTR_TRANSITION] = transition - # The switch might be off and not have _settings set. self._settings = self._sun_light_settings.get_settings( self.sleep_mode_switch.is_on, transition ) + # Build service data. + service_data = {ATTR_ENTITY_ID: light} + features = _supported_features(self.hass, light) + + # Check transition == 0 to fix #378 + if "transition" in features and transition > 0: + service_data[ATTR_TRANSITION] = transition if "brightness" in features and adapt_brightness: brightness = round(255 * self._settings["brightness_pct"] / 100) service_data[ATTR_BRIGHTNESS] = brightness @@ -1135,19 +1135,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"] context = context or self.create_context("adapt_lights") - if ( - self._take_over_control - and self._detect_non_ha_changes - and not force - and await self.turn_on_off_listener.significant_change( - self, - light, - adapt_brightness, - adapt_color, - context, - ) - ): - return + # See #80. Doesn't check if transitions differ but it does the job. last_service_data = self.turn_on_off_listener.last_service_data if not force and last_service_data.get(light) == service_data: @@ -1236,9 +1224,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if not filtered_lights: return - await self._adapt_lights(filtered_lights, transition, force, context) + await self._update_manual_control_and_maybe_adapt( + filtered_lights, transition, force, context + ) - async def _adapt_lights( + async def _update_manual_control_and_maybe_adapt( self, lights: list[str], transition: int | None, @@ -1247,34 +1237,53 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) -> None: assert context is not None _LOGGER.debug( - "%s: '_adapt_lights(%s, %s, force=%s, context.id=%s)' called", + "%s: '_update_manual_control_and_maybe_adapt(%s, %s, force=%s, context.id=%s)' called", self.name, lights, transition, force, context.id, ) + + adapt_brightness = self.adapt_brightness_switch.is_on + adapt_color = self.adapt_color_switch.is_on + for light in lights: if not is_on(self.hass, light): continue - if ( - self._take_over_control - and self.turn_on_off_listener.is_manually_controlled( + + manually_controlled = self.turn_on_off_listener.is_manually_controlled( + self, + light, + force, + adapt_brightness, + adapt_color, + ) + + significant_change = ( + self._detect_non_ha_changes + and not force + and await self.turn_on_off_listener.significant_change( self, light, - force, - self.adapt_brightness_switch.is_on, - self.adapt_color_switch.is_on, + adapt_brightness, + adapt_color, + context, ) - ): - _LOGGER.debug( - "%s: '%s' is being manually controlled, stop adapting, context.id=%s.", - self._name, - light, - context.id, - ) - continue - await self._adapt_light(light, transition, force=force, context=context) + ) + + if self._take_over_control and (manually_controlled or significant_change): + if manually_controlled: + _LOGGER.debug( + "%s: '%s' is being manually controlled, stop adapting, context.id=%s.", + self._name, + light, + context.id, + ) + else: + _fire_manual_control_event(self, light, context) + else: + await self._adapt_light(light, transition, force=force, context=context) async def _sleep_mode_switch_state_event(self, event: Event) -> None: if not match_switch_state_event(event, (STATE_ON, STATE_OFF)): @@ -1900,7 +1909,7 @@ class TurnOnOffListener: ): # Light was already on and 'light.turn_on' was not called by # the adaptive_lighting integration. - manual_control = self.mark_as_manual_control(light) + manual_control = True _fire_manual_control_event(switch, light, turn_on_event.context) _LOGGER.debug( "'%s' was already on and 'light.turn_on' was not called by the" @@ -1968,8 +1977,6 @@ class TurnOnOffListener: light, context.id, ) - self.mark_as_manual_control(light) - _fire_manual_control_event(switch, light, context, is_async=False) return True _LOGGER.debug( "%s: Light '%s' correctly matches our last adapt's service data, continuing..." From a888afd6dcdfd335a22e45f8b6be0bbd959ebd87 Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Mon, 10 Apr 2023 12:07:31 -0500 Subject: [PATCH 0552/1077] Refactor `_supported_features` (#565) * initial commit * change features to dict * another slight refactor --- custom_components/adaptive_lighting/const.py | 2 + custom_components/adaptive_lighting/switch.py | 95 +++++++++++++------ 2 files changed, 66 insertions(+), 31 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 64620927..6d36ae2e 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -273,6 +273,8 @@ VALIDATION_TUPLES = [ ), ] +CONST_COLOR = "color" + def timedelta_as_int(value): """Convert a `datetime.timedelta` object to an integer. diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 98747a17..3eabf7ff 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -23,7 +23,11 @@ from homeassistant.components.light import ( ATTR_COLOR_NAME, ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, + ATTR_MAX_COLOR_TEMP_KELVIN, + ATTR_MIN_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_XY_COLOR, @@ -32,6 +36,7 @@ from homeassistant.components.light import ( COLOR_MODE_HS, COLOR_MODE_RGB, COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, COLOR_MODE_XY, ) from homeassistant.components.light import ( @@ -129,6 +134,7 @@ from .const import ( CONF_TRANSITION, CONF_TURN_ON_LIGHTS, CONF_USE_DEFAULTS, + CONST_COLOR, DOMAIN, EXTRA_VALIDATION, ICON_BRIGHTNESS, @@ -155,6 +161,16 @@ _SUPPORT_OPTS = { "transition": SUPPORT_TRANSITION, } +VALID_COLOR_MODES = { + COLOR_MODE_BRIGHTNESS: ATTR_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP: ATTR_COLOR_TEMP_KELVIN, + COLOR_MODE_HS: ATTR_HS_COLOR, + COLOR_MODE_RGB: ATTR_RGB_COLOR, + COLOR_MODE_RGBW: ATTR_RGBW_COLOR, + COLOR_MODE_RGBWW: ATTR_RGBWW_COLOR, + COLOR_MODE_XY: ATTR_XY_COLOR, +} + _ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT) _ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))} @@ -623,33 +639,51 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]: return list(all_lights) +def _supported_to_attributes(supported): + supported_attributes = {} + supports_colors = False + for mode, attr in VALID_COLOR_MODES.items(): + if mode not in supported: + continue + supported_attributes[attr] = True + if ( + not supports_colors + and mode != COLOR_MODE_BRIGHTNESS + and mode != COLOR_MODE_COLOR_TEMP + ): + supports_colors = True + return supported_attributes, supports_colors + + def _supported_features(hass: HomeAssistant, light: str): state = hass.states.get(light) - supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - supported = { - key for key, value in _SUPPORT_OPTS.items() if supported_features & value + legacy_supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + legacy_supported = { + key for key, value in _SUPPORT_OPTS.items() if legacy_supported_features & value } supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES, set()) - if COLOR_MODE_RGB in supported_color_modes: - supported.add("color") + supported, supports_colors = _supported_to_attributes( + legacy_supported.union(supported_color_modes) + ) + min_kelvin = state.attributes.get(ATTR_MIN_COLOR_TEMP_KELVIN) + max_kelvin = state.attributes.get(ATTR_MAX_COLOR_TEMP_KELVIN) + supported.update( + { + ATTR_MIN_COLOR_TEMP_KELVIN: min_kelvin, + ATTR_MAX_COLOR_TEMP_KELVIN: max_kelvin, + } + ) + if supports_colors: # Adding brightness here, see # comment https://github.com/basnijholt/adaptive-lighting/issues/112#issuecomment-836944011 - supported.add("brightness") - if COLOR_MODE_RGBW in supported_color_modes: - supported.add("color") - supported.add("brightness") # see above url - if COLOR_MODE_XY in supported_color_modes: - supported.add("color") - supported.add("brightness") # see above url - if COLOR_MODE_HS in supported_color_modes: - supported.add("color") - supported.add("brightness") # see above url - if COLOR_MODE_COLOR_TEMP in supported_color_modes: - supported.add("color_temp") - supported.add("brightness") # see above url - if COLOR_MODE_BRIGHTNESS in supported_color_modes: - supported.add("brightness") - return supported + supported[ATTR_BRIGHTNESS] = True + if CONST_COLOR not in legacy_supported: + # supports_colors = False + _LOGGER.debug( + "'supported_color_modes' supports color but the legacy 'supported_features'" + " bitfield says we do not. Despite this we'll assume light '%s' supports colors", + ) + return supported, supports_colors def color_difference_redmean( @@ -1104,12 +1138,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Build service data. service_data = {ATTR_ENTITY_ID: light} - features = _supported_features(self.hass, light) + features, supports_colors = _supported_features(self.hass, light) # Check transition == 0 to fix #378 - if "transition" in features and transition > 0: + if ATTR_TRANSITION in features and transition > 0: service_data[ATTR_TRANSITION] = transition - if "brightness" in features and adapt_brightness: + if ATTR_BRIGHTNESS in features and adapt_brightness: brightness = round(255 * self._settings["brightness_pct"] / 100) service_data[ATTR_BRIGHTNESS] = brightness @@ -1118,19 +1152,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): and self._sun_light_settings.sleep_rgb_or_color_temp == "rgb_color" ) if ( - "color_temp" in features + ATTR_COLOR_TEMP_KELVIN in features and adapt_color - and not (prefer_rgb_color and "color" in features) - and not (sleep_rgb and "color" in features) + and not (prefer_rgb_color and supports_colors) + and not (sleep_rgb and supports_colors) ): _LOGGER.debug("%s: Setting color_temp of light %s", self._name, light) - attributes = self.hass.states.get(light).attributes - min_kelvin = attributes["min_color_temp_kelvin"] - max_kelvin = attributes["max_color_temp_kelvin"] + min_kelvin = features[ATTR_MIN_COLOR_TEMP_KELVIN] + max_kelvin = features[ATTR_MAX_COLOR_TEMP_KELVIN] color_temp_kelvin = self._settings["color_temp_kelvin"] color_temp_kelvin = max(min(color_temp_kelvin, max_kelvin), min_kelvin) service_data[ATTR_COLOR_TEMP_KELVIN] = color_temp_kelvin - elif "color" in features and adapt_color: + elif supports_colors and adapt_color: _LOGGER.debug("%s: Setting rgb_color of light %s", self._name, light) service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"] From adf0d0cf308a6cd3a57462d691f335a32665b75d Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Wed, 26 Apr 2023 21:10:52 -0500 Subject: [PATCH 0553/1077] Auto reload YAML config changes (#573) * Update __init__.py * Update __init__.py * skip installing codecov * remove in CI * test 2023.4.6 --------- Co-authored-by: Bas Nijholt --- .github/workflows/install_dependencies/action.yml | 2 ++ .github/workflows/pytest.yaml | 2 +- custom_components/adaptive_lighting/__init__.py | 12 +++++++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index df0bee8a..3041698b 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -36,6 +36,8 @@ runs: run: | echo "::warning::### WARNING! Deprecation warnings muted with option '--use-pep517' please address this at some point in pytest.yaml. ###" pip install -r core/requirements.txt --use-pep517 + # because they decided to pull codecov the package from PyPI... + sed -i '/codecov/d' core/requirements_test.txt pip install -r core/requirements_test.txt --use-pep517 pip install -e core/ --use-pep517 pip install ulid-transform # this is in Adaptive-lighting's manifest.json diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index cea75602..394acef3 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -14,7 +14,7 @@ jobs: fail-fast: false matrix: python-version: ["3.10"] - core-version: ["2023.2.5", "2023.3.6", "2023.4.0", "dev"] + core-version: ["2023.2.5", "2023.3.6", "2023.4.6", "dev"] steps: - name: Check out code from GitHub uses: actions/checkout@v3 diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index dc928a6b..07a0a758 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -6,7 +6,6 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.reload import async_setup_reload_service import voluptuous as vol from .const import ( @@ -36,10 +35,13 @@ CONFIG_SCHEMA = vol.Schema( ) +async def reload_configuration_yaml(event: dict, hass: HomeAssistant): + """Reload configuration.yaml.""" + await hass.services.async_call("homeassistant", "check_config", {}) + + async def async_setup(hass: HomeAssistant, config: dict[str, Any]): """Import integration from config.""" - # This will reload any changes the user made to any YAML configurations. - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) if DOMAIN in config: for entry in config[DOMAIN]: @@ -55,6 +57,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Set up the component.""" data = hass.data.setdefault(DOMAIN, {}) + # This will reload any changes the user made to any YAML configurations. + # Called during 'quick reload' or hass.reload_config_entry + hass.bus.async_listen("hass.config.entry_updated", reload_configuration_yaml) + undo_listener = config_entry.add_update_listener(async_update_options) data[config_entry.entry_id] = {UNDO_UPDATE_LISTENER: undo_listener} for platform in PLATFORMS: From 47c5ea93e0051c88d728df9ab289dc25df74fcd2 Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Thu, 27 Apr 2023 14:56:49 -0500 Subject: [PATCH 0554/1077] Add `test_supported_features` and fix the problem introduced in #565 (#575) * Update test_switch.py * Update test_switch.py * test is now done. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix the test only. test is backwards compatible with the old method. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix _supported_to_attributes everything works now. * pre-commit fixes cannot fix the `function too complex` problem. * ignore test_switch.py in `pre-commit-config.yaml` * Add ignore C901 to test_supported_features * remove commented out code --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/switch.py | 31 +++---- tests/test_switch.py | 84 ++++++++++++++++++- 2 files changed, 100 insertions(+), 15 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 3eabf7ff..f933f068 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -155,12 +155,13 @@ from .const import ( ) _SUPPORT_OPTS = { - "brightness": SUPPORT_BRIGHTNESS, - "color_temp": SUPPORT_COLOR_TEMP, - "color": SUPPORT_COLOR, - "transition": SUPPORT_TRANSITION, + COLOR_MODE_BRIGHTNESS: SUPPORT_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP: SUPPORT_COLOR_TEMP, + CONST_COLOR: SUPPORT_COLOR, + ATTR_TRANSITION: SUPPORT_TRANSITION, } + VALID_COLOR_MODES = { COLOR_MODE_BRIGHTNESS: ATTR_BRIGHTNESS, COLOR_MODE_COLOR_TEMP: ATTR_COLOR_TEMP_KELVIN, @@ -642,16 +643,18 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]: def _supported_to_attributes(supported): supported_attributes = {} supports_colors = False - for mode, attr in VALID_COLOR_MODES.items(): - if mode not in supported: - continue - supported_attributes[attr] = True - if ( - not supports_colors - and mode != COLOR_MODE_BRIGHTNESS - and mode != COLOR_MODE_COLOR_TEMP - ): - supports_colors = True + for mode in supported: + attr = VALID_COLOR_MODES.get(mode) + if attr: + supported_attributes[attr] = True + if attr in COLOR_ATTRS: + supports_colors = True + # ATTR_SUPPORTED_FEATURES only + elif mode in _SUPPORT_OPTS: + supported_attributes[mode] = True + if CONST_COLOR in supported_attributes: + supports_colors = True + supported_attributes.pop(CONST_COLOR) return supported_attributes, supports_colors diff --git a/tests/test_switch.py b/tests/test_switch.py index 476e83cc..884b796b 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -3,9 +3,10 @@ import asyncio from copy import deepcopy import datetime +import itertools import logging from random import randint -from unittest.mock import patch +from unittest.mock import MagicMock, patch from homeassistant.components.adaptive_lighting.const import ( ADAPT_BRIGHTNESS_SWITCH, @@ -25,6 +26,7 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_TRANSITION, CONF_TURN_ON_LIGHTS, CONF_USE_DEFAULTS, + CONST_COLOR, DEFAULT_MAX_BRIGHTNESS, DEFAULT_NAME, DEFAULT_SLEEP_BRIGHTNESS, @@ -37,7 +39,10 @@ from homeassistant.components.adaptive_lighting.const import ( UNDO_UPDATE_LISTENER, ) from homeassistant.components.adaptive_lighting.switch import ( + _SUPPORT_OPTS, + VALID_COLOR_MODES, _attributes_have_changed, + _supported_features, color_difference_redmean, create_context, is_our_context, @@ -47,9 +52,13 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP_KELVIN, + ATTR_MAX_COLOR_TEMP_KELVIN, + ATTR_MIN_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR, + ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_XY_COLOR, + COLOR_MODE_BRIGHTNESS, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.light import SERVICE_TURN_OFF @@ -515,6 +524,79 @@ async def test_turn_on_off_listener_not_tracking_untracked_lights(hass): assert light not in switch.turn_on_off_listener.lights +def test_supported_features(hass): # noqa: C901 + """Test the supported features of a light.""" + + possible_legacy_features = {} + MAX_COMBINATIONS = 4 # maximum number of elements that can be combined + for i in range(1, min(MAX_COMBINATIONS, len(_SUPPORT_OPTS)) + 1): + for combination in itertools.combinations(_SUPPORT_OPTS.keys(), i): + key = "_".join(combination) + value = [v for k, v in _SUPPORT_OPTS.items() if k in combination] + possible_legacy_features[key] = value + + possible_color_modes = {} + for i in range(1, len(VALID_COLOR_MODES) + 1): + for combination in itertools.combinations(VALID_COLOR_MODES.keys(), i): + key = "_".join(combination) + value = [v for k, v in VALID_COLOR_MODES.items() if k in combination] + possible_color_modes[key] = value + + # create a mock HomeAssistant object + hass = MagicMock() + + # iterate over possible legacy features + for feature_key, feature_values in possible_legacy_features.items(): + # _LOGGER.debug(feature_values) + # set the attributes of the mock state object to the possible legacy feature values + state_attrs = {ATTR_SUPPORTED_FEATURES: sum(feature_values)} + hass.states.get.return_value.attributes = state_attrs + + # iterate over possible color modes + for mode_key, mode_values in possible_color_modes.items(): + # _LOGGER.debug(mode_values) + # set the attributes of the mock state object to the possible color mode values + state_attrs[ATTR_SUPPORTED_COLOR_MODES] = set(mode_values) + hass.states.get.return_value.attributes = state_attrs + + # Handle both the new and the old _supported_features. + result = _supported_features(hass, ENTITY_LIGHT) + supported, supports_colors = ( + result if isinstance(result, tuple) else (result, None) + ) + expected_supported = {} if supports_colors is not None else set() + for mode, attr in VALID_COLOR_MODES.items(): + if mode in mode_values: + if supports_colors is None: + expected_supported.add(mode) + else: + expected_supported[attr] = True + if supports_colors is True: + expected_supported[COLOR_MODE_BRIGHTNESS] = True + for opt, value in _SUPPORT_OPTS.items(): + if value in feature_values: + if supports_colors is None: + expected_supported.add(opt) + else: + if supports_colors is True: + expected_supported[COLOR_MODE_BRIGHTNESS] = True + if opt in VALID_COLOR_MODES: + expected_supported[VALID_COLOR_MODES[opt]] = True + elif opt != CONST_COLOR: + expected_supported[opt] = True + if ATTR_MIN_COLOR_TEMP_KELVIN in supported: + supported.pop(ATTR_MIN_COLOR_TEMP_KELVIN) + if ATTR_MAX_COLOR_TEMP_KELVIN in supported: + supported.pop(ATTR_MAX_COLOR_TEMP_KELVIN) + assert supported == expected_supported, ( + f"\nExpected supported: {expected_supported}\n" + f"Actual supported: {supported}\n" + f"feature_values: {feature_values}\n" + f"mode_values: {mode_values}\n" + f"supports_colors: {supports_colors}\n" + ) + + @pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_manual_control(hass): """Test the 'manual control' tracking.""" From 9caf64509349958a340f8cef9207fdb0ea35dbef Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 16:56:03 -0700 Subject: [PATCH 0555/1077] [pre-commit.ci] pre-commit autoupdate (#584) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.3.1 → v3.3.2](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.3.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 462e0dc2..1dbf741d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: hooks: - id: black - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.3.2 hooks: - id: pyupgrade args: ["--py39-plus"] From 29e54185e3218d0692cd7a3f3be7607f411cc71c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 12 May 2023 11:36:24 -0700 Subject: [PATCH 0556/1077] Add "See also" to README (#591) * Add "See also" section * Add date * chore(docs): update TOC * Add YouTube videos * Add components.cloud * verbose loggin --------- Co-authored-by: basnijholt --- .github/workflows/pytest.yaml | 3 +- README.md | 56 +++++++++++++++++++---------------- test_dependencies.py | 1 + 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 394acef3..4973849f 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -14,7 +14,7 @@ jobs: fail-fast: false matrix: python-version: ["3.10"] - core-version: ["2023.2.5", "2023.3.6", "2023.4.6", "dev"] + core-version: ["2023.2.5", "2023.3.6", "2023.4.6", "2023.5.2", "dev"] steps: - name: Check out code from GitHub uses: actions/checkout@v3 @@ -54,6 +54,7 @@ jobs: run: | cd core python3 -X dev -m pytest \ + -vvv \ -qq \ --timeout=9 \ --durations=10 \ diff --git a/README.md b/README.md index 9ba7cc03..d1895b91 100644 --- a/README.md +++ b/README.md @@ -41,27 +41,28 @@ The `adaptive_lighting.manual_control` event is fired when a light is marked as - - [:gear: Configuration](#gear-configuration) - - [:memo: Options](#memo-options) - - [:hammer_and_wrench: Services](#hammer_and_wrench-services) - - [`adaptive_lighting.apply`](#adaptive_lightingapply) - - [`adaptive_lighting.set_manual_control`](#adaptive_lightingset_manual_control) - - [`adaptive_lighting.change_switch_settings`](#adaptive_lightingchange_switch_settings) - - [:robot: Automation examples](#robot-automation-examples) +- [:gear: Configuration](#gear-configuration) + - [:memo: Options](#memo-options) + - [:hammer_and_wrench: Services](#hammer_and_wrench-services) + - [`adaptive_lighting.apply`](#adaptive_lightingapply) + - [`adaptive_lighting.set_manual_control`](#adaptive_lightingset_manual_control) + - [`adaptive_lighting.change_switch_settings`](#adaptive_lightingchange_switch_settings) +- [:robot: Automation examples](#robot-automation-examples) - [Additional Information](#additional-information) - [:sos: Troubleshooting](#sos-troubleshooting) - [:exclamation: Common Problems & Solutions](#exclamation-common-problems--solutions) - [:bulb: Lights Not Responding or Turning On by Themselves](#bulb-lights-not-responding-or-turning-on-by-themselves) - - [:signal_strength: WiFi Networks](#signal_strength-wifi-networks) - - [:spider_web: Zigbee, Z-Wave, and Other Mesh Networks](#spider_web-zigbee-z-wave-and-other-mesh-networks) + - [:signal_strength: WiFi Networks](#signal_strength-wifi-networks) + - [:spider_web: Zigbee, Z-Wave, and Other Mesh Networks](#spider_web-zigbee-z-wave-and-other-mesh-networks) - [:rainbow: Light Colors Not Matching](#rainbow-light-colors-not-matching) - [:bulb: Bulb-Specific Issues](#bulb-bulb-specific-issues) - - [:bar_chart: Graphs!](#bar_chart-graphs) - - [:sunny: Sun Position](#sunny-sun-position) - - [:thermometer: Color Temperature](#thermometer-color-temperature) - - [:high_brightness: Brightness](#high_brightness-brightness) - - [While using `transition_until_sleep: true`](#while-using-transition_until_sleep-true) - - [:busts_in_silhouette: Contributors](#busts_in_silhouette-contributors) +- [:bar_chart: Graphs!](#bar_chart-graphs) + - [:sunny: Sun Position](#sunny-sun-position) + - [:thermometer: Color Temperature](#thermometer-color-temperature) + - [:high_brightness: Brightness](#high_brightness-brightness) + - [While using `transition_until_sleep: true`](#while-using-transition_until_sleep-true) +- [:eyes: See also](#eyes-see-also) +- [:busts_in_silhouette: Contributors](#busts_in_silhouette-contributors) @@ -310,13 +311,13 @@ iphone_carly_wakeup: -# Additional Information +## Additional Information For more details on adding the integration and setting options, refer to the [documentation of the PR](https://deploy-preview-14877--home-assistant-docs.netlify.app/integrations/adaptive_lighting/) and [this video tutorial on Reddit](https://www.reddit.com/r/homeassistant/comments/jabhso/ha_has_it_before_apple_has_even_finished_it_i/). Adaptive Lighting was initially inspired by @claytonjn's [hass-circadian\_lighting](https://github.com/claytonjn/hass-circadian_lighting), but has since been entirely rewritten and expanded with new features. -# :sos: Troubleshooting +## :sos: Troubleshooting Encountering issues? Enable debug logging in your `configuration.yaml`: @@ -329,9 +330,9 @@ logger: After the issue occurs, create a new issue report with the log (`/config/home-assistant.log`). -## :exclamation: Common Problems & Solutions +### :exclamation: Common Problems & Solutions -### :bulb: Lights Not Responding or Turning On by Themselves +#### :bulb: Lights Not Responding or Turning On by Themselves Adaptive Lighting sends more commands to lights than a typical human user would. If your light control network is unhealthy, you may experience: @@ -353,7 +354,7 @@ For most Zigbee networks, **using groups is essential for optimal performance**. As a rule of thumb, if you always control lights together (e.g., bulbs in a ceiling fixture), they should be in a Zigbee group. Expose only the group (not individual bulbs) in Home Assistant Dashboards and external systems like Google Home or Apple HomeKit. -### :rainbow: Light Colors Not Matching +#### :rainbow: Light Colors Not Matching Bulbs from different manufacturers or models may have varying color temperature specifications. For instance, if you have two Adaptive Lighting configurations—one with only Philips Hue White Ambiance bulbs and another with a mix of Philips Hue White Ambiance and Sengled bulbs—the Philips Hue bulbs may appear to have different color temperatures despite having identical settings. @@ -362,7 +363,7 @@ To resolve this: 1. Include only bulbs of the same make and model in a single Adaptive Lighting configuration. 2. Rearrange bulbs so that different color temperatures are not visible simultaneously. -### :bulb: Bulb-Specific Issues +#### :bulb: Bulb-Specific Issues Certain bulbs may have issues with long light transition commands: @@ -372,18 +373,23 @@ Certain bulbs may have issues with long light transition commands: ## :bar_chart: Graphs! These graphs were generated using the values calculated by the Adaptive Lighting sensor/switch(es). -#### :sunny: Sun Position +### :sunny: Sun Position ![cl_percent|690x131](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/6/5/657ff98beb65a94598edeb4bdfd939095db1a22c.PNG) -#### :thermometer: Color Temperature +### :thermometer: Color Temperature ![cl_color_temp|690x129](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/9/59e84263cbecd8e428cb08777a0413672c48dfcd.PNG) -#### :high_brightness: Brightness +### :high_brightness: Brightness ![cl_brightness|690x130](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/8/58ebd994b62a8b1abfb3497a5288d923ff4e2330.PNG) -#### While using `transition_until_sleep: true` +### While using `transition_until_sleep: true` ![image](https://user-images.githubusercontent.com/2219836/228949675-f9699624-8abc-466c-bb04-250ce0f495b8.png) +## :eyes: See also + +- [*Sleep better with Adaptive Lighting in Home Assistant*](https://wartner.io/sleep-better-with-adaptive-lightning-in-home-assistant/) by Florian Wartner on 2023-02-23 (blog post 📜) +- [*Automatic smart light brightness and color based on the sun*](https://www.youtube.com/watch?v=Rg3zI1Oyk3c) by Home Automation Guy on 2022-08-31 (YouTube video 📺) +- [*Adaptive Lighting Blew My Mind in Home Assistant - How to set it up*](https://www.youtube.com/watch?v=c1cnccmgl3k) by Smart Home Junkie on 2022-06-26 (YouTube video 📺) ## :busts_in_silhouette: Contributors diff --git a/test_dependencies.py b/test_dependencies.py index a4dc0e89..58b4718f 100644 --- a/test_dependencies.py +++ b/test_dependencies.py @@ -25,6 +25,7 @@ required = [ "components.http", "components.stream", "components.conversation", + "components.cloud", ] to_install = [] for r in required: From 2033fcebe9d6edb0c792f0c7c505d21362629599 Mon Sep 17 00:00:00 2001 From: Mario Guggenberger Date: Sun, 21 May 2023 19:36:16 +0200 Subject: [PATCH 0557/1077] fix: lights unexpectedly turn back on after switch-off (#590) --- custom_components/adaptive_lighting/switch.py | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index f933f068..9ed3e0da 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1111,7 +1111,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context=self.create_context("interval"), ) - async def _adapt_light( + async def _adapt_light( # noqa: C901 self, light: str, transition: int | None = None, @@ -1200,9 +1200,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context=context, ) - if not self._separate_turn_on_commands: - await turn_on(service_data) - else: + async def turn_on_split(): # Could be a list of length 1 or 2 service_datas = _split_service_data( service_data, adapt_brightness, adapt_color @@ -1215,6 +1213,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): await asyncio.sleep(self._send_split_delay / 1000.0) await turn_on(service_datas[1]) + if not self._separate_turn_on_commands: + await turn_on(service_data) + else: + split_tasks = self.turn_on_off_listener.split_adaptation_tasks + if (previous_task := split_tasks.get(light)) is not None: + previous_task.cancel() + try: + split_tasks[light] = asyncio.ensure_future(turn_on_split()) + await split_tasks[light] + except asyncio.CancelledError: + _LOGGER.debug("Split adaptation of %s cancelled", light) + async def _update_attrs_and_maybe_adapt_lights( self, lights: list[str] | None = None, @@ -1685,6 +1695,8 @@ class TurnOnOffListener: self.last_state_change: dict[str, list[State]] = {} # Track last 'service_data' to 'light.turn_on' resulting from this integration self.last_service_data: dict[str, dict[str, Any]] = {} + # Track ongoing split adaptations to be able to cancel them + self.split_adaptation_tasks: dict[str, asyncio.Task] = {} # Track auto reset of manual_control self.auto_reset_manual_control_timers: dict[str, _AsyncSingleShotTimer] = {} @@ -1801,6 +1813,9 @@ class TurnOnOffListener: self.last_state_change.pop(light, None) self.last_service_data.pop(light, None) + if (task := self.split_adaptation_tasks.get(light)) is not None: + task.cancel() + async def turn_on_off_event_listener(self, event: Event) -> None: """Track 'light.turn_off' and 'light.turn_on' service calls.""" domain = event.data.get(ATTR_DOMAIN) From bf3b709ba62d57f70ff5d04c9416cb35b1bd7eb9 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 21 May 2023 13:35:45 -0700 Subject: [PATCH 0558/1077] docs: add protyposis as a contributor for code (#597) --- .all-contributorsrc | 9 +++++++++ README.md | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 6c825dad..33b875f2 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -440,6 +440,15 @@ "contributions": [ "code" ] + }, + { + "login": "protyposis", + "name": "Mario Guggenberger", + "avatar_url": "https://avatars.githubusercontent.com/u/189372?v=4", + "profile": "http://protyposis.net", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index d1895b91..1a0830b0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-47-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-48-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -429,6 +429,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + @@ -457,6 +458,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting + From 4683f083f69952af7e9c26f9064305a62eddfe1a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 22 May 2023 12:16:40 -0700 Subject: [PATCH 0559/1077] Update version to 1.12.0 in manifest.json (#596) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 2dec4c83..85aa0c08 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.11.0" + "version": "1.12.0" } From 2c8a45604ad070b4f67edb10c07a70b6b01318ff Mon Sep 17 00:00:00 2001 From: Mario Guggenberger Date: Fri, 9 Jun 2023 01:01:19 +0200 Subject: [PATCH 0560/1077] feat: brightness prioritization (#598) * feat: optional brightness prioritization * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Remove config flag and change default split order * Fix test * Fix edge case * Add tests * Backwards compatiblity * Fix another edge case --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- custom_components/adaptive_lighting/switch.py | 122 +++++++++++------- tests/test_switch.py | 113 +++++++++++++++- 2 files changed, 188 insertions(+), 47 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 9ed3e0da..32d3661a 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -202,6 +202,8 @@ BRIGHTNESS_ATTRS = { # Keep a short domain version for the context instances (which can only be 36 chars) _DOMAIN_SHORT = "al" +ServiceData = dict[str, Any] + def _int_to_base36(num: int) -> str: """ @@ -280,25 +282,39 @@ def is_our_context(context: Context | None) -> bool: return f":{_DOMAIN_SHORT}:" in context.id -def _split_service_data(service_data, adapt_brightness, adapt_color): - """Split service_data into two dictionaries (for color and brightness).""" - transition = service_data.get(ATTR_TRANSITION) - if transition is not None: - # Split the transition over both commands - service_data[ATTR_TRANSITION] /= 2 - service_datas = [] - if adapt_color: - service_data_color = service_data.copy() - service_data_color.pop(ATTR_BRIGHTNESS, None) - service_datas.append(service_data_color) - if adapt_brightness: - service_data_brightness = service_data.copy() - service_data_brightness.pop(ATTR_RGB_COLOR, None) - service_data_brightness.pop(ATTR_COLOR_TEMP_KELVIN, None) - service_datas.append(service_data_brightness) +def _prepare_service_calls(service_data: ServiceData, split=False) -> list[ServiceData]: + """Prepares the service data for service calls. - if not service_datas: # neither adapt_brightness nor adapt_color + Processes the service_data according to the config flags, optionally splitting + it into multiple data items for the separate adaptation of different attributes. + Returns a list of service_datas that indicates the required service calls. If + no splitting is necessary, the output is a list with a single item. + """ + if not split: return [service_data] + + common_attrs = {ATTR_ENTITY_ID} + common_data = {k: service_data[k] for k in common_attrs if k in service_data} + + attributes_split_sequence = [BRIGHTNESS_ATTRS, COLOR_ATTRS] + service_datas = [] + + for attributes in attributes_split_sequence: + split_data = { + attribute: service_data[attribute] + for attribute in attributes + if service_data.get(attribute) + } + if split_data: + service_datas.append(common_data | split_data) + + # Distribute the transition duration across all service calls + if service_datas and (transition := service_data.get(ATTR_TRANSITION)) is not None: + transition = service_data[ATTR_TRANSITION] / len(service_datas) + + for service_data in service_datas: + service_data[ATTR_TRANSITION] = transition + return service_datas @@ -1185,7 +1201,23 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): else: self.turn_on_off_listener.last_service_data[light] = service_data - async def turn_on(service_data): + service_datas = _prepare_service_calls( + service_data, self._separate_turn_on_commands + ) + await self._make_cancellable_adaptation_calls(service_datas, context, light) + + async def _make_adaptation_calls( + self, service_datas: list[ServiceData], context: Context + ): + """Executes a sequence of adaptation service calls for the given service datas.""" + for i, service_data in enumerate(service_datas): + is_first_call = i == 0 + + # Sleep _between_ multiple service calls, but not before the first or a single one. + if not is_first_call: + await asyncio.sleep(service_data.get(ATTR_TRANSITION, 0)) + await asyncio.sleep(self._send_split_delay / 1000.0) + _LOGGER.debug( "%s: Scheduling 'light.turn_on' with the following 'service_data': %s" " with context.id='%s'", @@ -1200,30 +1232,27 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context=context, ) - async def turn_on_split(): - # Could be a list of length 1 or 2 - service_datas = _split_service_data( - service_data, adapt_brightness, adapt_color - ) - await turn_on(service_datas[0]) - if len(service_datas) == 2: - transition = service_datas[0].get(ATTR_TRANSITION) - if transition is not None: - await asyncio.sleep(transition) - await asyncio.sleep(self._send_split_delay / 1000.0) - await turn_on(service_datas[1]) + async def _make_cancellable_adaptation_calls( + self, service_datas: list[ServiceData], context: Context, light_id: str + ): + """Executes a cancellable sequence of adaptation service calls for the given service datas. - if not self._separate_turn_on_commands: - await turn_on(service_data) - else: - split_tasks = self.turn_on_off_listener.split_adaptation_tasks - if (previous_task := split_tasks.get(light)) is not None: - previous_task.cancel() - try: - split_tasks[light] = asyncio.ensure_future(turn_on_split()) - await split_tasks[light] - except asyncio.CancelledError: - _LOGGER.debug("Split adaptation of %s cancelled", light) + Wraps the sequence of service calls in a task that can be cancelled from elsewhere, e.g., + to cancel an ongoing adaptation when a light is turned off. + """ + # Prevent overlap of multiple adaptation sequences + self.turn_on_off_listener.cancel_ongoing_adaptation_calls(light_id) + + # Execute adaptation calls within a task + try: + task = self.turn_on_off_listener.adaptation_tasks[ + light_id + ] = asyncio.ensure_future( + self._make_adaptation_calls(service_datas, context) + ) + await task + except asyncio.CancelledError: + _LOGGER.debug("Ongoing adaptation of %s cancelled", light_id) async def _update_attrs_and_maybe_adapt_lights( self, @@ -1696,7 +1725,7 @@ class TurnOnOffListener: # Track last 'service_data' to 'light.turn_on' resulting from this integration self.last_service_data: dict[str, dict[str, Any]] = {} # Track ongoing split adaptations to be able to cancel them - self.split_adaptation_tasks: dict[str, asyncio.Task] = {} + self.adaptation_tasks: dict[str, asyncio.Task] = {} # Track auto reset of manual_control self.auto_reset_manual_control_timers: dict[str, _AsyncSingleShotTimer] = {} @@ -1802,6 +1831,11 @@ class TurnOnOffListener: self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset) + def cancel_ongoing_adaptation_calls(self, light_id: str): + """Cancels an ongoing sequence of adaptation service calls for a specific light entity.""" + if (previous_task := self.adaptation_tasks.get(light_id)) is not None: + previous_task.cancel() + def reset(self, *lights, reset_manual_control=True) -> None: """Reset the 'manual_control' status of the lights.""" for light in lights: @@ -1812,9 +1846,7 @@ class TurnOnOffListener: timer.cancel() self.last_state_change.pop(light, None) self.last_service_data.pop(light, None) - - if (task := self.split_adaptation_tasks.get(light)) is not None: - task.cancel() + self.cancel_ongoing_adaptation_calls(light) async def turn_on_off_event_listener(self, event: Event) -> None: """Track 'light.turn_off' and 'light.turn_on' service calls.""" diff --git a/tests/test_switch.py b/tests/test_switch.py index 884b796b..0b0826dc 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -42,6 +42,7 @@ from homeassistant.components.adaptive_lighting.switch import ( _SUPPORT_OPTS, VALID_COLOR_MODES, _attributes_have_changed, + _prepare_service_calls, _supported_features, color_difference_redmean, create_context, @@ -777,8 +778,8 @@ async def test_auto_reset_manual_control(hass): assert ( switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] > 0 ) - await asyncio.sleep(0.3) # Should be enough time for auto reset await update() + await asyncio.sleep(0.3) # Should be enough time for auto reset assert not manual_control[light.entity_id], (light, manual_control) assert ( light.entity_id not in switch.extra_state_attributes["autoreset_time_remaining"] @@ -791,8 +792,8 @@ async def test_auto_reset_manual_control(hass): await asyncio.sleep(0.05) # Less than 0.1 assert manual_control[light.entity_id] - await asyncio.sleep(0.3) # Wait the auto reset time await update() + await asyncio.sleep(0.3) # Wait the auto reset time assert not manual_control[light.entity_id] @@ -1385,3 +1386,111 @@ async def test_change_switch_settings_service(hass): # testing with "configuration" should revert back to 2500 await change_switch_settings(**{CONF_USE_DEFAULTS: "configuration"}) assert switch._sun_light_settings.min_color_temp == 2500 + + +@pytest.mark.parametrize( + "service_data_input,split,service_data_expected", + [ + ( + {"foo": 1, ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, + False, + [{"foo": 1, ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}], + ), + ( + {"foo": 1}, + True, + [], + ), + ( + {ATTR_BRIGHTNESS: 10}, + True, + [{ATTR_BRIGHTNESS: 10}], + ), + ( + {ATTR_COLOR_TEMP_KELVIN: 3500}, + True, + [{ATTR_COLOR_TEMP_KELVIN: 3500}], + ), + ( + {ATTR_ENTITY_ID: "foo", ATTR_BRIGHTNESS: 10}, + True, + [{ATTR_ENTITY_ID: "foo", ATTR_BRIGHTNESS: 10}], + ), + ( + {ATTR_BRIGHTNESS: 10, ATTR_COLOR_TEMP_KELVIN: 3500}, + True, + [{ATTR_BRIGHTNESS: 10}, {ATTR_COLOR_TEMP_KELVIN: 3500}], + ), + ( + {ATTR_BRIGHTNESS: 10, ATTR_COLOR_TEMP_KELVIN: 3500, ATTR_TRANSITION: 2}, + True, + [ + {ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 1}, + {ATTR_COLOR_TEMP_KELVIN: 3500, ATTR_TRANSITION: 1}, + ], + ), + ( + {ATTR_TRANSITION: 1}, + True, + [], + ), + ], + ids=[ + "pass through when splitting is disabled", + "remove irrelevant attributes", + "brightness only yields one service call", + "color only yields one service call", + "include entity ID", + "brightness and color are split into two with brightness first", + "transition time is distributed among service calls", + "ignore transition time without service calls", + ], +) +async def test_prepare_service_calls(service_data_input, split, service_data_expected): + """Test the preparation of service calls, e.g., splitting.""" + assert _prepare_service_calls(service_data_input, split) == service_data_expected + + +@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) +async def test_cancellable_service_calls_task(hass): + """Test the creation and execution of the task that wraps adaptation service calls.""" + (light, *_) = await setup_lights(hass) + _, switch = await setup_switch(hass, {CONF_SEPARATE_TURN_ON_COMMANDS: True}) + context = switch.create_context("test") + + assert switch.turn_on_off_listener.adaptation_tasks.get(light.entity_id) is None + + await switch._make_cancellable_adaptation_calls( + [ + { + ATTR_BRIGHTNESS: 10, + ATTR_COLOR_TEMP_KELVIN: 10, + ATTR_ENTITY_ID: light.entity_id, + } + ], + context, + light.entity_id, + ) + + task = switch.turn_on_off_listener.adaptation_tasks.get(light.entity_id) + assert task is not None + assert task.done() + + +@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) +async def test_service_calls_task_cancellation(hass): + """Tests if the task that wraps ongoing adaptation service calls gets cancelled.""" + _, switch = await setup_switch(hass, {}) + entity_id = "test_id" + + task = asyncio.ensure_future(asyncio.sleep(1)) + switch.turn_on_off_listener.adaptation_tasks[entity_id] = task + + switch.turn_on_off_listener.cancel_ongoing_adaptation_calls(entity_id) + + try: + await task + except asyncio.CancelledError: + pass + + assert task.cancelled() From 90b8d6089d74b401c8708d5828489fd2f16c5f4a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 8 Jun 2023 16:09:57 -0700 Subject: [PATCH 0561/1077] Bump to 1.13.0 in manifest.json (#610) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 85aa0c08..a5e514cb 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.12.0" + "version": "1.13.0" } From 12dae4b6a65b4705c5ec32bd05c97053bc4150ac Mon Sep 17 00:00:00 2001 From: Mario Guggenberger Date: Sun, 11 Jun 2023 23:12:37 +0200 Subject: [PATCH 0562/1077] fix: 2023.6 compatibility (#607) * fix: 2023.6.0 compatibility * ci: test with Python 3.11 against 2023.6.0b4 * Set 2023.06.0 * Bump to 2023.06.1 --------- Co-authored-by: Bas Nijholt --- .github/workflows/pytest.yaml | 15 +++++++++++++-- custom_components/adaptive_lighting/switch.py | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 4973849f..1b51f257 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -13,8 +13,19 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10"] - core-version: ["2023.2.5", "2023.3.6", "2023.4.6", "2023.5.2", "dev"] + include: + - python-version: "3.10" + core-version: "2023.2.5" + - python-version: "3.10" + core-version: "2023.3.6" + - python-version: "3.10" + core-version: "2023.4.6" + - python-version: "3.10" + core-version: "2023.5.2" + - python-version: "3.11" + core-version: "2023.6.1" + - python-version: "3.11" + core-version: "dev" steps: - name: Check out code from GitHub uses: actions/checkout@v3 diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 32d3661a..4d36fce1 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -502,7 +502,7 @@ async def async_setup_entry( data[config_entry.entry_id][SWITCH_DOMAIN] = switch async_add_entities( - [switch, sleep_mode_switch, adapt_color_switch, adapt_brightness_switch], + [sleep_mode_switch, adapt_color_switch, adapt_brightness_switch, switch], update_before_add=True, ) From 6283158ff730644f76d8007c70edbf0ffd6846fe Mon Sep 17 00:00:00 2001 From: Mario Guggenberger Date: Mon, 12 Jun 2023 00:33:07 +0200 Subject: [PATCH 0563/1077] VS Code Dev Container (dev & test environment) (#605) * build: dev container Add a VS Code Dev Container from the blueprint at https://github.com/ludeeus/integration_blueprint/tree/bceaae212fefefae84d9529cde5cb6f4b60cc865 * build: dev container test setup Add support for unit testing in the dev container environment with debugging and code coverage. * ci: adjust to dev container test restructuring * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * ci: fix coverage collection * build: add dummy light to HA config * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * build: update dev container to Python 3.11 (for HA 2023.6) * Add VS Code tasks * Use pre-commit hooks for linting * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Unpin HA version --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .devcontainer.json | 42 ++++++++ .gitattributes | 1 + .github/workflows/pytest.yaml | 10 +- .gitignore | 8 ++ .ruff.toml | 48 +++++++++ .vscode/tasks.json | 17 +++ Dockerfile | 14 ++- config/configuration.yaml | 19 ++++ requirements.txt | 10 ++ scripts/develop | 20 ++++ scripts/lint | 7 ++ scripts/setup | 8 ++ setup.cfg | 4 + tests/README.md | 2 +- tests/conftest.py | 19 ++++ tests/test_config_flow.py | 10 +- tests/test_init.py | 9 +- tests/test_switch.py | 192 ++++++++++++++++++---------------- 18 files changed, 329 insertions(+), 111 deletions(-) create mode 100644 .devcontainer.json create mode 100644 .gitattributes create mode 100644 .ruff.toml create mode 100644 .vscode/tasks.json create mode 100644 config/configuration.yaml create mode 100644 requirements.txt create mode 100644 scripts/develop create mode 100644 scripts/lint create mode 100644 scripts/setup create mode 100644 tests/conftest.py diff --git a/.devcontainer.json b/.devcontainer.json new file mode 100644 index 00000000..d842d1dd --- /dev/null +++ b/.devcontainer.json @@ -0,0 +1,42 @@ +{ + "name": "basnijholt/adaptive_lighting", + "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.11-bullseye", + "postCreateCommand": "scripts/setup", + "forwardPorts": [ + 8123 + ], + "portsAttributes": { + "8123": { + "label": "Home Assistant", + "onAutoForward": "notify" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "github.vscode-pull-request-github", + "ryanluker.vscode-coverage-gutters", + "ms-python.vscode-pylance" + ], + "settings": { + "files.eol": "\n", + "editor.tabSize": 4, + "python.pythonPath": "/usr/bin/python3", + "python.analysis.autoSearchPaths": false, + "python.linting.pylintEnabled": true, + "python.linting.enabled": true, + "python.formatting.provider": "black", + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true + } + } + }, + "remoteUser": "vscode", + "features": { + "ghcr.io/devcontainers/features/rust:1": {} + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6313b56c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 1b51f257..b5089ded 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -50,11 +50,6 @@ jobs: run: | cd core - # Link homeassitant.components.adaptive_lighting - cd homeassistant/components - ln -fs ../../../custom_components/adaptive_lighting adaptive_lighting - cd - - # Link adaptive_lighting tests cd tests/components/ ln -fs ../../../tests adaptive_lighting @@ -63,14 +58,17 @@ jobs: - name: Run pytest timeout-minutes: 60 run: | + export PYTHONPATH=${PYTHONPATH}:${PWD} cd core python3 -X dev -m pytest \ -vvv \ -qq \ --timeout=9 \ --durations=10 \ - --cov="homeassistant" \ + --cov="custom_components.adaptive_lighting" \ --cov-report=xml \ -o console_output_style=count \ -p no:sugar \ tests/components/adaptive_lighting + env: + HA_CLONE: true diff --git a/.gitignore b/.gitignore index b6e47617..eeef9022 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,11 @@ dmypy.json # Pyre type checker .pyre/ + +# IDEs +.vscode +.idea + +# Home Assistant configuration +config/* +!config/configuration.yaml diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 00000000..260b1883 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,48 @@ +# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml + +target-version = "py310" + +select = [ + "B007", # Loop control variable {name} not used within loop body + "B014", # Exception handler with duplicate exception + "C", # complexity + "D", # docstrings + "E", # pycodestyle + "F", # pyflakes/autoflake + "ICN001", # import concentions; {name} should be imported as {asname} + "PGH004", # Use specific rule codes when using noqa + "PLC0414", # Useless import alias. Import alias does not rename original package. + "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass + "SIM117", # Merge with-statements that use the same scope + "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() + "SIM201", # Use {left} != {right} instead of not {left} == {right} + "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} + "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. + "SIM401", # Use get from dict with default instead of an if block + "T20", # flake8-print + "TRY004", # Prefer TypeError exception for invalid type + "RUF006", # Store a reference to the return value of asyncio.create_task + "UP", # pyupgrade + "W", # pycodestyle +] + +ignore = [ + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D213", # Multi-line docstring summary should start at the second line + "D404", # First word of the docstring should not be This + "D406", # Section name should end with a newline + "D407", # Section name underlining + "D411", # Missing blank line before section + "E501", # line too long + "E731", # do not assign a lambda expression, use a def +] + +[flake8-pytest-style] +fixture-parentheses = false + +[pyupgrade] +keep-runtime-typing = true + +[mccabe] +max-complexity = 25 diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..cd2130b2 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Run Home Assistant on port 8123", + "type": "shell", + "command": "scripts/develop", + "problemMatcher": [] + }, + { + "label": "Lint (run pre-commit hooks)", + "type": "shell", + "command": "scripts/lint", + "problemMatcher": [] + } + ] +} diff --git a/Dockerfile b/Dockerfile index 45522da8..4baac59a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,12 +23,11 @@ RUN pip3 install -r /core/requirements.txt --use-pep517 && \ pip3 install -r /core/requirements_test.txt --use-pep517 && \ pip3 install -e /core/ --use-pep517 -# Clone the Adaptive Lighting repository -RUN git clone https://github.com/basnijholt/adaptive-lighting.git /app +# Copy the Adaptive Lighting repository +COPY . /app/ # Setup symlinks in core -RUN ln -s /app/custom_components/adaptive_lighting /core/homeassistant/components/adaptive_lighting && \ - ln -s /app/tests /core/tests/components/adaptive_lighting && \ +RUN ln -s /app/tests /core/tests/components/adaptive_lighting && \ # For test_dependencies.py ln -s /core /app/core @@ -37,6 +36,11 @@ RUN pip3 install $(python3 /app/test_dependencies.py) --use-pep517 WORKDIR /core +# Make 'custom_components/adaptive_lighting' imports available to tests +ENV PYTHONPATH="${PYTHONPATH}:/app" +# Enable testing against HA clone (instead of pytest_homeassistant_custom_component) +ENV HA_CLONE=true + ENTRYPOINT ["python3", \ # Enable Python development mode "-X", "dev", \ @@ -49,7 +53,7 @@ ENTRYPOINT ["python3", \ # Print the 10 slowest tests "--durations=10", \ # Measure code coverage for the 'homeassistant' package - "--cov='homeassistant'", \ + "--cov=custom_components.adaptive_lighting", \ # Generate an XML report of the code coverage "--cov-report=xml", \ # Generate an HTML report of the code coverage diff --git a/config/configuration.yaml b/config/configuration.yaml new file mode 100644 index 00000000..b9d7d45b --- /dev/null +++ b/config/configuration.yaml @@ -0,0 +1,19 @@ +# https://www.home-assistant.io/integrations/default_config/ +default_config: + +# https://www.home-assistant.io/integrations/logger/ +logger: + default: info + logs: + custom_components.adaptive_lighting: debug + +light: + - platform: template + lights: + dummylight: + friendly_name: "Dummy Light" + turn_on: + turn_off: + set_level: + set_temperature: + supports_transition_template: "{{ true }}" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..c560a858 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +colorlog==6.7.0 +pip>=21.0,<23.2 +ruff==0.0.265 +pre-commit + +# Install HA and test dependencies (pytest, coverage) +# To pin the dev container to a specific HA version, set this dependency +# to the adequate version (add `==`) and rebuild the dev container. +# See https://github.com/MatthewFlamm/pytest-homeassistant-custom-component/releases for version mappings. +pytest-homeassistant-custom-component diff --git a/scripts/develop b/scripts/develop new file mode 100644 index 00000000..e7ce50cd --- /dev/null +++ b/scripts/develop @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +# Create config dir if not present +if [[ ! -d "${PWD}/config" ]]; then + mkdir -p "${PWD}/config" + hass --config "${PWD}/config" --script ensure_config +fi + +# Set the path to custom_components +## This let's us have the structure we want /custom_components/adaptive_lighting +## while at the same time have Home Assistant configuration inside /config +## without resulting to symlinks. +export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" + +# Start Home Assistant +hass --config "${PWD}/config" --debug diff --git a/scripts/lint b/scripts/lint new file mode 100644 index 00000000..55a1f485 --- /dev/null +++ b/scripts/lint @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +pre-commit run --all-files diff --git a/scripts/setup b/scripts/setup new file mode 100644 index 00000000..0688d70d --- /dev/null +++ b/scripts/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +python3 -m pip install --requirement requirements.txt +pre-commit install-hooks diff --git a/setup.cfg b/setup.cfg index 284326f5..a82d84b8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,3 +9,7 @@ max-complexity = 18 select = B,C,E,F,W,T4,B9 per-file-ignores = code_example.py: E402, E501 + +[tool:pytest] +testpaths = tests +asyncio_mode = auto diff --git a/tests/README.md b/tests/README.md index 1c472541..dd125d97 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,7 +1,7 @@ # Developer notes for the tests directory To run the tests, check out the [CI configuration](../.github/workflows/pytest.yml) to see how they are executed in the CI pipeline. -Alternatively, you can use the provided Docker image to run the tests locally. +Alternatively, you can use the provided Docker image to run the tests locally or run them with VS Code directly in the dev container. To run the tests using the Docker image, navigate to the `adaptive-lighting` repo folder and execute the following command: diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..14f7e644 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,19 @@ +"""Fixtures for testing.""" +import os +import sys + +import pytest + +# Tests in the dev enviromentment use the pytest_homeassistant_custom_component instead of +# a cloned HA core repo for a simple and clean structure. To still test against a HA core +# clone (e.g. the dev branch for which no pytest_homeassistant_custom_component exists +# because HA does not publish dev snapshot packages), set the HA_CLONE env variable. +if "HA_CLONE" in os.environ: + # Rewire the testing package to the cloned test modules. See the test `Dockerfile` + # for setup details. + sys.modules["pytest_homeassistant_custom_component"] = __import__("tests") + + +@pytest.fixture(autouse=True) +def auto_enable_custom_integrations(enable_custom_integrations): + yield diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 53901cf9..22384143 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,6 +1,10 @@ """Test Adaptive Lighting config flow.""" from homeassistant import data_entry_flow -from homeassistant.components.adaptive_lighting.const import ( +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_NAME +from pytest_homeassistant_custom_component.common import MockConfigEntry + +from custom_components.adaptive_lighting.const import ( CONF_SUNRISE_TIME, CONF_SUNSET_TIME, DEFAULT_NAME, @@ -8,10 +12,6 @@ from homeassistant.components.adaptive_lighting.const import ( NONE_STR, VALIDATION_TUPLES, ) -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_NAME - -from tests.common import MockConfigEntry DEFAULT_DATA = {key: default for key, default, _ in VALIDATION_TUPLES} diff --git a/tests/test_init.py b/tests/test_init.py index b6f48e0b..bb0cf977 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,14 +1,11 @@ """Tests for Adaptive Lighting integration.""" -from homeassistant.components import adaptive_lighting -from homeassistant.components.adaptive_lighting.const import ( - DEFAULT_NAME, - UNDO_UPDATE_LISTENER, -) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_NAME from homeassistant.setup import async_setup_component +from pytest_homeassistant_custom_component.common import MockConfigEntry -from tests.common import MockConfigEntry +from custom_components import adaptive_lighting +from custom_components.adaptive_lighting.const import DEFAULT_NAME, UNDO_UPDATE_LISTENER async def test_setup_with_config(hass): diff --git a/tests/test_switch.py b/tests/test_switch.py index 0b0826dc..0e46f458 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -8,7 +8,49 @@ import logging from random import randint from unittest.mock import MagicMock, patch -from homeassistant.components.adaptive_lighting.const import ( +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_TEMP_KELVIN, + ATTR_MAX_COLOR_TEMP_KELVIN, + ATTR_MIN_COLOR_TEMP_KELVIN, + ATTR_RGB_COLOR, + ATTR_SUPPORTED_COLOR_MODES, + ATTR_TRANSITION, + ATTR_XY_COLOR, + COLOR_MODE_BRIGHTNESS, +) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.light import SERVICE_TURN_OFF +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +import homeassistant.config as config_util +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + ATTR_AREA_ID, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_LIGHTS, + CONF_NAME, + EVENT_STATE_CHANGED, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import Context, HomeAssistant, State +from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity_platform import async_get_platforms +from homeassistant.setup import async_setup_component +from homeassistant.util.color import color_temperature_mired_to_kelvin +import homeassistant.util.dt as dt_util +import pytest +from pytest_homeassistant_custom_component.common import ( + MockConfigEntry, + mock_area_registry, +) +import ulid_transform +import voluptuous.error + +from custom_components.adaptive_lighting.const import ( ADAPT_BRIGHTNESS_SWITCH, ADAPT_COLOR_SWITCH, ATTR_TURN_ON_OFF_LISTENER, @@ -38,7 +80,7 @@ from homeassistant.components.adaptive_lighting.const import ( SLEEP_MODE_SWITCH, UNDO_UPDATE_LISTENER, ) -from homeassistant.components.adaptive_lighting.switch import ( +from custom_components.adaptive_lighting.switch import ( _SUPPORT_OPTS, VALID_COLOR_MODES, _attributes_have_changed, @@ -48,47 +90,6 @@ from homeassistant.components.adaptive_lighting.switch import ( create_context, is_our_context, ) -from homeassistant.components.demo.light import DemoLight -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT, - ATTR_COLOR_TEMP_KELVIN, - ATTR_MAX_COLOR_TEMP_KELVIN, - ATTR_MIN_COLOR_TEMP_KELVIN, - ATTR_RGB_COLOR, - ATTR_SUPPORTED_COLOR_MODES, - ATTR_TRANSITION, - ATTR_XY_COLOR, - COLOR_MODE_BRIGHTNESS, -) -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.light import SERVICE_TURN_OFF -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -import homeassistant.config as config_util -from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ( - ATTR_AREA_ID, - ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, - CONF_LIGHTS, - CONF_NAME, - CONF_PLATFORM, - EVENT_STATE_CHANGED, - SERVICE_TURN_ON, - STATE_OFF, - STATE_ON, -) -from homeassistant.core import Context, State -from homeassistant.helpers import entity_registry -from homeassistant.setup import async_setup_component -from homeassistant.util.color import color_temperature_mired_to_kelvin -import homeassistant.util.dt as dt_util -import pytest -import ulid_transform -import voluptuous.error - -from tests.common import MockConfigEntry, mock_area_registry -from tests.components.demo.test_light import ENTITY_LIGHT _LOGGER = logging.getLogger(__name__) @@ -113,6 +114,7 @@ LAT_LONG_TZS = [ (32.87336, -117.22743, "US/Pacific"), ] +ENTITY_LIGHT = "light.bed_light" _SWITCH_FMT = f"{SWITCH_DOMAIN}.{DOMAIN}" ENTITY_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}" ENTITY_SLEEP_MODE_SWITCH = f"{_SWITCH_FMT}_sleep_mode_{DEFAULT_NAME}" @@ -149,50 +151,60 @@ async def setup_switch(hass, extra_data): return entry, switch -async def setup_lights(hass): - """Set up 3 light entities using the 'test' platform.""" +async def setup_lights(hass: HomeAssistant): + """Set up 3 light entities using the 'template' platform.""" await async_setup_component( - hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": "demo"}} + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + { + "platform": "template", + "lights": { + "bed_light": { + "friendly_name": "Bed Light", + "unique_id": "light_1", + "turn_on": None, + "turn_off": None, + "set_level": None, + "set_temperature": None, + "set_color": None, + }, + "ceiling_lights": { + "friendly_name": "Ceiling Lights", + "unique_id": "light_2", + "turn_on": None, + "turn_off": None, + "set_level": None, + "set_temperature": None, + "set_color": None, + }, + "kitchen_lights": { + "friendly_name": "Kitchen Lights", + "unique_id": "light_3", + "turn_on": None, + "turn_off": None, + "set_level": None, + "set_temperature": None, + "set_color": None, + }, + }, + }, + ] + }, ) - await hass.async_block_till_done() - platform = getattr(hass.components, "test.light") - while platform.ENTITIES: - # Make sure it is empty - platform.ENTITIES.pop() - lights = [ - DemoLight( - unique_id="light_1", - name="Bed Light", - state=True, - ct=200, - ), - DemoLight( - unique_id="light_2", - name="Ceiling Lights", - state=True, - ct=380, - ), - DemoLight( - unique_id="light_3", - name="Kitchen Lights", - state=False, - hs_color=(345, 75), - ct=240, - ), - ] + await hass.async_block_till_done() + platform = async_get_platforms(hass, "template") + lights = list(platform[0].entities.values()) + + await lights[0].async_turn_on() + await lights[1].async_turn_on() + for light in lights: - light.hass = hass - slug = light.name.lower().replace(" ", "_") - light.entity_id = f"light.{slug}" - await light.async_update_ha_state() + light._attr_brightness = 255 + light._attr_color_temp = 250 - platform.ENTITIES.extend(lights) - platform.init() - assert await async_setup_component( - hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {CONF_PLATFORM: "test"}} - ) - await hass.async_block_till_done() assert all(hass.states.get(light.entity_id) is not None for light in lights) return lights @@ -646,11 +658,12 @@ async def test_manual_control(hass): await update() def increased_brightness(): - return (light._brightness + 100) % 255 + return (light._attr_brightness + 100) % 255 def increased_color_temp(): return max( - (light._ct + 100) % light.max_color_temp_kelvin, light.min_color_temp_kelvin + (light._attr_color_temp + 100) % light.max_color_temp_kelvin, + light.min_color_temp_kelvin, ) # Nothing is manually controlled @@ -703,7 +716,9 @@ async def test_manual_control(hass): color_temperature_mired_to_kelvin(mired_range[0]), ) ptp_kelvin = kelvin_range[1] - kelvin_range[0] - await turn_light(True, color_temp_kelvin=(light._ct + 100) % ptp_kelvin) + await turn_light( + True, color_temp_kelvin=(light._attr_color_temp + 100) % ptp_kelvin + ) assert manual_control[ENTITY_LIGHT] await switch.adapt_brightness_switch.async_turn_on() # turn on again @@ -805,11 +820,12 @@ async def test_apply_service(hass): assert entity_id not in switch._lights def increased_brightness(): - return (light._brightness + 100) % 255 + return (light._attr_brightness + 100) % 255 def increased_color_temp(): return max( - (light._ct + 100) % light.max_color_temp_kelvin, light.min_color_temp_kelvin + (light._attr_color_temp + 100) % light.max_color_temp_kelvin, + light.min_color_temp_kelvin, ) async def change_light(): @@ -1306,7 +1322,7 @@ async def test_area(hass): area_registry.async_create("test_area") entity = entity_registry.async_get(hass).async_get_or_create( - LIGHT_DOMAIN, "demo", light.unique_id + LIGHT_DOMAIN, "template", light.unique_id ) entity = entity_registry.async_get(hass).async_update_entity( entity.entity_id, area_id="test_area" From 68d500bd12bdc4877e1596405690c112bc4f5123 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 12 Jun 2023 16:13:29 -0700 Subject: [PATCH 0564/1077] Update manifest.json (#613) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index a5e514cb..4bbdd6f3 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.13.0" + "version": "1.14.0" } From 42edd5eb8f673e399fa15fb4a644b9d49ea2a88e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 17:43:18 -0700 Subject: [PATCH 0565/1077] [pre-commit.ci] pre-commit autoupdate (#587) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.3.2 → v3.6.0](https://github.com/asottile/pyupgrade/compare/v3.3.2...v3.6.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1dbf741d..d8c823c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: hooks: - id: black - repo: https://github.com/asottile/pyupgrade - rev: v3.3.2 + rev: v3.6.0 hooks: - id: pyupgrade args: ["--py39-plus"] From 36c656b7958f6a9a637b50800534749038e146ee Mon Sep 17 00:00:00 2001 From: Mario Guggenberger Date: Wed, 14 Jun 2023 18:39:47 +0200 Subject: [PATCH 0566/1077] docs: Ikea light config recommendations (#614) --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1a0830b0..bdfcd0e6 100644 --- a/README.md +++ b/README.md @@ -365,10 +365,14 @@ To resolve this: #### :bulb: Bulb-Specific Issues -Certain bulbs may have issues with long light transition commands: +These lights are known to exhibit disadvantageous behaviour due to firmware bugs, insufficient functionality, or hardware limitations: -- [Sengled Z01-A19NAE26](https://www.zigbee2mqtt.io/devices/Z01-A19NAE26.html#sengled-z01-a19nae26): If Adaptive Lighting sends a long transition time (like the default 45 seconds), and the bulb is turned off during that time, it may turn back on after approximately 10 seconds to continue the transition command. Since the bulb is turning itself on, there will be no obvious trigger in Home Assistant or other logs indicating the cause of the light turning on. To fix this, set a much shorter transition time, such as 1 second. -- Additionally, these bulbs may perform poorly in enclosed "dome" style ceiling lights, particularly when hot. While most LEDs (even non-smart ones) state in the fine print that they do not support working in enclosed fixtures, in practice, more expensive bulbs like Philips Hue generally perform better. To resolve this issue, move the problematic bulbs to open-air fixtures. +- [Sengled Z01-A19NAE26](https://www.zigbee2mqtt.io/devices/Z01-A19NAE26.html#sengled-z01-a19nae26) + - Unexpected turn-ons: If Adaptive Lighting sends a long transition time (like the default 45 seconds), and the bulb is turned off during that time, it may turn back on after approximately 10 seconds to continue the transition command. Since the bulb is turning itself on, there will be no obvious trigger in Home Assistant or other logs indicating the cause of the light turning on. To fix this, set a much shorter `transition` time, such as 1 second. + - Heat sensitivity: Additionally, these bulbs may perform poorly in enclosed "dome" style ceiling lights, particularly when hot. While most LEDs (even non-smart ones) state in the fine print that they do not support working in enclosed fixtures, in practice, more expensive bulbs like Philips Hue generally perform better. To resolve this issue, move the problematic bulbs to open-air fixtures. +- Ikea Tradfri bulbs/drivers (and related Ikea smart light products) + - Unsupported simultaneous transition of brightness and color: When receiving such a command, they switch the brightness instantly and only transition the color. To get smooth transitions of both brightness and color, enable `separate_turn_on_commands`. + - Unresponsiveness during color transitions: No other commands are processed during an ongoing color transition, e.g., turn-off commands are ignored and lights stay on despite being reported as off to Home Assistant. The default config with long transitions thus results in long periods of unresponsiveness. To work around this, disable transitions by setting `transition` to `0`, and increase the adaptation frequency by setting `interval` to a short time, e.g., `15` seconds, to retain the impression of smooth continuous adaptations. Keeping the `initial_transition` is recommended for a smooth fade-in (lights are usually not turned off momentarily after being turned on, in which case a short period of unresponsiveness is tolerable). ## :bar_chart: Graphs! These graphs were generated using the values calculated by the Adaptive Lighting sensor/switch(es). From 72e140e293785410815084d7e8e03249caed4d6c Mon Sep 17 00:00:00 2001 From: Mario Guggenberger Date: Mon, 3 Jul 2023 00:26:07 +0200 Subject: [PATCH 0567/1077] feat: skip redundant adaptation commands (#615) * feat: skip redundant adaptation commands * update README * style * Remove "experimental" --------- Co-authored-by: Bas Nijholt Co-authored-by: Bas Nijholt --- README.md | 63 ++-- .../adaptive_lighting/adaptation_utils.py | 164 +++++++++ custom_components/adaptive_lighting/const.py | 16 + .../adaptive_lighting/strings.json | 3 +- custom_components/adaptive_lighting/switch.py | 143 +++----- .../adaptive_lighting/translations/de.json | 3 +- .../adaptive_lighting/translations/en.json | 3 +- tests/test_adaptation_utils.py | 336 ++++++++++++++++++ tests/test_switch.py | 87 +---- 9 files changed, 617 insertions(+), 201 deletions(-) create mode 100644 custom_components/adaptive_lighting/adaptation_utils.py create mode 100644 tests/test_adaptation_utils.py diff --git a/README.md b/README.md index bdfcd0e6..e0ba67fc 100644 --- a/README.md +++ b/README.md @@ -91,37 +91,38 @@ The YAML and frontend configuration methods support all of the options listed be -| Variable name | Description | Default | Type | -|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------| -| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | -| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | -| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | -| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | -| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | -| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | -| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | -| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | -| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | -| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | -| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | -| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | -| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | -| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | -| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | -| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | -| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | -| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` | -| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | -| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` | -| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | -| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | -| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` | -| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | -| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | -| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | -| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| Variable name | Description | Default | Type | +| :------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------- | :----------------------------------- | +| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | +| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | +| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | +| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | +| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py new file mode 100644 index 00000000..0cfbc7dd --- /dev/null +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -0,0 +1,164 @@ +"""Utility functions for adaptation commands.""" +from collections.abc import AsyncGenerator +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_BRIGHTNESS_STEP, + ATTR_BRIGHTNESS_STEP_PCT, + ATTR_COLOR_NAME, + ATTR_COLOR_TEMP_KELVIN, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_TRANSITION, + ATTR_XY_COLOR, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, HomeAssistant, State + +COLOR_ATTRS = { # Should ATTR_PROFILE be in here? + ATTR_COLOR_NAME, + ATTR_COLOR_TEMP_KELVIN, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_XY_COLOR, +} + +BRIGHTNESS_ATTRS = { + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_BRIGHTNESS_STEP, + ATTR_BRIGHTNESS_STEP_PCT, +} + +ServiceData = dict[str, Any] + + +def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]: + """Splits the service data by the adapted attributes, i.e., into separate data + items for brightness and color. + """ + + common_attrs = {ATTR_ENTITY_ID} + common_data = {k: service_data[k] for k in common_attrs if k in service_data} + + attributes_split_sequence = [BRIGHTNESS_ATTRS, COLOR_ATTRS] + service_datas = [] + + for attributes in attributes_split_sequence: + split_data = { + attribute: service_data[attribute] + for attribute in attributes + if service_data.get(attribute) + } + if split_data: + service_datas.append(common_data | split_data) + + # Distribute the transition duration across all service calls + if service_datas and (transition := service_data.get(ATTR_TRANSITION)) is not None: + transition = service_data[ATTR_TRANSITION] / len(service_datas) + + for service_data in service_datas: + service_data[ATTR_TRANSITION] = transition + + return service_datas + + +def _filter_service_data(service_data: ServiceData, state: State | None) -> ServiceData: + """Filter service data by removing attributes that already equal the given state. + + Removes all attributes from service call data whose values are already present + in the target entity's state.""" + + if not state: + return service_data + + filtered_service_data = { + k: service_data[k] + for k in service_data.keys() + if k not in state.attributes or service_data[k] != state.attributes[k] + } + + return filtered_service_data + + +def _has_relevant_service_data_attributes(service_data: ServiceData) -> bool: + """Determines whether the service data justifies an adaptation service call. + + A service call is not justified for data which does not contain any entries that + change relevant attributes of an adapting entity, e.g., brightness or color.""" + common_attrs = {ATTR_ENTITY_ID, ATTR_TRANSITION} + relevant_attrs = set(service_data) - common_attrs + + return bool(relevant_attrs) + + +async def _create_service_call_data_iterator( + hass: HomeAssistant, + service_datas: list[ServiceData], + filter_by_state: bool = False, +) -> AsyncGenerator[ServiceData, None]: + """Enumerates and filters a list of service datas on the fly. + + If filtering is enabled, every service data is filtered by the current state of + the related entity and only returned if it contains relevant data that justifies + a service call. + The main advantage of this generator over a list is that it applies the filter + at the time when the service data is read instead of up front. This gives greater + flexibility because entity states can change while the items are iterated. + """ + + for service_data in service_datas: + if filter_by_state and (entity_id := service_data.get(ATTR_ENTITY_ID)): + current_entity_state = hass.states.get(entity_id) + + # Filter data to remove attributes that equal the current state + if current_entity_state: + service_data = _filter_service_data(service_data, current_entity_state) + + # Emit service data if it still contains relevant attributes (else try next) + if _has_relevant_service_data_attributes(service_data): + yield service_data + else: + yield service_data + + +@dataclass +class AdaptationData: + """Holds all data required to execute an adaptation.""" + + entity_id: str + context: Context + sleep_time: float + service_call_datas: AsyncGenerator[ServiceData, None] + + async def next_service_call_data(self) -> ServiceData | None: + """Return data for the next service call, or none if no more data exists.""" + return await anext(self.service_call_datas, None) + + +def prepare_adaptation_data( + hass: HomeAssistant, + entity_id: str, + context: Context, + transition: float | None, + split_delay: float, + service_data: ServiceData, + split: bool, + filter_by_state: bool, +) -> AdaptationData: + service_datas = ( + [service_data] if not split else _split_service_call_data(service_data) + ) + + sleep_time = ( + transition / max(1, len(service_datas)) if transition is not None else 0 + ) + split_delay + + service_data_iterator = _create_service_call_data_iterator( + hass, service_datas, filter_by_state + ) + + return AdaptationData(entity_id, context, sleep_time, service_data_iterator) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 6d36ae2e..3aaf9dda 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -176,6 +176,17 @@ DOCS[CONF_AUTORESET_CONTROL] = ( "Set to 0 to disable. ⏲️" ) +CONF_SKIP_REDUNDANT_COMMANDS, DEFAULT_SKIP_REDUNDANT_COMMANDS = ( + "skip_redundant_commands", + False, +) +DOCS[CONF_SKIP_REDUNDANT_COMMANDS] = ( + "Skip sending adaptation commands whose target state already " + "equals the light's known state. Minimizes network traffic and improves the " + "adaptation responsivity in some situations. " + "Disable if physical light states get out of sync with HA's recorded state." +) + SLEEP_MODE_SWITCH = "sleep_mode_switch" ADAPT_COLOR_SWITCH = "adapt_color_switch" ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch" @@ -271,6 +282,11 @@ VALIDATION_TUPLES = [ DEFAULT_AUTORESET_CONTROL, int_between(0, 365 * 24 * 60 * 60), # 1 year max ), + ( + CONF_SKIP_REDUNDANT_COMMANDS, + DEFAULT_SKIP_REDUNDANT_COMMANDS, + bool, + ), ] CONST_COLOR = "color" diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 54af5e11..00126027 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -47,7 +47,8 @@ "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", - "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️" + "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "skip_redundant_commands": "Experimental: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state." } } }, diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 4d36fce1..a8707bcf 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -17,10 +17,6 @@ from typing import Any, Literal import astral from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT, - ATTR_BRIGHTNESS_STEP, - ATTR_BRIGHTNESS_STEP_PCT, - ATTR_COLOR_NAME, ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, ATTR_MAX_COLOR_TEMP_KELVIN, @@ -96,6 +92,12 @@ import homeassistant.util.dt as dt_util import ulid_transform import voluptuous as vol +from .adaptation_utils import ( + BRIGHTNESS_ATTRS, + COLOR_ATTRS, + AdaptationData, + prepare_adaptation_data, +) from .const import ( ADAPT_BRIGHTNESS_SWITCH, ADAPT_COLOR_SWITCH, @@ -121,6 +123,7 @@ from .const import ( CONF_PREFER_RGB_COLOR, CONF_SEND_SPLIT_DELAY, CONF_SEPARATE_TURN_ON_COMMANDS, + CONF_SKIP_REDUNDANT_COMMANDS, CONF_SLEEP_BRIGHTNESS, CONF_SLEEP_COLOR_TEMP, CONF_SLEEP_RGB_COLOR, @@ -184,26 +187,10 @@ BRIGHTNESS_CHANGE = 25 # ≈10% of total range COLOR_TEMP_CHANGE = 100 # ≈3% of total range (2000-6500) RGB_REDMEAN_CHANGE = 80 # ≈10% of total range -COLOR_ATTRS = { # Should ATTR_PROFILE be in here? - ATTR_COLOR_NAME, - ATTR_COLOR_TEMP_KELVIN, - ATTR_HS_COLOR, - ATTR_RGB_COLOR, - ATTR_XY_COLOR, -} - -BRIGHTNESS_ATTRS = { - ATTR_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT, - ATTR_BRIGHTNESS_STEP, - ATTR_BRIGHTNESS_STEP_PCT, -} # Keep a short domain version for the context instances (which can only be 36 chars) _DOMAIN_SHORT = "al" -ServiceData = dict[str, Any] - def _int_to_base36(num: int) -> str: """ @@ -282,42 +269,6 @@ def is_our_context(context: Context | None) -> bool: return f":{_DOMAIN_SHORT}:" in context.id -def _prepare_service_calls(service_data: ServiceData, split=False) -> list[ServiceData]: - """Prepares the service data for service calls. - - Processes the service_data according to the config flags, optionally splitting - it into multiple data items for the separate adaptation of different attributes. - Returns a list of service_datas that indicates the required service calls. If - no splitting is necessary, the output is a list with a single item. - """ - if not split: - return [service_data] - - common_attrs = {ATTR_ENTITY_ID} - common_data = {k: service_data[k] for k in common_attrs if k in service_data} - - attributes_split_sequence = [BRIGHTNESS_ATTRS, COLOR_ATTRS] - service_datas = [] - - for attributes in attributes_split_sequence: - split_data = { - attribute: service_data[attribute] - for attribute in attributes - if service_data.get(attribute) - } - if split_data: - service_datas.append(common_data | split_data) - - # Distribute the transition duration across all service calls - if service_datas and (transition := service_data.get(ATTR_TRANSITION)) is not None: - transition = service_data[ATTR_TRANSITION] / len(service_datas) - - for service_data in service_datas: - service_data[ATTR_TRANSITION] = transition - - return service_datas - - def _get_switches_with_lights( hass: HomeAssistant, lights: list[str] ) -> list[AdaptiveSwitch]: @@ -530,7 +481,6 @@ async def async_setup_entry( data[ATTR_ADAPT_BRIGHTNESS], data[ATTR_ADAPT_COLOR], data[CONF_PREFER_RGB_COLOR], - force=True, context=switch.create_context( "service", parent=service_call.context ), @@ -937,6 +887,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) self._take_over_control = True self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL] + self._skip_redundant_commands = data[CONF_SKIP_REDUNDANT_COMMANDS] self._expand_light_groups() # updates manual control timers _loc = get_astral_location(self.hass) if isinstance(_loc, tuple): @@ -1134,7 +1085,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): adapt_brightness: bool | None = None, adapt_color: bool | None = None, prefer_rgb_color: bool | None = None, - force: bool = False, context: Context | None = None, ) -> None: lock = self._locks.get(light) @@ -1160,7 +1110,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): features, supports_colors = _supported_features(self.hass, light) # Check transition == 0 to fix #378 - if ATTR_TRANSITION in features and transition > 0: + use_transition = ATTR_TRANSITION in features and transition > 0 + if use_transition: service_data[ATTR_TRANSITION] = transition if ATTR_BRIGHTNESS in features and adapt_brightness: brightness = round(255 * self._settings["brightness_pct"] / 100) @@ -1188,52 +1139,59 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context = context or self.create_context("adapt_lights") - # See #80. Doesn't check if transitions differ but it does the job. - last_service_data = self.turn_on_off_listener.last_service_data - if not force and last_service_data.get(light) == service_data: - _LOGGER.debug( - "%s: Cancelling adapt to light %s, there's no new values to set (context.id='%s')", - self._name, - light, - context.id, - ) - return - else: - self.turn_on_off_listener.last_service_data[light] = service_data + self.turn_on_off_listener.last_service_data[light] = service_data - service_datas = _prepare_service_calls( - service_data, self._separate_turn_on_commands + data = prepare_adaptation_data( + self.hass, + light, + context, + transition if use_transition else 0, + self._send_split_delay / 1000.0, + service_data, + split=self._separate_turn_on_commands, + filter_by_state=self._skip_redundant_commands, ) - await self._make_cancellable_adaptation_calls(service_datas, context, light) - async def _make_adaptation_calls( - self, service_datas: list[ServiceData], context: Context - ): + await self._execute_cancellable_adaptation_calls(data) + + async def _execute_adaptation_calls(self, data: AdaptationData): """Executes a sequence of adaptation service calls for the given service datas.""" - for i, service_data in enumerate(service_datas): - is_first_call = i == 0 - # Sleep _between_ multiple service calls, but not before the first or a single one. + index = 0 + while True: + is_first_call = index == 0 + index += 1 + + # Sleep between multiple service calls. if not is_first_call: - await asyncio.sleep(service_data.get(ATTR_TRANSITION, 0)) - await asyncio.sleep(self._send_split_delay / 1000.0) + await asyncio.sleep(data.sleep_time) + + # Instead of directly iterating the generator in the while-loop, we get + # the next item here after the sleep to make sure it incorporates state + # changes which happened during the sleep. + service_data = await data.next_service_call_data() + + if not service_data: + # All service datas processed + break _LOGGER.debug( "%s: Scheduling 'light.turn_on' with the following 'service_data': %s" " with context.id='%s'", self._name, service_data, - context.id, + data.context.id, ) await self.hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, service_data, - context=context, + context=data.context, ) - async def _make_cancellable_adaptation_calls( - self, service_datas: list[ServiceData], context: Context, light_id: str + async def _execute_cancellable_adaptation_calls( + self, + data: AdaptationData, ): """Executes a cancellable sequence of adaptation service calls for the given service datas. @@ -1241,18 +1199,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): to cancel an ongoing adaptation when a light is turned off. """ # Prevent overlap of multiple adaptation sequences - self.turn_on_off_listener.cancel_ongoing_adaptation_calls(light_id) + self.turn_on_off_listener.cancel_ongoing_adaptation_calls(data.entity_id) # Execute adaptation calls within a task try: - task = self.turn_on_off_listener.adaptation_tasks[ - light_id - ] = asyncio.ensure_future( - self._make_adaptation_calls(service_datas, context) - ) + task = asyncio.ensure_future(self._execute_adaptation_calls(data)) + self.turn_on_off_listener.adaptation_tasks[data.entity_id] = task await task except asyncio.CancelledError: - _LOGGER.debug("Ongoing adaptation of %s cancelled", light_id) + _LOGGER.debug("Ongoing adaptation of %s cancelled", data.entity_id) async def _update_attrs_and_maybe_adapt_lights( self, @@ -1358,7 +1313,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): else: _fire_manual_control_event(self, light, context) else: - await self._adapt_light(light, transition, force=force, context=context) + await self._adapt_light(light, transition, context=context) async def _sleep_mode_switch_state_event(self, event: Event) -> None: if not match_switch_state_event(event, (STATE_ON, STATE_OFF)): diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json index cc427138..79eb0df8 100644 --- a/custom_components/adaptive_lighting/translations/de.json +++ b/custom_components/adaptive_lighting/translations/de.json @@ -45,7 +45,8 @@ "take_over_control": "take_over_control, wenn irgendetwas während ein Licht an ist außer Adaptive Lighting den Service 'light.turn_on' aufruft, stoppe die Anpassung des Lichtes (oder des Schalters) bis dieser wieder von off -> on geschaltet wird.", "detect_non_ha_changes": "detect_non_ha_changes, entdeckt alle Änderungen über 10% am Licht (auch außerhalb von HA gemacht), 'take_over_control' muss aktiviert sein (ruft 'homeassistant.update_entity' jede 'interval' auf!)", "transition": "transition, Wechselzeit in Sekunden", - "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden." + "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.", + "skip_redundant_commands": "Keine Adaptierungsbefehle senden, deren erwünschter Status schon dem bekanntes Status von Lichtern entspricht. Minimiert die Netzwerkbelastung und verbessert die Adaptierung in manchen Situationen. Deaktiviert lassen falls der pysikalische Status der Lichter und der erkannte Status in HA nicht synchron bleiben." } } }, diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 38fb7b0e..a930a8ce 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -48,7 +48,8 @@ "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", - "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️" + "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "skip_redundant_commands": "Experimental: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state." } } }, diff --git a/tests/test_adaptation_utils.py b/tests/test_adaptation_utils.py new file mode 100644 index 00000000..b1cfcb5b --- /dev/null +++ b/tests/test_adaptation_utils.py @@ -0,0 +1,336 @@ +"""Tests for Adaptive Lighting utils.""" + +from unittest.mock import Mock + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP_KELVIN, + ATTR_TRANSITION, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_ON +from homeassistant.core import Context, State +import pytest + +from custom_components.adaptive_lighting.adaptation_utils import ( + ServiceData, + _create_service_call_data_iterator, + _filter_service_data, + _has_relevant_service_data_attributes, + _split_service_call_data, + prepare_adaptation_data, +) + + +@pytest.mark.parametrize( + "input_data,expected_data_list", + [ + ( + {"foo": 1}, + [], + ), + ( + {ATTR_BRIGHTNESS: 10}, + [{ATTR_BRIGHTNESS: 10}], + ), + ( + {ATTR_COLOR_TEMP_KELVIN: 3500}, + [{ATTR_COLOR_TEMP_KELVIN: 3500}], + ), + ( + {ATTR_ENTITY_ID: "foo", ATTR_BRIGHTNESS: 10}, + [{ATTR_ENTITY_ID: "foo", ATTR_BRIGHTNESS: 10}], + ), + ( + {ATTR_BRIGHTNESS: 10, ATTR_COLOR_TEMP_KELVIN: 3500}, + [{ATTR_BRIGHTNESS: 10}, {ATTR_COLOR_TEMP_KELVIN: 3500}], + ), + ( + {ATTR_BRIGHTNESS: 10, ATTR_COLOR_TEMP_KELVIN: 3500, ATTR_TRANSITION: 2}, + [ + {ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 1}, + {ATTR_COLOR_TEMP_KELVIN: 3500, ATTR_TRANSITION: 1}, + ], + ), + ( + {ATTR_TRANSITION: 1}, + [], + ), + ], + ids=[ + "remove irrelevant attributes", + "brightness only yields one service call", + "color only yields one service call", + "include entity ID", + "brightness and color are split into two with brightness first", + "transition time is distributed among service calls", + "ignore transition time without service calls", + ], +) +async def test_split_service_call_data(input_data, expected_data_list): + """Test splitting of service call data.""" + assert _split_service_call_data(input_data) == expected_data_list + + +@pytest.mark.parametrize( + "service_data,state,service_data_expected", + [ + ( + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, + None, + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, + ), + ( + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, + State("light.test", STATE_ON), + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, + ), + ( + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, + State("light.test", STATE_ON, {ATTR_BRIGHTNESS: 10}), + {ATTR_ENTITY_ID: "light.test", ATTR_TRANSITION: 2}, + ), + ( + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, + State("light.test", STATE_ON, {ATTR_BRIGHTNESS: 11}), + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, + ), + ], + ids=[ + "pass all attributes on missing state", + "pass all attributes on empty state", + "remove attributes whose values equal the state", + "keep attributes whose values differ from the state", + ], +) +async def test_filter_service_data( + service_data: ServiceData, state: State | None, service_data_expected: ServiceData +): + """Test filtering of service data.""" + assert _filter_service_data(service_data, state) == service_data_expected + + +@pytest.mark.parametrize( + "service_data,expected_relevant", + [ + ( + {ATTR_ENTITY_ID: "light.test"}, + False, + ), + ( + {ATTR_TRANSITION: 2}, + False, + ), + ( + {ATTR_BRIGHTNESS: 10}, + True, + ), + ( + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, + True, + ), + ], +) +async def test_has_relevant_service_data_attributes( + service_data: ServiceData, expected_relevant: bool +): + """Test the determination of relevancy of service data""" + assert _has_relevant_service_data_attributes(service_data) == expected_relevant + + +@pytest.mark.parametrize( + "service_datas,filter_by_state,service_datas_expected", + [ + ( + [{ATTR_ENTITY_ID: "light.test"}], + False, + [{ATTR_ENTITY_ID: "light.test"}], + ), + ( + [{ATTR_ENTITY_ID: "light.test"}, {ATTR_ENTITY_ID: "light.test2"}], + False, + [{ATTR_ENTITY_ID: "light.test"}, {ATTR_ENTITY_ID: "light.test2"}], + ), + ( + [{ATTR_ENTITY_ID: "light.test"}], + True, + [], + ), + ( + [{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10}], + True, + [], + ), + ( + [{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 11}], + True, + [{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 11}], + ), + ( + [ + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 11}, + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 22}, + ], + True, + [ + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 11}, + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 22}, + ], + ), + ( + [ + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10}, + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 22}, + ], + True, + [ + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 22}, + ], + ), + ], + ids=[ + "single item passed through without filtering", + "two items passed through without filtering", + "filter removes item without relevant attributes", + "filter removes item with relevant attribute that equals the state", + "filter keeps item with relevant attribute that is different from state", + "filter keeps two items with relevant attributes that are different from state", + "filter removes item that equals state and keeps items that differs from state", + ], +) +async def test_create_service_call_data_iterator( + service_datas: list[ServiceData], + filter_by_state: bool, + service_datas_expected: list[ServiceData], + hass_states_mock, +): + """Test the generator function for correct enumeration and filtering.""" + + generated_service_datas = [ + data + async for data in _create_service_call_data_iterator( + hass_states_mock, service_datas, filter_by_state + ) + ] + + assert generated_service_datas == service_datas_expected + assert ( + hass_states_mock.states.get.call_count == 0 + if not filter_by_state + else len(service_datas) + ) + + +@pytest.mark.parametrize( + "service_data,split,filter_by_state,service_datas_expected,sleep_time_expected", + [ + ( + { + ATTR_ENTITY_ID: "light.test", + ATTR_BRIGHTNESS: 10, + ATTR_COLOR_TEMP_KELVIN: 4000, + }, + False, + False, + [ + { + ATTR_ENTITY_ID: "light.test", + ATTR_BRIGHTNESS: 10, + ATTR_COLOR_TEMP_KELVIN: 4000, + } + ], + 1.2, + ), + ( + { + ATTR_ENTITY_ID: "light.test", + ATTR_BRIGHTNESS: 10, + ATTR_COLOR_TEMP_KELVIN: 4000, + }, + True, + False, + [ + { + ATTR_ENTITY_ID: "light.test", + ATTR_BRIGHTNESS: 10, + }, + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 4000, + }, + ], + 0.7, + ), + ( + { + ATTR_ENTITY_ID: "light.test", + ATTR_BRIGHTNESS: 10, + ATTR_COLOR_TEMP_KELVIN: 4000, + }, + False, + True, + [ + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 4000, + } + ], + 1.2, + ), + ( + { + ATTR_ENTITY_ID: "light.test", + ATTR_BRIGHTNESS: 10, + ATTR_COLOR_TEMP_KELVIN: 4000, + }, + True, + True, + [ + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 4000, + } + ], + 0.7, + ), + ], + ids=[ + "service data passed through", + "service data split", + "service data filtered", + "service data split and filtered", + ], +) +async def test_prepare_adaptation_data( + hass_states_mock, + service_data, + split, + filter_by_state, + service_datas_expected, + sleep_time_expected, +): + """Test creation of correct service data objects.""" + data = prepare_adaptation_data( + hass_states_mock, + "test.entity", + Context(id="test-id"), + 1, + 0.2, + service_data, + split, + filter_by_state, + ) + + generated_service_datas = [item async for item in data.service_call_datas] + + assert data.entity_id == "test.entity" + assert data.context.id == "test-id" + assert data.sleep_time == sleep_time_expected + assert generated_service_datas == service_datas_expected + + +@pytest.fixture(name="hass_states_mock") +def fixture_hass_states_mock(): + """Mocks a HA state machine which returns a mock state.""" + hass = Mock() + hass.states.get.return_value = Mock(attributes={ATTR_BRIGHTNESS: 10}) + return hass diff --git a/tests/test_switch.py b/tests/test_switch.py index 0e46f458..bb5d1c49 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -50,6 +50,10 @@ from pytest_homeassistant_custom_component.common import ( import ulid_transform import voluptuous.error +from custom_components.adaptive_lighting.adaptation_utils import ( + AdaptationData, + _create_service_call_data_iterator, +) from custom_components.adaptive_lighting.const import ( ADAPT_BRIGHTNESS_SWITCH, ADAPT_COLOR_SWITCH, @@ -84,7 +88,6 @@ from custom_components.adaptive_lighting.switch import ( _SUPPORT_OPTS, VALID_COLOR_MODES, _attributes_have_changed, - _prepare_service_calls, _supported_features, color_difference_redmean, create_context, @@ -1404,69 +1407,6 @@ async def test_change_switch_settings_service(hass): assert switch._sun_light_settings.min_color_temp == 2500 -@pytest.mark.parametrize( - "service_data_input,split,service_data_expected", - [ - ( - {"foo": 1, ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, - False, - [{"foo": 1, ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}], - ), - ( - {"foo": 1}, - True, - [], - ), - ( - {ATTR_BRIGHTNESS: 10}, - True, - [{ATTR_BRIGHTNESS: 10}], - ), - ( - {ATTR_COLOR_TEMP_KELVIN: 3500}, - True, - [{ATTR_COLOR_TEMP_KELVIN: 3500}], - ), - ( - {ATTR_ENTITY_ID: "foo", ATTR_BRIGHTNESS: 10}, - True, - [{ATTR_ENTITY_ID: "foo", ATTR_BRIGHTNESS: 10}], - ), - ( - {ATTR_BRIGHTNESS: 10, ATTR_COLOR_TEMP_KELVIN: 3500}, - True, - [{ATTR_BRIGHTNESS: 10}, {ATTR_COLOR_TEMP_KELVIN: 3500}], - ), - ( - {ATTR_BRIGHTNESS: 10, ATTR_COLOR_TEMP_KELVIN: 3500, ATTR_TRANSITION: 2}, - True, - [ - {ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 1}, - {ATTR_COLOR_TEMP_KELVIN: 3500, ATTR_TRANSITION: 1}, - ], - ), - ( - {ATTR_TRANSITION: 1}, - True, - [], - ), - ], - ids=[ - "pass through when splitting is disabled", - "remove irrelevant attributes", - "brightness only yields one service call", - "color only yields one service call", - "include entity ID", - "brightness and color are split into two with brightness first", - "transition time is distributed among service calls", - "ignore transition time without service calls", - ], -) -async def test_prepare_service_calls(service_data_input, split, service_data_expected): - """Test the preparation of service calls, e.g., splitting.""" - assert _prepare_service_calls(service_data_input, split) == service_data_expected - - @pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_cancellable_service_calls_task(hass): """Test the creation and execution of the task that wraps adaptation service calls.""" @@ -1476,17 +1416,18 @@ async def test_cancellable_service_calls_task(hass): assert switch.turn_on_off_listener.adaptation_tasks.get(light.entity_id) is None - await switch._make_cancellable_adaptation_calls( - [ - { - ATTR_BRIGHTNESS: 10, - ATTR_COLOR_TEMP_KELVIN: 10, - ATTR_ENTITY_ID: light.entity_id, - } - ], - context, + service_data = { + ATTR_BRIGHTNESS: 10, + ATTR_COLOR_TEMP_KELVIN: 10, + ATTR_ENTITY_ID: light.entity_id, + } + adaptation_data = AdaptationData( light.entity_id, + context, + 0, + _create_service_call_data_iterator(hass, [service_data]), ) + await switch._execute_cancellable_adaptation_calls(adaptation_data) task = switch.turn_on_off_listener.adaptation_tasks.get(light.entity_id) assert task is not None From d16a9a5751bea85ee5ee897863916816361456e3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 2 Jul 2023 15:32:44 -0700 Subject: [PATCH 0568/1077] [pre-commit.ci] pre-commit autoupdate (#620) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/asottile/pyupgrade: v3.6.0 → v3.7.0](https://github.com/asottile/pyupgrade/compare/v3.6.0...v3.7.0) * Update README.md, strings.json, and services.yaml --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt Co-authored-by: github-actions[bot] --- .pre-commit-config.yaml | 2 +- README.md | 64 +++++++++---------- .../adaptive_lighting/strings.json | 2 +- .../adaptive_lighting/translations/en.json | 2 +- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d8c823c4..8c35e268 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: hooks: - id: black - repo: https://github.com/asottile/pyupgrade - rev: v3.6.0 + rev: v3.7.0 hooks: - id: pyupgrade args: ["--py39-plus"] diff --git a/README.md b/README.md index e0ba67fc..20a21dad 100644 --- a/README.md +++ b/README.md @@ -91,38 +91,38 @@ The YAML and frontend configuration methods support all of the options listed be -| Variable name | Description | Default | Type | -| :------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------- | :----------------------------------- | -| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | -| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | -| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | -| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | -| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | -| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | -| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | -| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | -| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | -| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | -| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | -| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | -| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | -| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | -| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | -| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | -| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | -| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` | -| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | -| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` | -| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | -| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | -| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` | -| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | -| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | -| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | -| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | -| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | +| Variable name | Description | Default | Type | +|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------| +| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | +| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | +| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | +| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | +| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 00126027..7ad84741 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -48,7 +48,7 @@ "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", - "skip_redundant_commands": "Experimental: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state." + "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state." } } }, diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index a930a8ce..e6d1f9a9 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -49,7 +49,7 @@ "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", - "skip_redundant_commands": "Experimental: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state." + "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state." } } }, From ed80bd78298e5860d382485275c99f762eeb3f14 Mon Sep 17 00:00:00 2001 From: Mario Guggenberger Date: Wed, 19 Jul 2023 09:10:21 +0200 Subject: [PATCH 0569/1077] feat: service call adaptation (#628) * feat: service call adaptation * feat: toggle-on service call adaptation * feat: prefer service call transition --- .../adaptive_lighting/__init__.py | 3 +- .../adaptive_lighting/adaptation_utils.py | 2 + .../adaptive_lighting/hass_utils.py | 64 ++++ custom_components/adaptive_lighting/switch.py | 315 +++++++++++++++--- tests/test_hass_utils.py | 81 +++++ tests/test_switch.py | 212 +++++++++++- 6 files changed, 610 insertions(+), 67 deletions(-) create mode 100644 custom_components/adaptive_lighting/hass_utils.py create mode 100644 tests/test_hass_utils.py diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 07a0a758..f985e8c5 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -89,8 +89,7 @@ async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool: if len(data) == 1 and ATTR_TURN_ON_OFF_LISTENER in data: # no more config_entries turn_on_off_listener = data.pop(ATTR_TURN_ON_OFF_LISTENER) - turn_on_off_listener.remove_listener() - turn_on_off_listener.remove_listener2() + turn_on_off_listener.disable() if not data: hass.data.pop(DOMAIN) diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py index 0cfbc7dd..599805a6 100644 --- a/custom_components/adaptive_lighting/adaptation_utils.py +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -133,6 +133,7 @@ class AdaptationData: context: Context sleep_time: float service_call_datas: AsyncGenerator[ServiceData, None] + initial_sleep: bool = False async def next_service_call_data(self) -> ServiceData | None: """Return data for the next service call, or none if no more data exists.""" @@ -149,6 +150,7 @@ def prepare_adaptation_data( split: bool, filter_by_state: bool, ) -> AdaptationData: + "Prepares a data object carrying all data required to execute an adaptation." service_datas = ( [service_data] if not split else _split_service_call_data(service_data) ) diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py new file mode 100644 index 00000000..5a195bcc --- /dev/null +++ b/custom_components/adaptive_lighting/hass_utils.py @@ -0,0 +1,64 @@ +"""Utility functions for HA core.""" +from collections.abc import Awaitable +from typing import Callable + +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.util.read_only_dict import ReadOnlyDict + +from .adaptation_utils import ServiceData + + +def setup_service_call_interceptor( + hass: HomeAssistant, + domain: str, + service: str, + intercept_func: Callable[[ServiceCall, ServiceData], Awaitable[None] | None], +) -> Callable[[], None]: + """Inject a function into a registered service call to preprocess service data. + + The injected interceptor function receives the service call and a writeable data dictionary + (the data of the service call is read-only) before the service call is executed.""" + try: + # HACK: Access protected attribute of HA service registry. + # This is necessary to replace a registered service handler with our + # proxy handler to intercept calls. + registered_services = ( + hass.services._services # pylint: disable=protected-access + ) + except AttributeError as error: + raise RuntimeError( + "Intercept failed because registered services are no longer accessible " + "(internal API may have changed)" + ) from error + + if domain not in registered_services or service not in registered_services[domain]: + raise RuntimeError( + f"Intercept failed because service {domain}.{service} is not registered" + ) + + existing_service = registered_services[domain][service] + + async def service_func_proxy(call: ServiceCall) -> None: + # Convert read-only data to writeable dictionary for modification by interceptor + data = dict(call.data) + + # Call interceptor + await intercept_func(call, data) + + # Convert data back to read-only + call.data = ReadOnlyDict(data) + + # Call original service handler with processed data + await existing_service.job.target(call) + + hass.services.async_register( + domain, service, service_func_proxy, existing_service.schema + ) + + def remove(): + # Remove the interceptor by reinstalling the original service handler + hass.services.async_register( + domain, service, existing_service.job.target, existing_service.schema + ) + + return remove diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index a8707bcf..1789439d 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -41,6 +41,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, is_on, + preprocess_turn_on_alternatives, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -54,9 +55,11 @@ from homeassistant.const import ( ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, CONF_NAME, + CONF_PARAMS, EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_STARTED, EVENT_STATE_CHANGED, + SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, @@ -96,6 +99,7 @@ from .adaptation_utils import ( BRIGHTNESS_ATTRS, COLOR_ATTRS, AdaptationData, + ServiceData, prepare_adaptation_data, ) from .const import ( @@ -156,6 +160,7 @@ from .const import ( apply_service_schema, replace_none_str, ) +from .hass_utils import setup_service_call_interceptor _SUPPORT_OPTS = { COLOR_MODE_BRIGHTNESS: SUPPORT_BRIGHTNESS, @@ -182,6 +187,11 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=10) +# A (non-user-configurable, thus internal) flag to control the proactive adaptation mode. +# This exists to disable the proactive adaptation in the unit tests and enable it +# only for specific unit tests and when running as integration.""" +INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION = "proactive_adaptation" + # Consider it a significant change when attribute changes more than BRIGHTNESS_CHANGE = 25 # ≈10% of total range COLOR_TEMP_CHANGE = 100 # ≈3% of total range (2000-6500) @@ -262,11 +272,17 @@ def create_context( return Context(id=context_id, parent_id=parent_id) +def is_our_context_id(context_id: str | None) -> bool: + if context_id is None: + return False + return f":{_DOMAIN_SHORT}:" in context_id + + def is_our_context(context: Context | None) -> bool: """Check whether this integration created 'context'.""" if context is None: return False - return f":{_DOMAIN_SHORT}:" in context.id + return is_our_context_id(context.id) def _get_switches_with_lights( @@ -284,7 +300,7 @@ def _get_switches_with_lights( all_check_lights = _expand_light_groups(hass, lights) switch._expand_light_groups() # Check if any of the lights are in the switch's lights - if set(switch._lights) & set(all_check_lights): + if set(switch.lights) & set(all_check_lights): switches.append(switch) return switches @@ -385,12 +401,12 @@ async def handle_change_switch_settings( data, ) - all_lights = switch._lights # pylint: disable=protected-access + all_lights = switch.lights # pylint: disable=protected-access switch.turn_on_off_listener.reset(*all_lights, reset_manual_control=False) if switch.is_on: await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access all_lights, - transition=switch._initial_transition, + transition=switch.initial_transition, force=True, context=switch.create_context("service", parent=service_call.context), ) @@ -424,8 +440,8 @@ async def async_setup_entry( assert config_entry.entry_id in data if ATTR_TURN_ON_OFF_LISTENER not in data: - data[ATTR_TURN_ON_OFF_LISTENER] = TurnOnOffListener(hass) - turn_on_off_listener = data[ATTR_TURN_ON_OFF_LISTENER] + data[ATTR_TURN_ON_OFF_LISTENER] = TurnOnOffListener(hass, config_entry) + turn_on_off_listener: TurnOnOffListener = data[ATTR_TURN_ON_OFF_LISTENER] sleep_mode_switch = SimpleSwitch( "Sleep Mode", False, hass, config_entry, ICON_SLEEP ) @@ -443,6 +459,7 @@ async def async_setup_entry( adapt_color_switch, adapt_brightness_switch, ) + turn_on_off_listener.adaptive_switch = switch # save our switch instance, allows us to make switch's entity_id optional in service calls. hass.data[DOMAIN][config_entry.entry_id]["instance"] = switch @@ -469,7 +486,7 @@ async def async_setup_entry( lights = data[CONF_LIGHTS] for switch in switches: if not lights: - all_lights = switch._lights # pylint: disable=protected-access + all_lights = switch.lights else: all_lights = _expand_light_groups(switch.hass, lights) switch.turn_on_off_listener.lights.update(all_lights) @@ -498,7 +515,7 @@ async def async_setup_entry( lights = data[CONF_LIGHTS] for switch in switches: if not lights: - all_lights = switch._lights # pylint: disable=protected-access + all_lights = switch.lights else: all_lights = _expand_light_groups(switch.hass, lights) if service_call.data[CONF_MANUAL_CONTROL]: @@ -510,7 +527,7 @@ async def async_setup_entry( # pylint: disable=protected-access await switch._update_attrs_and_maybe_adapt_lights( all_lights, - transition=switch._initial_transition, + transition=switch.initial_transition, force=True, context=switch.create_context( "service", parent=service_call.context @@ -523,7 +540,7 @@ async def async_setup_entry( service=SERVICE_APPLY, service_func=handle_apply, schema=apply_service_schema( - switch._initial_transition + switch.initial_transition ), # pylint: disable=protected-access ) @@ -803,7 +820,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._name = data[CONF_NAME] self._interval = data[CONF_INTERVAL] - self._lights = data[CONF_LIGHTS] + self.lights: list[str] = data[CONF_LIGHTS] # backup data for use in change_switch_settings "configuration" CONF_USE_DEFAULTS self._config_backup = deepcopy(data) @@ -835,7 +852,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): " config_entry.data: '%s'," " config_entry.options: '%s', converted to '%s'.", self._name, - self._lights, + self.lights, config_entry.data, config_entry.options, data, @@ -868,7 +885,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): attrdata[k] = v.total_seconds() self._config.update(attrdata) - self._initial_transition = data[CONF_INITIAL_TRANSITION] + self.initial_transition = data[CONF_INITIAL_TRANSITION] self._sleep_transition = data[CONF_SLEEP_TRANSITION] self._only_once = data[CONF_ONLY_ONCE] self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR] @@ -921,7 +938,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug( "%s: Set switch settings for lights '%s'. now using data: '%s'", self._name, - self._lights, + self.lights, data, ) @@ -961,12 +978,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._remove_listeners() def _expand_light_groups(self) -> None: - all_lights = _expand_light_groups(self.hass, self._lights) + all_lights = _expand_light_groups(self.hass, self.lights) self.turn_on_off_listener.lights.update(all_lights) self.turn_on_off_listener.set_auto_reset_manual_control_times( all_lights, self._auto_reset_manual_control_time ) - self._lights = list(all_lights) + self.lights = list(all_lights) async def _setup_listeners(self, _=None) -> None: _LOGGER.debug("%s: Called '_setup_listeners'", self._name) @@ -987,10 +1004,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self.remove_listeners.extend([remove_interval, remove_sleep]) - if self._lights: + if self.lights: self._expand_light_groups() remove_state = async_track_state_change_event( - self.hass, self._lights, self._light_event + self.hass, self.lights, self._light_event ) self.remove_listeners.append(remove_state) @@ -1014,14 +1031,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return extra_state_attributes extra_state_attributes["manual_control"] = [ light - for light in self._lights + for light in self.lights if self.turn_on_off_listener.manual_control.get(light) ] extra_state_attributes.update(self._settings) timers = self.turn_on_off_listener.auto_reset_manual_control_timers extra_state_attributes["autoreset_time_remaining"] = { light: time - for light in self._lights + for light in self.lights if (timer := timers.get(light)) and (time := timer.remaining_time()) > 0 } return extra_state_attributes @@ -1054,11 +1071,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self.is_on: return self._state = True - self.turn_on_off_listener.reset(*self._lights) + self.turn_on_off_listener.reset(*self.lights) await self._setup_listeners() if adapt_lights: await self._update_attrs_and_maybe_adapt_lights( - transition=self._initial_transition, + transition=self.initial_transition, force=True, context=self.create_context("turn_on"), ) @@ -1069,7 +1086,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return self._state = False self._remove_listeners() - self.turn_on_off_listener.reset(*self._lights) + self.turn_on_off_listener.reset(*self.lights) async def _async_update_at_interval(self, now=None) -> None: await self._update_attrs_and_maybe_adapt_lights( @@ -1078,7 +1095,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context=self.create_context("interval"), ) - async def _adapt_light( # noqa: C901 + async def prepare_adaptation_data( self, light: str, transition: int | None = None, @@ -1086,11 +1103,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): adapt_color: bool | None = None, prefer_rgb_color: bool | None = None, context: Context | None = None, - ) -> None: - lock = self._locks.get(light) - if lock is not None and lock.locked(): - _LOGGER.debug("%s: '%s' is locked", self._name, light) - return + ) -> AdaptationData: if transition is None: transition = self._transition if adapt_brightness is None: @@ -1141,7 +1154,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self.turn_on_off_listener.last_service_data[light] = service_data - data = prepare_adaptation_data( + return prepare_adaptation_data( self.hass, light, context, @@ -1152,7 +1165,35 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): filter_by_state=self._skip_redundant_commands, ) - await self._execute_cancellable_adaptation_calls(data) + async def _adapt_light( # noqa: C901 + self, + light: str, + transition: int | None = None, + adapt_brightness: bool | None = None, + adapt_color: bool | None = None, + prefer_rgb_color: bool | None = None, + context: Context | None = None, + ) -> None: + lock = self._locks.get(light) + if lock is not None and lock.locked(): + _LOGGER.debug("%s: '%s' is locked", self._name, light) + return + + if self.turn_on_off_listener.is_proactively_adapting(context.parent_id): + # Skip if adaptation was already executed by the service call interceptor + _LOGGER.debug("Skipping reactive adaptation of %s", context.parent_id) + return + + data = await self.prepare_adaptation_data( + light, + transition, + adapt_brightness, + adapt_color, + prefer_rgb_color, + context, + ) + + await self.execute_cancellable_adaptation_calls(data) async def _execute_adaptation_calls(self, data: AdaptationData): """Executes a sequence of adaptation service calls for the given service datas.""" @@ -1163,7 +1204,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): index += 1 # Sleep between multiple service calls. - if not is_first_call: + if not is_first_call or data.initial_sleep: await asyncio.sleep(data.sleep_time) # Instead of directly iterating the generator in the while-loop, we get @@ -1189,7 +1230,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context=data.context, ) - async def _execute_cancellable_adaptation_calls( + async def execute_cancellable_adaptation_calls( self, data: AdaptationData, ): @@ -1231,7 +1272,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self.async_write_ha_state() if lights is None: - lights = self._lights + lights = self.lights filtered_lights = [] if not force: @@ -1323,7 +1364,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): "%s: _sleep_mode_switch_state_event, event: '%s'", self._name, event ) # Reset the manually controlled status when the "sleep mode" changes - self.turn_on_off_listener.reset(*self._lights) + self.turn_on_off_listener.reset(*self.lights) await self._update_attrs_and_maybe_adapt_lights( transition=self._sleep_transition, force=True, @@ -1346,7 +1387,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): entity_id, event.context.id, ) - self.turn_on_off_listener.reset(entity_id, reset_manual_control=False) + + if ( + event.context.parent_id + and not self.turn_on_off_listener.is_proactively_adapting( + event.context.id + ) + ): + self.turn_on_off_listener.reset(entity_id, reset_manual_control=False) + # Tracks 'off' → 'on' state changes self._off_to_on_event[entity_id] = event lock = self._locks.get(entity_id) @@ -1381,7 +1430,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): await self._update_attrs_and_maybe_adapt_lights( lights=[entity_id], - transition=self._initial_transition, + transition=self.initial_transition, force=True, context=self.create_context("light_event", parent=event.context), ) @@ -1662,9 +1711,10 @@ class SunLightSettings: class TurnOnOffListener: """Track 'light.turn_off' and 'light.turn_on' service calls.""" - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry): """Initialize the TurnOnOffListener that is shared among all switches.""" self.hass = hass + data = validate(config_entry) self.lights = set() # Tracks 'light.turn_off' service calls @@ -1689,11 +1739,169 @@ class TurnOnOffListener: # Track light transitions self.transition_timers: dict[str, _AsyncSingleShotTimer] = {} - self.remove_listener = self.hass.bus.async_listen( - EVENT_CALL_SERVICE, self.turn_on_off_event_listener + self.listener_removers = [] + + self.listener_removers.append( + self.hass.bus.async_listen( + EVENT_CALL_SERVICE, self.turn_on_off_event_listener + ) ) - self.remove_listener2 = self.hass.bus.async_listen( - EVENT_STATE_CHANGED, self.state_changed_event_listener + self.listener_removers.append( + self.hass.bus.async_listen( + EVENT_STATE_CHANGED, self.state_changed_event_listener + ) + ) + + self.adaptive_switch: AdaptiveSwitch | None + self._proactively_adapting_contexts: dict[str, str] = {} + + is_proactive_adaptation_enabled = ( + data.get(INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION, True) is not False + ) + + if is_proactive_adaptation_enabled: + try: + self.listener_removers.append( + setup_service_call_interceptor( + hass, + LIGHT_DOMAIN, + SERVICE_TURN_ON, + self._service_interceptor_turn_on_handler, + ) + ) + + self.listener_removers.append( + setup_service_call_interceptor( + hass, + LIGHT_DOMAIN, + SERVICE_TOGGLE, + self._service_interceptor_turn_on_handler, + ) + ) + + _LOGGER.debug("Proactive adaptation enabled") + except RuntimeError: + _LOGGER.warning( + "Failed to set up service call interceptors, " + "falling back to event-reactive mode", + exc_info=True, + ) + + def disable(self): + """Disable the listener by removing all subscribed handlers.""" + for remove in self.listener_removers: + remove() + + def set_proactively_adapting(self, context_id: str, entity_id: str) -> None: + """Declare the adaptation with the given context ID as proactively adapting, + and associate it to an entity ID.""" + self._proactively_adapting_contexts[context_id] = entity_id + + def is_proactively_adapting(self, context_id: str) -> bool: + """Determine whether an adaptation with the given context ID is proactive.""" + is_proactively_adapting_context = ( + context_id in self._proactively_adapting_contexts + ) + + _LOGGER.debug( + "is_proactively_adapting_context %s %s", + context_id, + is_proactively_adapting_context, + ) + + return is_proactively_adapting_context + + def clear_proactively_adapting(self, entity_id: str) -> None: + """Clear all context IDs associated with the given entity ID. + + Call this method to clear past context IDs and avoid a memory leak.""" + keys = [ + k for k, v in self._proactively_adapting_contexts.items() if v == entity_id + ] + + for key in keys: + self._proactively_adapting_contexts.pop(key) + + async def _service_interceptor_turn_on_handler( + self, call: ServiceCall, data: ServiceData + ): + # Don't adapt our own service calls + if is_our_context(call.context): + return + + entity_ids = self._get_entity_list(data) + + # For simplicity, only service calls affecting a single entity are currently handled. + # + # To add support for adapting multiple entities, the following properties + # need to hold for _all_ entities: + # - managed by this AL instance + # - not manually controlled + # - supporting the same relevant feature set + # - off state + if len(entity_ids) != 1: + return + + entity_id = entity_ids[0] + + if entity_id not in self.adaptive_switch.lights: + return + + if self.manual_control.get(entity_id, False): + return + + # Prevent adaptation of TURN_ON calls when light is already on, + # and of TOGGLE calls when toggling off. + if self.hass.states.is_state(entity_id, STATE_ON): + return + + _LOGGER.debug( + "Intercepted TURN_ON call with data %s (%s)", data, call.context.id + ) + + self.reset(entity_id, reset_manual_control=False) + self.clear_proactively_adapting(entity_id) + + adapt_brightness = self.adaptive_switch.adapt_brightness_switch.is_on or False + adapt_color = self.adaptive_switch.adapt_color_switch.is_on or False + transition = ( + data[CONF_PARAMS].get(ATTR_TRANSITION, None) + or self.adaptive_switch.initial_transition + ) + + adaptation_data = await self.adaptive_switch.prepare_adaptation_data( + entity_id, + transition, + adapt_brightness, + adapt_color, + ) + + # Take first adaptation item to apply it to this service call + first_service_data = await adaptation_data.next_service_call_data() + + if not first_service_data: + return + + # Update/adapt service call data + first_service_data.pop(ATTR_ENTITY_ID, None) + # This is called as a preprocessing step by the schema validation of the original + # service call and needs to be repeated here to also process the added adaptation data. + # (A more generic alternative would be re-executing the validation, but that is more + # complicated and unstable because it requires transformation of the data object back + # into its original service call structure which cannot be reliably done due to the + # lack of a bijective mapping.) + preprocess_turn_on_alternatives(self.hass, first_service_data) + data[CONF_PARAMS].update(first_service_data) + + # Schedule additional service calls for the remaining adaptation data. + # We cannot know here whether there is another call to follow (since the + # state can change until the next call), so we just schedule it and let + # it sort out by itself. + self.set_proactively_adapting(call.context.id, entity_id) + self.set_proactively_adapting(adaptation_data.context.id, entity_id) + adaptation_data.initial_sleep = True + asyncio.create_task( # Don't await to avoid blocking the service call + self.adaptive_switch.execute_cancellable_adaptation_calls(adaptation_data) ) def _handle_timer( @@ -1772,7 +1980,7 @@ class TurnOnOffListener: continue await switch._update_attrs_and_maybe_adapt_lights( [light], - transition=switch._initial_transition, + transition=switch.initial_transition, force=True, context=switch.create_context("autoreset"), ) @@ -1803,19 +2011,13 @@ class TurnOnOffListener: self.last_service_data.pop(light, None) self.cancel_ongoing_adaptation_calls(light) - async def turn_on_off_event_listener(self, event: Event) -> None: - """Track 'light.turn_off' and 'light.turn_on' service calls.""" - domain = event.data.get(ATTR_DOMAIN) - if domain != LIGHT_DOMAIN: - return + def _get_entity_list(self, service_data: ServiceData) -> list[str]: + entity_ids = [] - service = event.data[ATTR_SERVICE] - service_data = event.data[ATTR_SERVICE_DATA] if ATTR_ENTITY_ID in service_data: entity_ids = cv.ensure_list_csv(service_data[ATTR_ENTITY_ID]) elif ATTR_AREA_ID in service_data: area_ids = cv.ensure_list_csv(service_data[ATTR_AREA_ID]) - entity_ids = [] for area_id in area_ids: area_entity_ids = area_entities(self.hass, area_id) for entity_id in area_entity_ids: @@ -1828,8 +2030,19 @@ class TurnOnOffListener: _LOGGER.debug( "No entity_ids or area_ids found in service_data: %s", service_data ) + + return entity_ids + + async def turn_on_off_event_listener(self, event: Event) -> None: + """Track 'light.turn_off' and 'light.turn_on' service calls.""" + domain = event.data.get(ATTR_DOMAIN) + if domain != LIGHT_DOMAIN: return + service = event.data[ATTR_SERVICE] + service_data = event.data[ATTR_SERVICE_DATA] + entity_ids = self._get_entity_list(service_data) + if not any(eid in self.lights for eid in entity_ids): return diff --git a/tests/test_hass_utils.py b/tests/test_hass_utils.py new file mode 100644 index 00000000..9f044d8a --- /dev/null +++ b/tests/test_hass_utils.py @@ -0,0 +1,81 @@ +"""Tests for Adaptive Lighting HASS utils.""" + +from unittest.mock import AsyncMock + +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.const import SERVICE_TURN_ON +from homeassistant.core import ServiceCall +from homeassistant.util.read_only_dict import ReadOnlyDict + +from custom_components.adaptive_lighting.adaptation_utils import ServiceData +from custom_components.adaptive_lighting.hass_utils import ( + setup_service_call_interceptor, +) + + +async def test_setup_service_call_interceptor(hass): + """Test setup and removal of service call interceptor.""" + service_func_mock = AsyncMock() + hass.services.async_register(LIGHT_DOMAIN, SERVICE_TURN_ON, service_func_mock) + + async def service_call(): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {}, + blocking=True, + ) + + # Test if service is called + + await service_call() + assert service_func_mock.call_count == 1 + + # Test if interceptor is called after setup + + intercept_func_mock = AsyncMock() + remove_interceptor = setup_service_call_interceptor( + hass, + LIGHT_DOMAIN, + SERVICE_TURN_ON, + intercept_func_mock, + ) + + await service_call() + assert service_func_mock.call_count == 2 + assert intercept_func_mock.call_count == 1 + + # Test if interceptor is no longer called after removal + + remove_interceptor() + await service_call() + assert service_func_mock.call_count == 3 + assert intercept_func_mock.call_count == 1 + + +async def test_service_call_interceptor_data_manipulation(hass): + """Test service call data manipulation by service call interceptor.""" + service_func_mock = AsyncMock() + hass.services.async_register(LIGHT_DOMAIN, SERVICE_TURN_ON, service_func_mock) + + async def intercept_func(call: ServiceCall, data: ServiceData): + data["test1"] = "changed" + data["test2"] = "added" + + setup_service_call_interceptor( + hass, + LIGHT_DOMAIN, + SERVICE_TURN_ON, + intercept_func, + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {"test1": "initial"}, + blocking=True, + ) + + (service_call,) = service_func_mock.call_args[0] + assert service_call.data == {"test1": "changed", "test2": "added"} + assert isinstance(service_call.data, ReadOnlyDict) diff --git a/tests/test_switch.py b/tests/test_switch.py index bb5d1c49..4ac92c64 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -6,7 +6,8 @@ import datetime import itertools import logging from random import randint -from unittest.mock import MagicMock, patch +from typing import Any +from unittest.mock import MagicMock, Mock, patch from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -31,12 +32,14 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_LIGHTS, CONF_NAME, + EVENT_CALL_SERVICE, EVENT_STATE_CHANGED, + SERVICE_TOGGLE, SERVICE_TURN_ON, STATE_OFF, STATE_ON, ) -from homeassistant.core import Context, HomeAssistant, State +from homeassistant.core import Context, Event, HomeAssistant, State from homeassistant.helpers import entity_registry from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.setup import async_setup_component @@ -86,12 +89,15 @@ from custom_components.adaptive_lighting.const import ( ) from custom_components.adaptive_lighting.switch import ( _SUPPORT_OPTS, + INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION, VALID_COLOR_MODES, + AdaptiveSwitch, _attributes_have_changed, _supported_features, color_difference_redmean, create_context, is_our_context, + is_our_context_id, ) _LOGGER = logging.getLogger(__name__) @@ -118,6 +124,7 @@ LAT_LONG_TZS = [ ] ENTITY_LIGHT = "light.bed_light" +ENTITY_LIGHT3 = "light.kitchen_lights" _SWITCH_FMT = f"{SWITCH_DOMAIN}.{DOMAIN}" ENTITY_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}" ENTITY_SLEEP_MODE_SWITCH = f"{_SWITCH_FMT}_sleep_mode_{DEFAULT_NAME}" @@ -143,9 +150,16 @@ def reset_time_zone(): dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE -async def setup_switch(hass, extra_data): +async def setup_switch(hass, extra_data) -> tuple[MockConfigEntry, AdaptiveSwitch]: """Create the switch entry.""" - entry = MockConfigEntry(domain=DOMAIN, data={CONF_NAME: DEFAULT_NAME, **extra_data}) + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_NAME: DEFAULT_NAME, + INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: False, + **extra_data, + }, + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -190,6 +204,7 @@ async def setup_lights(hass: HomeAssistant): "set_level": None, "set_temperature": None, "set_color": None, + "supports_transition_template": True, }, }, }, @@ -212,7 +227,7 @@ async def setup_lights(hass: HomeAssistant): return lights -async def setup_lights_and_switch(hass, extra_conf=None): +async def setup_lights_and_switch(hass, extra_conf=None, all_lights: bool = False): """Create switch and demo lights.""" # Setup demo lights and turn on lights_instances = await setup_lights(hass) @@ -228,6 +243,10 @@ async def setup_lights_and_switch(hass, extra_conf=None): ENTITY_LIGHT, "light.ceiling_lights", ] + + if all_lights: + lights.append(ENTITY_LIGHT3) + assert all(hass.states.get(light) is not None for light in lights) _, switch = await setup_switch( hass, @@ -421,7 +440,7 @@ async def test_adaptive_lighting_time_zones_and_sun_settings( async def test_light_settings(hass): """Test that light settings are correctly applied.""" switch, _ = await setup_lights_and_switch(hass) - lights = switch._lights + lights = switch.lights # Turn on "sleep mode" await hass.services.async_call( @@ -525,7 +544,7 @@ async def test_turn_on_off_listener_not_tracking_untracked_lights(hass): """Test that lights that are not in a Adaptive Lighting switch aren't tracked.""" switch, _ = await setup_lights_and_switch(hass) light = "light.kitchen_lights" - assert light not in switch._lights + assert light not in switch.lights for state in [True, False]: await hass.services.async_call( LIGHT_DOMAIN, @@ -757,11 +776,11 @@ async def test_manual_control(hass): await switch.adapt_brightness_switch.async_turn_on() # Check that when no lights are specified, all are reset - await change_manual_control(True, {CONF_LIGHTS: switch._lights}) - assert all([manual_control[eid] for eid in switch._lights]) + await change_manual_control(True, {CONF_LIGHTS: switch.lights}) + assert all([manual_control[eid] for eid in switch.lights]) # do not pass "lights" so reset all await change_manual_control(False, {}) - assert all([not manual_control[eid] for eid in switch._lights]) + assert all([not manual_control[eid] for eid in switch.lights]) @pytest.mark.dependency(depends=[*GLOBAL_TEST_DEPENDENCIES, "test_manual_control"]) @@ -820,7 +839,7 @@ async def test_apply_service(hass): """Test adaptive_lighting.apply service.""" switch, (_, _, light) = await setup_lights_and_switch(hass) entity_id = light.entity_id - assert entity_id not in switch._lights + assert entity_id not in switch.lights def increased_brightness(): return (light._attr_brightness + 100) % 255 @@ -1074,7 +1093,7 @@ async def test_state_change_handlers(hass): current_service_data = switch.turn_on_off_listener.last_service_data assert current_service_data != last_service_data - for light in switch._lights: + for light in switch.lights: # current_service_data should have changed after the last update. assert current_service_data.get(light) assert last_service_data.get(light) @@ -1359,7 +1378,7 @@ async def test_change_switch_settings_service(hass): """Test adaptive_lighting.change_switch_settings service.""" switch, (_, _, light) = await setup_lights_and_switch(hass) entity_id = light.entity_id - assert entity_id not in switch._lights + assert entity_id not in switch.lights async def change_switch_settings(**kwargs): await hass.services.async_call( @@ -1427,7 +1446,7 @@ async def test_cancellable_service_calls_task(hass): 0, _create_service_call_data_iterator(hass, [service_data]), ) - await switch._execute_cancellable_adaptation_calls(adaptation_data) + await switch.execute_cancellable_adaptation_calls(adaptation_data) task = switch.turn_on_off_listener.adaptation_tasks.get(light.entity_id) assert task is not None @@ -1451,3 +1470,168 @@ async def test_service_calls_task_cancellation(hass): pass assert task.cancelled() + + +async def _turn_on_and_track_event_contexts( + hass: HomeAssistant, context_id: str, entity_id +): + context = Context(id=context_id) + event_context_ids = [] + + async def turn_on_off_event_listener(event: Event) -> None: + event_context_ids.append(event.context.id) + + hass.bus.async_listen(EVENT_CALL_SERVICE, turn_on_off_event_listener) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + context=context, + ) + await hass.async_block_till_done() + + return event_context_ids + + +def _mock_sun_light_settings(switch: AdaptiveSwitch, settings: dict[str, Any]): + sun_light_settings_mock = Mock() + sun_light_settings_mock.get_settings = Mock(return_value=settings) + switch._sun_light_settings = sun_light_settings_mock + + +async def test_proactive_adaptation(hass): + """Validate that a proactive adaptation updates the original service call.""" + switch, _ = await setup_lights_and_switch( + hass, {INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True}, True + ) + + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 67, + ATTR_COLOR_TEMP_KELVIN: 3448, + }, + ) + + event_context_ids = await _turn_on_and_track_event_contexts( + hass, "test_context", ENTITY_LIGHT3 + ) + + # Expect a single service call + assert len(event_context_ids) == 1 + assert event_context_ids == ["test_context"] + + # Expect adapted light state + state = hass.states.get(ENTITY_LIGHT3) + # Sun light settings use %, state only contains absolute + assert state.attributes[ATTR_BRIGHTNESS] == 171 # == 67% + assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 3448 + + +async def test_proactive_adaptation_with_separate_commands(hass): + """Validate that a split proactive adaptation yields one additional service call.""" + switch, _ = await setup_lights_and_switch( + hass, + { + INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True, + CONF_SEPARATE_TURN_ON_COMMANDS: True, + }, + True, + ) + + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 67, + ATTR_COLOR_TEMP_KELVIN: 3448, + }, + ) + + event_context_ids = await _turn_on_and_track_event_contexts( + hass, "test_context", ENTITY_LIGHT3 + ) + + # Expect two service calls + assert len(event_context_ids) == 2 + assert event_context_ids[0] == "test_context" + assert is_our_context_id(event_context_ids[1]) + + # Expect adapted light state + state = hass.states.get(ENTITY_LIGHT3) + assert state.attributes[ATTR_BRIGHTNESS] == 171 + assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 3448 + + +async def test_proactive_adaptation_toggle(hass): + """Validate that a proactive adaptation updates service calls which toggle a light on, + but not those which toggle off. + + This test is based on the fact that contexts of proactive adaptations are recorded. + """ + switch, _ = await setup_lights_and_switch( + hass, {INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True}, True + ) + + # Toggle ON + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: ENTITY_LIGHT3}, + blocking=True, + context=Context(id="test1"), + ) + + assert switch.turn_on_off_listener.is_proactively_adapting("test1") + + # Toggle OFF + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: ENTITY_LIGHT3}, + blocking=True, + context=Context(id="test2"), + ) + + assert not switch.turn_on_off_listener.is_proactively_adapting("test2") + + +async def test_proactive_adaptation_transition_override(hass): + """Validate that transitions in service calls are preferred over the default transition.""" + switch, (_, _, light3) = await setup_lights_and_switch( + hass, + { + INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True, + CONF_INITIAL_TRANSITION: 123, + }, + True, + ) + + with patch.object( + light3, "async_turn_on", wraps=light3.async_turn_on + ) as patched_async_turn_on: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT3}, + blocking=True, + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT3, ATTR_TRANSITION: 456}, + blocking=True, + ) + + # Assert that default is used when no transition is specified in service call + kwargs = patched_async_turn_on.call_args_list[0].kwargs + assert set({ATTR_TRANSITION: 123}.items()).issubset(kwargs.items()) + + # Assert that specified service call transition takes precedence over default + kwargs = patched_async_turn_on.call_args_list[1].kwargs + assert set({ATTR_TRANSITION: 456}.items()).issubset(kwargs.items()) + + # Cleanup + switch.turn_on_off_listener.cancel_ongoing_adaptation_calls(ENTITY_LIGHT3) From 1cedb3fbc7672f2de7043b08ebd38af8f698ee64 Mon Sep 17 00:00:00 2001 From: Mario Guggenberger Date: Wed, 19 Jul 2023 18:53:42 +0200 Subject: [PATCH 0570/1077] chore: bump manifest version to 1.15.0 (#634) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 4bbdd6f3..6cb2cd5a 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.14.0" + "version": "1.15.0" } From 1133f0defa53040db291f697ab78cbab6d790750 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 20 Jul 2023 08:52:14 -0700 Subject: [PATCH 0571/1077] Find adaptive_switch in TurnOnOffListener (#635) * Find adaptive_switch in TurnOnOffListener * remove unused argument * Bump to 1.15.1 --- .../adaptive_lighting/manifest.json | 2 +- custom_components/adaptive_lighting/switch.py | 19 ++++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 6cb2cd5a..ffc87e86 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.15.0" + "version": "1.15.1" } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 1789439d..edbc8214 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -308,7 +308,6 @@ def _get_switches_with_lights( def find_switch_for_lights( hass: HomeAssistant, lights: list[str], - is_on: bool = False, ) -> AdaptiveSwitch: """Find the switch that controls the lights in 'lights'.""" switches = _get_switches_with_lights(hass, lights) @@ -365,7 +364,7 @@ def _get_switches_from_service_call( return switches if lights: - switch = find_switch_for_lights(hass, lights, service_call) + switch = find_switch_for_lights(hass, lights) return [switch] raise ValueError( @@ -459,7 +458,6 @@ async def async_setup_entry( adapt_color_switch, adapt_brightness_switch, ) - turn_on_off_listener.adaptive_switch = switch # save our switch instance, allows us to make switch's entity_id optional in service calls. hass.data[DOMAIN][config_entry.entry_id]["instance"] = switch @@ -1752,7 +1750,6 @@ class TurnOnOffListener: ) ) - self.adaptive_switch: AdaptiveSwitch | None self._proactively_adapting_contexts: dict[str, str] = {} is_proactive_adaptation_enabled = ( @@ -1843,8 +1840,8 @@ class TurnOnOffListener: return entity_id = entity_ids[0] - - if entity_id not in self.adaptive_switch.lights: + adaptive_switch = find_switch_for_lights(self.hass, [entity_id]) + if entity_id not in adaptive_switch.lights: return if self.manual_control.get(entity_id, False): @@ -1862,14 +1859,14 @@ class TurnOnOffListener: self.reset(entity_id, reset_manual_control=False) self.clear_proactively_adapting(entity_id) - adapt_brightness = self.adaptive_switch.adapt_brightness_switch.is_on or False - adapt_color = self.adaptive_switch.adapt_color_switch.is_on or False + adapt_brightness = adaptive_switch.adapt_brightness_switch.is_on or False + adapt_color = adaptive_switch.adapt_color_switch.is_on or False transition = ( data[CONF_PARAMS].get(ATTR_TRANSITION, None) - or self.adaptive_switch.initial_transition + or adaptive_switch.initial_transition ) - adaptation_data = await self.adaptive_switch.prepare_adaptation_data( + adaptation_data = await adaptive_switch.prepare_adaptation_data( entity_id, transition, adapt_brightness, @@ -1901,7 +1898,7 @@ class TurnOnOffListener: self.set_proactively_adapting(adaptation_data.context.id, entity_id) adaptation_data.initial_sleep = True asyncio.create_task( # Don't await to avoid blocking the service call - self.adaptive_switch.execute_cancellable_adaptation_calls(adaptation_data) + adaptive_switch.execute_cancellable_adaptation_calls(adaptation_data) ) def _handle_timer( From 1b31b2693818b975b978a85361541f97393983fe Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 20 Jul 2023 08:53:11 -0700 Subject: [PATCH 0572/1077] Add @protyposis to codeowners (#636) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index ffc87e86..40131076 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -1,7 +1,7 @@ { "domain": "adaptive_lighting", "name": "Adaptive Lighting", - "codeowners": ["@basnijholt", "@RubenKelevra", "@th3w1zard1"], + "codeowners": ["@basnijholt", "@RubenKelevra", "@th3w1zard1", "@protyposis"], "config_flow": true, "dependencies": [], "documentation": "https://github.com/basnijholt/adaptive-lighting#readme", From fd5f6bf3106aa05cbcb94169ad0cdc3759534f9f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 20 Jul 2023 11:53:38 -0700 Subject: [PATCH 0573/1077] Pass on lights that are not managed by AL, closes #638 (#639) * Pass on lights that are not managed by AL, closes #638 * Rename exception --- custom_components/adaptive_lighting/manifest.json | 2 +- custom_components/adaptive_lighting/switch.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 40131076..52f0554c 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.15.1" + "version": "1.15.2" } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index edbc8214..4cbd6854 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -305,6 +305,10 @@ def _get_switches_with_lights( return switches +class NoSwitchFoundError(ValueError): + """No switches found for lights.""" + + def find_switch_for_lights( hass: HomeAssistant, lights: list[str], @@ -318,13 +322,13 @@ def find_switch_for_lights( if len(on_switches) == 1: # Of the multiple switches, only one is on return on_switches[0] - raise ValueError( + raise NoSwitchFoundError( f"find_switch_for_lights: Light(s) {lights} found in multiple switch configs" f" ({[s.entity_id for s in switches]}). You must pass a switch under" f" 'entity_id'." ) else: - raise ValueError( + raise NoSwitchFoundError( f"find_switch_for_lights: Light(s) {lights} not found in any switch's" f" configuration. You must either include the light(s) that is/are" f" in the integration config, or pass a switch under 'entity_id'." @@ -1840,7 +1844,12 @@ class TurnOnOffListener: return entity_id = entity_ids[0] - adaptive_switch = find_switch_for_lights(self.hass, [entity_id]) + try: + adaptive_switch = find_switch_for_lights(self.hass, [entity_id]) + except NoSwitchFoundError: + # This might be a light that is not managed by this AL instance. + return + if entity_id not in adaptive_switch.lights: return From 19d503b4d7e3c317a9254caacadf008c820cc2c5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 20 Jul 2023 13:59:19 -0700 Subject: [PATCH 0574/1077] Revert supported_features from #565 and #575, fixes #601 (#637) --- custom_components/adaptive_lighting/const.py | 2 - custom_components/adaptive_lighting/switch.py | 109 ++++++------------ tests/test_switch.py | 84 +------------- 3 files changed, 39 insertions(+), 156 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 3aaf9dda..241689fc 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -289,8 +289,6 @@ VALIDATION_TUPLES = [ ), ] -CONST_COLOR = "color" - def timedelta_as_int(value): """Convert a `datetime.timedelta` object to an integer. diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 4cbd6854..2bc17862 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -18,12 +18,7 @@ import astral from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP_KELVIN, - ATTR_HS_COLOR, - ATTR_MAX_COLOR_TEMP_KELVIN, - ATTR_MIN_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR, - ATTR_RGBW_COLOR, - ATTR_RGBWW_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_XY_COLOR, @@ -141,7 +136,6 @@ from .const import ( CONF_TRANSITION, CONF_TURN_ON_LIGHTS, CONF_USE_DEFAULTS, - CONST_COLOR, DOMAIN, EXTRA_VALIDATION, ICON_BRIGHTNESS, @@ -163,21 +157,10 @@ from .const import ( from .hass_utils import setup_service_call_interceptor _SUPPORT_OPTS = { - COLOR_MODE_BRIGHTNESS: SUPPORT_BRIGHTNESS, - COLOR_MODE_COLOR_TEMP: SUPPORT_COLOR_TEMP, - CONST_COLOR: SUPPORT_COLOR, - ATTR_TRANSITION: SUPPORT_TRANSITION, -} - - -VALID_COLOR_MODES = { - COLOR_MODE_BRIGHTNESS: ATTR_BRIGHTNESS, - COLOR_MODE_COLOR_TEMP: ATTR_COLOR_TEMP_KELVIN, - COLOR_MODE_HS: ATTR_HS_COLOR, - COLOR_MODE_RGB: ATTR_RGB_COLOR, - COLOR_MODE_RGBW: ATTR_RGBW_COLOR, - COLOR_MODE_RGBWW: ATTR_RGBWW_COLOR, - COLOR_MODE_XY: ATTR_XY_COLOR, + "brightness": SUPPORT_BRIGHTNESS, + "color_temp": SUPPORT_COLOR_TEMP, + "color": SUPPORT_COLOR, + "transition": SUPPORT_TRANSITION, } _ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT) @@ -625,53 +608,36 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]: return list(all_lights) -def _supported_to_attributes(supported): - supported_attributes = {} - supports_colors = False - for mode in supported: - attr = VALID_COLOR_MODES.get(mode) - if attr: - supported_attributes[attr] = True - if attr in COLOR_ATTRS: - supports_colors = True - # ATTR_SUPPORTED_FEATURES only - elif mode in _SUPPORT_OPTS: - supported_attributes[mode] = True - if CONST_COLOR in supported_attributes: - supports_colors = True - supported_attributes.pop(CONST_COLOR) - return supported_attributes, supports_colors - - def _supported_features(hass: HomeAssistant, light: str): state = hass.states.get(light) - legacy_supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - legacy_supported = { - key for key, value in _SUPPORT_OPTS.items() if legacy_supported_features & value + supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + supported = { + key for key, value in _SUPPORT_OPTS.items() if supported_features & value } supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES, set()) - supported, supports_colors = _supported_to_attributes( - legacy_supported.union(supported_color_modes) - ) - min_kelvin = state.attributes.get(ATTR_MIN_COLOR_TEMP_KELVIN) - max_kelvin = state.attributes.get(ATTR_MAX_COLOR_TEMP_KELVIN) - supported.update( - { - ATTR_MIN_COLOR_TEMP_KELVIN: min_kelvin, - ATTR_MAX_COLOR_TEMP_KELVIN: max_kelvin, - } - ) - if supports_colors: + if COLOR_MODE_RGB in supported_color_modes: + supported.add("color") # Adding brightness here, see # comment https://github.com/basnijholt/adaptive-lighting/issues/112#issuecomment-836944011 - supported[ATTR_BRIGHTNESS] = True - if CONST_COLOR not in legacy_supported: - # supports_colors = False - _LOGGER.debug( - "'supported_color_modes' supports color but the legacy 'supported_features'" - " bitfield says we do not. Despite this we'll assume light '%s' supports colors", - ) - return supported, supports_colors + supported.add("brightness") + if COLOR_MODE_RGBW in supported_color_modes: + supported.add("color") + supported.add("brightness") # see above url + if COLOR_MODE_RGBWW: + supported.add("color") + supported.add("brightness") # see above url + if COLOR_MODE_XY in supported_color_modes: + supported.add("color") + supported.add("brightness") # see above url + if COLOR_MODE_HS in supported_color_modes: + supported.add("color") + supported.add("brightness") # see above url + if COLOR_MODE_COLOR_TEMP in supported_color_modes: + supported.add("color_temp") + supported.add("brightness") # see above url + if COLOR_MODE_BRIGHTNESS in supported_color_modes: + supported.add("brightness") + return supported def color_difference_redmean( @@ -1122,13 +1088,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Build service data. service_data = {ATTR_ENTITY_ID: light} - features, supports_colors = _supported_features(self.hass, light) + features = _supported_features(self.hass, light) # Check transition == 0 to fix #378 - use_transition = ATTR_TRANSITION in features and transition > 0 + use_transition = "transition" in features and transition > 0 if use_transition: service_data[ATTR_TRANSITION] = transition - if ATTR_BRIGHTNESS in features and adapt_brightness: + if "brightness" in features and adapt_brightness: brightness = round(255 * self._settings["brightness_pct"] / 100) service_data[ATTR_BRIGHTNESS] = brightness @@ -1137,18 +1103,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): and self._sun_light_settings.sleep_rgb_or_color_temp == "rgb_color" ) if ( - ATTR_COLOR_TEMP_KELVIN in features + "color_temp" in features and adapt_color - and not (prefer_rgb_color and supports_colors) - and not (sleep_rgb and supports_colors) + and not (prefer_rgb_color and "color" in features) + and not (sleep_rgb and "color" in features) ): _LOGGER.debug("%s: Setting color_temp of light %s", self._name, light) - min_kelvin = features[ATTR_MIN_COLOR_TEMP_KELVIN] - max_kelvin = features[ATTR_MAX_COLOR_TEMP_KELVIN] + attributes = self.hass.states.get(light).attributes + min_kelvin = attributes["min_color_temp_kelvin"] + max_kelvin = attributes["max_color_temp_kelvin"] color_temp_kelvin = self._settings["color_temp_kelvin"] color_temp_kelvin = max(min(color_temp_kelvin, max_kelvin), min_kelvin) service_data[ATTR_COLOR_TEMP_KELVIN] = color_temp_kelvin - elif supports_colors and adapt_color: + elif "color" in features and adapt_color: _LOGGER.debug("%s: Setting rgb_color of light %s", self._name, light) service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"] diff --git a/tests/test_switch.py b/tests/test_switch.py index 4ac92c64..2136b707 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -3,23 +3,18 @@ import asyncio from copy import deepcopy import datetime -import itertools import logging from random import randint from typing import Any -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import Mock, patch from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP_KELVIN, - ATTR_MAX_COLOR_TEMP_KELVIN, - ATTR_MIN_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR, - ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_XY_COLOR, - COLOR_MODE_BRIGHTNESS, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.light import SERVICE_TURN_OFF @@ -75,7 +70,6 @@ from custom_components.adaptive_lighting.const import ( CONF_TRANSITION, CONF_TURN_ON_LIGHTS, CONF_USE_DEFAULTS, - CONST_COLOR, DEFAULT_MAX_BRIGHTNESS, DEFAULT_NAME, DEFAULT_SLEEP_BRIGHTNESS, @@ -88,12 +82,9 @@ from custom_components.adaptive_lighting.const import ( UNDO_UPDATE_LISTENER, ) from custom_components.adaptive_lighting.switch import ( - _SUPPORT_OPTS, INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION, - VALID_COLOR_MODES, AdaptiveSwitch, _attributes_have_changed, - _supported_features, color_difference_redmean, create_context, is_our_context, @@ -559,79 +550,6 @@ async def test_turn_on_off_listener_not_tracking_untracked_lights(hass): assert light not in switch.turn_on_off_listener.lights -def test_supported_features(hass): # noqa: C901 - """Test the supported features of a light.""" - - possible_legacy_features = {} - MAX_COMBINATIONS = 4 # maximum number of elements that can be combined - for i in range(1, min(MAX_COMBINATIONS, len(_SUPPORT_OPTS)) + 1): - for combination in itertools.combinations(_SUPPORT_OPTS.keys(), i): - key = "_".join(combination) - value = [v for k, v in _SUPPORT_OPTS.items() if k in combination] - possible_legacy_features[key] = value - - possible_color_modes = {} - for i in range(1, len(VALID_COLOR_MODES) + 1): - for combination in itertools.combinations(VALID_COLOR_MODES.keys(), i): - key = "_".join(combination) - value = [v for k, v in VALID_COLOR_MODES.items() if k in combination] - possible_color_modes[key] = value - - # create a mock HomeAssistant object - hass = MagicMock() - - # iterate over possible legacy features - for feature_key, feature_values in possible_legacy_features.items(): - # _LOGGER.debug(feature_values) - # set the attributes of the mock state object to the possible legacy feature values - state_attrs = {ATTR_SUPPORTED_FEATURES: sum(feature_values)} - hass.states.get.return_value.attributes = state_attrs - - # iterate over possible color modes - for mode_key, mode_values in possible_color_modes.items(): - # _LOGGER.debug(mode_values) - # set the attributes of the mock state object to the possible color mode values - state_attrs[ATTR_SUPPORTED_COLOR_MODES] = set(mode_values) - hass.states.get.return_value.attributes = state_attrs - - # Handle both the new and the old _supported_features. - result = _supported_features(hass, ENTITY_LIGHT) - supported, supports_colors = ( - result if isinstance(result, tuple) else (result, None) - ) - expected_supported = {} if supports_colors is not None else set() - for mode, attr in VALID_COLOR_MODES.items(): - if mode in mode_values: - if supports_colors is None: - expected_supported.add(mode) - else: - expected_supported[attr] = True - if supports_colors is True: - expected_supported[COLOR_MODE_BRIGHTNESS] = True - for opt, value in _SUPPORT_OPTS.items(): - if value in feature_values: - if supports_colors is None: - expected_supported.add(opt) - else: - if supports_colors is True: - expected_supported[COLOR_MODE_BRIGHTNESS] = True - if opt in VALID_COLOR_MODES: - expected_supported[VALID_COLOR_MODES[opt]] = True - elif opt != CONST_COLOR: - expected_supported[opt] = True - if ATTR_MIN_COLOR_TEMP_KELVIN in supported: - supported.pop(ATTR_MIN_COLOR_TEMP_KELVIN) - if ATTR_MAX_COLOR_TEMP_KELVIN in supported: - supported.pop(ATTR_MAX_COLOR_TEMP_KELVIN) - assert supported == expected_supported, ( - f"\nExpected supported: {expected_supported}\n" - f"Actual supported: {supported}\n" - f"feature_values: {feature_values}\n" - f"mode_values: {mode_values}\n" - f"supports_colors: {supports_colors}\n" - ) - - @pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_manual_control(hass): """Test the 'manual control' tracking.""" From 6c001b413292f265b80c7fc73941fe63402a71fb Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 20 Jul 2023 14:42:36 -0700 Subject: [PATCH 0575/1077] Update version to 1.16.0 in manifest.json (#640) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 52f0554c..edb27fe8 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.15.2" + "version": "1.16.0" } From 2575f4ec30dd9fe3b6cf4493530beb8519d940d5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 20 Jul 2023 17:12:41 -0700 Subject: [PATCH 0576/1077] Only adapt if switch if on in _service_interceptor_turn_on_handler (#642) * Do not adapt if switch is off in _service_interceptor_turn_on_handler * Bump to 1.16.1 --- custom_components/adaptive_lighting/manifest.json | 2 +- custom_components/adaptive_lighting/switch.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index edb27fe8..91be72a6 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.16.0" + "version": "1.16.1" } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 2bc17862..e5a031c6 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1817,6 +1817,9 @@ class TurnOnOffListener: # This might be a light that is not managed by this AL instance. return + if not adaptive_switch.is_on: + return + if entity_id not in adaptive_switch.lights: return From 49857632cc2184d5cac2bf717631a2427481f61a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 21 Jul 2023 14:46:18 -0700 Subject: [PATCH 0577/1077] Fix multiple switches controlling one light (reactive path) (#644) * Fix multiple switches controlling one light * WIP * Keep length in AdaptationData * Do not proceed if there is nothing to do * Simplify * Set tasks correctly * log more * WIP * realize that intercept and double switch not possible * Remove which from find_switch_for_lights * Rephrase * Check which one to cancel * length -> max_length * Implement test_two_switches_for_single_light * Clean up * Simplify is_color_brightness_or_both * Improve test * no wait * logging * just block * make test such that it fails on main --- .../adaptive_lighting/adaptation_utils.py | 45 ++++++++++- custom_components/adaptive_lighting/switch.py | 81 ++++++++++++++++--- tests/test_switch.py | 76 ++++++++++++++++- 3 files changed, 183 insertions(+), 19 deletions(-) diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py index 599805a6..a5f33a30 100644 --- a/custom_components/adaptive_lighting/adaptation_utils.py +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -1,7 +1,8 @@ """Utility functions for adaptation commands.""" from collections.abc import AsyncGenerator from dataclasses import dataclass -from typing import Any +import logging +from typing import Any, Literal from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -18,6 +19,8 @@ from homeassistant.components.light import ( from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, HomeAssistant, State +_LOGGER = logging.getLogger(__name__) + COLOR_ATTRS = { # Should ATTR_PROFILE be in here? ATTR_COLOR_NAME, ATTR_COLOR_TEMP_KELVIN, @@ -98,7 +101,7 @@ def _has_relevant_service_data_attributes(service_data: ServiceData) -> bool: async def _create_service_call_data_iterator( hass: HomeAssistant, service_datas: list[ServiceData], - filter_by_state: bool = False, + filter_by_state: bool, ) -> AsyncGenerator[ServiceData, None]: """Enumerates and filters a list of service datas on the fly. @@ -133,6 +136,8 @@ class AdaptationData: context: Context sleep_time: float service_call_datas: AsyncGenerator[ServiceData, None] + max_length: int + which: Literal["brightness", "color", "both"] initial_sleep: bool = False async def next_service_call_data(self) -> ServiceData | None: @@ -140,6 +145,26 @@ class AdaptationData: return await anext(self.service_call_datas, None) +class NoColorOrBrightnessInServiceData(Exception): + """Exception raised when no color or brightness attributes are found in service data.""" + + +def is_color_brightness_or_both( + service_data: ServiceData, +) -> Literal["brightness", "color", "both"]: + """Extract the 'which' attribute from the service data.""" + has_brightness = ATTR_BRIGHTNESS in service_data + has_color = any(attr in service_data for attr in COLOR_ATTRS) + if has_brightness and has_color: + return "both" + if has_brightness: + return "brightness" + if has_color: + return "color" + msg = f"Invalid service_data, no brightness or color attributes found: {service_data=}" + raise NoColorOrBrightnessInServiceData(msg) + + def prepare_adaptation_data( hass: HomeAssistant, entity_id: str, @@ -150,7 +175,12 @@ def prepare_adaptation_data( split: bool, filter_by_state: bool, ) -> AdaptationData: - "Prepares a data object carrying all data required to execute an adaptation." + """Prepares a data object carrying all data required to execute an adaptation.""" + _LOGGER.debug( + "Preparing adaptation data for %s with service data %s", + entity_id, + service_data, + ) service_datas = ( [service_data] if not split else _split_service_call_data(service_data) ) @@ -163,4 +193,11 @@ def prepare_adaptation_data( hass, service_datas, filter_by_state ) - return AdaptationData(entity_id, context, sleep_time, service_data_iterator) + return AdaptationData( + entity_id, + context, + sleep_time=sleep_time, + service_call_datas=service_data_iterator, + max_length=len(service_datas), + which=is_color_brightness_or_both(service_data), + ) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index e5a031c6..b07a3c8e 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1071,7 +1071,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): adapt_color: bool | None = None, prefer_rgb_color: bool | None = None, context: Context | None = None, - ) -> AdaptationData: + ) -> AdaptationData | None: if transition is None: transition = self._transition if adapt_brightness is None: @@ -1081,6 +1081,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if prefer_rgb_color is None: prefer_rgb_color = self._prefer_rgb_color + if not adapt_color and not adapt_brightness: + _LOGGER.debug( + "%s: Skipping adaptation of %s because both adapt_brightness and" + " adapt_color are False", + self._name, + light, + ) + return None + # The switch might be off and not have _settings set. self._settings = self._sun_light_settings.get_settings( self.sleep_mode_switch.is_on, transition @@ -1150,7 +1159,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self.turn_on_off_listener.is_proactively_adapting(context.parent_id): # Skip if adaptation was already executed by the service call interceptor - _LOGGER.debug("Skipping reactive adaptation of %s", context.parent_id) + _LOGGER.debug( + "%s: Skipping reactive adaptation of %s", self._name, context.parent_id + ) return data = await self.prepare_adaptation_data( @@ -1161,6 +1172,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): prefer_rgb_color, context, ) + if data is None: + return None # nothing to adapt await self.execute_cancellable_adaptation_calls(data) @@ -1209,15 +1222,32 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): to cancel an ongoing adaptation when a light is turned off. """ # Prevent overlap of multiple adaptation sequences - self.turn_on_off_listener.cancel_ongoing_adaptation_calls(data.entity_id) - + listener = self.turn_on_off_listener + listener.cancel_ongoing_adaptation_calls(data.entity_id, which=data.which) + _LOGGER.debug( + "%s: execute_cancellable_adaptation_calls with data: %s" + "adaptation_tasks_brightness: %s" + "adaptation_tasks_color: %s", + self._name, + data, + listener.adaptation_tasks_brightness, + listener.adaptation_tasks_color, + ) # Execute adaptation calls within a task try: task = asyncio.ensure_future(self._execute_adaptation_calls(data)) - self.turn_on_off_listener.adaptation_tasks[data.entity_id] = task + if data.which in ("both", "brightness"): + listener.adaptation_tasks_brightness[data.entity_id] = task + if data.which in ("both", "color"): + listener.adaptation_tasks_color[data.entity_id] = task await task except asyncio.CancelledError: - _LOGGER.debug("Ongoing adaptation of %s cancelled", data.entity_id) + _LOGGER.debug( + "%s: Ongoing adaptation of %s cancelled, with AdaptationData: %s", + self._name, + data.entity_id, + data, + ) async def _update_attrs_and_maybe_adapt_lights( self, @@ -1699,7 +1729,8 @@ class TurnOnOffListener: # Track last 'service_data' to 'light.turn_on' resulting from this integration self.last_service_data: dict[str, dict[str, Any]] = {} # Track ongoing split adaptations to be able to cancel them - self.adaptation_tasks: dict[str, asyncio.Task] = {} + self.adaptation_tasks_brightness: dict[str, asyncio.Task] = {} + self.adaptation_tasks_color: dict[str, asyncio.Task] = {} # Track auto reset of manual_control self.auto_reset_manual_control_timers: dict[str, _AsyncSingleShotTimer] = {} @@ -1815,6 +1846,11 @@ class TurnOnOffListener: adaptive_switch = find_switch_for_lights(self.hass, [entity_id]) except NoSwitchFoundError: # This might be a light that is not managed by this AL instance. + _LOGGER.debug( + "No (or multiple) adaptive switch(es) found for entity %s," + " skipping adaptation by intercepting service call", + entity_id, + ) return if not adaptive_switch.is_on: @@ -1851,6 +1887,8 @@ class TurnOnOffListener: adapt_brightness, adapt_color, ) + # if adaptation_data is None: + # return # Take first adaptation item to apply it to this service call first_service_data = await adaptation_data.next_service_call_data() @@ -1970,10 +2008,31 @@ class TurnOnOffListener: self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset) - def cancel_ongoing_adaptation_calls(self, light_id: str): - """Cancels an ongoing sequence of adaptation service calls for a specific light entity.""" - if (previous_task := self.adaptation_tasks.get(light_id)) is not None: - previous_task.cancel() + def cancel_ongoing_adaptation_calls( + self, light_id: str, which: Literal["color", "brightness", "both"] = "both" + ): + """Cancel ongoing adaptation service calls for a specific light entity.""" + brightness_task = self.adaptation_tasks_brightness.get(light_id) + color_task = self.adaptation_tasks_color.get(light_id) + if which in ("both", "brightness") and brightness_task is not None: + _LOGGER.debug( + "Cancelled ongoing brightness adaptation calls (%s) for '%s'", + brightness_task, + light_id, + ) + brightness_task.cancel() + if ( + which in ("both", "color") + and color_task is not None + and color_task is not brightness_task + ): + _LOGGER.debug( + "Cancelled ongoing color adaptation calls (%s) for '%s'", + color_task, + light_id, + ) + # color_task might be the same as brightness_task + color_task.cancel() def reset(self, *lights, reset_manual_control=True) -> None: """Reset the 'manual_control' status of the lights.""" diff --git a/tests/test_switch.py b/tests/test_switch.py index 2136b707..0c42b74d 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1351,7 +1351,9 @@ async def test_cancellable_service_calls_task(hass): _, switch = await setup_switch(hass, {CONF_SEPARATE_TURN_ON_COMMANDS: True}) context = switch.create_context("test") - assert switch.turn_on_off_listener.adaptation_tasks.get(light.entity_id) is None + assert ( + switch.turn_on_off_listener.adaptation_tasks_color.get(light.entity_id) is None + ) service_data = { ATTR_BRIGHTNESS: 10, @@ -1362,11 +1364,15 @@ async def test_cancellable_service_calls_task(hass): light.entity_id, context, 0, - _create_service_call_data_iterator(hass, [service_data]), + _create_service_call_data_iterator(hass, [service_data], False), + max_length=1, + which="both", ) await switch.execute_cancellable_adaptation_calls(adaptation_data) - task = switch.turn_on_off_listener.adaptation_tasks.get(light.entity_id) + task = switch.turn_on_off_listener.adaptation_tasks_brightness.get(light.entity_id) + task2 = switch.turn_on_off_listener.adaptation_tasks_color.get(light.entity_id) + assert task is task2 assert task is not None assert task.done() @@ -1378,7 +1384,7 @@ async def test_service_calls_task_cancellation(hass): entity_id = "test_id" task = asyncio.ensure_future(asyncio.sleep(1)) - switch.turn_on_off_listener.adaptation_tasks[entity_id] = task + switch.turn_on_off_listener.adaptation_tasks_brightness[entity_id] = task switch.turn_on_off_listener.cancel_ongoing_adaptation_calls(entity_id) @@ -1553,3 +1559,65 @@ async def test_proactive_adaptation_transition_override(hass): # Cleanup switch.turn_on_off_listener.cancel_ongoing_adaptation_calls(ENTITY_LIGHT3) + + +async def test_two_switches_for_single_light(hass): + """Test the case where someone has two switches for a single light. + + One switch for brightness and another for color. + """ + extra_conf = {INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True} + switch1, (light1, *_) = await setup_lights_and_switch( + hass, extra_conf | {CONF_NAME: "switch1"}, all_lights=True + ) + switch2, (light2, *_) = await setup_lights_and_switch( + hass, extra_conf | {CONF_NAME: "switch2"}, all_lights=True + ) + assert light1 is light2 + + # One switch controls brightness the other color + await switch1.adapt_color_switch.async_turn_off() + await switch2.adapt_brightness_switch.async_turn_off() + + assert switch1.adapt_brightness_switch.is_on + assert switch2.adapt_color_switch.is_on + + async def turn_light(state, **kwargs): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON if state else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs}, + blocking=True, + ) + await hass.async_block_till_done() + _LOGGER.debug("Turn light %s, to %s", state, kwargs) + + def increased_brightness(): + return (light1._attr_brightness + 100) % 255 + + def increased_color_temp(): + return max( + (light1._attr_color_temp + 100) % light1.max_color_temp_kelvin, + light1.min_color_temp_kelvin, + ) + + assert light1.is_on + await turn_light(True, brightness=increased_brightness()) + await turn_light(True, color_temp=increased_color_temp()) + + attrs = hass.states.get(light1.entity_id).attributes + before_brightness = attrs[ATTR_BRIGHTNESS] + before_color_temp = attrs[ATTR_COLOR_TEMP_KELVIN] + + # Turn off "light1" + await turn_light(False) + + # Turn on "light1" + await turn_light(True) + + # Assert that the brightness and color temp have changed + attrs = hass.states.get(light1.entity_id).attributes + after_brightness = attrs[ATTR_BRIGHTNESS] + after_color_temp = attrs[ATTR_COLOR_TEMP_KELVIN] + assert before_brightness != after_brightness + assert before_color_temp != after_color_temp From 30a310c514cbd600ddd3c6771210ddd0dc1ffdf0 Mon Sep 17 00:00:00 2001 From: Mario Guggenberger Date: Fri, 21 Jul 2023 23:54:38 +0200 Subject: [PATCH 0578/1077] fix: overlapping adaptations (#646) Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/switch.py | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index b07a3c8e..0e0da07f 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -382,6 +382,8 @@ async def handle_change_switch_settings( defaults=defaults, ) + switch._update_time_interval_listener() + _LOGGER.debug( "Called 'adaptive_lighting.change_switch_settings' service with '%s'", data, @@ -787,7 +789,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): data = validate(config_entry) self._name = data[CONF_NAME] - self._interval = data[CONF_INTERVAL] + self._interval: timedelta = data[CONF_INTERVAL] self.lights: list[str] = data[CONF_LIGHTS] # backup data for use in change_switch_settings "configuration" CONF_USE_DEFAULTS @@ -815,6 +817,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set and unset tracker in async_turn_on and async_turn_off self.remove_listeners = [] + self.remove_interval: Callable[[], None] = lambda: None + _LOGGER.debug( "%s: Setting up with '%s'," " config_entry.data: '%s'," @@ -961,16 +965,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): assert not self.remove_listeners - remove_interval = async_track_time_interval( - self.hass, self._async_update_at_interval, self._interval - ) + self._update_time_interval_listener() + remove_sleep = async_track_state_change_event( self.hass, self.sleep_mode_switch.entity_id, self._sleep_mode_switch_state_event, ) - self.remove_listeners.extend([remove_interval, remove_sleep]) + self.remove_listeners.append(remove_sleep) if self.lights: self._expand_light_groups() @@ -979,7 +982,36 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) self.remove_listeners.append(remove_state) + def _update_time_interval_listener(self) -> None: + """Create or recreate the adaptation interval listener. + + Recreation is necessary when the configuration has changed (e.g., `send_split_delay`). + """ + self._remove_interval_listener() + + # An adaptation takes a little longer than its nominal duration due processing overhead, + # so we factor this in to avoid overlapping adaptations. Since this is a constant value, + # it might not cover all cases, but if large enough, it covers most. + # Ideally, the interval and adaptation are a coupled process where a finished adaptation + # triggers the next, but that requires a larger architectural change. + processing_overhead_time = 0.5 + + adaptation_interval = ( + self._interval + + timedelta(milliseconds=self._send_split_delay) + + timedelta(seconds=processing_overhead_time) + ) + + self.remove_interval = async_track_time_interval( + self.hass, self._async_update_at_interval, adaptation_interval + ) + + def _remove_interval_listener(self) -> None: + self.remove_interval() + def _remove_listeners(self) -> None: + self._remove_interval_listener() + while self.remove_listeners: remove_listener = self.remove_listeners.pop() remove_listener() From 52c7e4b1d5abfb0bf07cc919be027fb7120b445b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 21 Jul 2023 14:56:44 -0700 Subject: [PATCH 0579/1077] Bump to v1.16.2 in manifest.json (#647) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 91be72a6..5e121064 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.16.1" + "version": "1.16.2" } From db40b9af6b86fd42b8d1317a7f582f9cae807137 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 21 Jul 2023 18:08:43 -0700 Subject: [PATCH 0580/1077] Only build Docker image on main (#648) --- .github/workflows/docker-build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 7326175a..1f6099d0 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -3,8 +3,7 @@ name: docker on: push: branches: - - "master" - pull_request: + - "main" jobs: docker: From fee90250eef694d62e9fbafbc1b07d53741e9e82 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 22 Jul 2023 12:34:20 -0700 Subject: [PATCH 0581/1077] Better logging and prevent KeyError in state_changed_event_listener (#650) * Better logging * Remove useless and erroneous logging statement * Bump to 1.16.3 --- .../adaptive_lighting/manifest.json | 2 +- custom_components/adaptive_lighting/switch.py | 24 +++++++------------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 5e121064..fcf644cd 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.16.2" + "version": "1.16.3" } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 0e0da07f..fbf6c79c 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1212,10 +1212,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _execute_adaptation_calls(self, data: AdaptationData): """Executes a sequence of adaptation service calls for the given service datas.""" - index = 0 - while True: + for index in range(data.max_length): is_first_call = index == 0 - index += 1 # Sleep between multiple service calls. if not is_first_call or data.initial_sleep: @@ -1257,13 +1255,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): listener = self.turn_on_off_listener listener.cancel_ongoing_adaptation_calls(data.entity_id, which=data.which) _LOGGER.debug( - "%s: execute_cancellable_adaptation_calls with data: %s" - "adaptation_tasks_brightness: %s" - "adaptation_tasks_color: %s", + "%s: execute_cancellable_adaptation_calls with data: %s", self._name, data, - listener.adaptation_tasks_brightness, - listener.adaptation_tasks_color, ) # Execute adaptation calls within a task try: @@ -1290,9 +1284,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) -> None: assert context is not None _LOGGER.debug( - "%s: '_update_attrs_and_maybe_adapt_lights' called with context.id='%s'", + "%s: '_update_attrs_and_maybe_adapt_lights' called with context.id='%s'" + " lights: '%s', transition: '%s', force: '%s'", self._name, context.id, + lights, + transition, + force, ) assert self.is_on self._settings.update( @@ -1919,8 +1917,8 @@ class TurnOnOffListener: adapt_brightness, adapt_color, ) - # if adaptation_data is None: - # return + if adaptation_data is None: + return # Take first adaptation item to apply it to this service call first_service_data = await adaptation_data.next_service_call_data() @@ -2193,10 +2191,6 @@ class TurnOnOffListener: entity_id, ) self.last_state_change[entity_id] = [new_state] - _LOGGER.debug( - "Last transition: %s", - self.last_service_data[entity_id].get(ATTR_TRANSITION), - ) self.start_transition_timer(entity_id) elif old_state is not None: self.last_state_change[entity_id].append(new_state) From 69592938dbbb29dcdc6f43d6a756409aeb838913 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 22 Jul 2023 17:57:49 -0700 Subject: [PATCH 0582/1077] Rename TurnOnOffListener to AdaptiveLightingManager (#654) * Rename TurnOnOffListener to AdaptiveLightingManager * Bump Python version --- .github/workflows/update-readme.yml | 2 +- .../adaptive_lighting/__init__.py | 8 +- custom_components/adaptive_lighting/const.py | 2 +- custom_components/adaptive_lighting/switch.py | 87 ++++++++---------- tests/test_switch.py | 90 +++++++++---------- 5 files changed, 85 insertions(+), 104 deletions(-) diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index 2c2754fb..a31d556e 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -20,7 +20,7 @@ jobs: - name: Install Home Assistant uses: ./.github/workflows/install_dependencies with: - python-version: "3.10" + python-version: "3.11" - name: Install markdown-code-runner and README code dependencies run: | diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index f985e8c5..c4187fa7 100755 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -10,7 +10,7 @@ import voluptuous as vol from .const import ( _DOMAIN_SCHEMA, - ATTR_TURN_ON_OFF_LISTENER, + ATTR_ADAPTIVE_LIGHTING_MANAGER, CONF_NAME, DOMAIN, UNDO_UPDATE_LISTENER, @@ -86,10 +86,10 @@ async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool: if unload_ok: data.pop(config_entry.entry_id) - if len(data) == 1 and ATTR_TURN_ON_OFF_LISTENER in data: + if len(data) == 1 and ATTR_ADAPTIVE_LIGHTING_MANAGER in data: # no more config_entries - turn_on_off_listener = data.pop(ATTR_TURN_ON_OFF_LISTENER) - turn_on_off_listener.disable() + manager = data.pop(ATTR_ADAPTIVE_LIGHTING_MANAGER) + manager.disable() if not data: hass.data.pop(DOMAIN) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 241689fc..15ce097b 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -190,7 +190,7 @@ DOCS[CONF_SKIP_REDUNDANT_COMMANDS] = ( SLEEP_MODE_SWITCH = "sleep_mode_switch" ADAPT_COLOR_SWITCH = "adapt_color_switch" ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch" -ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener" +ATTR_ADAPTIVE_LIGHTING_MANAGER = "manager" UNDO_UPDATE_LISTENER = "undo_update_listener" NONE_STR = "None" ATTR_ADAPT_COLOR = "adapt_color" diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index fbf6c79c..7deddcd8 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -102,7 +102,7 @@ from .const import ( ADAPT_COLOR_SWITCH, ATTR_ADAPT_BRIGHTNESS, ATTR_ADAPT_COLOR, - ATTR_TURN_ON_OFF_LISTENER, + ATTR_ADAPTIVE_LIGHTING_MANAGER, CONF_ADAPT_DELAY, CONF_ADAPT_UNTIL_SLEEP, CONF_AUTORESET_CONTROL, @@ -390,7 +390,7 @@ async def handle_change_switch_settings( ) all_lights = switch.lights # pylint: disable=protected-access - switch.turn_on_off_listener.reset(*all_lights, reset_manual_control=False) + switch.manager.reset(*all_lights, reset_manual_control=False) if switch.is_on: await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access all_lights, @@ -412,7 +412,7 @@ def _fire_manual_control_event( switch.entity_id, light, ) - switch.turn_on_off_listener.mark_as_manual_control(light) + switch.manager.mark_as_manual_control(light) fire( f"{DOMAIN}.manual_control", {ATTR_ENTITY_ID: light, SWITCH_DOMAIN: switch.entity_id}, @@ -427,9 +427,11 @@ async def async_setup_entry( data = hass.data[DOMAIN] assert config_entry.entry_id in data - if ATTR_TURN_ON_OFF_LISTENER not in data: - data[ATTR_TURN_ON_OFF_LISTENER] = TurnOnOffListener(hass, config_entry) - turn_on_off_listener: TurnOnOffListener = data[ATTR_TURN_ON_OFF_LISTENER] + if ATTR_ADAPTIVE_LIGHTING_MANAGER not in data: + data[ATTR_ADAPTIVE_LIGHTING_MANAGER] = AdaptiveLightingManager( + hass, config_entry + ) + manager: AdaptiveLightingManager = data[ATTR_ADAPTIVE_LIGHTING_MANAGER] sleep_mode_switch = SimpleSwitch( "Sleep Mode", False, hass, config_entry, ICON_SLEEP ) @@ -442,7 +444,7 @@ async def async_setup_entry( switch = AdaptiveSwitch( hass, config_entry, - turn_on_off_listener, + manager, sleep_mode_switch, adapt_color_switch, adapt_brightness_switch, @@ -476,7 +478,7 @@ async def async_setup_entry( all_lights = switch.lights else: all_lights = _expand_light_groups(switch.hass, lights) - switch.turn_on_off_listener.lights.update(all_lights) + switch.manager.lights.update(all_lights) for light in all_lights: if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light): await switch._adapt_light( # pylint: disable=protected-access @@ -509,7 +511,7 @@ async def async_setup_entry( for light in all_lights: _fire_manual_control_event(switch, light, service_call.context) else: - switch.turn_on_off_listener.reset(*all_lights) + switch.manager.reset(*all_lights) if switch.is_on: # pylint: disable=protected-access await switch._update_attrs_and_maybe_adapt_lights( @@ -594,7 +596,7 @@ def match_switch_state_event(event: Event, from_or_to_state: list[str]): def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]: all_lights = set() - turn_on_off_listener = hass.data[DOMAIN][ATTR_TURN_ON_OFF_LISTENER] + manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER] for light in lights: state = hass.states.get(light) if state is None: @@ -602,7 +604,7 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]: all_lights.add(light) elif "entity_id" in state.attributes: # it's a light group group = state.attributes["entity_id"] - turn_on_off_listener.lights.discard(light) + manager.lights.discard(light) all_lights.update(group) _LOGGER.debug("Expanded %s to %s", light, group) else: @@ -773,7 +775,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self, hass, config_entry: ConfigEntry, - turn_on_off_listener: TurnOnOffListener, + manager: AdaptiveLightingManager, sleep_mode_switch: SimpleSwitch, adapt_color_switch: SimpleSwitch, adapt_brightness_switch: SimpleSwitch, @@ -781,7 +783,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Initialize the Adaptive Lighting switch.""" # Set attributes that can't be modified during runtime self.hass = hass - self.turn_on_off_listener = turn_on_off_listener + self.manager = manager self.sleep_mode_switch = sleep_mode_switch self.adapt_color_switch = adapt_color_switch self.adapt_brightness_switch = adapt_brightness_switch @@ -951,8 +953,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): def _expand_light_groups(self) -> None: all_lights = _expand_light_groups(self.hass, self.lights) - self.turn_on_off_listener.lights.update(all_lights) - self.turn_on_off_listener.set_auto_reset_manual_control_times( + self.manager.lights.update(all_lights) + self.manager.set_auto_reset_manual_control_times( all_lights, self._auto_reset_manual_control_time ) self.lights = list(all_lights) @@ -1030,12 +1032,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): extra_state_attributes[key] = None return extra_state_attributes extra_state_attributes["manual_control"] = [ - light - for light in self.lights - if self.turn_on_off_listener.manual_control.get(light) + light for light in self.lights if self.manager.manual_control.get(light) ] extra_state_attributes.update(self._settings) - timers = self.turn_on_off_listener.auto_reset_manual_control_timers + timers = self.manager.auto_reset_manual_control_timers extra_state_attributes["autoreset_time_remaining"] = { light: time for light in self.lights @@ -1047,16 +1047,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self, which: str = "default", parent: Context | None = None ) -> Context: """Create a context that identifies this Adaptive Lighting instance.""" - # Right now the highest number of each context_id it can create is - # 'adapt_lgt:XXXX:turn_on:*************' - # 'adapt_lgt:XXXX:interval:************' - # 'adapt_lgt:XXXX:adapt_lights:********' - # 'adapt_lgt:XXXX:sleep:***************' - # 'adapt_lgt:XXXX:light_event:*********' - # 'adapt_lgt:XXXX:service:*************' - # The smallest space we have is for adapt_lights, which has - # 8 characters. In base85 encoding, that's enough space to hold values - # up to 2**48 - 1, which should give us plenty of calls before we wrap. context = create_context(self._name, which, self._context_cnt, parent=parent) self._context_cnt += 1 return context @@ -1071,7 +1061,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self.is_on: return self._state = True - self.turn_on_off_listener.reset(*self.lights) + self.manager.reset(*self.lights) await self._setup_listeners() if adapt_lights: await self._update_attrs_and_maybe_adapt_lights( @@ -1086,7 +1076,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return self._state = False self._remove_listeners() - self.turn_on_off_listener.reset(*self.lights) + self.manager.reset(*self.lights) async def _async_update_at_interval(self, now=None) -> None: await self._update_attrs_and_maybe_adapt_lights( @@ -1162,7 +1152,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context = context or self.create_context("adapt_lights") - self.turn_on_off_listener.last_service_data[light] = service_data + self.manager.last_service_data[light] = service_data return prepare_adaptation_data( self.hass, @@ -1189,7 +1179,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug("%s: '%s' is locked", self._name, light) return - if self.turn_on_off_listener.is_proactively_adapting(context.parent_id): + if self.manager.is_proactively_adapting(context.parent_id): # Skip if adaptation was already executed by the service call interceptor _LOGGER.debug( "%s: Skipping reactive adaptation of %s", self._name, context.parent_id @@ -1252,7 +1242,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): to cancel an ongoing adaptation when a light is turned off. """ # Prevent overlap of multiple adaptation sequences - listener = self.turn_on_off_listener + listener = self.manager listener.cancel_ongoing_adaptation_calls(data.entity_id, which=data.which) _LOGGER.debug( "%s: execute_cancellable_adaptation_calls with data: %s", @@ -1309,7 +1299,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return for light in lights: # Don't adapt lights that haven't finished prior transitions. - timer = self.turn_on_off_listener.transition_timers.get(light) + timer = self.manager.transition_timers.get(light) if timer is not None and timer.is_running(): _LOGGER.debug( "%s: Light '%s' is still transitioning", @@ -1352,7 +1342,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if not is_on(self.hass, light): continue - manually_controlled = self.turn_on_off_listener.is_manually_controlled( + manually_controlled = self.manager.is_manually_controlled( self, light, force, @@ -1363,7 +1353,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): significant_change = ( self._detect_non_ha_changes and not force - and await self.turn_on_off_listener.significant_change( + and await self.manager.significant_change( self, light, adapt_brightness, @@ -1393,7 +1383,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): "%s: _sleep_mode_switch_state_event, event: '%s'", self._name, event ) # Reset the manually controlled status when the "sleep mode" changes - self.turn_on_off_listener.reset(*self.lights) + self.manager.reset(*self.lights) await self._update_attrs_and_maybe_adapt_lights( transition=self._sleep_transition, force=True, @@ -1417,13 +1407,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): event.context.id, ) - if ( - event.context.parent_id - and not self.turn_on_off_listener.is_proactively_adapting( - event.context.id - ) + if event.context.parent_id and not self.manager.is_proactively_adapting( + event.context.id ): - self.turn_on_off_listener.reset(entity_id, reset_manual_control=False) + self.manager.reset(entity_id, reset_manual_control=False) # Tracks 'off' → 'on' state changes self._off_to_on_event[entity_id] = event @@ -1431,7 +1418,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if lock is None: lock = self._locks[entity_id] = asyncio.Lock() async with lock: - if await self.turn_on_off_listener.maybe_cancel_adjusting( + if await self.manager.maybe_cancel_adjusting( entity_id, off_to_on_event=event, on_to_off_event=self._on_to_off_event.get(entity_id), @@ -1471,7 +1458,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ): # Tracks 'off' → 'on' state changes self._on_to_off_event[entity_id] = event - self.turn_on_off_listener.reset(entity_id) + self.manager.reset(entity_id) class SimpleSwitch(SwitchEntity, RestoreEntity): @@ -1737,11 +1724,11 @@ class SunLightSettings: } -class TurnOnOffListener: +class AdaptiveLightingManager: """Track 'light.turn_off' and 'light.turn_on' service calls.""" def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry): - """Initialize the TurnOnOffListener that is shared among all switches.""" + """Initialize the AdaptiveLightingManager that is shared among all switches.""" self.hass = hass data = validate(config_entry) self.lights = set() @@ -2177,7 +2164,7 @@ class TurnOnOffListener: and old_state[0].context.id == new_state.context.id ): _LOGGER.debug( - "TurnOnOffListener: State change event of '%s' is already" + "AdaptiveLightingManager: State change event of '%s' is already" " in 'self.last_state_change' (%s)" " adding this state also", entity_id, @@ -2186,7 +2173,7 @@ class TurnOnOffListener: self.last_state_change[entity_id].append(new_state) else: _LOGGER.debug( - "TurnOnOffListener: New adapt '%s' found for %s", + "AdaptiveLightingManager: New adapt '%s' found for %s", new_state, entity_id, ) diff --git a/tests/test_switch.py b/tests/test_switch.py index 0c42b74d..f356c10a 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -55,7 +55,7 @@ from custom_components.adaptive_lighting.adaptation_utils import ( from custom_components.adaptive_lighting.const import ( ADAPT_BRIGHTNESS_SWITCH, ADAPT_COLOR_SWITCH, - ATTR_TURN_ON_OFF_LISTENER, + ATTR_ADAPTIVE_LIGHTING_MANAGER, CONF_AUTORESET_CONTROL, CONF_DETECT_NON_HA_CHANGES, CONF_INITIAL_TRANSITION, @@ -321,7 +321,7 @@ async def test_adaptive_lighting_switches(hass): ENTITY_ADAPT_COLOR_SWITCH, ENTITY_ADAPT_BRIGHTNESS_SWITCH, } - assert ATTR_TURN_ON_OFF_LISTENER in hass.data[DOMAIN] + assert ATTR_ADAPTIVE_LIGHTING_MANAGER in hass.data[DOMAIN] assert entry.entry_id in hass.data[DOMAIN] assert len(hass.data[DOMAIN].keys()) == 2 @@ -446,9 +446,7 @@ async def test_light_settings(hass): assert state.attributes[ATTR_BRIGHTNESS] == round( 255 * switch._settings[ATTR_BRIGHTNESS_PCT] / 100 ) - last_service_data = switch.turn_on_off_listener.last_service_data[ - state.entity_id - ] + last_service_data = switch.manager.last_service_data[state.entity_id] assert state.attributes[ATTR_BRIGHTNESS] == last_service_data[ATTR_BRIGHTNESS] assert ( state.attributes[ATTR_COLOR_TEMP_KELVIN] @@ -483,9 +481,7 @@ async def test_light_settings(hass): return [hass.states.get(light) for light in lights] def assert_expected_color_temp(state): - last_service_data = switch.turn_on_off_listener.last_service_data[ - state.entity_id - ] + last_service_data = switch.manager.last_service_data[state.entity_id] assert ( state.attributes[ATTR_COLOR_TEMP_KELVIN] == last_service_data[ATTR_COLOR_TEMP_KELVIN] @@ -531,7 +527,7 @@ async def test_light_settings(hass): @pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) -async def test_turn_on_off_listener_not_tracking_untracked_lights(hass): +async def test_manager_not_tracking_untracked_lights(hass): """Test that lights that are not in a Adaptive Lighting switch aren't tracked.""" switch, _ = await setup_lights_and_switch(hass) light = "light.kitchen_lights" @@ -547,7 +543,7 @@ async def test_turn_on_off_listener_not_tracking_untracked_lights(hass): context=switch.create_context("test") ) await hass.async_block_till_done() - assert light not in switch.turn_on_off_listener.lights + assert light not in switch.manager.lights @pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) @@ -555,7 +551,7 @@ async def test_manual_control(hass): """Test the 'manual control' tracking.""" switch, (light, *_) = await setup_lights_and_switch(hass) context = switch.create_context("test") # needs to be passed to update method - manual_control = switch.turn_on_off_listener.manual_control + manual_control = switch.manager.manual_control async def update(): await switch._update_attrs_and_maybe_adapt_lights(transition=0, context=context) @@ -707,7 +703,7 @@ async def test_auto_reset_manual_control(hass): hass, {CONF_AUTORESET_CONTROL: 0.1} ) context = switch.create_context("test") # needs to be passed to update method - manual_control = switch.turn_on_off_listener.manual_control + manual_control = switch.manager.manual_control async def update(): await switch._update_attrs_and_maybe_adapt_lights(transition=0, context=context) @@ -846,7 +842,7 @@ async def test_switch_off_on_off(hass): # Turn light off with transition await turn_light(False, transition=1) - assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + assert not switch.manager.manual_control[ENTITY_LIGHT] # Set state to on after a second (like happens IRL) await asyncio.sleep(1e-3) hass.states.async_set(ENTITY_LIGHT, STATE_ON) @@ -855,8 +851,8 @@ async def test_switch_off_on_off(hass): hass.states.async_set(ENTITY_LIGHT, STATE_OFF) # Now we test whether the sleep task is there - assert ENTITY_LIGHT in switch.turn_on_off_listener.sleep_tasks - sleep_task = switch.turn_on_off_listener.sleep_tasks[ENTITY_LIGHT] + assert ENTITY_LIGHT in switch.manager.sleep_tasks + sleep_task = switch.manager.sleep_tasks[ENTITY_LIGHT] assert not sleep_task.cancelled() # A 'light.turn_on' event should cancel that task @@ -936,7 +932,7 @@ def test_attributes_have_changed(): @pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_state_change_handlers(hass): """ - Test TurnOnOffListener's EVENT_STATE_CHANGED listener. + Test AdaptiveLightingManager's EVENT_STATE_CHANGED listener. ====================== Sequence of events: 1. Transition from sleep mode to normal. @@ -958,7 +954,7 @@ async def test_state_change_handlers(hass): ENTITY_LIGHT, "on", {ATTR_BRIGHTNESS: val, ATTR_SUPPORTED_FEATURES: 1} ) await hass.async_block_till_done() - # Call code in TurnOnOffListener + # Call code in AdaptiveLightingManager hass.bus.async_fire( EVENT_STATE_CHANGED, { @@ -996,10 +992,10 @@ async def test_state_change_handlers(hass): blocking=True, ) await hass.async_block_till_done() - assert switch.turn_on_off_listener.last_state_change.get(ENTITY_LIGHT) - assert len(switch.turn_on_off_listener.last_state_change[ENTITY_LIGHT]) == 1 - assert not switch.turn_on_off_listener.transition_timers.get(ENTITY_LIGHT) - last_service_data = deepcopy(switch.turn_on_off_listener.last_service_data) + assert switch.manager.last_state_change.get(ENTITY_LIGHT) + assert len(switch.manager.last_state_change[ENTITY_LIGHT]) == 1 + assert not switch.manager.transition_timers.get(ENTITY_LIGHT) + last_service_data = deepcopy(switch.manager.last_service_data) assert last_service_data.get(ENTITY_LIGHT) # 2 Adapt from sleep with a 'transition'. @@ -1008,7 +1004,7 @@ async def test_state_change_handlers(hass): force=False, transition=0, context=context ) await hass.async_block_till_done() - current_service_data = switch.turn_on_off_listener.last_service_data + current_service_data = switch.manager.last_service_data assert current_service_data != last_service_data for light in switch.lights: @@ -1029,7 +1025,7 @@ async def test_state_change_handlers(hass): ), }, ) - assert not switch.turn_on_off_listener.transition_timers.get(light) + assert not switch.manager.transition_timers.get(light) # 2.3 Refire and overwrite the original state_changed event with our 'transition' hass.bus.async_fire( @@ -1048,7 +1044,7 @@ async def test_state_change_handlers(hass): ) await hass.async_block_till_done() # Assert our transition timer was created. - assert switch.turn_on_off_listener.transition_timers.get(light) + assert switch.manager.transition_timers.get(light) # 2.5 Simulate a transition. There's no other way to do this in the demo. events = create_transition_events( light=light, @@ -1057,7 +1053,7 @@ async def test_state_change_handlers(hass): current=current_service_data[light], total_events=total_events, ) - # 3. Fire simulated events for our TurnOnOffListener + # 3. Fire simulated events for our AdaptiveLightingManager for event in events: _LOGGER.debug("Test EVENT_STATE_CHANGED listener") hass.bus.async_fire(EVENT_STATE_CHANGED, event) @@ -1065,7 +1061,7 @@ async def test_state_change_handlers(hass): # On real systems HA fires transition state changes every ~3 seconds. # asyncio.sleep(3) # 4. Assert the transition timer started and everything was filled. - listener = switch.turn_on_off_listener + listener = switch.manager assert listener.last_state_change.get(ENTITY_LIGHT) assert len(listener.last_state_change[ENTITY_LIGHT]) == total_events assert listener.transition_timers.get(ENTITY_LIGHT) @@ -1082,9 +1078,9 @@ async def test_state_change_handlers(hass): assert timer and timer.is_running() last_service_data = deepcopy(current_service_data) await update() - assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + assert not switch.manager.manual_control[ENTITY_LIGHT] await update() - assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + assert not switch.manager.manual_control[ENTITY_LIGHT] timer = listener.transition_timers.get(ENTITY_LIGHT) assert timer and timer.is_running() # Ensure the light did not adapt during the transition. @@ -1106,23 +1102,23 @@ async def test_state_change_handlers(hass): await turn_light(True, brightness=40) await turn_light(True, brightness=20) await update(force=False) - assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + assert switch.manager.manual_control[ENTITY_LIGHT] await update(force=True) - assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + assert switch.manager.manual_control[ENTITY_LIGHT] # turn light off then on should reset manual control. await turn_light(False) await turn_light(True) - assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + assert not switch.manager.manual_control[ENTITY_LIGHT] await turn_light(True, brightness=50) _LOGGER.debug("Test: Brightness set to %s", 50) # On next update ENTITY_LIGHT should be marked as manually controlled await update(force=False) - assert switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None - assert switch.turn_on_off_listener.last_state_change.get(ENTITY_LIGHT) is not None - assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] + assert switch.manager.last_service_data.get(ENTITY_LIGHT) is not None + assert switch.manager.last_state_change.get(ENTITY_LIGHT) is not None + assert switch.manager.manual_control[ENTITY_LIGHT] @pytest.mark.dependency( @@ -1275,7 +1271,7 @@ async def test_area(hass): blocking=True, ) await hass.async_block_till_done() - assert light.entity_id in switch.turn_on_off_listener.last_service_data + assert light.entity_id in switch.manager.last_service_data await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, @@ -1285,10 +1281,10 @@ async def test_area(hass): await hass.async_block_till_done() _LOGGER.debug( - "switch.turn_on_off_listener.last_service_data: %s", - switch.turn_on_off_listener.last_service_data, + "switch.manager.last_service_data: %s", + switch.manager.last_service_data, ) - assert light.entity_id not in switch.turn_on_off_listener.last_service_data + assert light.entity_id not in switch.manager.last_service_data @pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) @@ -1351,9 +1347,7 @@ async def test_cancellable_service_calls_task(hass): _, switch = await setup_switch(hass, {CONF_SEPARATE_TURN_ON_COMMANDS: True}) context = switch.create_context("test") - assert ( - switch.turn_on_off_listener.adaptation_tasks_color.get(light.entity_id) is None - ) + assert switch.manager.adaptation_tasks_color.get(light.entity_id) is None service_data = { ATTR_BRIGHTNESS: 10, @@ -1370,8 +1364,8 @@ async def test_cancellable_service_calls_task(hass): ) await switch.execute_cancellable_adaptation_calls(adaptation_data) - task = switch.turn_on_off_listener.adaptation_tasks_brightness.get(light.entity_id) - task2 = switch.turn_on_off_listener.adaptation_tasks_color.get(light.entity_id) + task = switch.manager.adaptation_tasks_brightness.get(light.entity_id) + task2 = switch.manager.adaptation_tasks_color.get(light.entity_id) assert task is task2 assert task is not None assert task.done() @@ -1384,9 +1378,9 @@ async def test_service_calls_task_cancellation(hass): entity_id = "test_id" task = asyncio.ensure_future(asyncio.sleep(1)) - switch.turn_on_off_listener.adaptation_tasks_brightness[entity_id] = task + switch.manager.adaptation_tasks_brightness[entity_id] = task - switch.turn_on_off_listener.cancel_ongoing_adaptation_calls(entity_id) + switch.manager.cancel_ongoing_adaptation_calls(entity_id) try: await task @@ -1507,7 +1501,7 @@ async def test_proactive_adaptation_toggle(hass): context=Context(id="test1"), ) - assert switch.turn_on_off_listener.is_proactively_adapting("test1") + assert switch.manager.is_proactively_adapting("test1") # Toggle OFF await hass.services.async_call( @@ -1518,7 +1512,7 @@ async def test_proactive_adaptation_toggle(hass): context=Context(id="test2"), ) - assert not switch.turn_on_off_listener.is_proactively_adapting("test2") + assert not switch.manager.is_proactively_adapting("test2") async def test_proactive_adaptation_transition_override(hass): @@ -1558,7 +1552,7 @@ async def test_proactive_adaptation_transition_override(hass): assert set({ATTR_TRANSITION: 456}.items()).issubset(kwargs.items()) # Cleanup - switch.turn_on_off_listener.cancel_ongoing_adaptation_calls(ENTITY_LIGHT3) + switch.manager.cancel_ongoing_adaptation_calls(ENTITY_LIGHT3) async def test_two_switches_for_single_light(hass): From c1528ec10a514b7b0e52a1a95f122d9964130961 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 22 Jul 2023 21:19:01 -0700 Subject: [PATCH 0583/1077] =?UTF-8?q?Refactor,=20simplify=20code,=20rename?= =?UTF-8?q?,=20and=20set=20minimal=20HA=20core=20=E2=89=A52022.11=20(#655)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rename TurnOnOffListener to AdaptiveLightingManager * Bump Python version * Refactor and unify methods that are called once * Simplify adaptation_utils.py * Improve readability in adaptation_utils.py * More renames * Simplify * More renames and simplifications * fix test * simplify _supported_features * rename * walrus * drop old astral support * Only support HA ≥2021.06 * Require 2016.06 * Try 2023.1 * even more old versions * test * more versions * verify that only ≥2022.11 works * named args * setdefault * no astral v1 * var * simplify * no need to pass adapt_brightness and adapt_color * Add comment --- .github/workflows/pytest.yaml | 6 + .../adaptive_lighting/adaptation_utils.py | 57 ++-- custom_components/adaptive_lighting/switch.py | 254 +++++++----------- hacs.json | 3 +- test_dependencies.py | 36 +-- tests/test_adaptation_utils.py | 12 +- tests/test_switch.py | 6 +- 7 files changed, 166 insertions(+), 208 deletions(-) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index b5089ded..ba7eb1a7 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -14,6 +14,12 @@ jobs: fail-fast: false matrix: include: + - python-version: "3.10" + core-version: "2022.11.5" + - python-version: "3.10" + core-version: "2022.12.9" + - python-version: "3.10" + core-version: "2023.1.7" - python-version: "3.10" core-version: "2023.2.5" - python-version: "3.10" diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py index a5f33a30..374defd1 100644 --- a/custom_components/adaptive_lighting/adaptation_utils.py +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -61,7 +61,7 @@ def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]: # Distribute the transition duration across all service calls if service_datas and (transition := service_data.get(ATTR_TRANSITION)) is not None: - transition = service_data[ATTR_TRANSITION] / len(service_datas) + transition /= len(service_datas) for service_data in service_datas: service_data[ATTR_TRANSITION] = transition @@ -69,23 +69,20 @@ def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]: return service_datas -def _filter_service_data(service_data: ServiceData, state: State | None) -> ServiceData: +def _remove_redundant_attributes( + service_data: ServiceData, state: State +) -> ServiceData: """Filter service data by removing attributes that already equal the given state. Removes all attributes from service call data whose values are already present in the target entity's state.""" - if not state: - return service_data - - filtered_service_data = { - k: service_data[k] - for k in service_data.keys() - if k not in state.attributes or service_data[k] != state.attributes[k] + return { + k: v + for k, v in service_data.items() + if k not in state.attributes or v != state.attributes[k] } - return filtered_service_data - def _has_relevant_service_data_attributes(service_data: ServiceData) -> bool: """Determines whether the service data justifies an adaptation service call. @@ -93,9 +90,8 @@ def _has_relevant_service_data_attributes(service_data: ServiceData) -> bool: A service call is not justified for data which does not contain any entries that change relevant attributes of an adapting entity, e.g., brightness or color.""" common_attrs = {ATTR_ENTITY_ID, ATTR_TRANSITION} - relevant_attrs = set(service_data) - common_attrs - return bool(relevant_attrs) + return any(attr not in common_attrs for attr in service_data) async def _create_service_call_data_iterator( @@ -118,8 +114,10 @@ async def _create_service_call_data_iterator( current_entity_state = hass.states.get(entity_id) # Filter data to remove attributes that equal the current state - if current_entity_state: - service_data = _filter_service_data(service_data, current_entity_state) + if current_entity_state is not None: + service_data = _remove_redundant_attributes( + service_data, current_entity_state + ) # Emit service data if it still contains relevant attributes (else try next) if _has_relevant_service_data_attributes(service_data): @@ -149,7 +147,7 @@ class NoColorOrBrightnessInServiceData(Exception): """Exception raised when no color or brightness attributes are found in service data.""" -def is_color_brightness_or_both( +def _identify_lighting_type( service_data: ServiceData, ) -> Literal["brightness", "color", "both"]: """Extract the 'which' attribute from the service data.""" @@ -181,23 +179,30 @@ def prepare_adaptation_data( entity_id, service_data, ) - service_datas = ( - [service_data] if not split else _split_service_call_data(service_data) - ) + if split: + service_datas = _split_service_call_data(service_data) + else: + service_datas = [service_data] - sleep_time = ( - transition / max(1, len(service_datas)) if transition is not None else 0 - ) + split_delay + service_datas_length = len(service_datas) + + if transition is not None: + transition_duration_per_data = transition / max(1, service_datas_length) + sleep_time = transition_duration_per_data + split_delay + else: + sleep_time = split_delay service_data_iterator = _create_service_call_data_iterator( hass, service_datas, filter_by_state ) + lighting_type = _identify_lighting_type(service_data) + return AdaptationData( - entity_id, - context, + entity_id=entity_id, + context=context, sleep_time=sleep_time, service_call_datas=service_data_iterator, - max_length=len(service_datas), - which=is_color_brightness_or_both(service_data), + max_length=service_datas_length, + which=lighting_type, ) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 7deddcd8..82fd34db 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import base64 import bisect -from collections.abc import Callable, Coroutine +from collections.abc import Callable, Coroutine, Iterable from copy import deepcopy from dataclasses import dataclass import datetime @@ -186,8 +186,7 @@ _DOMAIN_SHORT = "al" def _int_to_base36(num: int) -> str: - """ - Convert an integer to its base-36 representation using numbers and uppercase letters. + """Convert an integer to its base-36 representation using numbers and uppercase letters. Base-36 encoding uses digits 0-9 and uppercase letters A-Z, providing a case-insensitive alphanumeric representation. The function takes an integer `num` as input and returns @@ -268,7 +267,7 @@ def is_our_context(context: Context | None) -> bool: return is_our_context_id(context.id) -def _get_switches_with_lights( +def _switches_with_lights( hass: HomeAssistant, lights: list[str] ) -> list[AdaptiveSwitch]: """Get all switches that control at least one of the lights passed.""" @@ -292,12 +291,12 @@ class NoSwitchFoundError(ValueError): """No switches found for lights.""" -def find_switch_for_lights( +def _switch_with_lights( hass: HomeAssistant, lights: list[str], ) -> AdaptiveSwitch: """Find the switch that controls the lights in 'lights'.""" - switches = _get_switches_with_lights(hass, lights) + switches = _switches_with_lights(hass, lights) if len(switches) == 1: return switches[0] elif len(switches) > 1: @@ -306,13 +305,13 @@ def find_switch_for_lights( # Of the multiple switches, only one is on return on_switches[0] raise NoSwitchFoundError( - f"find_switch_for_lights: Light(s) {lights} found in multiple switch configs" + f"_switch_with_lights: Light(s) {lights} found in multiple switch configs" f" ({[s.entity_id for s in switches]}). You must pass a switch under" f" 'entity_id'." ) else: raise NoSwitchFoundError( - f"find_switch_for_lights: Light(s) {lights} not found in any switch's" + f"_switch_with_lights: Light(s) {lights} not found in any switch's" f" configuration. You must either include the light(s) that is/are" f" in the integration config, or pass a switch under 'entity_id'." ) @@ -320,7 +319,7 @@ def find_switch_for_lights( # For documentation on this function, see integration_entities() from HomeAssistant Core: # https://github.com/home-assistant/core/blob/dev/homeassistant/helpers/template.py#L1109 -def _get_switches_from_service_call( +def _switches_from_service_call( hass: HomeAssistant, service_call: ServiceCall ) -> list[AdaptiveSwitch]: data = service_call.data @@ -351,7 +350,7 @@ def _get_switches_from_service_call( return switches if lights: - switch = find_switch_for_lights(hass, lights) + switch = _switch_with_lights(hass, lights) return [switch] raise ValueError( @@ -377,11 +376,7 @@ async def handle_change_switch_settings( else: defaults = None - switch._set_changeable_settings( - data=data, - defaults=defaults, - ) - + switch._set_changeable_settings(data=data, defaults=defaults) switch._update_time_interval_listener() _LOGGER.debug( @@ -389,11 +384,10 @@ async def handle_change_switch_settings( data, ) - all_lights = switch.lights # pylint: disable=protected-access - switch.manager.reset(*all_lights, reset_manual_control=False) + switch.manager.reset(*switch.lights, reset_manual_control=False) if switch.is_on: await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access - all_lights, + switch.lights, transition=switch.initial_transition, force=True, context=switch.create_context("service", parent=service_call.context), @@ -471,7 +465,7 @@ async def async_setup_entry( "Called 'adaptive_lighting.apply' service with '%s'", data, ) - switches = _get_switches_from_service_call(hass, service_call) + switches = _switches_from_service_call(hass, service_call) lights = data[CONF_LIGHTS] for switch in switches: if not lights: @@ -500,7 +494,7 @@ async def async_setup_entry( "Called 'adaptive_lighting.set_manual_control' service with '%s'", data, ) - switches = _get_switches_from_service_call(hass, service_call) + switches = _switches_from_service_call(hass, service_call) lights = data[CONF_LIGHTS] for switch in switches: if not lights: @@ -528,9 +522,7 @@ async def async_setup_entry( domain=DOMAIN, service=SERVICE_APPLY, service_func=handle_apply, - schema=apply_service_schema( - switch.initial_transition - ), # pylint: disable=protected-access + schema=apply_service_schema(switch.initial_transition), ) # Register `set_manual_control` service @@ -582,16 +574,15 @@ def validate( return data -def match_switch_state_event(event: Event, from_or_to_state: list[str]): +def _is_state_event(event: Event, from_or_to_state: Iterable[str]): """Match state event when either 'from_state' or 'to_state' matches.""" - old_state = event.data.get("old_state") - from_state_match = old_state is not None and old_state.state in from_or_to_state - - new_state = event.data.get("new_state") - to_state_match = new_state is not None and new_state.state in from_or_to_state - - match = from_state_match or to_state_match - return match + return ( + (old_state := event.data.get("old_state")) is not None + and old_state.state in from_or_to_state + ) or ( + (new_state := event.data.get("new_state")) is not None + and new_state.state in from_or_to_state + ) def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]: @@ -612,35 +603,36 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]: return list(all_lights) -def _supported_features(hass: HomeAssistant, light: str): +def _supported_features(hass: HomeAssistant, light: str) -> set[str]: state = hass.states.get(light) supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported = { key for key, value in _SUPPORT_OPTS.items() if supported_features & value } + supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES, set()) - if COLOR_MODE_RGB in supported_color_modes: - supported.add("color") - # Adding brightness here, see - # comment https://github.com/basnijholt/adaptive-lighting/issues/112#issuecomment-836944011 - supported.add("brightness") - if COLOR_MODE_RGBW in supported_color_modes: - supported.add("color") - supported.add("brightness") # see above url - if COLOR_MODE_RGBWW: - supported.add("color") - supported.add("brightness") # see above url - if COLOR_MODE_XY in supported_color_modes: - supported.add("color") - supported.add("brightness") # see above url - if COLOR_MODE_HS in supported_color_modes: - supported.add("color") - supported.add("brightness") # see above url + color_modes = { + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, + COLOR_MODE_XY, + COLOR_MODE_HS, + } + + # Adding brightness when color mode is supported, see + # comment https://github.com/basnijholt/adaptive-lighting/issues/112#issuecomment-836944011 + + for mode in color_modes: + if mode in supported_color_modes: + supported.update({"color", "brightness"}) + break + if COLOR_MODE_COLOR_TEMP in supported_color_modes: - supported.add("color_temp") - supported.add("brightness") # see above url + supported.update({"color_temp", "brightness"}) + if COLOR_MODE_BRIGHTNESS in supported_color_modes: supported.add("brightness") + return supported @@ -670,16 +662,16 @@ def _convert_attributes(attributes: dict[str, Any]) -> dict[str, Any]: return attributes rgb = None - if ATTR_COLOR_TEMP_KELVIN in attributes: - rgb = color_temperature_to_rgb(attributes[ATTR_COLOR_TEMP_KELVIN]) - elif ATTR_XY_COLOR in attributes: - rgb = color_xy_to_RGB(*attributes[ATTR_XY_COLOR]) + if (color := attributes.get(ATTR_COLOR_TEMP_KELVIN)) is not None: + rgb = color_temperature_to_rgb(color) + elif (color := attributes.get(ATTR_XY_COLOR)) is not None: + rgb = color_xy_to_RGB(*color) if rgb is not None: attributes[ATTR_RGB_COLOR] = rgb _LOGGER.debug(f"Converted {attributes} to rgb {rgb}") else: - _LOGGER.debug("No suitable conversion found") + _LOGGER.debug("No suitable color conversion found for %s", attributes) return attributes @@ -796,10 +788,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # backup data for use in change_switch_settings "configuration" CONF_USE_DEFAULTS self._config_backup = deepcopy(data) - self._set_changeable_settings( - data=data, - defaults=None, - ) + self._set_changeable_settings(data=data, defaults=None) # Set other attributes self._icon = ICON_MAIN @@ -835,7 +824,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): def _set_changeable_settings( self, data: dict, - defaults: dict, + defaults: dict | None = None, ): # Only pass settings users can change during runtime data = validate( @@ -853,9 +842,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self._include_config_in_attributes: attrdata = deepcopy(data) for k, v in attrdata.items(): - if isinstance(v, (datetime.date, datetime.datetime)): + if isinstance(v, datetime.date | datetime.datetime): attrdata[k] = v.isoformat() - if isinstance(v, (datetime.timedelta)): + elif isinstance(v, datetime.timedelta): attrdata[k] = v.total_seconds() self._config.update(attrdata) @@ -880,13 +869,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL] self._skip_redundant_commands = data[CONF_SKIP_REDUNDANT_COMMANDS] self._expand_light_groups() # updates manual control timers - _loc = get_astral_location(self.hass) - if isinstance(_loc, tuple): - # Astral v2.2 - location, _ = _loc - else: - # Astral v1 - location = _loc + location, _ = get_astral_location(self.hass) self._sun_light_settings = SunLightSettings( name=self._name, @@ -971,8 +954,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): remove_sleep = async_track_state_change_event( self.hass, - self.sleep_mode_switch.entity_id, - self._sleep_mode_switch_state_event, + entity_ids=self.sleep_mode_switch.entity_id, + action=self._sleep_mode_switch_state_event_action, ) self.remove_listeners.append(remove_sleep) @@ -980,7 +963,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self.lights: self._expand_light_groups() remove_state = async_track_state_change_event( - self.hass, self.lights, self._light_event + self.hass, entity_ids=self.lights, action=self._light_event_action ) self.remove_listeners.append(remove_state) @@ -1005,7 +988,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) self.remove_interval = async_track_time_interval( - self.hass, self._async_update_at_interval, adaptation_interval + self.hass, + action=self._async_update_at_interval_action, + interval=adaptation_interval, ) def _remove_interval_listener(self) -> None: @@ -1078,7 +1063,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._remove_listeners() self.manager.reset(*self.lights) - async def _async_update_at_interval(self, now=None) -> None: + async def _async_update_at_interval_action(self, now=None) -> None: await self._update_attrs_and_maybe_adapt_lights( transition=self._transition, force=False, @@ -1174,8 +1159,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): prefer_rgb_color: bool | None = None, context: Context | None = None, ) -> None: - lock = self._locks.get(light) - if lock is not None and lock.locked(): + if (lock := self._locks.get(light)) is not None and lock.locked(): _LOGGER.debug("%s: '%s' is locked", self._name, light) return @@ -1293,10 +1277,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if lights is None: lights = self.lights - filtered_lights = [] - if not force: - if self._only_once: - return + if not force and self._only_once: + return + + if force: + filtered_lights = lights + else: + filtered_lights = [] for light in lights: # Don't adapt lights that haven't finished prior transitions. timer = self.manager.transition_timers.get(light) @@ -1308,37 +1295,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) else: filtered_lights.append(light) - else: - filtered_lights = lights if not filtered_lights: return - await self._update_manual_control_and_maybe_adapt( - filtered_lights, transition, force, context - ) - - async def _update_manual_control_and_maybe_adapt( - self, - lights: list[str], - transition: int | None, - force: bool, - context: Context | None, - ) -> None: - assert context is not None - _LOGGER.debug( - "%s: '_update_manual_control_and_maybe_adapt(%s, %s, force=%s, context.id=%s)' called", - self.name, - lights, - transition, - force, - context.id, - ) - adapt_brightness = self.adapt_brightness_switch.is_on adapt_color = self.adapt_color_switch.is_on - for light in lights: + for light in filtered_lights: if not is_on(self.hass, light): continue @@ -1375,12 +1339,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): else: await self._adapt_light(light, transition, context=context) - async def _sleep_mode_switch_state_event(self, event: Event) -> None: - if not match_switch_state_event(event, (STATE_ON, STATE_OFF)): + async def _sleep_mode_switch_state_event_action(self, event: Event) -> None: + if not _is_state_event(event, (STATE_ON, STATE_OFF)): _LOGGER.debug("%s: Ignoring sleep event %s", self._name, event) return _LOGGER.debug( - "%s: _sleep_mode_switch_state_event, event: '%s'", self._name, event + "%s: _sleep_mode_switch_state_event_action, event: '%s'", self._name, event ) # Reset the manually controlled status when the "sleep mode" changes self.manager.reset(*self.lights) @@ -1390,7 +1354,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context=self.create_context("sleep", parent=event.context), ) - async def _light_event(self, event: Event) -> None: + async def _light_event_action(self, event: Event) -> None: old_state = event.data.get("old_state") new_state = event.data.get("new_state") entity_id = event.data.get("entity_id") @@ -1414,9 +1378,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Tracks 'off' → 'on' state changes self._off_to_on_event[entity_id] = event - lock = self._locks.get(entity_id) - if lock is None: - lock = self._locks[entity_id] = asyncio.Lock() + lock = self._locks.setdefault(entity_id, asyncio.Lock()) async with lock: if await self.manager.maybe_cancel_adjusting( entity_id, @@ -1549,18 +1511,15 @@ class SunLightSettings: time_zone: datetime.tzinfo transition: int - def get_sun_events(self, date: datetime.datetime) -> dict[str, float]: + def get_sun_events(self, date: datetime.datetime) -> list[tuple[str, float]]: """Get the four sun event's timestamps at 'date'.""" def _replace_time(date: datetime.datetime, key: str) -> datetime.datetime: time = getattr(self, f"{key}_time") date_time = datetime.datetime.combine(date, time) - try: # HA ≤2021.05, https://github.com/basnijholt/adaptive-lighting/issues/128 - utc_time = self.time_zone.localize(date_time).astimezone(dt_util.UTC) - except AttributeError: # HA ≥2021.06 - utc_time = date_time.replace( - tzinfo=dt_util.DEFAULT_TIME_ZONE - ).astimezone(dt_util.UTC) + utc_time = date_time.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone( + dt_util.UTC + ) return utc_time def calculate_noon_and_midnight( @@ -1606,16 +1565,10 @@ class SunLightSettings: and self.max_sunrise_time is None and self.min_sunset_time is None ): - try: - # Astral v1 - solar_noon = location.solar_noon(date, local=False) - solar_midnight = location.solar_midnight(date, local=False) - except AttributeError: - # Astral v2 - solar_noon = location.noon(date, local=False) - solar_midnight = location.midnight(date, local=False) + solar_noon = location.noon(date, local=False) + solar_midnight = location.midnight(date, local=False) else: - (solar_noon, solar_midnight) = calculate_noon_and_midnight(sunset, sunrise) + solar_noon, solar_midnight = calculate_noon_and_midnight(sunset, sunrise) events = [ (SUN_EVENT_SUNRISE, sunrise.timestamp()), @@ -1641,9 +1594,10 @@ class SunLightSettings: def relevant_events(self, now: datetime.datetime) -> list[tuple[str, float]]: """Get the previous and next sun event.""" events = [ - self.get_sun_events(now + timedelta(days=days)) for days in [-1, 0, 1] + event + for days in [-1, 0, 1] + for event in self.get_sun_events(now + timedelta(days=days)) ] - events = sum(events, []) # flatten lists events = sorted(events, key=lambda x: x[1]) i_now = bisect.bisect([ts for _, ts in events], now.timestamp()) return events[i_now - 1 : i_now + 1] @@ -1756,23 +1710,22 @@ class AdaptiveLightingManager: # Track light transitions self.transition_timers: dict[str, _AsyncSingleShotTimer] = {} - self.listener_removers = [] - - self.listener_removers.append( + # Setup listeners and its callbacks to remove them later + self.listener_removers = [ self.hass.bus.async_listen( - EVENT_CALL_SERVICE, self.turn_on_off_event_listener - ) - ) - self.listener_removers.append( + EVENT_CALL_SERVICE, + self.turn_on_off_event_listener, + ), self.hass.bus.async_listen( - EVENT_STATE_CHANGED, self.state_changed_event_listener - ) - ) + EVENT_STATE_CHANGED, + self.state_changed_event_listener, + ), + ] self._proactively_adapting_contexts: dict[str, str] = {} - is_proactive_adaptation_enabled = ( - data.get(INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION, True) is not False + is_proactive_adaptation_enabled = data.get( + INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION, True ) if is_proactive_adaptation_enabled: @@ -1860,7 +1813,7 @@ class AdaptiveLightingManager: entity_id = entity_ids[0] try: - adaptive_switch = find_switch_for_lights(self.hass, [entity_id]) + adaptive_switch = _switch_with_lights(self.hass, [entity_id]) except NoSwitchFoundError: # This might be a light that is not managed by this AL instance. _LOGGER.debug( @@ -1891,18 +1844,13 @@ class AdaptiveLightingManager: self.reset(entity_id, reset_manual_control=False) self.clear_proactively_adapting(entity_id) - adapt_brightness = adaptive_switch.adapt_brightness_switch.is_on or False - adapt_color = adaptive_switch.adapt_color_switch.is_on or False - transition = ( - data[CONF_PARAMS].get(ATTR_TRANSITION, None) - or adaptive_switch.initial_transition + transition = data[CONF_PARAMS].get( + ATTR_TRANSITION, adaptive_switch.initial_transition ) adaptation_data = await adaptive_switch.prepare_adaptation_data( entity_id, transition, - adapt_brightness, - adapt_color, ) if adaptation_data is None: return @@ -1972,7 +1920,7 @@ class AdaptiveLightingManager: ) async def reset(): - ValueError("TEST") + # Called when the timer expires, doesn't need to do anything _LOGGER.debug( "Transition finished for light %s", light, @@ -2005,7 +1953,7 @@ class AdaptiveLightingManager: async def reset(): self.reset(light) - switches = _get_switches_with_lights(self.hass, [light]) + switches = _switches_with_lights(self.hass, [light]) for switch in switches: if not switch.is_on: continue diff --git a/hacs.json b/hacs.json index 1a865d2d..f21e6d60 100644 --- a/hacs.json +++ b/hacs.json @@ -1,4 +1,5 @@ { "name": "Adaptive Lighting", - "render_readme": true + "render_readme": true, + "homeassistant": "2022.11.0" } diff --git a/test_dependencies.py b/test_dependencies.py index 58b4718f..2cf8dfb0 100644 --- a/test_dependencies.py +++ b/test_dependencies.py @@ -1,22 +1,27 @@ +from collections import defaultdict + +deps = defaultdict(list) +components, packages = [], [] + with open("core/requirements_test_all.txt") as f: lines = f.readlines() -components = [] -packages = [] -deps = {} -for i, line in enumerate(lines): +for line in lines: line = line.strip() + if line.startswith("# homeassistant."): - component = line.split("# homeassistant.")[1] - components.append(component) + if components and packages: + for component in components: + deps[component].extend(packages) + components, packages = [], [] + components.append(line.split("# homeassistant.")[1]) elif components and line: packages.append(line) - else: - for component in components: - for package in packages: - deps.setdefault(component, []).append(package) - components = [] - packages = [] + +# The last batch of components and packages +if components and packages: + for component in components: + deps[component].extend(packages) required = [ "components.recorder", @@ -24,10 +29,9 @@ required = [ "components.zeroconf", "components.http", "components.stream", - "components.conversation", + "components.conversation", # only available after HA≥2023.2 "components.cloud", ] -to_install = [] -for r in required: - to_install.extend(deps[r]) +to_install = [package for r in required for package in deps[r]] + print(" ".join(to_install)) diff --git a/tests/test_adaptation_utils.py b/tests/test_adaptation_utils.py index b1cfcb5b..fc0df739 100644 --- a/tests/test_adaptation_utils.py +++ b/tests/test_adaptation_utils.py @@ -14,8 +14,8 @@ import pytest from custom_components.adaptive_lighting.adaptation_utils import ( ServiceData, _create_service_call_data_iterator, - _filter_service_data, _has_relevant_service_data_attributes, + _remove_redundant_attributes, _split_service_call_data, prepare_adaptation_data, ) @@ -74,11 +74,6 @@ async def test_split_service_call_data(input_data, expected_data_list): @pytest.mark.parametrize( "service_data,state,service_data_expected", [ - ( - {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, - None, - {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, - ), ( {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, State("light.test", STATE_ON), @@ -96,17 +91,16 @@ async def test_split_service_call_data(input_data, expected_data_list): ), ], ids=[ - "pass all attributes on missing state", "pass all attributes on empty state", "remove attributes whose values equal the state", "keep attributes whose values differ from the state", ], ) -async def test_filter_service_data( +async def test_remove_redundant_attributes( service_data: ServiceData, state: State | None, service_data_expected: ServiceData ): """Test filtering of service data.""" - assert _filter_service_data(service_data, state) == service_data_expected + assert _remove_redundant_attributes(service_data, state) == service_data_expected @pytest.mark.parametrize( diff --git a/tests/test_switch.py b/tests/test_switch.py index f356c10a..59692f1e 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1206,10 +1206,10 @@ async def test_turn_on_and_off_when_already_at_that_state(hass): @pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) -async def test_async_update_at_interval(hass): - """Test '_async_update_at_interval' method.""" +async def test_async_update_at_interval_action(hass): + """Test '_async_update_at_interval_action' method.""" _, switch = await setup_switch(hass, {}) - await switch._async_update_at_interval() + await switch._async_update_at_interval_action() @pytest.mark.parametrize("separate_turn_on_commands", (True, False)) From 97f388608ab31d0e95ed6b673533a0599d6b795f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 23 Jul 2023 13:40:03 -0700 Subject: [PATCH 0584/1077] With transition_until_sleep and sleep_rgb, after sunset, use RGB colors (#656) * With transition_until_sleep and sleep_rgb, after sunset, use RGB colors Closes #624 * Add comment * Add test --- custom_components/adaptive_lighting/switch.py | 44 ++++++++-- tests/test_switch.py | 86 +++++++++++++++++++ 2 files changed, 125 insertions(+), 5 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 82fd34db..58fa3eaa 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1123,6 +1123,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): and adapt_color and not (prefer_rgb_color and "color" in features) and not (sleep_rgb and "color" in features) + and not (self._settings["force_rgb_color"] and "color" in features) ): _LOGGER.debug("%s: Setting color_temp of light %s", self._name, light) attributes = self.hass.states.get(light).attributes @@ -1296,9 +1297,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): else: filtered_lights.append(light) + _LOGGER.debug("%s: filtered_lights: '%s'", self._name, filtered_lights) if not filtered_lights: return - adapt_brightness = self.adapt_brightness_switch.is_on adapt_color = self.adapt_color_switch.is_on @@ -1337,6 +1338,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): else: _fire_manual_control_event(self, light, context) else: + _LOGGER.debug( + "%s: Calling _adapt_light from _update_attrs_and_maybe_adapt_lights:" + " '%s' with transition %s", + self._name, + light, + transition, + ) await self._adapt_light(light, transition, context=context) async def _sleep_mode_switch_state_event_action(self, event: Event) -> None: @@ -1487,6 +1495,17 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): self._state = False +def lerp_color( + rgb1: tuple[int, int, int], rgb2: tuple[int, int, int], t: float +) -> tuple[int, int, int]: + """Linearly interpolate between two RGB colors.""" + return ( + int(rgb1[0] + t * (rgb2[0] - rgb1[0])), + int(rgb1[1] + t * (rgb2[1] - rgb1[1])), + int(rgb1[2] + t * (rgb2[2] - rgb1[2])), + ) + + @dataclass(frozen=True) class SunLightSettings: """Track the state of the sun and associated light settings.""" @@ -1654,15 +1673,29 @@ class SunLightSettings: if transition is not None else self.calc_percent(0) ) + rgb_color: tuple[float, float, float] + # Variable `force_rgb_color` is needed for RGB color after sunset (if enabled) + force_rgb_color = False brightness_pct = self.calc_brightness_pct(percent, is_sleep) if is_sleep: color_temp_kelvin = self.sleep_color_temp - rgb_color: tuple[float, float, float] = self.sleep_rgb_color + rgb_color = self.sleep_rgb_color + elif ( + self.sleep_rgb_or_color_temp == "rgb_color" + and self.adapt_until_sleep + and percent < 0 + ): + # Feature requested in + # https://github.com/basnijholt/adaptive-lighting/issues/624 + # This will result in a perceptible jump in color at sunset and sunrise + # because the `color_temperature_to_rgb` function is not 100% accurate. + min_color_rgb = color_temperature_to_rgb(self.min_color_temp) + rgb_color = lerp_color(min_color_rgb, self.sleep_rgb_color, percent) + color_temp_kelvin = self.calc_color_temp_kelvin(percent) + force_rgb_color = True else: color_temp_kelvin = self.calc_color_temp_kelvin(percent) - rgb_color: tuple[float, float, float] = color_temperature_to_rgb( - color_temp_kelvin - ) + rgb_color = color_temperature_to_rgb(color_temp_kelvin) # backwards compatibility for versions < 1.3.1 - see #403 color_temp_mired: float = math.floor(1000000 / color_temp_kelvin) xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color) @@ -1675,6 +1708,7 @@ class SunLightSettings: "xy_color": xy_color, "hs_color": hs_color, "sun_position": percent, + "force_rgb_color": force_rgb_color, } diff --git a/tests/test_switch.py b/tests/test_switch.py index 59692f1e..9b679a86 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -56,6 +56,7 @@ from custom_components.adaptive_lighting.const import ( ADAPT_BRIGHTNESS_SWITCH, ADAPT_COLOR_SWITCH, ATTR_ADAPTIVE_LIGHTING_MANAGER, + CONF_ADAPT_UNTIL_SLEEP, CONF_AUTORESET_CONTROL, CONF_DETECT_NON_HA_CHANGES, CONF_INITIAL_TRANSITION, @@ -64,6 +65,7 @@ from custom_components.adaptive_lighting.const import ( CONF_MIN_COLOR_TEMP, CONF_PREFER_RGB_COLOR, CONF_SEPARATE_TURN_ON_COMMANDS, + CONF_SLEEP_RGB_OR_COLOR_TEMP, CONF_SUNRISE_OFFSET, CONF_SUNRISE_TIME, CONF_SUNSET_TIME, @@ -74,6 +76,7 @@ from custom_components.adaptive_lighting.const import ( DEFAULT_NAME, DEFAULT_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_COLOR_TEMP, + DEFAULT_SLEEP_RGB_COLOR, DOMAIN, SERVICE_APPLY, SERVICE_CHANGE_SWITCH_SETTINGS, @@ -1430,6 +1433,7 @@ async def test_proactive_adaptation(hass): { ATTR_BRIGHTNESS_PCT: 67, ATTR_COLOR_TEMP_KELVIN: 3448, + "force_rgb_color": False, }, ) @@ -1464,6 +1468,7 @@ async def test_proactive_adaptation_with_separate_commands(hass): { ATTR_BRIGHTNESS_PCT: 67, ATTR_COLOR_TEMP_KELVIN: 3448, + "force_rgb_color": False, }, ) @@ -1615,3 +1620,84 @@ async def test_two_switches_for_single_light(hass): after_color_temp = attrs[ATTR_COLOR_TEMP_KELVIN] assert before_brightness != after_brightness assert before_color_temp != after_color_temp + + +async def test_adapt_until_sleep_and_rgb_colors(hass): + """Test setting up the Adaptive Lighting switches with different timezones. + + Also test the (sleep) brightness and color temperature settings. + """ + lat, long, timezone = (32.87336, -117.22743, "US/Pacific") + await config_util.async_process_ha_core_config( + hass, + {"latitude": lat, "longitude": long, "time_zone": timezone}, + ) + switch, lights = await setup_lights_and_switch( + hass, + { + CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour), + CONF_SUNSET_TIME: datetime.time(SUNSET.hour), + CONF_ADAPT_UNTIL_SLEEP: True, + CONF_SLEEP_RGB_OR_COLOR_TEMP: "rgb_color", + }, + ) + + context = switch.create_context("test") # needs to be passed to update method + min_color_temp = switch._sun_light_settings.min_color_temp + + sunset = SUNSET.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + before_sunset = sunset - datetime.timedelta(hours=1) + after_sunset = sunset + datetime.timedelta(hours=1) + sunrise = SUNRISE.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + before_sunrise = sunrise - datetime.timedelta(hours=1) + after_sunrise = sunrise + datetime.timedelta(hours=1) + + async def patch_time_and_update(time): + with patch("homeassistant.util.dt.utcnow", return_value=time): + await switch._update_attrs_and_maybe_adapt_lights(context=context) + await hass.async_block_till_done() + + # At sunset the brightness should be max and color_temp at the smallest value + await patch_time_and_update(sunset) + assert not switch._settings["force_rgb_color"] + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == min_color_temp + + # One hour before sunset the brightness should be max and color_temp + # not at the smallest value yet. + await patch_time_and_update(before_sunset) + assert not switch._settings["force_rgb_color"] + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] > min_color_temp + assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT] + + # One hour after sunset the brightness should be down + await patch_time_and_update(after_sunset) + assert switch._settings["force_rgb_color"] + assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS + assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT] + + # At sunrise the brightness should be max and color_temp at the smallest value + await patch_time_and_update(sunrise) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] == min_color_temp + assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT] + + # One hour before sunrise the brightness should smaller than max + # and color_temp at the min value. + await patch_time_and_update(before_sunrise) + assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS + assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT] + + # One hour after sunrise the brightness should be up + await patch_time_and_update(after_sunrise) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS + assert switch._settings["color_temp_kelvin"] > min_color_temp + assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT] + + # Turn on sleep mode which make the brightness and color_temp + # deterministic regardless of the time + await switch.sleep_mode_switch.async_turn_on() + await switch._update_attrs_and_maybe_adapt_lights(context=context) + assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_SLEEP_BRIGHTNESS + assert switch._settings["rgb_color"] == DEFAULT_SLEEP_RGB_COLOR From bb84684bcee7d25a34b43f9c2b905f3af1040976 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 23 Jul 2023 14:24:22 -0700 Subject: [PATCH 0585/1077] Style with ruff and logging (#643) * Formatting with ruff * more logging * Require on_only * Different implementation * More ruff * Remove new logging statements * Remove 'pylint: disable=protected' * style * fixes * ruff * Switch to ruff * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix .github * Fix * Add ignores * fix arg * fix B905 * Fix D * Fix more * Fix more * order * use path * moer path * Remove unused ignores * fix tes deps * fix typo --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/update-services.py | 9 +- .github/update-strings.py | 16 +- .pre-commit-config.yaml | 16 +- .ruff.toml | 55 ++-- .../adaptive_lighting/__init__.py | 18 +- .../adaptive_lighting/_docs_helpers.py | 35 +- .../adaptive_lighting/adaptation_utils.py | 37 +-- .../adaptive_lighting/config_flow.py | 10 +- custom_components/adaptive_lighting/const.py | 34 +- .../adaptive_lighting/hass_utils.py | 30 +- custom_components/adaptive_lighting/switch.py | 304 +++++++++++------- test_dependencies.py | 10 +- 12 files changed, 327 insertions(+), 247 deletions(-) mode change 100755 => 100644 custom_components/adaptive_lighting/__init__.py diff --git a/.github/update-services.py b/.github/update-services.py index df4c7b30..ee001beb 100644 --- a/.github/update-services.py +++ b/.github/update-services.py @@ -1,5 +1,6 @@ -from pathlib import Path +"""Creates a services.yaml file with the latest docs.""" import sys +from pathlib import Path import yaml @@ -7,8 +8,8 @@ sys.path.append(str(Path(__file__).parent.parent)) from custom_components.adaptive_lighting import const # noqa: E402 -services_filename = "custom_components/adaptive_lighting/services.yaml" -with open(services_filename) as f: +services_filename = Path("custom_components") / "adaptive_lighting" / "services.yaml" +with open(services_filename) as f: # noqa: PTH123 services = yaml.safe_load(f) for service_name, dct in services.items(): @@ -20,6 +21,6 @@ for service_name, dct in services.items(): comment = "# This file is auto-generated by .github/update-services.py." -with open(services_filename, "w") as f: +with services_filename.open("w") as f: f.write(comment + "\n") yaml.dump(services, f, sort_keys=False, width=1000, allow_unicode=True) diff --git a/.github/update-strings.py b/.github/update-strings.py index aabc7443..8f8f9ef4 100644 --- a/.github/update-strings.py +++ b/.github/update-strings.py @@ -1,31 +1,33 @@ +"""Update strings.json and en.json from const.py.""" import json -from pathlib import Path import sys +from pathlib import Path sys.path.append(str(Path(__file__).parent.parent)) from custom_components.adaptive_lighting import const # noqa: E402 -strings_fname = "custom_components/adaptive_lighting/strings.json" -en_fname = "custom_components/adaptive_lighting/translations/en.json" -with open(strings_fname) as f: +folder = Path("custom_components") / "adaptive_lighting" +strings_fname = folder / "strings.json" +en_fname = folder / "translations" / "en.json" +with strings_fname.open() as f: strings = json.load(f) data = {k: f"{k}: {const.DOCS[k]}" for k, _, _ in const.VALIDATION_TUPLES} strings["options"]["step"]["init"]["data"] = data -with open(strings_fname, "w") as f: +with strings_fname.open("w") as f: json.dump(strings, f, indent=2, ensure_ascii=False) f.write("\n") # Sync changes from strings.json to en.json -with open(en_fname) as f: +with en_fname.open() as f: en = json.load(f) en["config"]["step"]["user"] = strings["config"]["step"]["user"] en["options"]["step"]["init"]["data"] = data -with open(en_fname, "w") as f: +with en_fname.open("w") as f: json.dump(en, f, indent=2, ensure_ascii=False) f.write("\n") diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c35e268..6418f6a3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,20 +7,12 @@ repos: - id: end-of-file-fixer - id: mixed-line-ending args: ["--fix=lf"] - - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.279 hooks: - - id: flake8 + - id: ruff + args: ["--fix"] - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black - - repo: https://github.com/asottile/pyupgrade - rev: v3.7.0 - hooks: - - id: pyupgrade - args: ["--py39-plus"] - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort diff --git a/.ruff.toml b/.ruff.toml index 260b1883..c1b6b784 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -2,41 +2,30 @@ target-version = "py310" -select = [ - "B007", # Loop control variable {name} not used within loop body - "B014", # Exception handler with duplicate exception - "C", # complexity - "D", # docstrings - "E", # pycodestyle - "F", # pyflakes/autoflake - "ICN001", # import concentions; {name} should be imported as {asname} - "PGH004", # Use specific rule codes when using noqa - "PLC0414", # Useless import alias. Import alias does not rename original package. - "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass - "SIM117", # Merge with-statements that use the same scope - "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() - "SIM201", # Use {left} != {right} instead of not {left} == {right} - "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} - "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. - "SIM401", # Use get from dict with default instead of an if block - "T20", # flake8-print - "TRY004", # Prefer TypeError exception for invalid type - "RUF006", # Store a reference to the return value of asyncio.create_task - "UP", # pyupgrade - "W", # pycodestyle +select = ["ALL"] + +# All the ones without a comment were the ones that are currently violated +# by the codebase. The plan is to fix them all (when sensible) and then enable them. +ignore = [ + "ANN", + "ANN101", # Missing type annotation for {name} in method + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {name} + "D401", # First line of docstring should be in imperative mood + "E501", # line too long + "FBT001", # Boolean positional arg in function definition + "FBT002", # Boolean default value in function definition + "FIX004", # Line contains HACK, consider resolving the issue + "PD901", # df is a bad variable name. Be kinder to your future self. + "PERF203",# `try`-`except` within a loop incurs performance overhead + "PLR0913", # Too many arguments to function call (N > 5) + "PLR2004", # Magic value used in comparison, consider replacing X with a constant variable + "S101", # Use of assert detected + "SLF001", # Private member accessed ] -ignore = [ - "D202", # No blank lines allowed after function docstring - "D203", # 1 blank line required before class docstring - "D213", # Multi-line docstring summary should start at the second line - "D404", # First word of the docstring should not be This - "D406", # Section name should end with a newline - "D407", # Section name underlining - "D411", # Missing blank line before section - "E501", # line too long - "E731", # do not assign a lambda expression, use a def -] +[per-file-ignores] +"tests/*.py" = ["ALL"] +".github/*py" = ["INP001"] [flake8-pytest-style] fixture-parentheses = false diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py old mode 100755 new mode 100644 index c4187fa7..98c2e94f --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -2,11 +2,11 @@ import logging from typing import Any +import homeassistant.helpers.config_validation as cv +import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -import voluptuous as vol from .const import ( _DOMAIN_SCHEMA, @@ -35,20 +35,21 @@ CONFIG_SCHEMA = vol.Schema( ) -async def reload_configuration_yaml(event: dict, hass: HomeAssistant): +async def reload_configuration_yaml(event: dict, hass: HomeAssistant): # noqa: ARG001 """Reload configuration.yaml.""" await hass.services.async_call("homeassistant", "check_config", {}) async def async_setup(hass: HomeAssistant, config: dict[str, Any]): """Import integration from config.""" - if DOMAIN in config: for entry in config[DOMAIN]: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=entry - ) + DOMAIN, + context={CONF_SOURCE: SOURCE_IMPORT}, + data=entry, + ), ) return True @@ -65,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): data[config_entry.entry_id] = {UNDO_UPDATE_LISTENER: undo_listener} for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) + hass.config_entries.async_forward_entry_setup(config_entry, platform), ) return True @@ -79,7 +80,8 @@ async def async_update_options(hass, config_entry: ConfigEntry): async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_forward_entry_unload( - config_entry, "switch" + config_entry, + "switch", ) data = hass.data[DOMAIN] data[config_entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/custom_components/adaptive_lighting/_docs_helpers.py b/custom_components/adaptive_lighting/_docs_helpers.py index 40afc235..31225a6c 100644 --- a/custom_components/adaptive_lighting/_docs_helpers.py +++ b/custom_components/adaptive_lighting/_docs_helpers.py @@ -1,9 +1,9 @@ from typing import Any -from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv import pandas as pd import voluptuous as vol +from homeassistant.helpers import selector from .const import ( DOCS, @@ -23,38 +23,37 @@ def _format_voluptuous_instance(instance): for validator in instance.validators: if isinstance(validator, vol.Coerce): coerce_type = validator.type.__name__ - elif isinstance(validator, (vol.Clamp, vol.Range)): + elif isinstance(validator, vol.Clamp | vol.Range): min_val = validator.min max_val = validator.max if min_val is not None and max_val is not None: return f"`{coerce_type}` {min_val}-{max_val}" - elif min_val is not None: + if min_val is not None: return f"`{coerce_type} > {min_val}`" - elif max_val is not None: + if max_val is not None: return f"`{coerce_type} < {max_val}`" - else: - return f"`{coerce_type}`" + return f"`{coerce_type}`" -def _type_to_str(type_: Any) -> str: +def _type_to_str(type_: Any) -> str: # noqa: PLR0911 """Convert a (voluptuous) type to a string.""" if type_ == cv.entity_ids: return "list of `entity_id`s" - elif type_ in (bool, int, float, str): + if type_ in (bool, int, float, str): return f"`{type_.__name__}`" - elif type_ == cv.boolean: + if type_ == cv.boolean: return "bool" - elif isinstance(type_, vol.All): + if isinstance(type_, vol.All): return _format_voluptuous_instance(type_) - elif isinstance(type_, vol.In): + if isinstance(type_, vol.In): return f"one of `{type_.container}`" - elif isinstance(type_, selector.SelectSelector): + if isinstance(type_, selector.SelectSelector): return f"one of `{type_.config['options']}`" - elif isinstance(type_, selector.ColorRGBSelector): + if isinstance(type_, selector.ColorRGBSelector): return "RGB color" - else: - raise ValueError(f"Unknown type: {type_}") + msg = f"Unknown type: {type_}" + raise ValueError(msg) def generate_config_markdown_table(): @@ -85,7 +84,8 @@ def _schema_to_dict(schema: vol.Schema) -> dict[str, tuple[Any, Any]]: def _generate_service_markdown_table( - schema: dict[str, tuple[Any, Any]], alternative_docs: dict[str, str] = None + schema: dict[str, tuple[Any, Any]], + alternative_docs: dict[str, str] | None = None, ): schema = _schema_to_dict(schema) rows = [] @@ -112,5 +112,6 @@ def generate_apply_markdown_table(): def generate_set_manual_control_markdown_table(): return _generate_service_markdown_table( - SET_MANUAL_CONTROL_SCHEMA, DOCS_MANUAL_CONTROL + SET_MANUAL_CONTROL_SCHEMA, + DOCS_MANUAL_CONTROL, ) diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py index 374defd1..99d1eea5 100644 --- a/custom_components/adaptive_lighting/adaptation_utils.py +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -1,7 +1,7 @@ """Utility functions for adaptation commands.""" +import logging from collections.abc import AsyncGenerator from dataclasses import dataclass -import logging from typing import Any, Literal from homeassistant.components.light import ( @@ -40,10 +40,10 @@ ServiceData = dict[str, Any] def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]: - """Splits the service data by the adapted attributes, i.e., into separate data - items for brightness and color. - """ + """Splits the service data by the adapted attributes. + i.e., into separate data items for brightness and color. + """ common_attrs = {ATTR_ENTITY_ID} common_data = {k: service_data[k] for k in common_attrs if k in service_data} @@ -70,13 +70,14 @@ def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]: def _remove_redundant_attributes( - service_data: ServiceData, state: State + service_data: ServiceData, + state: State, ) -> ServiceData: """Filter service data by removing attributes that already equal the given state. Removes all attributes from service call data whose values are already present - in the target entity's state.""" - + in the target entity's state. + """ return { k: v for k, v in service_data.items() @@ -88,7 +89,8 @@ def _has_relevant_service_data_attributes(service_data: ServiceData) -> bool: """Determines whether the service data justifies an adaptation service call. A service call is not justified for data which does not contain any entries that - change relevant attributes of an adapting entity, e.g., brightness or color.""" + change relevant attributes of an adapting entity, e.g., brightness or color. + """ common_attrs = {ATTR_ENTITY_ID, ATTR_TRANSITION} return any(attr not in common_attrs for attr in service_data) @@ -108,15 +110,15 @@ async def _create_service_call_data_iterator( at the time when the service data is read instead of up front. This gives greater flexibility because entity states can change while the items are iterated. """ - for service_data in service_datas: if filter_by_state and (entity_id := service_data.get(ATTR_ENTITY_ID)): current_entity_state = hass.states.get(entity_id) # Filter data to remove attributes that equal the current state if current_entity_state is not None: - service_data = _remove_redundant_attributes( - service_data, current_entity_state + service_data = _remove_redundant_attributes( # noqa: PLW2901 + service_data, + state=current_entity_state, ) # Emit service data if it still contains relevant attributes (else try next) @@ -143,7 +145,7 @@ class AdaptationData: return await anext(self.service_call_datas, None) -class NoColorOrBrightnessInServiceData(Exception): +class NoColorOrBrightnessInServiceDataError(Exception): """Exception raised when no color or brightness attributes are found in service data.""" @@ -160,7 +162,7 @@ def _identify_lighting_type( if has_color: return "color" msg = f"Invalid service_data, no brightness or color attributes found: {service_data=}" - raise NoColorOrBrightnessInServiceData(msg) + raise NoColorOrBrightnessInServiceDataError(msg) def prepare_adaptation_data( @@ -179,10 +181,7 @@ def prepare_adaptation_data( entity_id, service_data, ) - if split: - service_datas = _split_service_call_data(service_data) - else: - service_datas = [service_data] + service_datas = _split_service_call_data(service_data) if split else [service_data] service_datas_length = len(service_datas) @@ -193,7 +192,9 @@ def prepare_adaptation_data( sleep_time = split_delay service_data_iterator = _create_service_call_data_iterator( - hass, service_datas, filter_by_state + hass, + service_datas, + filter_by_state, ) lighting_type = _identify_lighting_type(service_data) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 10ba5d86..170c8503 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -1,11 +1,11 @@ """Config flow for Adaptive Lighting integration.""" import logging +import homeassistant.helpers.config_validation as cv +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_NAME from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -import voluptuous as vol from .const import ( # pylint: disable=unused-import CONF_LIGHTS, @@ -75,7 +75,7 @@ def validate_options(user_input, errors): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for Adaptive Lighting.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry @@ -114,5 +114,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): options_schema[key] = value return self.async_show_form( - step_id="init", data_schema=vol.Schema(options_schema), errors=errors + step_id="init", + data_schema=vol.Schema(options_schema), + errors=errors, ) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 15ce097b..64b96583 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,10 +1,10 @@ """Constants for the Adaptive Lighting integration.""" +import homeassistant.helpers.config_validation as cv +import voluptuous as vol from homeassistant.components.light import VALID_TRANSITION from homeassistant.const import CONF_ENTITY_ID from homeassistant.helpers import selector -import homeassistant.helpers.config_validation as cv -import voluptuous as vol ICON_MAIN = "mdi:theme-light-dark" ICON_BRIGHTNESS = "mdi:brightness-4" @@ -49,9 +49,9 @@ DOCS[CONF_INITIAL_TRANSITION] = ( ) CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION = "sleep_transition", 1 -DOCS[CONF_SLEEP_TRANSITION] = ( - 'Duration of transition when "sleep mode" is toggled ' "in seconds. 😴" -) +DOCS[ + CONF_SLEEP_TRANSITION +] = 'Duration of transition when "sleep mode" is toggled in seconds. 😴' CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 DOCS[CONF_INTERVAL] = "Frequency to adapt the lights, in seconds. 🔄" @@ -99,22 +99,22 @@ DOCS[CONF_SLEEP_COLOR_TEMP] = ( ) CONF_SLEEP_RGB_COLOR, DEFAULT_SLEEP_RGB_COLOR = "sleep_rgb_color", [255, 56, 0] -DOCS[CONF_SLEEP_RGB_COLOR] = ( - "RGB color in sleep mode (used when " '`sleep_rgb_or_color_temp` is "rgb_color"). 🌈' -) +DOCS[ + CONF_SLEEP_RGB_COLOR +] = 'RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈' CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = ( "sleep_rgb_or_color_temp", "color_temp", ) -DOCS[CONF_SLEEP_RGB_OR_COLOR_TEMP] = ( - 'Use either `"rgb_color"` or `"color_temp"` ' "in sleep mode. 🌙" -) +DOCS[ + CONF_SLEEP_RGB_OR_COLOR_TEMP +] = 'Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙' CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 -DOCS[CONF_SUNRISE_OFFSET] = ( - "Adjust sunrise time with a positive or negative offset " "in seconds. ⏰" -) +DOCS[ + CONF_SUNRISE_OFFSET +] = "Adjust sunrise time with a positive or negative offset in seconds. ⏰" CONF_SUNRISE_TIME = "sunrise_time" DOCS[CONF_SUNRISE_TIME] = "Set a fixed time (HH:MM:SS) for sunrise. 🌅" @@ -333,7 +333,7 @@ _DOMAIN_SCHEMA = vol.Schema( { vol.Optional(key, default=replace_none_str(default, vol.UNDEFINED)): validation for key, default, validation in _yaml_validation_tuples - } + }, ) @@ -351,7 +351,7 @@ def apply_service_schema(initial_transition: int = 1): vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean, vol.Optional(CONF_PREFER_RGB_COLOR, default=False): cv.boolean, vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean, - } + }, ) @@ -360,5 +360,5 @@ SET_MANUAL_CONTROL_SCHEMA = vol.Schema( vol.Optional(CONF_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean, - } + }, ) diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py index 5a195bcc..3f08b3fc 100644 --- a/custom_components/adaptive_lighting/hass_utils.py +++ b/custom_components/adaptive_lighting/hass_utils.py @@ -1,6 +1,5 @@ """Utility functions for HA core.""" -from collections.abc import Awaitable -from typing import Callable +from collections.abc import Awaitable, Callable from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.util.read_only_dict import ReadOnlyDict @@ -17,7 +16,8 @@ def setup_service_call_interceptor( """Inject a function into a registered service call to preprocess service data. The injected interceptor function receives the service call and a writeable data dictionary - (the data of the service call is read-only) before the service call is executed.""" + (the data of the service call is read-only) before the service call is executed. + """ try: # HACK: Access protected attribute of HA service registry. # This is necessary to replace a registered service handler with our @@ -26,15 +26,15 @@ def setup_service_call_interceptor( hass.services._services # pylint: disable=protected-access ) except AttributeError as error: - raise RuntimeError( - "Intercept failed because registered services are no longer accessible " - "(internal API may have changed)" - ) from error + msg = ( + "Intercept failed because registered services are no longer" + " accessible (internal API may have changed)" + ) + raise RuntimeError(msg) from error if domain not in registered_services or service not in registered_services[domain]: - raise RuntimeError( - f"Intercept failed because service {domain}.{service} is not registered" - ) + msg = f"Intercept failed because service {domain}.{service} is not registered" + raise RuntimeError(msg) existing_service = registered_services[domain][service] @@ -52,13 +52,19 @@ def setup_service_call_interceptor( await existing_service.job.target(call) hass.services.async_register( - domain, service, service_func_proxy, existing_service.schema + domain, + service, + service_func_proxy, + existing_service.schema, ) def remove(): # Remove the interceptor by reinstalling the original service handler hass.services.async_register( - domain, service, existing_service.job.target, existing_service.schema + domain, + service, + existing_service.job.target, + existing_service.schema, ) return remove diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 58fa3eaa..d42cccae 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -4,17 +4,19 @@ from __future__ import annotations import asyncio import base64 import bisect -from collections.abc import Callable, Coroutine, Iterable -from copy import deepcopy -from dataclasses import dataclass import datetime -from datetime import timedelta import functools import logging import math -from typing import Any, Literal +from copy import deepcopy +from dataclasses import dataclass +from datetime import timedelta +from typing import TYPE_CHECKING, Any, Literal -import astral +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util +import ulid_transform +import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP_KELVIN, @@ -29,8 +31,6 @@ from homeassistant.components.light import ( COLOR_MODE_RGBW, COLOR_MODE_RGBWW, COLOR_MODE_XY, -) -from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, @@ -41,7 +41,6 @@ from homeassistant.components.light import ( from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_AREA_ID, ATTR_DOMAIN, @@ -71,7 +70,6 @@ from homeassistant.core import ( callback, ) from homeassistant.helpers import entity_platform, entity_registry -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( async_track_state_change_event, async_track_time_interval, @@ -86,9 +84,6 @@ from homeassistant.util.color import ( color_xy_to_hs, color_xy_to_RGB, ) -import homeassistant.util.dt as dt_util -import ulid_transform -import voluptuous as vol from .adaptation_utils import ( BRIGHTNESS_ATTRS, @@ -156,6 +151,13 @@ from .const import ( ) from .hass_utils import setup_service_call_interceptor +if TYPE_CHECKING: + from collections.abc import Callable, Coroutine, Iterable + + import astral + from homeassistant.config_entries import ConfigEntry + from homeassistant.helpers.entity_platform import AddEntitiesCallback + _SUPPORT_OPTS = { "brightness": SUPPORT_BRIGHTNESS, "color_temp": SUPPORT_COLOR_TEMP, @@ -209,17 +211,17 @@ def _int_to_base36(num: int) -> str: >>> print(base36_num) '2N9' """ - ALPHANUMERIC_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + alphanumeric_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" if num == 0: - return ALPHANUMERIC_CHARS[0] + return alphanumeric_chars[0] base36_str = "" - base = len(ALPHANUMERIC_CHARS) + base = len(alphanumeric_chars) while num: num, remainder = divmod(num, base) - base36_str = ALPHANUMERIC_CHARS[remainder] + base36_str + base36_str = alphanumeric_chars[remainder] + base36_str return base36_str @@ -236,7 +238,10 @@ def _remove_vowels(input_str: str, length: int = 4) -> str: def create_context( - name: str, which: str, index: int, parent: Context | None = None + name: str, + which: str, + index: int, + parent: Context | None = None, ) -> Context: """Create a context that can identify this integration.""" # Use a hash for the name because otherwise the context might become @@ -255,6 +260,7 @@ def create_context( def is_our_context_id(context_id: str | None) -> bool: + """Check whether this integration created 'context_id'.""" if context_id is None: return False return f":{_DOMAIN_SHORT}:" in context_id @@ -268,7 +274,8 @@ def is_our_context(context: Context | None) -> bool: def _switches_with_lights( - hass: HomeAssistant, lights: list[str] + hass: HomeAssistant, + lights: list[str], ) -> list[AdaptiveSwitch]: """Get all switches that control at least one of the lights passed.""" config_entries = hass.config_entries.async_entries(DOMAIN) @@ -299,48 +306,52 @@ def _switch_with_lights( switches = _switches_with_lights(hass, lights) if len(switches) == 1: return switches[0] - elif len(switches) > 1: + if len(switches) > 1: on_switches = [s for s in switches if s.is_on] if len(on_switches) == 1: # Of the multiple switches, only one is on return on_switches[0] - raise NoSwitchFoundError( + msg = ( f"_switch_with_lights: Light(s) {lights} found in multiple switch configs" f" ({[s.entity_id for s in switches]}). You must pass a switch under" - f" 'entity_id'." - ) - else: - raise NoSwitchFoundError( - f"_switch_with_lights: Light(s) {lights} not found in any switch's" - f" configuration. You must either include the light(s) that is/are" - f" in the integration config, or pass a switch under 'entity_id'." + " 'entity_id'." ) + raise NoSwitchFoundError(msg) + msg = ( + f"_switch_with_lights: Light(s) {lights} not found in any switch's" + " configuration. You must either include the light(s) that is/are" + " in the integration config, or pass a switch under 'entity_id'." + ) + raise NoSwitchFoundError(msg) # For documentation on this function, see integration_entities() from HomeAssistant Core: # https://github.com/home-assistant/core/blob/dev/homeassistant/helpers/template.py#L1109 def _switches_from_service_call( - hass: HomeAssistant, service_call: ServiceCall + hass: HomeAssistant, + service_call: ServiceCall, ) -> list[AdaptiveSwitch]: data = service_call.data lights = data[CONF_LIGHTS] switch_entity_ids: list[str] | None = data.get("entity_id") if not lights and not switch_entity_ids: - raise ValueError( + msg = ( "adaptive-lighting: Neither a switch nor a light was provided in the service call." - " If you intend to adapt all lights on all switches, please inform the developers at" - " https://github.com/basnijholt/adaptive-lighting about your use case." - " Currently, you must pass either an adaptive-lighting switch or the lights to an" - " `adaptive_lighting` service call." + " If you intend to adapt all lights on all switches, please inform the" + " developers at https://github.com/basnijholt/adaptive-lighting about your" + " use case. Currently, you must pass either an adaptive-lighting switch or" + " the lights to an `adaptive_lighting` service call." ) + raise ValueError(msg) if switch_entity_ids is not None: if len(switch_entity_ids) > 1 and lights: - raise ValueError( - f"adaptive-lighting: Cannot pass multiple switches with lights argument." + msg = ( + "adaptive-lighting: Cannot pass multiple switches with lights argument." f" Invalid service data received: {service_call.data}" ) + raise ValueError(msg) switches = [] ent_reg = entity_registry.async_get(hass) for entity_id in switch_entity_ids: @@ -353,14 +364,16 @@ def _switches_from_service_call( switch = _switch_with_lights(hass, lights) return [switch] - raise ValueError( - f"adaptive-lighting: Incorrect data provided in service call." + msg = ( + "adaptive-lighting: Incorrect data provided in service call." f" Entities not found in the integration. Service data: {service_call.data}" ) + raise ValueError(msg) async def handle_change_switch_settings( - switch: AdaptiveSwitch, service_call: ServiceCall + switch: AdaptiveSwitch, + service_call: ServiceCall, ) -> None: """Allows HASS to change config values via a service call.""" data = service_call.data @@ -396,7 +409,10 @@ async def handle_change_switch_settings( @callback def _fire_manual_control_event( - switch: AdaptiveSwitch, light: str, context: Context, is_async=True + switch: AdaptiveSwitch, + light: str, + context: Context, + is_async: bool = True, ): """Fire an event that 'light' is marked as manual_control.""" hass = switch.hass @@ -414,8 +430,10 @@ def _fire_manual_control_event( ) -async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: bool +async def async_setup_entry( # noqa: PLR0915 + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up the AdaptiveLighting switch.""" data = hass.data[DOMAIN] @@ -423,17 +441,30 @@ async def async_setup_entry( if ATTR_ADAPTIVE_LIGHTING_MANAGER not in data: data[ATTR_ADAPTIVE_LIGHTING_MANAGER] = AdaptiveLightingManager( - hass, config_entry + hass, + config_entry, ) manager: AdaptiveLightingManager = data[ATTR_ADAPTIVE_LIGHTING_MANAGER] sleep_mode_switch = SimpleSwitch( - "Sleep Mode", False, hass, config_entry, ICON_SLEEP + which="Sleep Mode", + initial_state=False, + hass=hass, + config_entry=config_entry, + icon=ICON_SLEEP, ) adapt_color_switch = SimpleSwitch( - "Adapt Color", True, hass, config_entry, ICON_COLOR_TEMP + which="Adapt Color", + initial_state=True, + hass=hass, + config_entry=config_entry, + icon=ICON_COLOR_TEMP, ) adapt_brightness_switch = SimpleSwitch( - "Adapt Brightness", True, hass, config_entry, ICON_BRIGHTNESS + which="Adapt Brightness", + initial_state=True, + hass=hass, + config_entry=config_entry, + icon=ICON_BRIGHTNESS, ) switch = AdaptiveSwitch( hass, @@ -482,7 +513,8 @@ async def async_setup_entry( data[ATTR_ADAPT_COLOR], data[CONF_PREFER_RGB_COLOR], context=switch.create_context( - "service", parent=service_call.context + "service", + parent=service_call.context, ), ) @@ -513,7 +545,8 @@ async def async_setup_entry( transition=switch.initial_transition, force=True, context=switch.create_context( - "service", parent=service_call.context + "service", + parent=service_call.context, ), ) @@ -637,7 +670,8 @@ def _supported_features(hass: HomeAssistant, light: str) -> set[str]: def color_difference_redmean( - rgb1: tuple[float, float, float], rgb2: tuple[float, float, float] + rgb1: tuple[float, float, float], + rgb2: tuple[float, float, float], ) -> float: """Distance between colors in RGB space (redmean metric). @@ -648,7 +682,9 @@ def color_difference_redmean( - https://www.compuphase.com/cmetric.htm """ r_hat = (rgb1[0] + rgb2[0]) / 2 - delta_r, delta_g, delta_b = ((col1 - col2) for col1, col2 in zip(rgb1, rgb2)) + delta_r, delta_g, delta_b = ( + (col1 - col2) for col1, col2 in zip(rgb1, rgb2, strict=True) + ) red_term = (2 + r_hat / 256) * delta_r**2 green_term = 4 * delta_g**2 blue_term = (2 + (255 - r_hat) / 256) * delta_b**2 @@ -700,7 +736,8 @@ def _attributes_have_changed( ) -> bool: if adapt_color: old_attributes, new_attributes = _add_missing_attributes( - old_attributes, new_attributes + old_attributes, + new_attributes, ) if ( @@ -771,7 +808,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): sleep_mode_switch: SimpleSwitch, adapt_color_switch: SimpleSwitch, adapt_brightness_switch: SimpleSwitch, - ): + ) -> None: """Initialize the Adaptive Lighting switch.""" # Set attributes that can't be modified during runtime self.hass = hass @@ -920,7 +957,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): await self._setup_listeners() else: self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, self._setup_listeners + EVENT_HOMEASSISTANT_STARTED, + self._setup_listeners, ) last_state = await self.async_get_last_state() is_new_entry = last_state is None # newly added to HA @@ -938,7 +976,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): all_lights = _expand_light_groups(self.hass, self.lights) self.manager.lights.update(all_lights) self.manager.set_auto_reset_manual_control_times( - all_lights, self._auto_reset_manual_control_time + all_lights, + self._auto_reset_manual_control_time, ) self.lights = list(all_lights) @@ -963,7 +1002,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self.lights: self._expand_light_groups() remove_state = async_track_state_change_event( - self.hass, entity_ids=self.lights, action=self._light_event_action + self.hass, + entity_ids=self.lights, + action=self._light_event_action, ) self.remove_listeners.append(remove_state) @@ -1029,7 +1070,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return extra_state_attributes def create_context( - self, which: str = "default", parent: Context | None = None + self, + which: str = "default", + parent: Context | None = None, ) -> Context: """Create a context that identifies this Adaptive Lighting instance.""" context = create_context(self._name, which, self._context_cnt, parent=parent) @@ -1037,11 +1080,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return context async def async_turn_on( # pylint: disable=arguments-differ - self, adapt_lights: bool = True + self, + adapt_lights: bool = True, ) -> None: """Turn on adaptive lighting.""" _LOGGER.debug( - "%s: Called 'async_turn_on', current state is '%s'", self._name, self._state + "%s: Called 'async_turn_on', current state is '%s'", + self._name, + self._state, ) if self.is_on: return @@ -1055,7 +1101,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context=self.create_context("turn_on"), ) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: # noqa: ARG002 """Turn off adaptive lighting.""" if not self.is_on: return @@ -1063,7 +1109,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._remove_listeners() self.manager.reset(*self.lights) - async def _async_update_at_interval_action(self, now=None) -> None: + async def _async_update_at_interval_action(self, now=None) -> None: # noqa: ARG002 await self._update_attrs_and_maybe_adapt_lights( transition=self._transition, force=False, @@ -1079,6 +1125,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): prefer_rgb_color: bool | None = None, context: Context | None = None, ) -> AdaptationData | None: + """Prepare `AdaptationData` for adapting a light.""" if transition is None: transition = self._transition if adapt_brightness is None: @@ -1099,7 +1146,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # The switch might be off and not have _settings set. self._settings = self._sun_light_settings.get_settings( - self.sleep_mode_switch.is_on, transition + self.sleep_mode_switch.is_on, + transition, ) # Build service data. @@ -1151,7 +1199,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): filter_by_state=self._skip_redundant_commands, ) - async def _adapt_light( # noqa: C901 + async def _adapt_light( self, light: str, transition: int | None = None, @@ -1167,7 +1215,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self.manager.is_proactively_adapting(context.parent_id): # Skip if adaptation was already executed by the service call interceptor _LOGGER.debug( - "%s: Skipping reactive adaptation of %s", self._name, context.parent_id + "%s: Skipping reactive adaptation of %s", + self._name, + context.parent_id, ) return @@ -1180,13 +1230,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context, ) if data is None: - return None # nothing to adapt + return # nothing to adapt await self.execute_cancellable_adaptation_calls(data) async def _execute_adaptation_calls(self, data: AdaptationData): """Executes a sequence of adaptation service calls for the given service datas.""" - for index in range(data.max_length): is_first_call = index == 0 @@ -1250,7 +1299,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): data, ) - async def _update_attrs_and_maybe_adapt_lights( + async def _update_attrs_and_maybe_adapt_lights( # noqa: PLR0912 self, lights: list[str] | None = None, transition: int | None = None, @@ -1270,8 +1319,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): assert self.is_on self._settings.update( self._sun_light_settings.get_settings( - self.sleep_mode_switch.is_on, transition - ) + self.sleep_mode_switch.is_on, + transition, + ), ) self.async_write_ha_state() @@ -1352,7 +1402,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug("%s: Ignoring sleep event %s", self._name, event) return _LOGGER.debug( - "%s: _sleep_mode_switch_state_event_action, event: '%s'", self._name, event + "%s: _sleep_mode_switch_state_event_action, event: '%s'", + self._name, + event, ) # Reset the manually controlled status when the "sleep mode" changes self.manager.reset(*self.lights) @@ -1380,7 +1432,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) if event.context.parent_id and not self.manager.is_proactively_adapting( - event.context.id + event.context.id, ): self.manager.reset(entity_id, reset_manual_control=False) @@ -1395,7 +1447,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ): # Stop if a rapid 'off' → 'on' → 'off' happens. _LOGGER.debug( - "%s: Cancelling adjusting lights for %s", self._name, entity_id + "%s: Cancelling adjusting lights for %s", + self._name, + entity_id, ) return @@ -1441,7 +1495,7 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): hass: HomeAssistant, config_entry: ConfigEntry, icon: str, - ): + ) -> None: """Initialize the Adaptive Lighting switch.""" self.hass = hass data = validate(config_entry) @@ -1484,19 +1538,21 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): else: await self.async_turn_off() - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs) -> None: # noqa: ARG002 """Turn on adaptive lighting sleep mode.""" _LOGGER.debug("%s: Turning on", self._name) self._state = True - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: # noqa: ARG002 """Turn off adaptive lighting sleep mode.""" _LOGGER.debug("%s: Turning off", self._name) self._state = False def lerp_color( - rgb1: tuple[int, int, int], rgb2: tuple[int, int, int], t: float + rgb1: tuple[int, int, int], + rgb2: tuple[int, int, int], + t: float, ) -> tuple[int, int, int]: """Linearly interpolate between two RGB colors.""" return ( @@ -1536,13 +1592,13 @@ class SunLightSettings: def _replace_time(date: datetime.datetime, key: str) -> datetime.datetime: time = getattr(self, f"{key}_time") date_time = datetime.datetime.combine(date, time) - utc_time = date_time.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone( - dt_util.UTC + return date_time.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone( + dt_util.UTC, ) - return utc_time def calculate_noon_and_midnight( - sunset: datetime.datetime, sunrise: datetime.datetime + sunset: datetime.datetime, + sunrise: datetime.datetime, ) -> tuple[datetime.datetime, datetime.datetime]: middle = abs(sunset - sunrise) / 2 if sunset > sunrise: @@ -1597,7 +1653,7 @@ class SunLightSettings: ] # Check whether order is correct events = sorted(events, key=lambda x: x[1]) - events_names, _ = zip(*events) + events_names, _ = zip(*events, strict=True) if events_names not in _ALLOWED_ORDERS: msg = ( f"{self.name}: The sun events {events_names} are not in the expected" @@ -1635,8 +1691,7 @@ class SunLightSettings: else (next_ts, prev_ts) ) k = 1 if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_NOON) else -1 - percentage = (0 - k) * ((target_ts - h) / (h - x)) ** 2 + k - return percentage + return (0 - k) * ((target_ts - h) / (h - x)) ** 2 + k def calc_brightness_pct(self, percent: float, is_sleep: bool) -> float: """Calculate the brightness in %.""" @@ -1660,9 +1715,12 @@ class SunLightSettings: delta = abs(self.min_color_temp - self.sleep_color_temp) ct = (delta * abs(1 + percent)) + self.sleep_color_temp return 5 * round(ct / 5) # round to nearest 5 + return None def get_settings( - self, is_sleep, transition + self, + is_sleep, + transition, ) -> dict[str, float | int | tuple[float, float] | tuple[float, float, float]]: """Get all light settings. @@ -1715,7 +1773,7 @@ class SunLightSettings: class AdaptiveLightingManager: """Track 'light.turn_off' and 'light.turn_on' service calls.""" - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry): + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize the AdaptiveLightingManager that is shared among all switches.""" self.hass = hass data = validate(config_entry) @@ -1759,7 +1817,8 @@ class AdaptiveLightingManager: self._proactively_adapting_contexts: dict[str, str] = {} is_proactive_adaptation_enabled = data.get( - INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION, True + INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION, + True, ) if is_proactive_adaptation_enabled: @@ -1770,7 +1829,7 @@ class AdaptiveLightingManager: LIGHT_DOMAIN, SERVICE_TURN_ON, self._service_interceptor_turn_on_handler, - ) + ), ) self.listener_removers.append( @@ -1779,7 +1838,7 @@ class AdaptiveLightingManager: LIGHT_DOMAIN, SERVICE_TOGGLE, self._service_interceptor_turn_on_handler, - ) + ), ) _LOGGER.debug("Proactive adaptation enabled") @@ -1796,20 +1855,21 @@ class AdaptiveLightingManager: remove() def set_proactively_adapting(self, context_id: str, entity_id: str) -> None: - """Declare the adaptation with the given context ID as proactively adapting, - and associate it to an entity ID.""" + """Declare the adaptation with context_id as proactively adapting, + and associate it to an entity_id. + """ # noqa: D205 self._proactively_adapting_contexts[context_id] = entity_id def is_proactively_adapting(self, context_id: str) -> bool: - """Determine whether an adaptation with the given context ID is proactive.""" + """Determine whether an adaptation with the given context_id is proactive.""" is_proactively_adapting_context = ( context_id in self._proactively_adapting_contexts ) _LOGGER.debug( - "is_proactively_adapting_context %s %s", - context_id, + "is_proactively_adapting_context='%s', context_id='%s'", is_proactively_adapting_context, + context_id, ) return is_proactively_adapting_context @@ -1817,16 +1877,19 @@ class AdaptiveLightingManager: def clear_proactively_adapting(self, entity_id: str) -> None: """Clear all context IDs associated with the given entity ID. - Call this method to clear past context IDs and avoid a memory leak.""" + Call this method to clear past context IDs and avoid a memory leak. + """ + # First get the keys to avoid modifying the dict while iterating it keys = [ k for k, v in self._proactively_adapting_contexts.items() if v == entity_id ] - for key in keys: self._proactively_adapting_contexts.pop(key) - async def _service_interceptor_turn_on_handler( - self, call: ServiceCall, data: ServiceData + async def _service_interceptor_turn_on_handler( # noqa: PLR0911 + self, + call: ServiceCall, + data: ServiceData, ): # Don't adapt our own service calls if is_our_context(call.context): @@ -1872,14 +1935,17 @@ class AdaptiveLightingManager: return _LOGGER.debug( - "Intercepted TURN_ON call with data %s (%s)", data, call.context.id + "Intercepted TURN_ON call with data %s (%s)", + data, + call.context.id, ) self.reset(entity_id, reset_manual_control=False) self.clear_proactively_adapting(entity_id) transition = data[CONF_PARAMS].get( - ATTR_TRANSITION, adaptive_switch.initial_transition + ATTR_TRANSITION, + adaptive_switch.initial_transition, ) adaptation_data = await adaptive_switch.prepare_adaptation_data( @@ -1913,8 +1979,8 @@ class AdaptiveLightingManager: self.set_proactively_adapting(call.context.id, entity_id) self.set_proactively_adapting(adaptation_data.context.id, entity_id) adaptation_data.initial_sleep = True - asyncio.create_task( # Don't await to avoid blocking the service call - adaptive_switch.execute_cancellable_adaptation_calls(adaptation_data) + _ = asyncio.create_task( # Don't await to avoid blocking the service call + adaptive_switch.execute_cancellable_adaptation_calls(adaptation_data), ) def _handle_timer( @@ -1946,11 +2012,14 @@ class AdaptiveLightingManager: last_transition = last_service_data.get(ATTR_TRANSITION) if not last_transition: _LOGGER.debug( - "No transition in last adapt for light %s, continuing...", light + "No transition in last adapt for light %s, continuing...", + light, ) return _LOGGER.debug( - "Start transition timer of %s seconds for light %s", last_transition, light + "Start transition timer of %s seconds for light %s", + last_transition, + light, ) async def reset(): @@ -2008,7 +2077,9 @@ class AdaptiveLightingManager: self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset) def cancel_ongoing_adaptation_calls( - self, light_id: str, which: Literal["color", "brightness", "both"] = "both" + self, + light_id: str, + which: Literal["color", "brightness", "both"] = "both", ): """Cancel ongoing adaptation service calls for a specific light entity.""" brightness_task = self.adaptation_tasks_brightness.get(light_id) @@ -2054,15 +2125,21 @@ class AdaptiveLightingManager: area_ids = cv.ensure_list_csv(service_data[ATTR_AREA_ID]) for area_id in area_ids: area_entity_ids = area_entities(self.hass, area_id) - for entity_id in area_entity_ids: - if entity_id.startswith(LIGHT_DOMAIN): - entity_ids.append(entity_id) + eids = [ + entity_id + for entity_id in area_entity_ids + if entity_id.startswith(LIGHT_DOMAIN) + ] + entity_ids.extend(eids) _LOGGER.debug( - "Found entity_ids '%s' for area_id '%s'", entity_ids, area_id + "Found entity_ids '%s' for area_id '%s'", + entity_ids, + area_id, ) else: _LOGGER.debug( - "No entity_ids or area_ids found in service_data: %s", service_data + "No entity_ids or area_ids found in service_data: %s", + service_data, ) return entity_ids @@ -2219,7 +2296,7 @@ class AdaptiveLightingManager: """ last_service_data = self.last_service_data.get(light) if last_service_data is None: - return + return None compare_to = functools.partial( _attributes_have_changed, light=light, @@ -2268,8 +2345,11 @@ class AdaptiveLightingManager: ) return False - async def maybe_cancel_adjusting( - self, entity_id: str, off_to_on_event: Event, on_to_off_event: Event | None + async def maybe_cancel_adjusting( # noqa: PLR0911, PLR0912 + self, + entity_id: str, + off_to_on_event: Event, + on_to_off_event: Event | None, ) -> bool: """Cancel the adjusting of a light if it has just been turned off. @@ -2368,7 +2448,7 @@ class AdaptiveLightingManager: class _AsyncSingleShotTimer: - def __init__(self, delay, callback): + def __init__(self, delay, callback) -> None: """Initialize the timer.""" self.delay = delay self.callback = callback diff --git a/test_dependencies.py b/test_dependencies.py index 2cf8dfb0..f8fbf5b9 100644 --- a/test_dependencies.py +++ b/test_dependencies.py @@ -1,13 +1,17 @@ +"""Extracts the dependencies of the components required for testing.""" from collections import defaultdict +from pathlib import Path deps = defaultdict(list) components, packages = [], [] -with open("core/requirements_test_all.txt") as f: +requirements = Path("core") / "requirements_test_all.txt" + +with requirements.open() as f: lines = f.readlines() for line in lines: - line = line.strip() + line = line.strip() # noqa: PLW2901 if line.startswith("# homeassistant."): if components and packages: @@ -34,4 +38,4 @@ required = [ ] to_install = [package for r in required for package in deps[r]] -print(" ".join(to_install)) +print(" ".join(to_install)) # noqa: T201 From 7650a1b3e633cc085af8b18d8a2c78df50f2d911 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 23 Jul 2023 14:44:17 -0700 Subject: [PATCH 0586/1077] Bump to 1.17.0 in manifest.json (#657) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index fcf644cd..c3a9c50a 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.16.3" + "version": "1.17.0" } From f9b6753e6d7a6a48aa6ad3a34ec5d66ab871f8ce Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Jul 2023 10:46:34 -0700 Subject: [PATCH 0587/1077] Ensure that interpolation is in [0, 1], fixes #624 (#660) * Ensure that interpolation is in [0, 1], fixes #624 * Bump to 1.17.1 * Add assert * Interpolate via HSV space * Add test --- .../adaptive_lighting/manifest.json | 2 +- custom_components/adaptive_lighting/switch.py | 28 ++++++++++++++----- tests/test_switch.py | 17 +++++++++++ 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index c3a9c50a..747fa2dd 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.17.0" + "version": "1.17.1" } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index d42cccae..ce9f6922 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import base64 import bisect +import colorsys import datetime import functools import logging @@ -1549,18 +1550,31 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): self._state = False -def lerp_color( +def lerp_color_hsv( rgb1: tuple[int, int, int], rgb2: tuple[int, int, int], t: float, ) -> tuple[int, int, int]: - """Linearly interpolate between two RGB colors.""" - return ( - int(rgb1[0] + t * (rgb2[0] - rgb1[0])), - int(rgb1[1] + t * (rgb2[1] - rgb1[1])), - int(rgb1[2] + t * (rgb2[2] - rgb1[2])), + """Linearly interpolate between two RGB colors in HSV color space.""" + t = abs(t) + assert 0 <= t <= 1 + + # Convert RGB to HSV + hsv1 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb1]) + hsv2 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb2]) + + # Linear interpolation in HSV space + hsv = ( + hsv1[0] + t * (hsv2[0] - hsv1[0]), + hsv1[1] + t * (hsv2[1] - hsv1[1]), + hsv1[2] + t * (hsv2[2] - hsv1[2]), ) + # Convert back to RGB + rgb = tuple(int(round(x * 255)) for x in colorsys.hsv_to_rgb(*hsv)) + assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}" + return rgb + @dataclass(frozen=True) class SunLightSettings: @@ -1748,7 +1762,7 @@ class SunLightSettings: # This will result in a perceptible jump in color at sunset and sunrise # because the `color_temperature_to_rgb` function is not 100% accurate. min_color_rgb = color_temperature_to_rgb(self.min_color_temp) - rgb_color = lerp_color(min_color_rgb, self.sleep_rgb_color, percent) + rgb_color = lerp_color_hsv(min_color_rgb, self.sleep_rgb_color, percent) color_temp_kelvin = self.calc_color_temp_kelvin(percent) force_rgb_color = True else: diff --git a/tests/test_switch.py b/tests/test_switch.py index 9b679a86..360fec55 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -92,6 +92,7 @@ from custom_components.adaptive_lighting.switch import ( create_context, is_our_context, is_our_context_id, + lerp_color_hsv, ) _LOGGER = logging.getLogger(__name__) @@ -1701,3 +1702,19 @@ async def test_adapt_until_sleep_and_rgb_colors(hass): await switch._update_attrs_and_maybe_adapt_lights(context=context) assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_SLEEP_BRIGHTNESS assert switch._settings["rgb_color"] == DEFAULT_SLEEP_RGB_COLOR + + +def test_lerp_color_hsv(): + assert lerp_color_hsv((255, 0, 0), (0, 255, 0), 0) == (255, 0, 0) + assert lerp_color_hsv((255, 0, 0), (0, 255, 0), 1) == (0, 255, 0) + assert lerp_color_hsv((255, 0, 0), (0, 255, 0), 0.5) == (255, 255, 0) + assert lerp_color_hsv((0, 0, 255), (255, 255, 255), 0.5) == (128, 255, 128) + + # Tests that the interpolation is consistent + for t in [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]: + color = lerp_color_hsv((255, 0, 0), (0, 255, 0), t) + inverted_color = lerp_color_hsv((0, 255, 0), (255, 0, 0), 1 - t) + assert color == inverted_color + + with pytest.raises(AssertionError): + lerp_color_hsv((255, 0, 0), (0, 255, 0), 1.1) From 8696092879d4967dccefaec6472937a74e6c2616 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 25 Jul 2023 19:17:11 -0700 Subject: [PATCH 0588/1077] Avoid adapting lights with nothing in service_data, closes #661 (#662) --- custom_components/adaptive_lighting/manifest.json | 2 +- custom_components/adaptive_lighting/switch.py | 12 ++++++++++++ tests/test_switch.py | 8 ++++---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 747fa2dd..1e232dab 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.17.1" + "version": "1.17.2" } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index ce9f6922..6e337a30 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1159,6 +1159,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): use_transition = "transition" in features and transition > 0 if use_transition: service_data[ATTR_TRANSITION] = transition + if "brightness" in features and adapt_brightness: brightness = round(255 * self._settings["brightness_pct"] / 100) service_data[ATTR_BRIGHTNESS] = brightness @@ -1185,6 +1186,17 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug("%s: Setting rgb_color of light %s", self._name, light) service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"] + required_attrs = [ATTR_RGB_COLOR, ATTR_COLOR_TEMP_KELVIN, ATTR_BRIGHTNESS] + if not any(attr in service_data for attr in required_attrs): + _LOGGER.debug( + "%s: Skipping adaptation of %s because no relevant attributes" + " are set in service_data: %s", + self._name, + light, + service_data, + ) + return None + context = context or self.create_context("adapt_lights") self.manager.last_service_data[light] = service_data diff --git a/tests/test_switch.py b/tests/test_switch.py index 360fec55..55aa30ea 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1672,25 +1672,25 @@ async def test_adapt_until_sleep_and_rgb_colors(hass): assert switch._settings["color_temp_kelvin"] > min_color_temp assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT] - # One hour after sunset the brightness should be down + # One hour after sunset the brightness should be down and use RGB await patch_time_and_update(after_sunset) assert switch._settings["force_rgb_color"] assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT] - # At sunrise the brightness should be max and color_temp at the smallest value + # At sunrise the brightness should be max and use Kelvin await patch_time_and_update(sunrise) assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS assert switch._settings["color_temp_kelvin"] == min_color_temp assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT] # One hour before sunrise the brightness should smaller than max - # and color_temp at the min value. + # and use RGB await patch_time_and_update(before_sunrise) assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT] - # One hour after sunrise the brightness should be up + # One hour after sunrise the brightness should be up and it should use Kelvin await patch_time_and_update(after_sunrise) assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS assert switch._settings["color_temp_kelvin"] > min_color_temp From e9297d562f11c208b568c425b478974a9c48aa29 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 26 Jul 2023 12:52:03 -0700 Subject: [PATCH 0589/1077] Cleanup after add_to_platform_abort is called, try-except intercept, update strings.json, fixes #658 (#659) * Cleanup after add_to_platform_abort is called, fixes #658 * Add note * try-catch because of priv * Catch all exceptions * Bump to 1.17.3 in manifest.json * assert that hass is not None * Rename _light_event_action -> _light_state_event_action * update .github/update-strings.py * Update README.md, strings.json, and services.yaml * update name * Fixes * Update README.md, strings.json, and services.yaml * Add name * Do not add name --------- Co-authored-by: github-actions[bot] --- .github/update-strings.py | 24 ++- .github/workflows/update-readme.yml | 8 +- .../adaptive_lighting/hass_utils.py | 20 +- .../adaptive_lighting/manifest.json | 2 +- .../adaptive_lighting/strings.json | 176 ++++++++++++++++++ custom_components/adaptive_lighting/switch.py | 92 +++++---- .../adaptive_lighting/translations/en.json | 176 ++++++++++++++++++ 7 files changed, 447 insertions(+), 51 deletions(-) diff --git a/.github/update-strings.py b/.github/update-strings.py index 8f8f9ef4..44dc6bd6 100644 --- a/.github/update-strings.py +++ b/.github/update-strings.py @@ -3,6 +3,8 @@ import json import sys from pathlib import Path +import yaml + sys.path.append(str(Path(__file__).parent.parent)) from custom_components.adaptive_lighting import const # noqa: E402 @@ -13,20 +15,40 @@ en_fname = folder / "translations" / "en.json" with strings_fname.open() as f: strings = json.load(f) +# Set "options" data = {k: f"{k}: {const.DOCS[k]}" for k, _, _ in const.VALIDATION_TUPLES} strings["options"]["step"]["init"]["data"] = data +# Set "services" +services_filename = Path("custom_components") / "adaptive_lighting" / "services.yaml" +with open(services_filename) as f: # noqa: PTH123 + services = yaml.safe_load(f) +services_json = {} +for service_name, dct in services.items(): + services_json[service_name] = { + "name": service_name, + "description": dct["description"], + "fields": {}, + } + for field_name, field in dct["fields"].items(): + services_json[service_name]["fields"][field_name] = { + "description": field["description"], + "name": field_name, + } +strings["services"] = services_json + +# Write changes to strings.json with strings_fname.open("w") as f: json.dump(strings, f, indent=2, ensure_ascii=False) f.write("\n") - # Sync changes from strings.json to en.json with en_fname.open() as f: en = json.load(f) en["config"]["step"]["user"] = strings["config"]["step"]["user"] en["options"]["step"]["init"]["data"] = data +en["services"] = services_json with en_fname.open("w") as f: json.dump(en, f, indent=2, ensure_ascii=False) diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index a31d556e..8fca3d77 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -1,4 +1,4 @@ -name: Update README.md +name: Update README.md, strings.json, and services.yaml on: push: @@ -34,12 +34,12 @@ jobs: - name: Run markdown-code-runner run: markdown-code-runner --debug README.md - - name: Run update strings.json - run: python .github/update-strings.py - - name: Run update services.yaml run: python .github/update-services.py + - name: Run update strings.json + run: python .github/update-strings.py + - name: Commit updated README.md, strings.json, and services.yaml id: commit run: | diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py index 3f08b3fc..cb9b8ab4 100644 --- a/custom_components/adaptive_lighting/hass_utils.py +++ b/custom_components/adaptive_lighting/hass_utils.py @@ -1,4 +1,5 @@ """Utility functions for HA core.""" +import logging from collections.abc import Awaitable, Callable from homeassistant.core import HomeAssistant, ServiceCall @@ -6,6 +7,8 @@ from homeassistant.util.read_only_dict import ReadOnlyDict from .adaptation_utils import ServiceData +_LOGGER = logging.getLogger(__name__) + def setup_service_call_interceptor( hass: HomeAssistant, @@ -39,15 +42,18 @@ def setup_service_call_interceptor( existing_service = registered_services[domain][service] async def service_func_proxy(call: ServiceCall) -> None: - # Convert read-only data to writeable dictionary for modification by interceptor - data = dict(call.data) + try: + # Convert read-only data to writeable dictionary for modification by interceptor + data = dict(call.data) - # Call interceptor - await intercept_func(call, data) - - # Convert data back to read-only - call.data = ReadOnlyDict(data) + # Call interceptor + await intercept_func(call, data) + # Convert data back to read-only + call.data = ReadOnlyDict(data) + except Exception as e: # noqa: BLE001 + # Blindly catch all exceptions to avoid breaking light.turn_on + _LOGGER.error("Error in service_func_proxy: %s", e) # Call original service handler with processed data await existing_service.job.target(call) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 1e232dab..a5293b7a 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.17.2" + "version": "1.17.3" } diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 7ad84741..d142c7b7 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -56,5 +56,181 @@ "option_error": "Invalid option", "entity_missing": "One or more selected light entities are missing from Home Assistant" } + }, + "services": { + "apply": { + "name": "apply", + "description": "Applies the current Adaptive Lighting settings to lights.", + "fields": { + "entity_id": { + "description": "The `entity_id` of the switch with the settings to apply. 📝", + "name": "entity_id" + }, + "lights": { + "description": "A light (or list of lights) to apply the settings to. 💡", + "name": "lights" + }, + "transition": { + "description": "Duration of transition when lights change, in seconds. 🕑", + "name": "transition" + }, + "adapt_brightness": { + "description": "Whether to adapt the brightness of the light. 🌞", + "name": "adapt_brightness" + }, + "adapt_color": { + "description": "Whether to adapt the color on supporting lights. 🌈", + "name": "adapt_color" + }, + "prefer_rgb_color": { + "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "name": "prefer_rgb_color" + }, + "turn_on_lights": { + "description": "Whether to turn on lights that are currently off. 🔆", + "name": "turn_on_lights" + } + } + }, + "set_manual_control": { + "name": "set_manual_control", + "description": "Mark whether a light is 'manually controlled'.", + "fields": { + "entity_id": { + "description": "The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝", + "name": "entity_id" + }, + "lights": { + "description": "entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡", + "name": "lights" + }, + "manual_control": { + "description": "Whether to add (\"true\") or remove (\"false\") the light from the \"manual_control\" list. 🔒", + "name": "manual_control" + } + } + }, + "change_switch_settings": { + "name": "change_switch_settings", + "description": "Change any settings you'd like in the switch. All options here are the same as in the config flow.", + "fields": { + "entity_id": { + "description": "Entity ID of the switch. 📝", + "name": "entity_id" + }, + "use_defaults": { + "description": "Sets the default values not specified in this service call. Options: \"current\" (default, retains current values), \"factory\" (resets to documented defaults), or \"configuration\" (reverts to switch config defaults). ⚙️", + "name": "use_defaults" + }, + "include_config_in_attributes": { + "description": "Show all options as attributes on the switch in Home Assistant when set to `true`. 📝", + "name": "include_config_in_attributes" + }, + "turn_on_lights": { + "description": "Whether to turn on lights that are currently off. 🔆", + "name": "turn_on_lights" + }, + "initial_transition": { + "description": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "name": "initial_transition" + }, + "sleep_transition": { + "description": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "name": "sleep_transition" + }, + "max_brightness": { + "description": "Maximum brightness percentage. 💡", + "name": "max_brightness" + }, + "max_color_temp": { + "description": "Coldest color temperature in Kelvin. ❄️", + "name": "max_color_temp" + }, + "min_brightness": { + "description": "Minimum brightness percentage. 💡", + "name": "min_brightness" + }, + "min_color_temp": { + "description": "Warmest color temperature in Kelvin. 🔥", + "name": "min_color_temp" + }, + "only_once": { + "description": "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", + "name": "only_once" + }, + "prefer_rgb_color": { + "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "name": "prefer_rgb_color" + }, + "separate_turn_on_commands": { + "description": "Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", + "name": "separate_turn_on_commands" + }, + "send_split_delay": { + "description": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", + "name": "send_split_delay" + }, + "sleep_brightness": { + "description": "Brightness percentage of lights in sleep mode. 😴", + "name": "sleep_brightness" + }, + "sleep_rgb_or_color_temp": { + "description": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", + "name": "sleep_rgb_or_color_temp" + }, + "sleep_rgb_color": { + "description": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "name": "sleep_rgb_color" + }, + "sleep_color_temp": { + "description": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", + "name": "sleep_color_temp" + }, + "sunrise_offset": { + "description": "Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "name": "sunrise_offset" + }, + "sunrise_time": { + "description": "Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "name": "sunrise_time" + }, + "sunset_offset": { + "description": "Adjust sunset time with a positive or negative offset in seconds. ⏰", + "name": "sunset_offset" + }, + "sunset_time": { + "description": "Set a fixed time (HH:MM:SS) for sunset. 🌇", + "name": "sunset_time" + }, + "max_sunrise_time": { + "description": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅", + "name": "max_sunrise_time" + }, + "min_sunset_time": { + "description": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇", + "name": "min_sunset_time" + }, + "take_over_control": { + "description": "Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", + "name": "take_over_control" + }, + "detect_non_ha_changes": { + "description": "Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️", + "name": "detect_non_ha_changes" + }, + "transition": { + "description": "Duration of transition when lights change, in seconds. 🕑", + "name": "transition" + }, + "adapt_delay": { + "description": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", + "name": "adapt_delay" + }, + "autoreset_control_seconds": { + "description": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "name": "autoreset_control_seconds" + } + } + } } } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 6e337a30..6dae39f2 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -437,15 +437,13 @@ async def async_setup_entry( # noqa: PLR0915 async_add_entities: AddEntitiesCallback, ): """Set up the AdaptiveLighting switch.""" + assert hass is not None data = hass.data[DOMAIN] assert config_entry.entry_id in data - - if ATTR_ADAPTIVE_LIGHTING_MANAGER not in data: - data[ATTR_ADAPTIVE_LIGHTING_MANAGER] = AdaptiveLightingManager( - hass, - config_entry, - ) - manager: AdaptiveLightingManager = data[ATTR_ADAPTIVE_LIGHTING_MANAGER] + manager = data.setdefault( + ATTR_ADAPTIVE_LIGHTING_MANAGER, + AdaptiveLightingManager(hass, config_entry), + ) sleep_mode_switch = SimpleSwitch( which="Sleep Mode", initial_state=False, @@ -503,7 +501,7 @@ async def async_setup_entry( # noqa: PLR0915 if not lights: all_lights = switch.lights else: - all_lights = _expand_light_groups(switch.hass, lights) + all_lights = _expand_light_groups(hass, lights) switch.manager.lights.update(all_lights) for light in all_lights: if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light): @@ -533,7 +531,7 @@ async def async_setup_entry( # noqa: PLR0915 if not lights: all_lights = switch.lights else: - all_lights = _expand_light_groups(switch.hass, lights) + all_lights = _expand_light_groups(hass, lights) if service_call.data[CONF_MANUAL_CONTROL]: for light in all_lights: _fire_manual_control_event(switch, light, service_call.context) @@ -585,7 +583,7 @@ def validate( config_entry: ConfigEntry, service_data: dict[str, Any] | None = None, defaults: dict[str, Any] | None = None, -): +) -> dict[str, Any]: """Get the options and data from the config_entry and add defaults.""" if defaults is None: data = {key: default for key, default, _ in VALIDATION_TUPLES} @@ -812,6 +810,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) -> None: """Initialize the Adaptive Lighting switch.""" # Set attributes that can't be modified during runtime + assert hass is not None self.hass = hass self.manager = manager self.sleep_mode_switch = sleep_mode_switch @@ -847,7 +846,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set and unset tracker in async_turn_on and async_turn_off self.remove_listeners = [] self.remove_interval: Callable[[], None] = lambda: None - _LOGGER.debug( "%s: Setting up with '%s'," " config_entry.data: '%s'," @@ -1005,7 +1003,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): remove_state = async_track_state_change_event( self.hass, entity_ids=self.lights, - action=self._light_event_action, + action=self._light_state_event_action, ) self.remove_listeners.append(remove_state) @@ -1035,8 +1033,27 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): interval=adaptation_interval, ) + def _call_on_remove_callbacks(self) -> None: + """Call callbacks registered by async_on_remove.""" + # This is called when the integration is removed from HA + # and in `Entity.add_to_platform_abort`. + # For some unknown reason (to me) `async_will_remove_from_hass` + # is not called in `add_to_platform_abort`. + # See https://github.com/basnijholt/adaptive-lighting/issues/658 + self._remove_listeners() + try: + # HACK: this is a private method in `Entity` which can change + super()._call_on_remove_callbacks() + except AttributeError as err: + _LOGGER.error( + "%s: Caught AttributeError in `_call_on_remove_callbacks`: %s", + self._name, + err, + ) + def _remove_interval_listener(self) -> None: self.remove_interval() + self.remove_interval = lambda: None def _remove_listeners(self) -> None: self._remove_interval_listener() @@ -1152,7 +1169,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) # Build service data. - service_data = {ATTR_ENTITY_ID: light} + service_data: dict[str, Any] = {ATTR_ENTITY_ID: light} features = _supported_features(self.hass, light) # Check transition == 0 to fix #378 @@ -1289,8 +1306,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): to cancel an ongoing adaptation when a light is turned off. """ # Prevent overlap of multiple adaptation sequences - listener = self.manager - listener.cancel_ongoing_adaptation_calls(data.entity_id, which=data.which) + self.manager.cancel_ongoing_adaptation_calls(data.entity_id, which=data.which) _LOGGER.debug( "%s: execute_cancellable_adaptation_calls with data: %s", self._name, @@ -1300,9 +1316,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): try: task = asyncio.ensure_future(self._execute_adaptation_calls(data)) if data.which in ("both", "brightness"): - listener.adaptation_tasks_brightness[data.entity_id] = task + self.manager.adaptation_tasks_brightness[data.entity_id] = task if data.which in ("both", "color"): - listener.adaptation_tasks_color[data.entity_id] = task + self.manager.adaptation_tasks_color[data.entity_id] = task await task except asyncio.CancelledError: _LOGGER.debug( @@ -1427,7 +1443,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context=self.create_context("sleep", parent=event.context), ) - async def _light_event_action(self, event: Event) -> None: + async def _light_state_event_action(self, event: Event) -> None: old_state = event.data.get("old_state") new_state = event.data.get("new_state") entity_id = event.data.get("entity_id") @@ -1801,9 +1817,10 @@ class AdaptiveLightingManager: def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize the AdaptiveLightingManager that is shared among all switches.""" + assert hass is not None self.hass = hass data = validate(config_entry) - self.lights = set() + self.lights: set[str] = set() # Tracks 'light.turn_off' service calls self.turn_off_event: dict[str, Event] = {} @@ -1935,6 +1952,15 @@ class AdaptiveLightingManager: return entity_id = entity_ids[0] + + # Prevent adaptation of TURN_ON calls when light is already on, + # and of TOGGLE calls when toggling off. + if self.hass.states.is_state(entity_id, STATE_ON): + return + + if self.manual_control.get(entity_id, False): + return + try: adaptive_switch = _switch_with_lights(self.hass, [entity_id]) except NoSwitchFoundError: @@ -1952,14 +1978,6 @@ class AdaptiveLightingManager: if entity_id not in adaptive_switch.lights: return - if self.manual_control.get(entity_id, False): - return - - # Prevent adaptation of TURN_ON calls when light is already on, - # and of TOGGLE calls when toggling off. - if self.hass.states.is_state(entity_id, STATE_ON): - return - _LOGGER.debug( "Intercepted TURN_ON call with data %s (%s)", data, @@ -2143,11 +2161,10 @@ class AdaptiveLightingManager: self.cancel_ongoing_adaptation_calls(light) def _get_entity_list(self, service_data: ServiceData) -> list[str]: - entity_ids = [] - if ATTR_ENTITY_ID in service_data: - entity_ids = cv.ensure_list_csv(service_data[ATTR_ENTITY_ID]) - elif ATTR_AREA_ID in service_data: + return cv.ensure_list_csv(service_data[ATTR_ENTITY_ID]) + if ATTR_AREA_ID in service_data: + entity_ids = [] area_ids = cv.ensure_list_csv(service_data[ATTR_AREA_ID]) for area_id in area_ids: area_entity_ids = area_entities(self.hass, area_id) @@ -2162,13 +2179,12 @@ class AdaptiveLightingManager: entity_ids, area_id, ) - else: - _LOGGER.debug( - "No entity_ids or area_ids found in service_data: %s", - service_data, - ) - - return entity_ids + return entity_ids + _LOGGER.debug( + "No entity_ids or area_ids found in service_data: %s", + service_data, + ) + return [] async def turn_on_off_event_listener(self, event: Event) -> None: """Track 'light.turn_off' and 'light.turn_on' service calls.""" diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index e6d1f9a9..199c333c 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -57,5 +57,181 @@ "option_error": "Invalid option", "entity_missing": "One or more selected light entities are missing from Home Assistant" } + }, + "services": { + "apply": { + "name": "apply", + "description": "Applies the current Adaptive Lighting settings to lights.", + "fields": { + "entity_id": { + "description": "The `entity_id` of the switch with the settings to apply. 📝", + "name": "entity_id" + }, + "lights": { + "description": "A light (or list of lights) to apply the settings to. 💡", + "name": "lights" + }, + "transition": { + "description": "Duration of transition when lights change, in seconds. 🕑", + "name": "transition" + }, + "adapt_brightness": { + "description": "Whether to adapt the brightness of the light. 🌞", + "name": "adapt_brightness" + }, + "adapt_color": { + "description": "Whether to adapt the color on supporting lights. 🌈", + "name": "adapt_color" + }, + "prefer_rgb_color": { + "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "name": "prefer_rgb_color" + }, + "turn_on_lights": { + "description": "Whether to turn on lights that are currently off. 🔆", + "name": "turn_on_lights" + } + } + }, + "set_manual_control": { + "name": "set_manual_control", + "description": "Mark whether a light is 'manually controlled'.", + "fields": { + "entity_id": { + "description": "The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝", + "name": "entity_id" + }, + "lights": { + "description": "entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡", + "name": "lights" + }, + "manual_control": { + "description": "Whether to add (\"true\") or remove (\"false\") the light from the \"manual_control\" list. 🔒", + "name": "manual_control" + } + } + }, + "change_switch_settings": { + "name": "change_switch_settings", + "description": "Change any settings you'd like in the switch. All options here are the same as in the config flow.", + "fields": { + "entity_id": { + "description": "Entity ID of the switch. 📝", + "name": "entity_id" + }, + "use_defaults": { + "description": "Sets the default values not specified in this service call. Options: \"current\" (default, retains current values), \"factory\" (resets to documented defaults), or \"configuration\" (reverts to switch config defaults). ⚙️", + "name": "use_defaults" + }, + "include_config_in_attributes": { + "description": "Show all options as attributes on the switch in Home Assistant when set to `true`. 📝", + "name": "include_config_in_attributes" + }, + "turn_on_lights": { + "description": "Whether to turn on lights that are currently off. 🔆", + "name": "turn_on_lights" + }, + "initial_transition": { + "description": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "name": "initial_transition" + }, + "sleep_transition": { + "description": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "name": "sleep_transition" + }, + "max_brightness": { + "description": "Maximum brightness percentage. 💡", + "name": "max_brightness" + }, + "max_color_temp": { + "description": "Coldest color temperature in Kelvin. ❄️", + "name": "max_color_temp" + }, + "min_brightness": { + "description": "Minimum brightness percentage. 💡", + "name": "min_brightness" + }, + "min_color_temp": { + "description": "Warmest color temperature in Kelvin. 🔥", + "name": "min_color_temp" + }, + "only_once": { + "description": "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", + "name": "only_once" + }, + "prefer_rgb_color": { + "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "name": "prefer_rgb_color" + }, + "separate_turn_on_commands": { + "description": "Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", + "name": "separate_turn_on_commands" + }, + "send_split_delay": { + "description": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", + "name": "send_split_delay" + }, + "sleep_brightness": { + "description": "Brightness percentage of lights in sleep mode. 😴", + "name": "sleep_brightness" + }, + "sleep_rgb_or_color_temp": { + "description": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", + "name": "sleep_rgb_or_color_temp" + }, + "sleep_rgb_color": { + "description": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "name": "sleep_rgb_color" + }, + "sleep_color_temp": { + "description": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", + "name": "sleep_color_temp" + }, + "sunrise_offset": { + "description": "Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "name": "sunrise_offset" + }, + "sunrise_time": { + "description": "Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "name": "sunrise_time" + }, + "sunset_offset": { + "description": "Adjust sunset time with a positive or negative offset in seconds. ⏰", + "name": "sunset_offset" + }, + "sunset_time": { + "description": "Set a fixed time (HH:MM:SS) for sunset. 🌇", + "name": "sunset_time" + }, + "max_sunrise_time": { + "description": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅", + "name": "max_sunrise_time" + }, + "min_sunset_time": { + "description": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇", + "name": "min_sunset_time" + }, + "take_over_control": { + "description": "Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", + "name": "take_over_control" + }, + "detect_non_ha_changes": { + "description": "Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️", + "name": "detect_non_ha_changes" + }, + "transition": { + "description": "Duration of transition when lights change, in seconds. 🕑", + "name": "transition" + }, + "adapt_delay": { + "description": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", + "name": "adapt_delay" + }, + "autoreset_control_seconds": { + "description": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "name": "autoreset_control_seconds" + } + } + } } } From 1cda675bbef3a987da434f2b71fc94c5f42fd5c3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 26 Jul 2023 15:01:29 -0700 Subject: [PATCH 0590/1077] Add @bind_hass to functions (#667) --- custom_components/adaptive_lighting/switch.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 6dae39f2..6bba8afe 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -78,6 +78,7 @@ from homeassistant.helpers.event import ( from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.sun import get_astral_location from homeassistant.helpers.template import area_entities +from homeassistant.loader import bind_hass from homeassistant.util import slugify from homeassistant.util.color import ( color_RGB_to_xy, @@ -274,6 +275,7 @@ def is_our_context(context: Context | None) -> bool: return is_our_context_id(context.id) +@bind_hass def _switches_with_lights( hass: HomeAssistant, lights: list[str], @@ -299,6 +301,7 @@ class NoSwitchFoundError(ValueError): """No switches found for lights.""" +@bind_hass def _switch_with_lights( hass: HomeAssistant, lights: list[str], @@ -328,6 +331,7 @@ def _switch_with_lights( # For documentation on this function, see integration_entities() from HomeAssistant Core: # https://github.com/home-assistant/core/blob/dev/homeassistant/helpers/template.py#L1109 +@bind_hass def _switches_from_service_call( hass: HomeAssistant, service_call: ServiceCall, @@ -617,6 +621,7 @@ def _is_state_event(event: Event, from_or_to_state: Iterable[str]): ) +@bind_hass def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]: all_lights = set() manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER] @@ -635,6 +640,7 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]: return list(all_lights) +@bind_hass def _supported_features(hass: HomeAssistant, light: str) -> set[str]: state = hass.states.get(light) supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) From c64d9cefbed1acf93b229684f852bfe44913bbf8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 26 Jul 2023 23:40:54 -0700 Subject: [PATCH 0591/1077] Delete AdaptiveLighting instances that have been removed from YAML (#669) * Do not re-add already added configs * Use async_remove --- .../adaptive_lighting/config_flow.py | 5 ++++- custom_components/adaptive_lighting/switch.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 170c8503..3dd89fd4 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -40,10 +40,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_import(self, user_input=None): - """Handle configuration by yaml file.""" + """Handle configuration by YAML file.""" await self.async_set_unique_id(user_input[CONF_NAME]) for entry in self._async_current_entries(): if entry.unique_id == self.unique_id: + # Keep a list of switches that are configured via YAML + data = self.hass.data.setdefault(DOMAIN, {}) + data.setdefault("__yaml__", []).append(self.unique_id) self.hass.config_entries.async_update_entry(entry, data=user_input) self._abort_if_unique_id_configured() return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 6bba8afe..72b53635 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -42,6 +42,7 @@ from homeassistant.components.light import ( from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_AREA_ID, ATTR_DOMAIN, @@ -444,6 +445,22 @@ async def async_setup_entry( # noqa: PLR0915 assert hass is not None data = hass.data[DOMAIN] assert config_entry.entry_id in data + _LOGGER.debug( + "Setting up AdaptiveLighting with data: %s and config_entry %s", + data, + config_entry, + ) + if ( # Skip deleted YAML config entries + config_entry.source == SOURCE_IMPORT + and config_entry.unique_id not in data.get("__yaml__", []) + ): + _LOGGER.warning( + "Deleting AdaptiveLighting switch '%s' because YAML" + " defined switch has been removed from YAML configuration", + config_entry.unique_id, + ) + await hass.config_entries.async_remove(config_entry.entry_id) + return manager = data.setdefault( ATTR_ADAPTIVE_LIGHTING_MANAGER, AdaptiveLightingManager(hass, config_entry), From 9e15d1bd22593aa11923332e4123181ba8d446de Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Jul 2023 09:51:59 -0700 Subject: [PATCH 0592/1077] Update to 1.17.4 in manifest.json (#670) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index a5293b7a..4ba4790c 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.17.3" + "version": "1.17.4" } From a6e987438df1d9e50963900dc0477248d74facb1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:22:12 +0000 Subject: [PATCH 0593/1077] [pre-commit.ci] pre-commit autoupdate (#627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.0.279 → v0.0.280](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.279...v0.0.280) - [github.com/psf/black: 23.3.0 → 23.7.0](https://github.com/psf/black/compare/23.3.0...23.7.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6418f6a3..dc82bbe7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,11 +8,11 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.279 + rev: v0.0.280 hooks: - id: ruff args: ["--fix"] - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.7.0 hooks: - id: black From f699486fd0752c99df4abb551fa77a65e918bb1f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Jul 2023 17:34:56 -0700 Subject: [PATCH 0594/1077] Only cancel ongoing adaptation calls if still running (#668) --- custom_components/adaptive_lighting/switch.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 72b53635..24304730 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2151,7 +2151,11 @@ class AdaptiveLightingManager: """Cancel ongoing adaptation service calls for a specific light entity.""" brightness_task = self.adaptation_tasks_brightness.get(light_id) color_task = self.adaptation_tasks_color.get(light_id) - if which in ("both", "brightness") and brightness_task is not None: + if ( + which in ("both", "brightness") + and brightness_task is not None + and not brightness_task.done() + ): _LOGGER.debug( "Cancelled ongoing brightness adaptation calls (%s) for '%s'", brightness_task, @@ -2162,6 +2166,7 @@ class AdaptiveLightingManager: which in ("both", "color") and color_task is not None and color_task is not brightness_task + and not color_task.done() ): _LOGGER.debug( "Cancelled ongoing color adaptation calls (%s) for '%s'", From 9d6f93538f1ef6e687d240987aedc8b6d3ad2b5f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Jul 2023 18:01:06 -0700 Subject: [PATCH 0595/1077] Do not create multiple AdaptiveLightingManager instances (#672) I didn't realize that setdefault always executes the default. --- custom_components/adaptive_lighting/switch.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 24304730..bd530f36 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -285,12 +285,12 @@ def _switches_with_lights( config_entries = hass.config_entries.async_entries(DOMAIN) data = hass.data[DOMAIN] switches = [] + all_check_lights = _expand_light_groups(hass, lights) for config in config_entries: entry = data.get(config.entry_id) if entry is None: # entry might be disabled and therefore missing continue switch = data[config.entry_id]["instance"] - all_check_lights = _expand_light_groups(hass, lights) switch._expand_light_groups() # Check if any of the lights are in the switch's lights if set(switch.lights) & set(all_check_lights): @@ -461,10 +461,11 @@ async def async_setup_entry( # noqa: PLR0915 ) await hass.config_entries.async_remove(config_entry.entry_id) return - manager = data.setdefault( - ATTR_ADAPTIVE_LIGHTING_MANAGER, - AdaptiveLightingManager(hass, config_entry), - ) + + if (manager := data.get(ATTR_ADAPTIVE_LIGHTING_MANAGER)) is None: + manager = AdaptiveLightingManager(hass, config_entry) + data[ATTR_ADAPTIVE_LIGHTING_MANAGER] = manager + sleep_mode_switch = SimpleSwitch( which="Sleep Mode", initial_state=False, From f23aeb269bf9150a17c925884d52d5d742dd2cb1 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Jul 2023 18:09:37 -0700 Subject: [PATCH 0596/1077] Bump to 1.17.5 --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 4ba4790c..8695c555 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.17.4" + "version": "1.17.5" } From 89c90adde040dab49882416e1dcb270212423411 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 28 Jul 2023 15:17:17 -0700 Subject: [PATCH 0597/1077] Only start adapting on `light.turn_on` when `detect_non_ha_changes: false` to prevent unwanted light turn ons (#663) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixes in maybe_cancel_adjusting to possibly fix accidental turn on * simplify logic in maybe_cancel_adjusting * Add logging statements * only control if turn_on called * more logs * add TODO * Add _state_event_is_from_our_turn_on * Ignore off->on state switches that are not accociated with light.turn_on * improve logging * rename * Update docs * Update README.md, strings.json, and services.yaml * Add caution message to README * Change order of emojis * Update README.md, strings.json, and services.yaml * Check that platform is not None * log the call * fix args * Do not re-add already added configs * Do not re-add already added configs * Use async_remove * remove unused code * [pre-commit.ci] pre-commit autoupdate (#627) updates: - [github.com/astral-sh/ruff-pre-commit: v0.0.279 → v0.0.280](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.279...v0.0.280) - [github.com/psf/black: 23.3.0 → 23.7.0](https://github.com/psf/black/compare/23.3.0...23.7.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt * Extra logging statement * Add to README * log context_id * return right indent * move comment * Skip on self.manager.is_proactively_adapting --------- Co-authored-by: github-actions[bot] Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- README.md | 74 +++---- custom_components/adaptive_lighting/const.py | 7 +- .../adaptive_lighting/hass_utils.py | 6 +- .../adaptive_lighting/services.yaml | 2 +- .../adaptive_lighting/strings.json | 4 +- custom_components/adaptive_lighting/switch.py | 182 +++++++++++------- .../adaptive_lighting/translations/en.json | 4 +- tests/test_switch.py | 2 +- 8 files changed, 171 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index 20a21dad..d1d7aa16 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,11 @@ Adaptive Lighting is designed to automatically detect when you or another source When this occurs, the affected light is marked as "manually controlled," and Adaptive Lighting will not make further adjustments until the light is turned off and back on or reset using the `adaptive_lighting.set_manual_control` service call. This feature is available when `take_over_control` is enabled. -Additionally, enabling detect_non_ha_changes allows Adaptive Lighting to detect all state changes, including those made outside of Home Assistant, by comparing the light's state to its previously used settings. +Additionally, enabling `detect_non_ha_changes` allows Adaptive Lighting to detect all state changes, including those made outside of Home Assistant, by comparing the light's state to its previously used settings. The `adaptive_lighting.manual_control` event is fired when a light is marked as "manually controlled," allowing for integration with automations 🤖. +> ⚠️ **_Caution: Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable `detect_non_ha_changes` if you encounter such issues._** + ## :books: Table of Contents @@ -91,38 +93,38 @@ The YAML and frontend configuration methods support all of the options listed be -| Variable name | Description | Default | Type | -|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------| -| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | -| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | -| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | -| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | -| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | -| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | -| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | -| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | -| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | -| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | -| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | -| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | -| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | -| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | -| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | -| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | -| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | -| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` | -| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | -| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` | -| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | -| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | -| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` | -| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | -| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | -| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | -| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | -| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | +| Variable name | Description | Default | Type | +|:-------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------| +| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | +| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | +| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | +| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | +| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | @@ -341,7 +343,11 @@ Adaptive Lighting sends more commands to lights than a typical human user would. - Unresponsive lights. - Home Assistant reporting incorrect light states, causing Adaptive Lighting to inadvertently turn lights back on. -Most issues that appear to be caused by Adaptive Lighting are actually due to unrelated problems. Addressing these issues will significantly improve your Home Assistant experience. +Most issues that appear to be caused by Adaptive Lighting are actually due to unrelated problems. +Addressing these issues will significantly improve your Home Assistant experience. + +In case lights are suddenly turning on by themselves, this is most likely due to the light incorrectly reporting an "on" state to Home Assistant, leading to an undesired Adaptive Lighting action. +To prevent adapting in cases *where the state of the light is suddenly "on" and only adapt if there is an associated `light.turn_on` service call*, set `detect_non_ha_changes: false`. #### :signal_strength: WiFi Networks diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 64b96583..94e9a50b 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -29,8 +29,11 @@ CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = ( False, ) DOCS[CONF_DETECT_NON_HA_CHANGES] = ( - "Detect non-`light.turn_on` state changes and stop adapting lights. " - "Requires `take_over_control`. 🕵️" + "Detects and halts adaptations for non-`light.turn_on` state changes. " + "Needs `take_over_control` enabled. 🕵️ " + "Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result " + "in lights turning on unexpectedly. " + "Disable this feature if you encounter such issues." ) CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES = ( diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py index cb9b8ab4..ba5bb8b0 100644 --- a/custom_components/adaptive_lighting/hass_utils.py +++ b/custom_components/adaptive_lighting/hass_utils.py @@ -53,7 +53,11 @@ def setup_service_call_interceptor( call.data = ReadOnlyDict(data) except Exception as e: # noqa: BLE001 # Blindly catch all exceptions to avoid breaking light.turn_on - _LOGGER.error("Error in service_func_proxy: %s", e) + _LOGGER.error( + "Error for call '%s' in service_func_proxy: '%s'", + call.data, + e, + ) # Call original service handler with processed data await existing_service.job.target(call) diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index cd25811b..5b857b32 100644 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -226,7 +226,7 @@ change_switch_settings: selector: boolean: null detect_non_ha_changes: - description: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ + description: 'Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an ''on'' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.' required: false example: false selector: diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index d142c7b7..b6d62b0b 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -43,7 +43,7 @@ "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", - "detect_non_ha_changes": "detect_non_ha_changes: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️", + "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", @@ -215,7 +215,7 @@ "name": "take_over_control" }, "detect_non_ha_changes": { - "description": "Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️", + "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "name": "detect_non_ha_changes" }, "transition": { diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index bd530f36..7cacf435 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -594,6 +594,7 @@ async def async_setup_entry( # noqa: PLR0915 if k not in skip: args[vol.Optional(k)] = valid platform = entity_platform.current_platform.get() + assert platform is not None platform.async_register_entity_service( SERVICE_CHANGE_SWITCH_SETTINGS, args, @@ -855,9 +856,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._icon = ICON_MAIN self._state = None - # Tracks 'off' → 'on' state changes - self._on_to_off_event: dict[str, Event] = {} # Tracks 'on' → 'off' state changes + self._on_to_off_event: dict[str, Event] = {} + # Tracks 'off' → 'on' state changes self._off_to_on_event: dict[str, Event] = {} # Locks that prevent light adjusting when waiting for a light to 'turn_off' self._locks: dict[str, asyncio.Lock] = {} @@ -1443,10 +1444,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): else: _LOGGER.debug( "%s: Calling _adapt_light from _update_attrs_and_maybe_adapt_lights:" - " '%s' with transition %s", + " '%s' with transition %s and context.id=%s", self._name, light, transition, + context.id, ) await self._adapt_light(light, transition, context=context) @@ -1471,12 +1473,24 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): old_state = event.data.get("old_state") new_state = event.data.get("new_state") entity_id = event.data.get("entity_id") - if ( - old_state is not None - and old_state.state == STATE_OFF - and new_state is not None - and new_state.state == STATE_ON - ): + + if old_state is None or new_state is None: + return + + if old_state.state == STATE_ON and new_state.state == STATE_OFF: + # Tracks 'on' → 'off' state changes + self._on_to_off_event[entity_id] = event + self.manager.reset(entity_id) + _LOGGER.debug( + "%s: Detected an 'on' → 'off' event for '%s' with context.id='%s'", + self._name, + entity_id, + event.context.id, + ) + + if old_state.state == STATE_OFF and new_state.state == STATE_ON: + # Tracks 'off' → 'on' state changes + self._off_to_on_event[entity_id] = event _LOGGER.debug( "%s: Detected an 'off' → 'on' event for '%s' with context.id='%s'", self._name, @@ -1484,13 +1498,36 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): event.context.id, ) + if ( + not self._detect_non_ha_changes + and not self.manager.is_proactively_adapting(event.context.id) + and not self.manager._off_to_on_state_event_is_from_turn_on( + entity_id, + event, + ) + ): + # If we don't detect non-HA changes, we're only adjusting lights that + # were turned on by HA. If the light was turned on by something else, + # we don't adjust it (e.g., when HA suddenly reports it as on). + # Sometimes the light incorrectly reports itself as on when it's + # actually off. This code path will ensure that the light is + # not controlled by Adaptive Lighting. + _LOGGER.debug( + "%s: Ignoring 'off' → 'on' event for '%s' with context.id='%s'" + " because 'light.turn_on' was not called by HA and" + " 'detect_non_ha_changes' is False", + self._name, + entity_id, + event.context.id, + ) + self.manager.mark_as_manual_control(entity_id) + return + if event.context.parent_id and not self.manager.is_proactively_adapting( event.context.id, ): self.manager.reset(entity_id, reset_manual_control=False) - # Tracks 'off' → 'on' state changes - self._off_to_on_event[entity_id] = event lock = self._locks.setdefault(entity_id, asyncio.Lock()) async with lock: if await self.manager.maybe_cancel_adjusting( @@ -1527,15 +1564,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): force=True, context=self.create_context("light_event", parent=event.context), ) - elif ( - old_state is not None - and old_state.state == STATE_ON - and new_state is not None - and new_state.state == STATE_OFF - ): - # Tracks 'off' → 'on' state changes - self._on_to_off_event[entity_id] = event - self.manager.reset(entity_id) class SimpleSwitch(SwitchEntity, RestoreEntity): @@ -2365,6 +2393,8 @@ class AdaptiveLightingManager: detected, we mark the light as 'manually controlled' until the light or switch is turned 'off' and 'on' again. """ + assert switch._detect_non_ha_changes + last_service_data = self.last_service_data.get(light) if last_service_data is None: return None @@ -2379,44 +2409,49 @@ class AdaptiveLightingManager: # Ensure HASS is correctly updating your light's state with # light.turn_on calls if any problems arise. This # can happen e.g. using zigbee2mqtt with 'report: false' in device settings. - if switch._detect_non_ha_changes: + await self.hass.helpers.entity_component.async_update_entity(light) + refreshed_state = self.hass.states.get(light) + + changed = compare_to( + old_attributes=last_service_data, + new_attributes=refreshed_state.attributes, + ) + if changed: _LOGGER.debug( - "%s: 'detect_non_ha_changes: true', calling update_entity(%s)" - " and check if it's last adapt succeeded.", + "%s: State attributes of '%s' (%s) didn't change wrt 'last_service_data' (%s) (context.id=%s)", switch._name, light, + refreshed_state.attributes, + last_service_data, + context.id, ) - # This update_entity probably isn't necessary now that we're checking - # if transitions finished from our last adapt. - await self.hass.helpers.entity_component.async_update_entity(light) - refreshed_state = self.hass.states.get(light) - _LOGGER.debug( - "%s: Current state of %s: %s", - switch._name, - light, - refreshed_state, - ) - changed = compare_to( - old_attributes=last_service_data, - new_attributes=refreshed_state.attributes, - ) - if changed: - _LOGGER.debug( - "State of '%s' didn't change wrt 'last_service_data' (context.id=%s)", - light, - context.id, - ) - return True + return True _LOGGER.debug( - "%s: Light '%s' correctly matches our last adapt's service data, continuing..." - " context.id=%s.", + "%s: State attributes of '%s' (%s) changed wrt 'last_service_data' (%s) (context.id=%s)", switch._name, light, + refreshed_state.attributes, + last_service_data, context.id, ) return False - async def maybe_cancel_adjusting( # noqa: PLR0911, PLR0912 + def _off_to_on_state_event_is_from_turn_on( + self, + entity_id: str, + off_to_on_event: Event, + ) -> bool: + # Adaptive Lighting should never turn on lights itself + assert not is_our_context(off_to_on_event.context) + turn_on_event: Event | None = self.turn_on_event.get(entity_id) + id_off_to_on = off_to_on_event.context.id + return ( + turn_on_event is not None + and id_off_to_on is not None + and id_off_to_on == turn_on_event.context.id + ) + + async def maybe_cancel_adjusting( # noqa: PLR0911 self, entity_id: str, off_to_on_event: Event, @@ -2435,7 +2470,11 @@ class AdaptiveLightingManager: adjust the lights. """ if on_to_off_event is None: - # No state change has been registered before. + _LOGGER.debug( + "maybe_cancel_adjusting: No 'on' → 'off' state change has been registered before for '%s'." + " It's possible that the light was already on when Home Assistant was turned on.", + entity_id, + ) return False id_on_to_off = on_to_off_event.context.id @@ -2446,18 +2485,10 @@ class AdaptiveLightingManager: else: transition = None - turn_on_event = self.turn_on_event.get(entity_id) - if turn_on_event is None: - # This means that the light never got a 'turn_on' call that we - # registered. I am not 100% sure why this happens, but it does. - # This is a fix for #170 and #232. - return False - id_turn_on = turn_on_event.context.id - - id_off_to_on = off_to_on_event.context.id - - if id_off_to_on == id_turn_on and id_off_to_on is not None: - # State change 'off' → 'on' triggered by 'light.turn_on'. + if self._off_to_on_state_event_is_from_turn_on(entity_id, off_to_on_event): + _LOGGER.debug( + "maybe_cancel_adjusting: State change 'off' → 'on' triggered by 'light.turn_on'", + ) return False if ( @@ -2476,6 +2507,11 @@ class AdaptiveLightingManager: delta_time = (dt_util.utcnow() - on_to_off_event.time_fired).total_seconds() if delta_time > delay: + _LOGGER.debug( + "maybe_cancel_adjusting: delta_time='%s' > delay='%s'", + delta_time, + delay, + ) return False # Here we could just `return True` but because we want to prevent any updates @@ -2484,23 +2520,33 @@ class AdaptiveLightingManager: # is 'off' or the time has passed. delay -= delta_time # delta_time has passed since the 'off' → 'on' event - _LOGGER.debug("Waiting with adjusting '%s' for %s", entity_id, delay) - + _LOGGER.debug( + "maybe_cancel_adjusting: Waiting with adjusting '%s' for %s", + entity_id, + delay, + ) + total_sleep = 0 for _ in range(3): # It can happen that the actual transition time is longer than the # specified time in the 'turn_off' service. coro = asyncio.sleep(delay) + total_sleep += delay task = self.sleep_tasks[entity_id] = asyncio.ensure_future(coro) try: await task except asyncio.CancelledError: # 'light.turn_on' has been called _LOGGER.debug( - "Sleep task is cancelled due to 'light.turn_on('%s')' call", + "maybe_cancel_adjusting: Sleep task is cancelled due to 'light.turn_on('%s')' call", entity_id, ) return False if not is_on(self.hass, entity_id): + _LOGGER.debug( + "maybe_cancel_adjusting: '%s' is off after %s seconds, cancelling adaptation", + entity_id, + total_sleep, + ) return True delay = TURNING_OFF_DELAY # next time only wait this long @@ -2511,10 +2557,12 @@ class AdaptiveLightingManager: return True # Now we assume that the lights are still on and they were intended - # to be on. In case this still gives problems for some, we might - # choose to **only** adapt on 'light.turn_on' events and ignore - # other 'off' → 'on' state switches resulting from polling. That - # would mean we 'return True' here. + # to be on. + _LOGGER.debug( + "maybe_cancel_adjusting: '%s' is still on after %s seconds, assuming it was intended to be on", + entity_id, + total_sleep, + ) return False diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 199c333c..358f7cbe 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -44,7 +44,7 @@ "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", - "detect_non_ha_changes": "detect_non_ha_changes: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️", + "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", @@ -216,7 +216,7 @@ "name": "take_over_control" }, "detect_non_ha_changes": { - "description": "Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️", + "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "name": "detect_non_ha_changes" }, "transition": { diff --git a/tests/test_switch.py b/tests/test_switch.py index 55aa30ea..328e493f 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1478,7 +1478,7 @@ async def test_proactive_adaptation_with_separate_commands(hass): ) # Expect two service calls - assert len(event_context_ids) == 2 + assert len(event_context_ids) == 2, event_context_ids assert event_context_ids[0] == "test_context" assert is_our_context_id(event_context_ids[1]) From 416f1eb2f15e4731f3a2771280b0109dcce32b36 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 28 Jul 2023 15:22:16 -0700 Subject: [PATCH 0598/1077] Rename maybe_cancel_adjusting to just_turned_off (#673) --- custom_components/adaptive_lighting/switch.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 7cacf435..129f7626 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1530,7 +1530,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): lock = self._locks.setdefault(entity_id, asyncio.Lock()) async with lock: - if await self.manager.maybe_cancel_adjusting( + if await self.manager.just_turned_off( entity_id, off_to_on_event=event, on_to_off_event=self._on_to_off_event.get(entity_id), @@ -2451,7 +2451,7 @@ class AdaptiveLightingManager: and id_off_to_on == turn_on_event.context.id ) - async def maybe_cancel_adjusting( # noqa: PLR0911 + async def just_turned_off( # noqa: PLR0911 self, entity_id: str, off_to_on_event: Event, @@ -2471,7 +2471,7 @@ class AdaptiveLightingManager: """ if on_to_off_event is None: _LOGGER.debug( - "maybe_cancel_adjusting: No 'on' → 'off' state change has been registered before for '%s'." + "just_turned_off: No 'on' → 'off' state change has been registered before for '%s'." " It's possible that the light was already on when Home Assistant was turned on.", entity_id, ) @@ -2487,7 +2487,7 @@ class AdaptiveLightingManager: if self._off_to_on_state_event_is_from_turn_on(entity_id, off_to_on_event): _LOGGER.debug( - "maybe_cancel_adjusting: State change 'off' → 'on' triggered by 'light.turn_on'", + "just_turned_off: State change 'off' → 'on' triggered by 'light.turn_on'", ) return False @@ -2508,7 +2508,7 @@ class AdaptiveLightingManager: delta_time = (dt_util.utcnow() - on_to_off_event.time_fired).total_seconds() if delta_time > delay: _LOGGER.debug( - "maybe_cancel_adjusting: delta_time='%s' > delay='%s'", + "just_turned_off: delta_time='%s' > delay='%s'", delta_time, delay, ) @@ -2521,7 +2521,7 @@ class AdaptiveLightingManager: delay -= delta_time # delta_time has passed since the 'off' → 'on' event _LOGGER.debug( - "maybe_cancel_adjusting: Waiting with adjusting '%s' for %s", + "just_turned_off: Waiting with adjusting '%s' for %s", entity_id, delay, ) @@ -2536,14 +2536,14 @@ class AdaptiveLightingManager: await task except asyncio.CancelledError: # 'light.turn_on' has been called _LOGGER.debug( - "maybe_cancel_adjusting: Sleep task is cancelled due to 'light.turn_on('%s')' call", + "just_turned_off: Sleep task is cancelled due to 'light.turn_on('%s')' call", entity_id, ) return False if not is_on(self.hass, entity_id): _LOGGER.debug( - "maybe_cancel_adjusting: '%s' is off after %s seconds, cancelling adaptation", + "just_turned_off: '%s' is off after %s seconds, cancelling adaptation", entity_id, total_sleep, ) @@ -2559,7 +2559,7 @@ class AdaptiveLightingManager: # Now we assume that the lights are still on and they were intended # to be on. _LOGGER.debug( - "maybe_cancel_adjusting: '%s' is still on after %s seconds, assuming it was intended to be on", + "just_turned_off: '%s' is still on after %s seconds, assuming it was intended to be on", entity_id, total_sleep, ) From 5fa74a838fbb08b5e21038601d2c617886c08d62 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 28 Jul 2023 16:55:46 -0700 Subject: [PATCH 0599/1077] Typing fixes (#674) * Typing fixes * Ignore tyoe --- custom_components/adaptive_lighting/switch.py | 69 ++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 129f7626..6b463bc9 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -12,7 +12,7 @@ import math from copy import deepcopy from dataclasses import dataclass from datetime import timedelta -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Any, Literal, cast import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -64,6 +64,7 @@ from homeassistant.const import ( SUN_EVENT_SUNSET, ) from homeassistant.core import ( + CALLBACK_TYPE, Context, Event, HomeAssistant, @@ -362,6 +363,7 @@ def _switches_from_service_call( ent_reg = entity_registry.async_get(hass) for entity_id in switch_entity_ids: ent_entry = ent_reg.async_get(entity_id) + assert ent_entry is not None config_id = ent_entry.config_entry_id switches.append(hass.data[DOMAIN][config_id]["instance"]) return switches @@ -418,18 +420,16 @@ def _fire_manual_control_event( switch: AdaptiveSwitch, light: str, context: Context, - is_async: bool = True, ): """Fire an event that 'light' is marked as manual_control.""" hass = switch.hass - fire = hass.bus.async_fire if is_async else hass.bus.fire _LOGGER.debug( "'adaptive_lighting.manual_control' event fired for %s for light %s", switch.entity_id, light, ) switch.manager.mark_as_manual_control(light) - fire( + hass.bus.async_fire( f"{DOMAIN}.manual_control", {ATTR_ENTITY_ID: light, SWITCH_DOMAIN: switch.entity_id}, context=context, @@ -527,16 +527,17 @@ async def async_setup_entry( # noqa: PLR0915 switch.manager.lights.update(all_lights) for light in all_lights: if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light): + context = switch.create_context( + "service", + parent=service_call.context, + ) await switch._adapt_light( # pylint: disable=protected-access light, - data[CONF_TRANSITION], - data[ATTR_ADAPT_BRIGHTNESS], - data[ATTR_ADAPT_COLOR], - data[CONF_PREFER_RGB_COLOR], - context=switch.create_context( - "service", - parent=service_call.context, - ), + context=context, + transition=data[CONF_TRANSITION], + adapt_brightness=data[ATTR_ADAPT_BRIGHTNESS], + adapt_color=data[ATTR_ADAPT_COLOR], + prefer_rgb_color=data[CONF_PREFER_RGB_COLOR], ) @callback @@ -603,7 +604,7 @@ async def async_setup_entry( # noqa: PLR0915 def validate( - config_entry: ConfigEntry, + config_entry: ConfigEntry | None, service_data: dict[str, Any] | None = None, defaults: dict[str, Any] | None = None, ) -> dict[str, Any]: @@ -662,7 +663,9 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]: @bind_hass def _supported_features(hass: HomeAssistant, light: str) -> set[str]: state = hass.states.get(light) + assert state is not None supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + assert isinstance(supported_features, int) supported = { key for key, value in _SUPPORT_OPTS.items() if supported_features & value } @@ -739,7 +742,7 @@ def _convert_attributes(attributes: dict[str, Any]) -> dict[str, Any]: def _add_missing_attributes( old_attributes: dict[str, Any], new_attributes: dict[str, Any], -) -> dict[str, Any]: +) -> tuple[dict[str, Any], dict[str, Any]]: if not any( attr in old_attributes and attr in new_attributes for attr in [ATTR_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR] @@ -854,7 +857,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set other attributes self._icon = ICON_MAIN - self._state = None + self._state: bool | None = None # Tracks 'on' → 'off' state changes self._on_to_off_event: dict[str, Event] = {} @@ -869,8 +872,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._settings: dict[str, Any] = {} # Set and unset tracker in async_turn_on and async_turn_off - self.remove_listeners = [] - self.remove_interval: Callable[[], None] = lambda: None + self.remove_listeners: list[CALLBACK_TYPE] = [] + self.remove_interval: CALLBACK_TYPE = lambda: None _LOGGER.debug( "%s: Setting up with '%s'," " config_entry.data: '%s'," @@ -950,7 +953,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): sunset_offset=data[CONF_SUNSET_OFFSET], sunset_time=data[CONF_SUNSET_TIME], min_sunset_time=data[CONF_MIN_SUNSET_TIME], - time_zone=self.hass.config.time_zone, transition=data[CONF_TRANSITION], ) _LOGGER.debug( @@ -984,9 +986,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): EVENT_HOMEASSISTANT_STARTED, self._setup_listeners, ) - last_state = await self.async_get_last_state() + last_state: State | None = await self.async_get_last_state() is_new_entry = last_state is None # newly added to HA - if is_new_entry or last_state.state == STATE_ON: + if is_new_entry or last_state.state == STATE_ON: # type: ignore[union-attr] await self.async_turn_on(adapt_lights=not self._only_once) else: self._state = False @@ -1095,7 +1097,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return the attributes of the switch.""" - extra_state_attributes = {"configuration": self._config} + extra_state_attributes: dict[str, Any] = {"configuration": self._config} if not self.is_on: for key in self._settings: extra_state_attributes[key] = None @@ -1218,7 +1220,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): and not (self._settings["force_rgb_color"] and "color" in features) ): _LOGGER.debug("%s: Setting color_temp of light %s", self._name, light) - attributes = self.hass.states.get(light).attributes + state = self.hass.states.get(light) + assert isinstance(state, State) + attributes = state.attributes min_kelvin = attributes["min_color_temp_kelvin"] max_kelvin = attributes["max_color_temp_kelvin"] color_temp_kelvin = self._settings["color_temp_kelvin"] @@ -1257,11 +1261,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _adapt_light( self, light: str, + context: Context, transition: int | None = None, adapt_brightness: bool | None = None, adapt_color: bool | None = None, prefer_rgb_color: bool | None = None, - context: Context | None = None, ) -> None: if (lock := self._locks.get(light)) is not None and lock.locked(): _LOGGER.debug("%s: '%s' is locked", self._name, light) @@ -1358,7 +1362,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): lights: list[str] | None = None, transition: int | None = None, force: bool = False, - context: Context | None = None, + context: Context = None, ) -> None: assert context is not None _LOGGER.debug( @@ -1406,6 +1410,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return adapt_brightness = self.adapt_brightness_switch.is_on adapt_color = self.adapt_color_switch.is_on + assert isinstance(adapt_brightness, bool) + assert isinstance(adapt_color, bool) for light in filtered_lights: if not is_on(self.hass, light): @@ -1450,7 +1456,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): transition, context.id, ) - await self._adapt_light(light, transition, context=context) + await self._adapt_light(light, context, transition) async def _sleep_mode_switch_state_event_action(self, event: Event) -> None: if not _is_state_event(event, (STATE_ON, STATE_OFF)): @@ -1581,7 +1587,7 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): self.hass = hass data = validate(config_entry) self._icon = icon - self._state = None + self._state: bool | None = None self._which = which name = data[CONF_NAME] self._unique_id = f"{name}_{slugify(self._which)}" @@ -1653,7 +1659,7 @@ def lerp_color_hsv( # Convert back to RGB rgb = tuple(int(round(x * 255)) for x in colorsys.hsv_to_rgb(*hsv)) assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}" - return rgb + return cast(tuple[int, int, int], rgb) @dataclass(frozen=True) @@ -1677,7 +1683,6 @@ class SunLightSettings: sunset_offset: datetime.timedelta | None sunset_time: datetime.time | None min_sunset_time: datetime.time | None - time_zone: datetime.tzinfo transition: int def get_sun_events(self, date: datetime.datetime) -> list[tuple[str, float]]: @@ -1809,7 +1814,8 @@ class SunLightSettings: delta = abs(self.min_color_temp - self.sleep_color_temp) ct = (delta * abs(1 + percent)) + self.sleep_color_temp return 5 * round(ct / 5) # round to nearest 5 - return None + msg = "Should not happen" + raise ValueError(msg) def get_settings( self, @@ -2397,7 +2403,7 @@ class AdaptiveLightingManager: last_service_data = self.last_service_data.get(light) if last_service_data is None: - return None + return False compare_to = functools.partial( _attributes_have_changed, light=light, @@ -2411,6 +2417,7 @@ class AdaptiveLightingManager: # can happen e.g. using zigbee2mqtt with 'report: false' in device settings. await self.hass.helpers.entity_component.async_update_entity(light) refreshed_state = self.hass.states.get(light) + assert refreshed_state is not None changed = compare_to( old_attributes=last_service_data, @@ -2572,7 +2579,7 @@ class _AsyncSingleShotTimer: self.delay = delay self.callback = callback self.task = None - self.start_time: int | None = None + self.start_time: datetime.datetime | None = None async def _run(self): """Run the timer. Don't call this directly, use start() instead.""" From 5c897f76da59169728132289852f9ffeaa6f4bc1 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 28 Jul 2023 17:17:26 -0700 Subject: [PATCH 0600/1077] More typing fixes (#675) --- custom_components/adaptive_lighting/switch.py | 44 ++++++++++--------- tests/test_switch.py | 14 +++--- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 6b463bc9..9f2c11e3 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -408,10 +408,10 @@ async def handle_change_switch_settings( switch.manager.reset(*switch.lights, reset_manual_control=False) if switch.is_on: await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access - switch.lights, + context=switch.create_context("service", parent=service_call.context), + lights=switch.lights, transition=switch.initial_transition, force=True, - context=switch.create_context("service", parent=service_call.context), ) @@ -561,15 +561,16 @@ async def async_setup_entry( # noqa: PLR0915 else: switch.manager.reset(*all_lights) if switch.is_on: + context = switch.create_context( + "service", + parent=service_call.context, + ) # pylint: disable=protected-access await switch._update_attrs_and_maybe_adapt_lights( - all_lights, + context=context, + lights=all_lights, transition=switch.initial_transition, force=True, - context=switch.create_context( - "service", - parent=service_call.context, - ), ) # Register `apply` service @@ -1124,7 +1125,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._context_cnt += 1 return context - async def async_turn_on( # pylint: disable=arguments-differ + async def async_turn_on( # type: ignore[override] self, adapt_lights: bool = True, ) -> None: @@ -1141,9 +1142,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): await self._setup_listeners() if adapt_lights: await self._update_attrs_and_maybe_adapt_lights( + context=self.create_context("turn_on"), transition=self.initial_transition, force=True, - context=self.create_context("turn_on"), ) async def async_turn_off(self, **kwargs) -> None: # noqa: ARG002 @@ -1156,9 +1157,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _async_update_at_interval_action(self, now=None) -> None: # noqa: ARG002 await self._update_attrs_and_maybe_adapt_lights( + context=self.create_context("interval"), transition=self._transition, force=False, - context=self.create_context("interval"), ) async def prepare_adaptation_data( @@ -1271,7 +1272,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug("%s: '%s' is locked", self._name, light) return - if self.manager.is_proactively_adapting(context.parent_id): + if context.parent_id is not None and self.manager.is_proactively_adapting( + context.parent_id, + ): # Skip if adaptation was already executed by the service call interceptor _LOGGER.debug( "%s: Skipping reactive adaptation of %s", @@ -1359,10 +1362,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _update_attrs_and_maybe_adapt_lights( # noqa: PLR0912 self, + *, + context: Context, lights: list[str] | None = None, transition: int | None = None, force: bool = False, - context: Context = None, ) -> None: assert context is not None _LOGGER.debug( @@ -1470,15 +1474,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Reset the manually controlled status when the "sleep mode" changes self.manager.reset(*self.lights) await self._update_attrs_and_maybe_adapt_lights( + context=self.create_context("sleep", parent=event.context), transition=self._sleep_transition, force=True, - context=self.create_context("sleep", parent=event.context), ) async def _light_state_event_action(self, event: Event) -> None: old_state = event.data.get("old_state") new_state = event.data.get("new_state") - entity_id = event.data.get("entity_id") + entity_id: str = event.data["entity_id"] if old_state is None or new_state is None: return @@ -1565,10 +1569,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) await self._update_attrs_and_maybe_adapt_lights( + context=self.create_context("light_event", parent=event.context), lights=[entity_id], transition=self.initial_transition, force=True, - context=self.create_context("light_event", parent=event.context), ) @@ -1637,8 +1641,8 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): def lerp_color_hsv( - rgb1: tuple[int, int, int], - rgb2: tuple[int, int, int], + rgb1: tuple[float, float, float], + rgb2: tuple[float, float, float], t: float, ) -> tuple[int, int, int]: """Linearly interpolate between two RGB colors in HSV color space.""" @@ -2163,10 +2167,10 @@ class AdaptiveLightingManager: if not switch.is_on: continue await switch._update_attrs_and_maybe_adapt_lights( - [light], + context=switch.create_context("autoreset"), + lights=[light], transition=switch.initial_transition, force=True, - context=switch.create_context("autoreset"), ) _LOGGER.debug( "Auto resetting 'manual_control' status of '%s' because" @@ -2289,7 +2293,7 @@ class AdaptiveLightingManager: if ( timer is not None and timer.is_running() - and event.time_fired > timer.start_time + and event.time_fired > timer.start_time # type: ignore[operator] ): # Restart the auto reset timer timer.start() diff --git a/tests/test_switch.py b/tests/test_switch.py index 328e493f..0838a843 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -479,7 +479,7 @@ async def test_light_settings(hass): async def patch_time_and_get_updated_states(time): with patch("homeassistant.util.dt.utcnow", return_value=time): await switch._update_attrs_and_maybe_adapt_lights( - transition=0, context=context, force=True + context=context, transition=0, force=True ) await hass.async_block_till_done() return [hass.states.get(light) for light in lights] @@ -558,7 +558,7 @@ async def test_manual_control(hass): manual_control = switch.manager.manual_control async def update(): - await switch._update_attrs_and_maybe_adapt_lights(transition=0, context=context) + await switch._update_attrs_and_maybe_adapt_lights(context=context, transition=0) await hass.async_block_till_done() async def turn_light(state, **kwargs): @@ -710,7 +710,7 @@ async def test_auto_reset_manual_control(hass): manual_control = switch.manager.manual_control async def update(): - await switch._update_attrs_and_maybe_adapt_lights(transition=0, context=context) + await switch._update_attrs_and_maybe_adapt_lights(context=context, transition=0) await hass.async_block_till_done() async def turn_light(state, **kwargs): @@ -834,7 +834,7 @@ async def test_switch_off_on_off(hass): async def update(): await switch._update_attrs_and_maybe_adapt_lights( - transition=0, context=switch.create_context("test") + context=switch.create_context("test"), transition=0 ) await hass.async_block_till_done() @@ -982,7 +982,9 @@ async def test_state_change_handlers(hass): async def update(force: bool = False): await switch._update_attrs_and_maybe_adapt_lights( - force=force, transition=0, context=context + context=context, + force=force, + transition=0, ) await hass.async_block_till_done() @@ -1005,7 +1007,7 @@ async def test_state_change_handlers(hass): # 2 Adapt from sleep with a 'transition'. await switch.sleep_mode_switch.async_turn_off() await switch._update_attrs_and_maybe_adapt_lights( - force=False, transition=0, context=context + context=context, force=False, transition=0 ) await hass.async_block_till_done() current_service_data = switch.manager.last_service_data From f28c0315513c080355641b3cd334a46dd9b750d6 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 28 Jul 2023 17:20:01 -0700 Subject: [PATCH 0601/1077] Fix alias in FUNDING.yml (#677) --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e6701aec..8bb28be4 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: [basnijholz, RubenKelevra] +github: [basnijholt] From 8967747f31345fdbf1f6f274fc8b434e9f2e572d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 28 Jul 2023 17:25:50 -0700 Subject: [PATCH 0602/1077] =?UTF-8?q?Release=201.18.0=20(fixes=20lights=20?= =?UTF-8?q?accidentally=20turning=20on=20=F0=9F=9A=80=F0=9F=8E=89)=20(#676?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 8695c555..34045b68 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.17.5" + "version": "1.18.0" } From 8322e516ba260cb0aca040f186b9ee43a5168ab6 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 28 Jul 2023 18:41:41 -0700 Subject: [PATCH 0603/1077] Add link to repo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d1d7aa16..8721fc42 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ![](https://github.com/home-assistant/brands/raw/b4a168b9af282ef916e120d31091ecd5e3c35e66/core_integrations/adaptive_lighting/icon.png) -Adaptive Lighting is a custom component for [Home Assistant](https://www.home-assistant.io/) that intelligently adjusts the brightness and color of your lights 💡 based on the sun's position, while still allowing for manual control. +[Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) is a custom component for [Home Assistant](https://www.home-assistant.io/) that intelligently adjusts the brightness and color of your lights 💡 based on the sun's position, while still allowing for manual control. Download and install directly through [HACS (Home Assistant Community Store)](https://hacs.xyz/) From 68985199f6ec6fb0f5a338534298e83a0a8f6f2f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 29 Jul 2023 21:48:45 -0700 Subject: [PATCH 0604/1077] Rename lights in tests (#681) * Rename lights in tests * Remove unused dependencies --- tests/test_switch.py | 194 ++++++++++++++++++------------------------- 1 file changed, 82 insertions(+), 112 deletions(-) diff --git a/tests/test_switch.py b/tests/test_switch.py index 0838a843..be0c7d37 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -118,8 +118,9 @@ LAT_LONG_TZS = [ (32.87336, -117.22743, "US/Pacific"), ] -ENTITY_LIGHT = "light.bed_light" -ENTITY_LIGHT3 = "light.kitchen_lights" +ENTITY_LIGHT_1 = "light.light_1" +ENTITY_LIGHT_2 = "light.light_2" +ENTITY_LIGHT_3 = "light.light_3" _SWITCH_FMT = f"{SWITCH_DOMAIN}.{DOMAIN}" ENTITY_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}" ENTITY_SLEEP_MODE_SWITCH = f"{_SWITCH_FMT}_sleep_mode_{DEFAULT_NAME}" @@ -128,11 +129,6 @@ ENTITY_ADAPT_COLOR_SWITCH = f"{_SWITCH_FMT}_adapt_color_{DEFAULT_NAME}" ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE -GLOBAL_TEST_DEPENDENCIES = [ - "test_adaptive_lighting_switches", - "test_light_settings", -] - def create_random_context() -> str: return Context(id=ulid_transform.ulid_now(), parent_id=None) @@ -173,8 +169,8 @@ async def setup_lights(hass: HomeAssistant): { "platform": "template", "lights": { - "bed_light": { - "friendly_name": "Bed Light", + "light_1": { + "friendly_name": "light_1", "unique_id": "light_1", "turn_on": None, "turn_off": None, @@ -182,8 +178,8 @@ async def setup_lights(hass: HomeAssistant): "set_temperature": None, "set_color": None, }, - "ceiling_lights": { - "friendly_name": "Ceiling Lights", + "light_2": { + "friendly_name": "light_2", "unique_id": "light_2", "turn_on": None, "turn_off": None, @@ -191,8 +187,8 @@ async def setup_lights(hass: HomeAssistant): "set_temperature": None, "set_color": None, }, - "kitchen_lights": { - "friendly_name": "Kitchen Lights", + "light_3": { + "friendly_name": "light_3", "unique_id": "light_3", "turn_on": None, "turn_off": None, @@ -229,18 +225,18 @@ async def setup_lights_and_switch(hass, extra_conf=None, all_lights: bool = Fals await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ENTITY_LIGHT}, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1}, blocking=True, ) # Setup switch lights = [ - ENTITY_LIGHT, - "light.ceiling_lights", + ENTITY_LIGHT_1, + ENTITY_LIGHT_2, ] if all_lights: - lights.append(ENTITY_LIGHT3) + lights.append(ENTITY_LIGHT_3) assert all(hass.states.get(light) is not None for light in lights) _, switch = await setup_switch( @@ -341,7 +337,6 @@ async def test_adaptive_lighting_switches(hass): @pytest.mark.parametrize("lat,long,timezone", LAT_LONG_TZS) -@pytest.mark.dependency("test_adaptive_lighting_switches") async def test_adaptive_lighting_time_zones_with_default_settings( hass, lat, long, timezone, reset_time_zone # pylint: disable=redefined-outer-name ): @@ -530,11 +525,10 @@ async def test_light_settings(hass): assert_expected_color_temp(state) -@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_manager_not_tracking_untracked_lights(hass): """Test that lights that are not in a Adaptive Lighting switch aren't tracked.""" switch, _ = await setup_lights_and_switch(hass) - light = "light.kitchen_lights" + light = ENTITY_LIGHT_3 assert light not in switch.lights for state in [True, False]: await hass.services.async_call( @@ -550,7 +544,6 @@ async def test_manager_not_tracking_untracked_lights(hass): assert light not in switch.manager.lights -@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_manual_control(hass): """Test the 'manual control' tracking.""" switch, (light, *_) = await setup_lights_and_switch(hass) @@ -565,7 +558,7 @@ async def test_manual_control(hass): await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON if state else SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs}, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, **kwargs}, blocking=True, ) await hass.async_block_till_done() @@ -583,7 +576,7 @@ async def test_manual_control(hass): async def change_manual_control(set_to, extra_service_data=None): if extra_service_data is None: - extra_service_data = {CONF_LIGHTS: [ENTITY_LIGHT]} + extra_service_data = {CONF_LIGHTS: [ENTITY_LIGHT_1]} await hass.services.async_call( DOMAIN, SERVICE_SET_MANUAL_CONTROL, @@ -608,48 +601,48 @@ async def test_manual_control(hass): # Nothing is manually controlled await update() - assert not manual_control[ENTITY_LIGHT] - # Call light.turn_on for ENTITY_LIGHT + assert not manual_control[ENTITY_LIGHT_1] + # Call light.turn_on for ENTITY_LIGHT_1 await turn_light(True, brightness=increased_brightness()) - # Check that ENTITY_LIGHT is manually controlled - assert manual_control[ENTITY_LIGHT] + # Check that ENTITY_LIGHT_1 is manually controlled + assert manual_control[ENTITY_LIGHT_1] # Test adaptive_lighting.set_manual_control await change_manual_control(False) - # Check that ENTITY_LIGHT is not manually controlled - assert not manual_control[ENTITY_LIGHT] + # Check that ENTITY_LIGHT_1 is not manually controlled + assert not manual_control[ENTITY_LIGHT_1] # Check that toggling light off to on resets manual control await change_manual_control(True) - assert manual_control[ENTITY_LIGHT] + assert manual_control[ENTITY_LIGHT_1] await turn_light(False) await turn_light(True, brightness=increased_brightness()) - assert hass.states.get(ENTITY_LIGHT).state == STATE_ON - assert not manual_control[ENTITY_LIGHT], manual_control + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + assert not manual_control[ENTITY_LIGHT_1], manual_control # Check that toggling (sleep mode) switch resets manual control for entity_id in [ENTITY_SWITCH, ENTITY_SLEEP_MODE_SWITCH]: await change_manual_control(True) - assert manual_control[ENTITY_LIGHT] + assert manual_control[ENTITY_LIGHT_1] await turn_switch(False, entity_id) await turn_switch(True, entity_id) - assert not manual_control[ENTITY_LIGHT] + assert not manual_control[ENTITY_LIGHT_1] # Check that manual control is still enabled if set while bulb is off. # Test issue #37 await turn_light(False) await change_manual_control(True) await turn_light(True) - assert manual_control[ENTITY_LIGHT] + assert manual_control[ENTITY_LIGHT_1] # Check that when 'adapt_brightness' is off, changing the brightness # doesn't mark it as manually controlled but changing color_temp # does await turn_light(False) await turn_light(True) # reset manually controlled status - assert not manual_control[ENTITY_LIGHT] + assert not manual_control[ENTITY_LIGHT_1] await switch.adapt_brightness_switch.async_turn_off() await turn_light(True, brightness=increased_brightness()) - assert not manual_control[ENTITY_LIGHT] + assert not manual_control[ENTITY_LIGHT_1] mired_range = (light.min_color_temp_kelvin, light.max_color_temp_kelvin) kelvin_range = ( color_temperature_mired_to_kelvin(mired_range[1]), @@ -659,7 +652,7 @@ async def test_manual_control(hass): await turn_light( True, color_temp_kelvin=(light._attr_color_temp + 100) % ptp_kelvin ) - assert manual_control[ENTITY_LIGHT] + assert manual_control[ENTITY_LIGHT_1] await switch.adapt_brightness_switch.async_turn_on() # turn on again # Check that when 'adapt_color' is off, changing the color @@ -667,12 +660,12 @@ async def test_manual_control(hass): # does await turn_light(False) # reset manually controlled status await turn_light(True) - assert not manual_control[ENTITY_LIGHT] + assert not manual_control[ENTITY_LIGHT_1] await switch.adapt_color_switch.async_turn_off() await turn_light(True, color_temp=increased_color_temp()) - assert not manual_control[ENTITY_LIGHT] + assert not manual_control[ENTITY_LIGHT_1] await turn_light(True, brightness=increased_brightness()) - assert manual_control[ENTITY_LIGHT] + assert manual_control[ENTITY_LIGHT_1] # Check that when 'adapt_color' adapt_brightness are both off # nothing marks it as manually controlled @@ -680,7 +673,7 @@ async def test_manual_control(hass): await turn_light(True) await switch.adapt_color_switch.async_turn_off() await switch.adapt_brightness_switch.async_turn_off() - assert not manual_control[ENTITY_LIGHT] + assert not manual_control[ENTITY_LIGHT_1] await turn_light(True, color_temp=increased_color_temp()) await turn_light(True, brightness=increased_brightness()) await turn_light( @@ -688,7 +681,7 @@ async def test_manual_control(hass): color_temp=increased_color_temp(), brightness=increased_brightness(), ) - assert not manual_control[ENTITY_LIGHT] + assert not manual_control[ENTITY_LIGHT_1] # Turn switches on again await switch.adapt_color_switch.async_turn_on() await switch.adapt_brightness_switch.async_turn_on() @@ -701,7 +694,6 @@ async def test_manual_control(hass): assert all([not manual_control[eid] for eid in switch.lights]) -@pytest.mark.dependency(depends=[*GLOBAL_TEST_DEPENDENCIES, "test_manual_control"]) async def test_auto_reset_manual_control(hass): switch, (light, *_) = await setup_lights_and_switch( hass, {CONF_AUTORESET_CONTROL: 0.1} @@ -752,7 +744,6 @@ async def test_auto_reset_manual_control(hass): assert not manual_control[light.entity_id] -@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_apply_service(hass): """Test adaptive_lighting.apply service.""" switch, (_, _, light) = await setup_lights_and_switch(hass) @@ -817,9 +808,6 @@ async def test_apply_service(hass): assert old_state[ATTR_COLOR_TEMP_KELVIN] == new_state[ATTR_COLOR_TEMP_KELVIN] -@pytest.mark.dependency( - depends=[*GLOBAL_TEST_DEPENDENCIES, "test_apply_service", "test_manual_control"] -) async def test_switch_off_on_off(hass): """Test switch rapid off_on_off.""" @@ -827,7 +815,7 @@ async def test_switch_off_on_off(hass): await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON if state else SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs}, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, **kwargs}, blocking=True, ) await hass.async_block_till_done() @@ -846,23 +834,23 @@ async def test_switch_off_on_off(hass): # Turn light off with transition await turn_light(False, transition=1) - assert not switch.manager.manual_control[ENTITY_LIGHT] + assert not switch.manager.manual_control[ENTITY_LIGHT_1] # Set state to on after a second (like happens IRL) await asyncio.sleep(1e-3) - hass.states.async_set(ENTITY_LIGHT, STATE_ON) + hass.states.async_set(ENTITY_LIGHT_1, STATE_ON) # Set state to off after a second (like happens IRL) await asyncio.sleep(1e-3) - hass.states.async_set(ENTITY_LIGHT, STATE_OFF) + hass.states.async_set(ENTITY_LIGHT_1, STATE_OFF) # Now we test whether the sleep task is there - assert ENTITY_LIGHT in switch.manager.sleep_tasks - sleep_task = switch.manager.sleep_tasks[ENTITY_LIGHT] + assert ENTITY_LIGHT_1 in switch.manager.sleep_tasks + sleep_task = switch.manager.sleep_tasks[ENTITY_LIGHT_1] assert not sleep_task.cancelled() # A 'light.turn_on' event should cancel that task await turn_light(turn_light_state_at_end) await update() - state = hass.states.get(ENTITY_LIGHT).state + state = hass.states.get(ENTITY_LIGHT_1).state if turn_light_state_at_end: assert sleep_task.cancelled() assert state == STATE_ON @@ -870,7 +858,6 @@ async def test_switch_off_on_off(hass): assert state == STATE_OFF -@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) def test_color_difference_redmean(): """Test color_difference_redmean function.""" for _ in range(10): @@ -933,7 +920,6 @@ def test_attributes_have_changed(): ) -@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_state_change_handlers(hass): """ Test AdaptiveLightingManager's EVENT_STATE_CHANGED listener. @@ -955,7 +941,7 @@ async def test_state_change_handlers(hass): async def set_brightness(val: int): # 'Unsafe' set but we know what we're doing. hass.states.async_set( - ENTITY_LIGHT, "on", {ATTR_BRIGHTNESS: val, ATTR_SUPPORTED_FEATURES: 1} + ENTITY_LIGHT_1, "on", {ATTR_BRIGHTNESS: val, ATTR_SUPPORTED_FEATURES: 1} ) await hass.async_block_till_done() # Call code in AdaptiveLightingManager @@ -963,7 +949,7 @@ async def test_state_change_handlers(hass): EVENT_STATE_CHANGED, { "new_state": { - ATTR_ENTITY_ID: ENTITY_LIGHT, + ATTR_ENTITY_ID: ENTITY_LIGHT_1, "state": "on", ATTR_BRIGHTNESS: val, } @@ -975,7 +961,7 @@ async def test_state_change_handlers(hass): await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON if state else SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs}, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, **kwargs}, blocking=True, ) await hass.async_block_till_done() @@ -998,11 +984,11 @@ async def test_state_change_handlers(hass): blocking=True, ) await hass.async_block_till_done() - assert switch.manager.last_state_change.get(ENTITY_LIGHT) - assert len(switch.manager.last_state_change[ENTITY_LIGHT]) == 1 - assert not switch.manager.transition_timers.get(ENTITY_LIGHT) + assert switch.manager.last_state_change.get(ENTITY_LIGHT_1) + assert len(switch.manager.last_state_change[ENTITY_LIGHT_1]) == 1 + assert not switch.manager.transition_timers.get(ENTITY_LIGHT_1) last_service_data = deepcopy(switch.manager.last_service_data) - assert last_service_data.get(ENTITY_LIGHT) + assert last_service_data.get(ENTITY_LIGHT_1) # 2 Adapt from sleep with a 'transition'. await switch.sleep_mode_switch.async_turn_off() @@ -1068,9 +1054,9 @@ async def test_state_change_handlers(hass): # asyncio.sleep(3) # 4. Assert the transition timer started and everything was filled. listener = switch.manager - assert listener.last_state_change.get(ENTITY_LIGHT) - assert len(listener.last_state_change[ENTITY_LIGHT]) == total_events - assert listener.transition_timers.get(ENTITY_LIGHT) + assert listener.last_state_change.get(ENTITY_LIGHT_1) + assert len(listener.last_state_change[ENTITY_LIGHT_1]) == total_events + assert listener.transition_timers.get(ENTITY_LIGHT_1) # 5. Execute some checks during a transition _LOGGER.debug("Test detect_non_ha_changes:") @@ -1080,25 +1066,25 @@ async def test_state_change_handlers(hass): assert switch._detect_non_ha_changes await asyncio.sleep(transition_used / 3) # Ensure the timer still exists - timer = listener.transition_timers.get(ENTITY_LIGHT) + timer = listener.transition_timers.get(ENTITY_LIGHT_1) assert timer and timer.is_running() last_service_data = deepcopy(current_service_data) await update() - assert not switch.manager.manual_control[ENTITY_LIGHT] + assert not switch.manager.manual_control[ENTITY_LIGHT_1] await update() - assert not switch.manager.manual_control[ENTITY_LIGHT] - timer = listener.transition_timers.get(ENTITY_LIGHT) + assert not switch.manager.manual_control[ENTITY_LIGHT_1] + timer = listener.transition_timers.get(ENTITY_LIGHT_1) assert timer and timer.is_running() # Ensure the light did not adapt during the transition. assert last_service_data == current_service_data # 6. Assert everything after the transition finishes. await asyncio.sleep(transition_used) - assert listener.last_state_change.get(ENTITY_LIGHT) - assert len(listener.last_state_change[ENTITY_LIGHT]) == total_events + assert listener.last_state_change.get(ENTITY_LIGHT_1) + assert len(listener.last_state_change[ENTITY_LIGHT_1]) == total_events # Timer should be done and reset now. # This is the assert that I can't fix. - timer = listener.transition_timers.get(ENTITY_LIGHT) + timer = listener.transition_timers.get(ENTITY_LIGHT_1) assert not timer or not timer.is_running() # build last service data @@ -1108,35 +1094,25 @@ async def test_state_change_handlers(hass): await turn_light(True, brightness=40) await turn_light(True, brightness=20) await update(force=False) - assert switch.manager.manual_control[ENTITY_LIGHT] + assert switch.manager.manual_control[ENTITY_LIGHT_1] await update(force=True) - assert switch.manager.manual_control[ENTITY_LIGHT] + assert switch.manager.manual_control[ENTITY_LIGHT_1] # turn light off then on should reset manual control. await turn_light(False) await turn_light(True) - assert not switch.manager.manual_control[ENTITY_LIGHT] + assert not switch.manager.manual_control[ENTITY_LIGHT_1] await turn_light(True, brightness=50) _LOGGER.debug("Test: Brightness set to %s", 50) - # On next update ENTITY_LIGHT should be marked as manually controlled + # On next update ENTITY_LIGHT_1 should be marked as manually controlled await update(force=False) - assert switch.manager.last_service_data.get(ENTITY_LIGHT) is not None - assert switch.manager.last_state_change.get(ENTITY_LIGHT) is not None - assert switch.manager.manual_control[ENTITY_LIGHT] + assert switch.manager.last_service_data.get(ENTITY_LIGHT_1) is not None + assert switch.manager.last_state_change.get(ENTITY_LIGHT_1) is not None + assert switch.manager.manual_control[ENTITY_LIGHT_1] -@pytest.mark.dependency( - depends=[ - *GLOBAL_TEST_DEPENDENCIES, - "test_manual_control", - "test_apply_service", - "test_attributes_have_changed", - "test_state_change_handling", - ] -) -@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) def test_is_our_context(): """Test is_our_context function.""" context = create_context(DOMAIN, "test", 0) @@ -1211,7 +1187,6 @@ async def test_turn_on_and_off_when_already_at_that_state(hass): await hass.async_block_till_done() -@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_async_update_at_interval_action(hass): """Test '_async_update_at_interval_action' method.""" _, switch = await setup_switch(hass, {}) @@ -1219,7 +1194,6 @@ async def test_async_update_at_interval_action(hass): @pytest.mark.parametrize("separate_turn_on_commands", (True, False)) -@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_separate_turn_on_commands(hass, separate_turn_on_commands): """Test 'separate_turn_on_commands' argument.""" switch, (light, *_) = await setup_lights_and_switch( @@ -1256,7 +1230,6 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands): assert sleep_color_temp != color_temp -@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_area(hass): switch, (light, *_) = await setup_lights_and_switch(hass) @@ -1293,7 +1266,6 @@ async def test_area(hass): assert light.entity_id not in switch.manager.last_service_data -@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_change_switch_settings_service(hass): """Test adaptive_lighting.change_switch_settings service.""" switch, (_, _, light) = await setup_lights_and_switch(hass) @@ -1346,7 +1318,6 @@ async def test_change_switch_settings_service(hass): assert switch._sun_light_settings.min_color_temp == 2500 -@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_cancellable_service_calls_task(hass): """Test the creation and execution of the task that wraps adaptation service calls.""" (light, *_) = await setup_lights(hass) @@ -1377,7 +1348,6 @@ async def test_cancellable_service_calls_task(hass): assert task.done() -@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_service_calls_task_cancellation(hass): """Tests if the task that wraps ongoing adaptation service calls gets cancelled.""" _, switch = await setup_switch(hass, {}) @@ -1441,7 +1411,7 @@ async def test_proactive_adaptation(hass): ) event_context_ids = await _turn_on_and_track_event_contexts( - hass, "test_context", ENTITY_LIGHT3 + hass, "test_context", ENTITY_LIGHT_3 ) # Expect a single service call @@ -1449,7 +1419,7 @@ async def test_proactive_adaptation(hass): assert event_context_ids == ["test_context"] # Expect adapted light state - state = hass.states.get(ENTITY_LIGHT3) + state = hass.states.get(ENTITY_LIGHT_3) # Sun light settings use %, state only contains absolute assert state.attributes[ATTR_BRIGHTNESS] == 171 # == 67% assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 3448 @@ -1476,7 +1446,7 @@ async def test_proactive_adaptation_with_separate_commands(hass): ) event_context_ids = await _turn_on_and_track_event_contexts( - hass, "test_context", ENTITY_LIGHT3 + hass, "test_context", ENTITY_LIGHT_3 ) # Expect two service calls @@ -1485,7 +1455,7 @@ async def test_proactive_adaptation_with_separate_commands(hass): assert is_our_context_id(event_context_ids[1]) # Expect adapted light state - state = hass.states.get(ENTITY_LIGHT3) + state = hass.states.get(ENTITY_LIGHT_3) assert state.attributes[ATTR_BRIGHTNESS] == 171 assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 3448 @@ -1504,7 +1474,7 @@ async def test_proactive_adaptation_toggle(hass): await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TOGGLE, - {ATTR_ENTITY_ID: ENTITY_LIGHT3}, + {ATTR_ENTITY_ID: ENTITY_LIGHT_3}, blocking=True, context=Context(id="test1"), ) @@ -1515,7 +1485,7 @@ async def test_proactive_adaptation_toggle(hass): await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TOGGLE, - {ATTR_ENTITY_ID: ENTITY_LIGHT3}, + {ATTR_ENTITY_ID: ENTITY_LIGHT_3}, blocking=True, context=Context(id="test2"), ) @@ -1540,14 +1510,14 @@ async def test_proactive_adaptation_transition_override(hass): await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ENTITY_LIGHT3}, + {ATTR_ENTITY_ID: ENTITY_LIGHT_3}, blocking=True, ) await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ENTITY_LIGHT3, ATTR_TRANSITION: 456}, + {ATTR_ENTITY_ID: ENTITY_LIGHT_3, ATTR_TRANSITION: 456}, blocking=True, ) @@ -1560,7 +1530,7 @@ async def test_proactive_adaptation_transition_override(hass): assert set({ATTR_TRANSITION: 456}.items()).issubset(kwargs.items()) # Cleanup - switch.manager.cancel_ongoing_adaptation_calls(ENTITY_LIGHT3) + switch.manager.cancel_ongoing_adaptation_calls(ENTITY_LIGHT_3) async def test_two_switches_for_single_light(hass): @@ -1588,7 +1558,7 @@ async def test_two_switches_for_single_light(hass): await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON if state else SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs}, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, **kwargs}, blocking=True, ) await hass.async_block_till_done() @@ -1672,31 +1642,31 @@ async def test_adapt_until_sleep_and_rgb_colors(hass): assert not switch._settings["force_rgb_color"] assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS assert switch._settings["color_temp_kelvin"] > min_color_temp - assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT] + assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT_1] # One hour after sunset the brightness should be down and use RGB await patch_time_and_update(after_sunset) assert switch._settings["force_rgb_color"] assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS - assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT] + assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT_1] # At sunrise the brightness should be max and use Kelvin await patch_time_and_update(sunrise) assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS assert switch._settings["color_temp_kelvin"] == min_color_temp - assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT] + assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT_1] # One hour before sunrise the brightness should smaller than max # and use RGB await patch_time_and_update(before_sunrise) assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS - assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT] + assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT_1] # One hour after sunrise the brightness should be up and it should use Kelvin await patch_time_and_update(after_sunrise) assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS assert switch._settings["color_temp_kelvin"] > min_color_temp - assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT] + assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT_1] # Turn on sleep mode which make the brightness and color_temp # deterministic regardless of the time From 2b35ee1e5e8959a1315384536792f036b2ce6b2c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 30 Jul 2023 11:49:40 -0700 Subject: [PATCH 0605/1077] Mark as manually controlled when using flash, effect, or RGBW(W) (#684) * Mark as manually controlled when using flash or effect * Add comment * Add RGBW and RGBWW --- .../adaptive_lighting/adaptation_utils.py | 5 +++++ custom_components/adaptive_lighting/switch.py | 10 ++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py index 99d1eea5..e0ab6c47 100644 --- a/custom_components/adaptive_lighting/adaptation_utils.py +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -13,6 +13,8 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, ATTR_TRANSITION, ATTR_XY_COLOR, ) @@ -27,8 +29,11 @@ COLOR_ATTRS = { # Should ATTR_PROFILE be in here? ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_XY_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, } + BRIGHTNESS_ATTRS = { ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 9f2c11e3..181bd20d 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -21,6 +21,8 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP_KELVIN, + ATTR_EFFECT, + ATTR_FLASH, ATTR_RGB_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, @@ -1450,6 +1452,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context.id, ) else: + # Need to fire manual control event because of significant_change _fire_manual_control_event(self, light, context) else: _LOGGER.debug( @@ -2371,8 +2374,11 @@ class AdaptiveLightingManager: and not force ): keys = turn_on_event.data[ATTR_SERVICE_DATA].keys() - if (adapt_color and COLOR_ATTRS.intersection(keys)) or ( - adapt_brightness and BRIGHTNESS_ATTRS.intersection(keys) + if ( + (adapt_color and COLOR_ATTRS.intersection(keys)) + or (adapt_brightness and BRIGHTNESS_ATTRS.intersection(keys)) + or (ATTR_FLASH in keys) + or (ATTR_EFFECT in keys) ): # Light was already on and 'light.turn_on' was not called by # the adaptive_lighting integration. From 2e25af3125544bb77720fcca270f61f01b6d87f8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 30 Jul 2023 12:32:59 -0700 Subject: [PATCH 0606/1077] Do not intercept effect and flash calls (#685) * Do not intercept effect and flash calls * log * Do not intercept flash --- custom_components/adaptive_lighting/switch.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 181bd20d..58638c3d 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2003,6 +2003,9 @@ class AdaptiveLightingManager: if is_our_context(call.context): return + if ATTR_EFFECT in data[CONF_PARAMS] or ATTR_FLASH in data[CONF_PARAMS]: + return + entity_ids = self._get_entity_list(data) # For simplicity, only service calls affecting a single entity are currently handled. From f4d872a54089a05f84292e0f58cb8e9d95562181 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 30 Jul 2023 13:01:08 -0700 Subject: [PATCH 0607/1077] Make sure that SimpleSwitches are added before AdaptiveSwitch (#686) --- custom_components/adaptive_lighting/switch.py | 24 +++++++++++++++---- tests/test_switch.py | 3 +-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 58638c3d..2cf2c5db 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -293,7 +293,7 @@ def _switches_with_lights( entry = data.get(config.entry_id) if entry is None: # entry might be disabled and therefore missing continue - switch = data[config.entry_id]["instance"] + switch = data[config.entry_id][SWITCH_DOMAIN] switch._expand_light_groups() # Check if any of the lights are in the switch's lights if set(switch.lights) & set(all_check_lights): @@ -367,7 +367,7 @@ def _switches_from_service_call( ent_entry = ent_reg.async_get(entity_id) assert ent_entry is not None config_id = ent_entry.config_entry_id - switches.append(hass.data[DOMAIN][config_id]["instance"]) + switches.append(hass.data[DOMAIN][config_id][SWITCH_DOMAIN]) return switches if lights: @@ -498,9 +498,6 @@ async def async_setup_entry( # noqa: PLR0915 adapt_brightness_switch, ) - # save our switch instance, allows us to make switch's entity_id optional in service calls. - hass.data[DOMAIN][config_entry.entry_id]["instance"] = switch - data[config_entry.entry_id][SLEEP_MODE_SWITCH] = sleep_mode_switch data[config_entry.entry_id][ADAPT_COLOR_SWITCH] = adapt_color_switch data[config_entry.entry_id][ADAPT_BRIGHTNESS_SWITCH] = adapt_brightness_switch @@ -1016,6 +1013,23 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug("%s: Cancelled '_setup_listeners'", self._name) return + while not all( + sw._state is not None + for sw in [ + self.sleep_mode_switch, + self.adapt_brightness_switch, + self.adapt_color_switch, + ] + ): + # Waits until `async_added_to_hass` is done, which in SimpleSwitch + # is when `_state` is set to `True` or `False`. + # Fixes first issue in https://github.com/basnijholt/adaptive-lighting/issues/682 + _LOGGER.debug( + "%s: Waiting for simple switches to be initialized", + self._name, + ) + await asyncio.sleep(0.1) + assert not self.remove_listeners self._update_time_interval_listener() diff --git a/tests/test_switch.py b/tests/test_switch.py index be0c7d37..2a49f41c 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -331,9 +331,8 @@ async def test_adaptive_lighting_switches(hass): assert ADAPT_COLOR_SWITCH in data assert ADAPT_BRIGHTNESS_SWITCH in data assert UNDO_UPDATE_LISTENER in data - assert "instance" in data - assert len(data.keys()) == 6 + assert len(data.keys()) == 5 @pytest.mark.parametrize("lat,long,timezone", LAT_LONG_TZS) From e98f0dfa550f6f2d7e4b00a61e8ec7767a860a53 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 30 Jul 2023 13:11:30 -0700 Subject: [PATCH 0608/1077] Bump to 1.18.1 --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 34045b68..87996208 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.18.0" + "version": "1.18.1" } From 1842ac9bd5db62e7af8fd2ead12121abd870edb2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 30 Jul 2023 13:28:58 -0700 Subject: [PATCH 0609/1077] Do not assert but issue a warning (#687) * Do not assert but issue a warning * bump to 1.18.2 --- custom_components/adaptive_lighting/manifest.json | 2 +- custom_components/adaptive_lighting/switch.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 87996208..8d6a80b9 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.18.1" + "version": "1.18.2" } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 2cf2c5db..a96351f2 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2476,7 +2476,16 @@ class AdaptiveLightingManager: off_to_on_event: Event, ) -> bool: # Adaptive Lighting should never turn on lights itself - assert not is_our_context(off_to_on_event.context) + if is_our_context(off_to_on_event.context): + _LOGGER.warning( + "Detected an 'off' → 'on' event for '%s' with context.id='%s' and" + " event='%s', triggered by the adaptive_lighting integration itself," + " which *should* not happen. If you see this please submit an issue with" + " your full logs at https://github.com/basnijholt/adaptive-lighting", + entity_id, + off_to_on_event.context.id, + off_to_on_event, + ) turn_on_event: Event | None = self.turn_on_event.get(entity_id) id_off_to_on = off_to_on_event.context.id return ( From d588a5984ad2eb3ecf3fe5e15c2d8ec9f560e7f2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 30 Jul 2023 21:57:30 -0700 Subject: [PATCH 0610/1077] Refactor _update_attrs_and_maybe_adapt_lights (#688) * Do not call self.manager.significant_change when not needed * refact * fix * fix * Add more logging to tests * remove --- custom_components/adaptive_lighting/switch.py | 79 ++++++++++--------- tests/test_switch.py | 9 ++- 2 files changed, 49 insertions(+), 39 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index a96351f2..4229f0ac 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1376,7 +1376,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): data, ) - async def _update_attrs_and_maybe_adapt_lights( # noqa: PLR0912 + async def _update_attrs_and_maybe_adapt_lights( self, *, context: Context, @@ -1403,17 +1403,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) self.async_write_ha_state() - if lights is None: - lights = self.lights - if not force and self._only_once: return + if lights is None: + lights = self.lights + + on_lights = [light for light in lights if is_on(self.hass, light)] + if force: - filtered_lights = lights + filtered_lights = on_lights else: filtered_lights = [] - for light in lights: + for light in on_lights: # Don't adapt lights that haven't finished prior transitions. timer = self.manager.transition_timers.get(light) if timer is not None and timer.is_running(): @@ -1428,25 +1430,35 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): _LOGGER.debug("%s: filtered_lights: '%s'", self._name, filtered_lights) if not filtered_lights: return + adapt_brightness = self.adapt_brightness_switch.is_on adapt_color = self.adapt_color_switch.is_on assert isinstance(adapt_brightness, bool) assert isinstance(adapt_color, bool) for light in filtered_lights: - if not is_on(self.hass, light): + manually_controlled = ( + self._take_over_control + and self.manager.is_manually_controlled( + self, + light, + force, + adapt_brightness, + adapt_color, + ) + ) + if manually_controlled: + _LOGGER.debug( + "%s: '%s' is being manually controlled, stop adapting, context.id=%s.", + self._name, + light, + context.id, + ) continue - manually_controlled = self.manager.is_manually_controlled( - self, - light, - force, - adapt_brightness, - adapt_color, - ) - significant_change = ( - self._detect_non_ha_changes + self._take_over_control + and self._detect_non_ha_changes and not force and await self.manager.significant_change( self, @@ -1456,28 +1468,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context, ) ) + if significant_change: + _fire_manual_control_event(self, light, context) + continue - if self._take_over_control and (manually_controlled or significant_change): - if manually_controlled: - _LOGGER.debug( - "%s: '%s' is being manually controlled, stop adapting, context.id=%s.", - self._name, - light, - context.id, - ) - else: - # Need to fire manual control event because of significant_change - _fire_manual_control_event(self, light, context) - else: - _LOGGER.debug( - "%s: Calling _adapt_light from _update_attrs_and_maybe_adapt_lights:" - " '%s' with transition %s and context.id=%s", - self._name, - light, - transition, - context.id, - ) - await self._adapt_light(light, context, transition) + _LOGGER.debug( + "%s: Calling _adapt_light from _update_attrs_and_maybe_adapt_lights:" + " '%s' with transition %s and context.id=%s", + self._name, + light, + transition, + context.id, + ) + await self._adapt_light(light, context, transition) async def _sleep_mode_switch_state_event_action(self, event: Event) -> None: if not _is_state_event(event, (STATE_ON, STATE_OFF)): @@ -2417,7 +2420,7 @@ class AdaptiveLightingManager: light: str, adapt_brightness: bool, adapt_color: bool, - context: Context, + context: Context, # just for logging ) -> bool: """Has the light made a significant change since last update. diff --git a/tests/test_switch.py b/tests/test_switch.py index 2a49f41c..43da74a2 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -546,6 +546,9 @@ async def test_manager_not_tracking_untracked_lights(hass): async def test_manual_control(hass): """Test the 'manual control' tracking.""" switch, (light, *_) = await setup_lights_and_switch(hass) + assert switch._take_over_control + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + context = switch.create_context("test") # needs to be passed to update method manual_control = switch.manager.manual_control @@ -560,9 +563,9 @@ async def test_manual_control(hass): {ATTR_ENTITY_ID: ENTITY_LIGHT_1, **kwargs}, blocking=True, ) + _LOGGER.debug("Turn light %s, to %s", "on" if state else "off", kwargs) await hass.async_block_till_done() await update() - _LOGGER.debug("Turn light %s, to %s", state, kwargs) async def turn_switch(state, entity_id): await hass.services.async_call( @@ -576,6 +579,7 @@ async def test_manual_control(hass): async def change_manual_control(set_to, extra_service_data=None): if extra_service_data is None: extra_service_data = {CONF_LIGHTS: [ENTITY_LIGHT_1]} + _LOGGER.debug(f"{switch.manager.manual_control=}") await hass.services.async_call( DOMAIN, SERVICE_SET_MANUAL_CONTROL, @@ -586,8 +590,11 @@ async def test_manual_control(hass): }, blocking=True, ) + _LOGGER.debug(f"{switch.manager.manual_control=}") + _LOGGER.debug("Called set_manual_control with %s", set_to) await hass.async_block_till_done() await update() + _LOGGER.debug("End of change_manual_control") def increased_brightness(): return (light._attr_brightness + 100) % 255 From aa9f69a7bbcfb471094778ee3e4a9ab369c233bf Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Jul 2023 17:52:53 -0700 Subject: [PATCH 0611/1077] Set last_service_data in the right place (#652) * Set last_service_data in the right place * Do not get but access key * rename --- custom_components/adaptive_lighting/switch.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 4229f0ac..7bcd78bb 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1262,8 +1262,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context = context or self.create_context("adapt_lights") - self.manager.last_service_data[light] = service_data - return prepare_adaptation_data( self.hass, light, @@ -1337,6 +1335,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): service_data, data.context.id, ) + light = service_data[ATTR_ENTITY_ID] + self.manager.last_service_data[light] = service_data await self.hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -2134,10 +2134,7 @@ class AdaptiveLightingManager: def start_transition_timer(self, light: str) -> None: """Mark a light as manually controlled.""" - last_service_data = self.last_service_data.get(light) - if not last_service_data: - _LOGGER.debug("This should not ever happen. Please report to the devs.") - return + last_service_data = self.last_service_data[light] last_transition = last_service_data.get(ATTR_TRANSITION) if not last_transition: _LOGGER.debug( From da2e27d7381b12e9362daa2a6eb76e0cb6d97277 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 18:15:36 -0700 Subject: [PATCH 0612/1077] [pre-commit.ci] pre-commit autoupdate (#691) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.0.280 → v0.0.281](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.280...v0.0.281) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dc82bbe7..6e27d970 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.280 + rev: v0.0.281 hooks: - id: ruff args: ["--fix"] From e5133936536cb02d6e8122c1d7dc5fde5810214c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 31 Jul 2023 18:40:32 -0700 Subject: [PATCH 0613/1077] Keep on and off state tracking in Manager and listen to toggle (#689) * Keep on and off state tracking in Manager * Move manual_control code * add todo * check is_on * Track toggle * handle toggle * fix debug * test * add comment * better log * Fix comment * log * test * get * check * cancel early * rephrase * add assert * Remove check --- custom_components/adaptive_lighting/switch.py | 312 +++++++++--------- tests/test_switch.py | 14 +- 2 files changed, 166 insertions(+), 160 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 7bcd78bb..772aa1e0 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -859,12 +859,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._icon = ICON_MAIN self._state: bool | None = None - # Tracks 'on' → 'off' state changes - self._on_to_off_event: dict[str, Event] = {} - # Tracks 'off' → 'on' state changes - self._off_to_on_event: dict[str, Event] = {} - # Locks that prevent light adjusting when waiting for a light to 'turn_off' - self._locks: dict[str, asyncio.Lock] = {} # To count the number of `Context` instances self._context_cnt: int = 0 @@ -1041,15 +1035,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) self.remove_listeners.append(remove_sleep) - - if self.lights: - self._expand_light_groups() - remove_state = async_track_state_change_event( - self.hass, - entity_ids=self.lights, - action=self._light_state_event_action, - ) - self.remove_listeners.append(remove_state) + self._expand_light_groups() def _update_time_interval_listener(self) -> None: """Create or recreate the adaptation interval listener. @@ -1282,19 +1268,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): adapt_color: bool | None = None, prefer_rgb_color: bool | None = None, ) -> None: - if (lock := self._locks.get(light)) is not None and lock.locked(): - _LOGGER.debug("%s: '%s' is locked", self._name, light) - return + # This should never happen if it's been proactively adapted. + # The context.parent_id is the context.id of the service call that was intercepted + # and context.id here is from the resulting "light_event" event. + assert not self.manager.is_proactively_adapting(context.parent_id) - if context.parent_id is not None and self.manager.is_proactively_adapting( - context.parent_id, - ): - # Skip if adaptation was already executed by the service call interceptor - _LOGGER.debug( - "%s: Skipping reactive adaptation of %s", - self._name, - context.parent_id, - ) + if (lock := self.manager.turn_off_locks.get(light)) and lock.locked(): + _LOGGER.debug("%s: '%s' is locked", self._name, light) return data = await self.prepare_adaptation_data( @@ -1482,6 +1462,39 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) await self._adapt_light(light, context, transition) + async def _respond_to_off_to_on_event(self, entity_id: str, event: Event) -> None: + assert not self.manager.is_proactively_adapting(event.context.id) + if ( + not self._detect_non_ha_changes + and not self.manager._off_to_on_state_event_is_from_turn_on( + entity_id, + event, + ) + ): + # There is an edge case where 2 switches control the same light, e.g., + # one for brightness and one for color. Now we will mark both switches + # as manually controlled, which is not 100% correct. + _LOGGER.debug( + "%s: Ignoring 'off' → 'on' event for '%s' with context.id='%s'" + " because 'light.turn_on' was not called by HA and" + " 'detect_non_ha_changes' is False", + self._name, + entity_id, + event.context.id, + ) + self.manager.mark_as_manual_control(entity_id) + return + + if self._adapt_delay > 0: + await asyncio.sleep(self._adapt_delay) + + await self._update_attrs_and_maybe_adapt_lights( + context=self.create_context("light_event", parent=event.context), + lights=[entity_id], + transition=self.initial_transition, + force=True, + ) + async def _sleep_mode_switch_state_event_action(self, event: Event) -> None: if not _is_state_event(event, (STATE_ON, STATE_OFF)): _LOGGER.debug("%s: Ignoring sleep event %s", self._name, event) @@ -1499,102 +1512,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): force=True, ) - async def _light_state_event_action(self, event: Event) -> None: - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") - entity_id: str = event.data["entity_id"] - - if old_state is None or new_state is None: - return - - if old_state.state == STATE_ON and new_state.state == STATE_OFF: - # Tracks 'on' → 'off' state changes - self._on_to_off_event[entity_id] = event - self.manager.reset(entity_id) - _LOGGER.debug( - "%s: Detected an 'on' → 'off' event for '%s' with context.id='%s'", - self._name, - entity_id, - event.context.id, - ) - - if old_state.state == STATE_OFF and new_state.state == STATE_ON: - # Tracks 'off' → 'on' state changes - self._off_to_on_event[entity_id] = event - _LOGGER.debug( - "%s: Detected an 'off' → 'on' event for '%s' with context.id='%s'", - self._name, - entity_id, - event.context.id, - ) - - if ( - not self._detect_non_ha_changes - and not self.manager.is_proactively_adapting(event.context.id) - and not self.manager._off_to_on_state_event_is_from_turn_on( - entity_id, - event, - ) - ): - # If we don't detect non-HA changes, we're only adjusting lights that - # were turned on by HA. If the light was turned on by something else, - # we don't adjust it (e.g., when HA suddenly reports it as on). - # Sometimes the light incorrectly reports itself as on when it's - # actually off. This code path will ensure that the light is - # not controlled by Adaptive Lighting. - _LOGGER.debug( - "%s: Ignoring 'off' → 'on' event for '%s' with context.id='%s'" - " because 'light.turn_on' was not called by HA and" - " 'detect_non_ha_changes' is False", - self._name, - entity_id, - event.context.id, - ) - self.manager.mark_as_manual_control(entity_id) - return - - if event.context.parent_id and not self.manager.is_proactively_adapting( - event.context.id, - ): - self.manager.reset(entity_id, reset_manual_control=False) - - lock = self._locks.setdefault(entity_id, asyncio.Lock()) - async with lock: - if await self.manager.just_turned_off( - entity_id, - off_to_on_event=event, - on_to_off_event=self._on_to_off_event.get(entity_id), - ): - # Stop if a rapid 'off' → 'on' → 'off' happens. - _LOGGER.debug( - "%s: Cancelling adjusting lights for %s", - self._name, - entity_id, - ) - return - - if self._adapt_delay > 0: - _LOGGER.debug( - "%s: sleep started for '%s' with context.id='%s'", - self._name, - entity_id, - event.context.id, - ) - await asyncio.sleep(self._adapt_delay) - _LOGGER.debug( - "%s: sleep ended for '%s' with context.id='%s'", - self._name, - entity_id, - event.context.id, - ) - - await self._update_attrs_and_maybe_adapt_lights( - context=self.create_context("light_event", parent=event.context), - lights=[entity_id], - transition=self.initial_transition, - force=True, - ) - class SimpleSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" @@ -1908,12 +1825,20 @@ class AdaptiveLightingManager: self.turn_off_event: dict[str, Event] = {} # Tracks 'light.turn_on' service calls self.turn_on_event: dict[str, Event] = {} + # Tracks 'light.toggle' service calls + self.toggle_event: dict[str, Event] = {} + # Tracks 'on' → 'off' state changes + self.on_to_off_event: dict[str, Event] = {} + # Tracks 'off' → 'on' state changes + self.off_to_on_event: dict[str, Event] = {} # Keep 'asyncio.sleep' tasks that can be cancelled by 'light.turn_on' events self.sleep_tasks: dict[str, asyncio.Task] = {} + # Locks that prevent light adjusting when waiting for a light to 'turn_off' + self.turn_off_locks: dict[str, asyncio.Lock] = {} # Tracks which lights are manually controlled self.manual_control: dict[str, bool] = {} # Track 'state_changed' events of self.lights resulting from this integration - self.last_state_change: dict[str, list[State]] = {} + self.our_last_state_on_change: dict[str, list[State]] = {} # Track last 'service_data' to 'light.turn_on' resulting from this integration self.last_service_data: dict[str, dict[str, Any]] = {} # Track ongoing split adaptations to be able to cancel them @@ -2069,7 +1994,11 @@ class AdaptiveLightingManager: call.context.id, ) + # Reset because turning on the light, this also happens in + # `turn_on_off_event_listener`, however, this function is called + # before that one. self.reset(entity_id, reset_manual_control=False) + self.clear_proactively_adapting(entity_id) transition = data[CONF_PARAMS].get( @@ -2243,7 +2172,7 @@ class AdaptiveLightingManager: timer = self.auto_reset_manual_control_timers.pop(light, None) if timer is not None: timer.cancel() - self.last_state_change.pop(light, None) + self.our_last_state_on_change.pop(light, None) self.last_service_data.pop(light, None) self.cancel_ongoing_adaptation_calls(light) @@ -2286,6 +2215,24 @@ class AdaptiveLightingManager: if not any(eid in self.lights for eid in entity_ids): return + def off(eid: str, event: Event): + self.turn_off_event[eid] = event + self.reset(eid) + + def on(eid: str, event: Event): + task = self.sleep_tasks.get(eid) + if task is not None: + task.cancel() + self.turn_on_event[eid] = event + timer = self.auto_reset_manual_control_timers.get(eid) + if ( + timer is not None + and timer.is_running() + and event.time_fired > timer.start_time # type: ignore[operator] + ): + # Restart the auto reset timer + timer.start() + if service == SERVICE_TURN_OFF: transition = service_data.get(ATTR_TRANSITION) _LOGGER.debug( @@ -2295,8 +2242,7 @@ class AdaptiveLightingManager: event.context.id, ) for eid in entity_ids: - self.turn_off_event[eid] = event - self.reset(eid) + off(eid, event) elif service == SERVICE_TURN_ON: _LOGGER.debug( @@ -2305,18 +2251,21 @@ class AdaptiveLightingManager: event.context.id, ) for eid in entity_ids: - task = self.sleep_tasks.get(eid) - if task is not None: - task.cancel() - self.turn_on_event[eid] = event - timer = self.auto_reset_manual_control_timers.get(eid) - if ( - timer is not None - and timer.is_running() - and event.time_fired > timer.start_time # type: ignore[operator] - ): - # Restart the auto reset timer - timer.start() + on(eid, event) + + elif service == SERVICE_TOGGLE: + _LOGGER.debug( + "Detected an 'light.toggle('%s')' event with context.id='%s'", + entity_ids, + event.context.id, + ) + for eid in entity_ids: + state = self.hass.states.get(eid).state + self.toggle_event[eid] = event + if state == STATE_ON: # is turning off + off(eid, event) + elif state == STATE_OFF: # is turning on + on(eid, event) async def state_changed_event_listener(self, event: Event) -> None: """Track 'state_changed' events.""" @@ -2324,16 +2273,21 @@ class AdaptiveLightingManager: if entity_id not in self.lights: return + old_state = event.data.get("old_state") new_state = event.data.get("new_state") - if new_state is not None and new_state.state == STATE_ON: + + new_on = new_state is not None and new_state.state == STATE_ON + new_off = new_state is not None and new_state.state == STATE_OFF + old_on = old_state is not None and old_state.state == STATE_ON + old_off = old_state is not None and old_state.state == STATE_OFF + + if new_on: _LOGGER.debug( "Detected a '%s' 'state_changed' event: '%s' with context.id='%s'", entity_id, new_state.attributes, new_state.context.id, ) - - if new_state is not None and new_state.state == STATE_ON: # It is possible to have multiple state change events with the same context. # This can happen because a `turn_on.light(brightness_pct=100, transition=30)` # event leads to an instant state change of @@ -2345,30 +2299,78 @@ class AdaptiveLightingManager: # called with a color_temp outside of its range (and HA reports the # incorrect 'min_kelvin' and 'max_kelvin', which happens e.g., for # Philips Hue White GU10 Bluetooth lights). - old_state: list[State] | None = self.last_state_change.get(entity_id) + last_state: list[State] | None = self.our_last_state_on_change.get( + entity_id, + ) if is_our_context(new_state.context): if ( - old_state is not None - and old_state[0].context.id == new_state.context.id + last_state is not None + and last_state[0].context.id == new_state.context.id ): _LOGGER.debug( "AdaptiveLightingManager: State change event of '%s' is already" - " in 'self.last_state_change' (%s)" + " in 'self.our_last_state_on_change' (%s)" " adding this state also", entity_id, new_state.context.id, ) - self.last_state_change[entity_id].append(new_state) + self.our_last_state_on_change[entity_id].append(new_state) else: _LOGGER.debug( "AdaptiveLightingManager: New adapt '%s' found for %s", new_state, entity_id, ) - self.last_state_change[entity_id] = [new_state] + self.our_last_state_on_change[entity_id] = [new_state] self.start_transition_timer(entity_id) - elif old_state is not None: - self.last_state_change[entity_id].append(new_state) + elif last_state is not None: + self.our_last_state_on_change[entity_id].append(new_state) + + if old_on and new_off: + # Tracks 'on' → 'off' state changes + self.on_to_off_event[entity_id] = event + self.reset(entity_id) + _LOGGER.debug( + "Detected an 'on' → 'off' event for '%s' with context.id='%s'", + entity_id, + event.context.id, + ) + elif old_off and new_on: + # Tracks 'off' → 'on' state changes + self.off_to_on_event[entity_id] = event + _LOGGER.debug( + "Detected an 'off' → 'on' event for '%s' with context.id='%s'", + entity_id, + event.context.id, + ) + + if self.is_proactively_adapting(event.context.id): + _LOGGER.debug( + "Skipping responding to 'off' → 'on' event for '%s' with context.id='%s' because" + " we are already proactively adapting", + entity_id, + event.context.id, + ) + return + + self.reset(entity_id, reset_manual_control=False) + lock = self.turn_off_locks.setdefault(entity_id, asyncio.Lock()) + async with lock: + if await self.just_turned_off(entity_id): + # Stop if a rapid 'off' → 'on' → 'off' happens. + _LOGGER.debug( + "Cancelling adjusting lights for %s", + entity_id, + ) + return + + switches = _switches_with_lights(self.hass, [entity_id]) + for switch in switches: + if switch.is_on: + await switch._respond_to_off_to_on_event( + entity_id, + event, + ) def is_manually_controlled( self, @@ -2497,8 +2499,6 @@ class AdaptiveLightingManager: async def just_turned_off( # noqa: PLR0911 self, entity_id: str, - off_to_on_event: Event, - on_to_off_event: Event | None, ) -> bool: """Cancel the adjusting of a light if it has just been turned off. @@ -2512,6 +2512,9 @@ class AdaptiveLightingManager: if the brightness is still decreasing. Only if it is the case we adjust the lights. """ + off_to_on_event = self.off_to_on_event[entity_id] + on_to_off_event = self.on_to_off_event.get(entity_id) + if on_to_off_event is None: _LOGGER.debug( "just_turned_off: No 'on' → 'off' state change has been registered before for '%s'." @@ -2529,8 +2532,11 @@ class AdaptiveLightingManager: transition = None if self._off_to_on_state_event_is_from_turn_on(entity_id, off_to_on_event): + is_toggle = off_to_on_event == self.toggle_event.get(entity_id) + from_service = "light.toggle" if is_toggle else "light.turn_on" _LOGGER.debug( - "just_turned_off: State change 'off' → 'on' triggered by 'light.turn_on'", + "just_turned_off: State change 'off' → 'on' triggered by '%s'", + from_service, ) return False diff --git a/tests/test_switch.py b/tests/test_switch.py index 43da74a2..c7220ee4 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -990,8 +990,8 @@ async def test_state_change_handlers(hass): blocking=True, ) await hass.async_block_till_done() - assert switch.manager.last_state_change.get(ENTITY_LIGHT_1) - assert len(switch.manager.last_state_change[ENTITY_LIGHT_1]) == 1 + assert switch.manager.our_last_state_on_change.get(ENTITY_LIGHT_1) + assert len(switch.manager.our_last_state_on_change[ENTITY_LIGHT_1]) == 1 assert not switch.manager.transition_timers.get(ENTITY_LIGHT_1) last_service_data = deepcopy(switch.manager.last_service_data) assert last_service_data.get(ENTITY_LIGHT_1) @@ -1060,8 +1060,8 @@ async def test_state_change_handlers(hass): # asyncio.sleep(3) # 4. Assert the transition timer started and everything was filled. listener = switch.manager - assert listener.last_state_change.get(ENTITY_LIGHT_1) - assert len(listener.last_state_change[ENTITY_LIGHT_1]) == total_events + assert listener.our_last_state_on_change.get(ENTITY_LIGHT_1) + assert len(listener.our_last_state_on_change[ENTITY_LIGHT_1]) == total_events assert listener.transition_timers.get(ENTITY_LIGHT_1) # 5. Execute some checks during a transition @@ -1086,8 +1086,8 @@ async def test_state_change_handlers(hass): # 6. Assert everything after the transition finishes. await asyncio.sleep(transition_used) - assert listener.last_state_change.get(ENTITY_LIGHT_1) - assert len(listener.last_state_change[ENTITY_LIGHT_1]) == total_events + assert listener.our_last_state_on_change.get(ENTITY_LIGHT_1) + assert len(listener.our_last_state_on_change[ENTITY_LIGHT_1]) == total_events # Timer should be done and reset now. # This is the assert that I can't fix. timer = listener.transition_timers.get(ENTITY_LIGHT_1) @@ -1115,7 +1115,7 @@ async def test_state_change_handlers(hass): # On next update ENTITY_LIGHT_1 should be marked as manually controlled await update(force=False) assert switch.manager.last_service_data.get(ENTITY_LIGHT_1) is not None - assert switch.manager.last_state_change.get(ENTITY_LIGHT_1) is not None + assert switch.manager.our_last_state_on_change.get(ENTITY_LIGHT_1) is not None assert switch.manager.manual_control[ENTITY_LIGHT_1] From 8d13f1387346fd6dd79caef4a44a140e718996b0 Mon Sep 17 00:00:00 2001 From: badboybeyer Date: Wed, 2 Aug 2023 08:09:34 -0700 Subject: [PATCH 0614/1077] bug: fix bug in comment in readme (#693) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8721fc42..9a70b89a 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ adaptive_lighting: sunrise_time: "08:00:00" # override the sunrise time sunrise_offset: sunset_time: - sunset_offset: 1800 # in seconds or '00:15:00' + sunset_offset: 1800 # in seconds or '00:30:00' take_over_control: true detect_non_ha_changes: false only_once: false From 807f7109d49898b2bbebcec3057eb1017bf21099 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 2 Aug 2023 17:59:44 -0700 Subject: [PATCH 0615/1077] Require take_over_control for to ignore non-turn_on state off -> on event (#695) --- custom_components/adaptive_lighting/switch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 772aa1e0..86f430bc 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1465,7 +1465,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _respond_to_off_to_on_event(self, entity_id: str, event: Event) -> None: assert not self.manager.is_proactively_adapting(event.context.id) if ( - not self._detect_non_ha_changes + self._take_over_control + and not self._detect_non_ha_changes and not self.manager._off_to_on_state_event_is_from_turn_on( entity_id, event, From 53ed220fa0802c87f3cdd3aafa96bfd2d51ced13 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 2 Aug 2023 22:59:01 -0700 Subject: [PATCH 0616/1077] Bail adapting if on event equals off event context (#696) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Bail adapting if on event equals off event context Should prevent this (I saw in my logs) ``` 2023-08-02 21:49:56.516 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected an 'light.turn_off('['light.philips_go', 'light.bed_led', 'light.bamboo']', transition=10.0)' event with context.id='01H6WVP8RJF4JZ78SFD98YA0RG' 2023-08-02 21:49:56.637 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected an 'on' → 'off' event for 'light.bamboo' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG' 2023-08-02 21:49:56.672 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected an 'on' → 'off' event for 'light.bed_led' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG' 2023-08-02 21:49:56.747 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected an 'on' → 'off' event for 'light.philips_go' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG' 2023-08-02 21:50:00.501 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected a 'light.philips_go' 'state_changed' event: '{'min_color_temp_kelvin': 2000, 'max_color_temp_kelvin': 6666, 'min_mireds': 150, 'max_mireds': 500, 'effect_list': ['blink', 'breathe', 'okay', 'channel_change', 'candle', 'fireplace', 'colorloop', 'finish_effect', 'stop_effect', 'stop_hue_effect'], 'supported_color_modes': ['color_temp', 'xy'], 'color_mode': , 'brightness': 10, 'hs_color': (0.0, 100.0), 'rgb_color': (255, 0, 0), 'xy_color': (0.701, 0.299), 'friendly_name': 'Philips Go', 'supported_features': }' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG' 2023-08-02 21:50:00.502 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected an 'off' → 'on' event for 'light.philips_go' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG' 2023-08-02 21:50:00.502 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] is_proactively_adapting_context='False', context_id='01H6WVP8RJF4JZ78SFD98YA0RG' 2023-08-02 21:50:00.502 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] just_turned_off: Waiting with adjusting 'light.philips_go' for 6.240101 2023-08-02 21:50:00.528 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected a 'light.bed_led' 'state_changed' event: '{'min_color_temp_kelvin': 2000, 'max_color_temp_kelvin': 6535, 'min_mireds': 153, 'max_mireds': 500, 'effect_list': ['blink', 'breathe', 'okay', 'channel_change', 'candle', 'fireplace', 'colorloop', 'finish_effect', 'stop_effect', 'stop_hue_effect'], 'supported_color_modes': ['color_temp', 'xy'], 'color_mode': , 'brightness': 10, 'hs_color': (10.824, 100.0), 'rgb_color': (255, 46, 0), 'xy_color': (0.689, 0.309), 'friendly_name': 'Bed LED', 'supported_features': }' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG' 2023-08-02 21:50:00.529 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected an 'off' → 'on' event for 'light.bed_led' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG' 2023-08-02 21:50:00.529 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] is_proactively_adapting_context='False', context_id='01H6WVP8RJF4JZ78SFD98YA0RG' 2023-08-02 21:50:00.529 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] just_turned_off: Waiting with adjusting 'light.bed_led' for 6.129017 2023-08-02 21:50:00.561 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected a 'light.bamboo' 'state_changed' event: '{'min_color_temp_kelvin': 2000, 'max_color_temp_kelvin': 6535, 'min_mireds': 153, 'max_mireds': 500, 'effect_list': ['blink', 'breathe', 'okay', 'channel_change', 'candle', 'fireplace', 'colorloop', 'finish_effect', 'stop_effect', 'stop_hue_effect'], 'supported_color_modes': ['color_temp', 'xy'], 'color_mode': , 'brightness': 10, 'hs_color': (299.434, 83.137), 'rgb_color': (253, 43, 255), 'xy_color': (0.382, 0.159), 'friendly_name': 'Bamboo', 'supported_features': }' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG' 2023-08-02 21:50:00.562 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected an 'off' → 'on' event for 'light.bamboo' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG' 2023-08-02 21:50:00.562 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] is_proactively_adapting_context='False', context_id='01H6WVP8RJF4JZ78SFD98YA0RG' 2023-08-02 21:50:00.562 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] just_turned_off: Waiting with adjusting 'light.bamboo' for 6.072956 ``` * log instead --- custom_components/adaptive_lighting/switch.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 86f430bc..b6f8a24e 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2497,7 +2497,7 @@ class AdaptiveLightingManager: and id_off_to_on == turn_on_event.context.id ) - async def just_turned_off( # noqa: PLR0911 + async def just_turned_off( # noqa: PLR0911, PLR0912 self, entity_id: str, ) -> bool: @@ -2524,6 +2524,14 @@ class AdaptiveLightingManager: ) return False + if off_to_on_event.context.id == on_to_off_event.context.id: + _LOGGER.debug( + "just_turned_off: 'on' → 'off' state change has the same context.id as the" + " 'off' → 'on' state change for '%s'. This is probably a false positive.", + entity_id, + ) + return True + id_on_to_off = on_to_off_event.context.id turn_off_event = self.turn_off_event.get(entity_id) From e0e04da0f6ea230fa4738d24fc456696c2806a7b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 2 Aug 2023 23:11:45 -0700 Subject: [PATCH 0617/1077] Before scheduling turn_on do a last-minute check if lights are off (#671) * Before scheduling turn_on do a last-minute check if lights are off * Pass force * add comment * fix for proactive * commetn * No default for force * rm newline * no force * Return bool --- .../adaptive_lighting/adaptation_utils.py | 3 +++ custom_components/adaptive_lighting/switch.py | 23 ++++++++++++++++++- tests/test_adaptation_utils.py | 1 + tests/test_switch.py | 1 + 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py index e0ab6c47..97914327 100644 --- a/custom_components/adaptive_lighting/adaptation_utils.py +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -141,6 +141,7 @@ class AdaptationData: context: Context sleep_time: float service_call_datas: AsyncGenerator[ServiceData, None] + force: bool max_length: int which: Literal["brightness", "color", "both"] initial_sleep: bool = False @@ -179,6 +180,7 @@ def prepare_adaptation_data( service_data: ServiceData, split: bool, filter_by_state: bool, + force: bool, ) -> AdaptationData: """Prepares a data object carrying all data required to execute an adaptation.""" _LOGGER.debug( @@ -209,6 +211,7 @@ def prepare_adaptation_data( context=context, sleep_time=sleep_time, service_call_datas=service_data_iterator, + force=force, max_length=service_datas_length, which=lighting_type, ) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index b6f8a24e..641ffab6 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -537,6 +537,7 @@ async def async_setup_entry( # noqa: PLR0915 adapt_brightness=data[ATTR_ADAPT_BRIGHTNESS], adapt_color=data[ATTR_ADAPT_COLOR], prefer_rgb_color=data[CONF_PREFER_RGB_COLOR], + force=True, ) @callback @@ -1171,6 +1172,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): adapt_brightness: bool | None = None, adapt_color: bool | None = None, prefer_rgb_color: bool | None = None, + force: bool = False, context: Context | None = None, ) -> AdaptationData | None: """Prepare `AdaptationData` for adapting a light.""" @@ -1257,6 +1259,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): service_data, split=self._separate_turn_on_commands, filter_by_state=self._skip_redundant_commands, + force=force, ) async def _adapt_light( @@ -1267,6 +1270,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): adapt_brightness: bool | None = None, adapt_color: bool | None = None, prefer_rgb_color: bool | None = None, + force: bool = False, ) -> None: # This should never happen if it's been proactively adapted. # The context.parent_id is the context.id of the service call that was intercepted @@ -1283,6 +1287,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): adapt_brightness, adapt_color, prefer_rgb_color, + force, context, ) if data is None: @@ -1308,6 +1313,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # All service datas processed break + if ( + not data.force + and not is_on(self.hass, data.entity_id) + # if proactively adapting, we are sure that it came from a `light.turn_on` + and not self.manager.is_proactively_adapting(data.context.id) + ): + # Do a last-minute check if the entity is still on. + _LOGGER.debug( + "%s: Skipping adaptation of %s because it is now off", + self._name, + data.entity_id, + ) + return + _LOGGER.debug( "%s: Scheduling 'light.turn_on' with the following 'service_data': %s" " with context.id='%s'", @@ -1440,6 +1459,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._take_over_control and self._detect_non_ha_changes and not force + # Note: This call updates the state of the light + # so it might suddenly be off. and await self.manager.significant_change( self, light, @@ -1460,7 +1481,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): transition, context.id, ) - await self._adapt_light(light, context, transition) + await self._adapt_light(light, context, transition, force=force) async def _respond_to_off_to_on_event(self, entity_id: str, event: Event) -> None: assert not self.manager.is_proactively_adapting(event.context.id) diff --git a/tests/test_adaptation_utils.py b/tests/test_adaptation_utils.py index fc0df739..20db5037 100644 --- a/tests/test_adaptation_utils.py +++ b/tests/test_adaptation_utils.py @@ -312,6 +312,7 @@ async def test_prepare_adaptation_data( service_data, split, filter_by_state, + force=False, ) generated_service_datas = [item async for item in data.service_call_datas] diff --git a/tests/test_switch.py b/tests/test_switch.py index c7220ee4..4963209b 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1342,6 +1342,7 @@ async def test_cancellable_service_calls_task(hass): context, 0, _create_service_call_data_iterator(hass, [service_data], False), + force=False, max_length=1, which="both", ) From b80b253fa1b840c63415d67b8a6df107cf7bbde3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 2 Aug 2023 23:12:13 -0700 Subject: [PATCH 0618/1077] Warning when calling light.turn_on with brightness=0 (#678) * Do not update the call when calling light.turn_on with brightness=0 * Simplify * issue warning instead --- custom_components/adaptive_lighting/switch.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 641ffab6..fd0fb9d1 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1993,6 +1993,13 @@ class AdaptiveLightingManager: if self.manual_control.get(entity_id, False): return + if data.get(ATTR_BRIGHTNESS) == 0: + _LOGGER.warning( + "Turn-on call with zero brightness detected, Adaptive Lighting" + " intercepted this service_call and adjusted it. If you use this as" + " a brightness workaround, please remove it, it is no longer necessary", + ) + try: adaptive_switch = _switch_with_lights(self.hass, [entity_id]) except NoSwitchFoundError: From 4251ccacc57552279e346b32e9e6d1ce6c208c7d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 3 Aug 2023 17:46:19 -0700 Subject: [PATCH 0619/1077] Bump to 1.18.3 in manifest.json (#698) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 8d6a80b9..7806e04a 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.18.2" + "version": "1.18.3" } From 1ef7ed507ee25a70a17dc83c5d58d1360da64447 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 3 Aug 2023 17:47:09 -0700 Subject: [PATCH 0620/1077] Implement call intercept for multiple lights (#679) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement call intercept for multiple lights * remove comment * skip if no eids * add comment * fix type * Fix skipped * indentation * Add logging and fix error * Fix for HA ≤2023.04 * simplify * remove unused ignores * Debug mode * Add test * Make test failing * rename switch * rename lights * Fix tests * Rename lights in tests * Remove unused dependencies * Improve tests * More tests * Remove the DEBUG_MODE * Add doc-string * Extra test * assert * extra test * Comments * fix * fix * expand light groups * more logging * sort * Revert is_proactively_adapting checks This reverts commit 39fd8f2be0f3d6bef27705d167b5eab8113d5709. * simplify the mapping * Revert "Revert is_proactively_adapting checks" This reverts commit 18803e8e507c9b44934e15320c1bbed7fa30b788. * test * no light groups * do not expand * Do not expand_light_groups in intercept * more logging * Fix * add comment * Add multi_light_intercept config option * Update README.md, strings.json, and services.yaml * add light group * fix platform * add simple test * turn off again * Test without take over control * improve test and fix it in one way * Fixes * add cleanup fixture * format * Update test_switch.py * add __str__ * remove unneeded call * simplify service_data construction * Generalize is_our_context * Fix multi_light_intercept: false * add comments * add docs * Update README.md, strings.json, and services.yaml * Add feature line * move function --------- Co-authored-by: github-actions[bot] --- .ruff.toml | 2 +- README.md | 6 +- .../adaptive_lighting/adaptation_utils.py | 14 + custom_components/adaptive_lighting/const.py | 13 +- .../adaptive_lighting/strings.json | 3 +- custom_components/adaptive_lighting/switch.py | 352 ++++++++++++---- .../adaptive_lighting/translations/en.json | 3 +- tests/test_switch.py | 377 ++++++++++++++++-- 8 files changed, 648 insertions(+), 122 deletions(-) diff --git a/.ruff.toml b/.ruff.toml index c1b6b784..1cfeeb74 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -16,7 +16,7 @@ ignore = [ "FBT002", # Boolean default value in function definition "FIX004", # Line contains HACK, consider resolving the issue "PD901", # df is a bad variable name. Be kinder to your future self. - "PERF203",# `try`-`except` within a loop incurs performance overhead + "PERF203", # `try`-`except` within a loop incurs performance overhead "PLR0913", # Too many arguments to function call (N > 5) "PLR2004", # Magic value used in comparison, consider replacing X with a constant variable "S101", # Use of assert detected diff --git a/README.md b/README.md index 9a70b89a..d48ee10d 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ In addition to its regular mode, Adaptive Lighting also offers a "sleep mode" ## :bulb: Features +When initially turning on a light that is controlled by Adaptive Lighting, the `light.turn_on` service call is intercepted, and the light's brightness and color are automatically adjusted based on the sun's position. +After that, the light's brightness and color are automatically adjusted at a regular interval. + Adaptive Lighting provides four switches (using "living_room" as an example component name): - `switch.adaptive_lighting_living_room`: Turn Adaptive Lighting on or off and view current light settings through its attributes. @@ -124,7 +127,8 @@ The YAML and frontend configuration methods support all of the options listed be | `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | | `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | | `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | -| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | +| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | +| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. | `True` | `bool` | diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py index 97914327..593a746b 100644 --- a/custom_components/adaptive_lighting/adaptation_utils.py +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -150,6 +150,20 @@ class AdaptationData: """Return data for the next service call, or none if no more data exists.""" return await anext(self.service_call_datas, None) + def __str__(self) -> str: + """Return a string representation of the data.""" + return ( + f"{self.__class__.__name__}(" + f"entity_id={self.entity_id}, " + f"context_id={self.context.id}, " + f"sleep_time={self.sleep_time}, " + f"force={self.force}, " + f"max_length={self.max_length}, " + f"which={self.which}, " + f"initial_sleep={self.initial_sleep}" + ")" + ) + class NoColorOrBrightnessInServiceDataError(Exception): """Exception raised when no color or brightness attributes are found in service data.""" diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 94e9a50b..d2b1dc1e 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -186,10 +186,20 @@ CONF_SKIP_REDUNDANT_COMMANDS, DEFAULT_SKIP_REDUNDANT_COMMANDS = ( DOCS[CONF_SKIP_REDUNDANT_COMMANDS] = ( "Skip sending adaptation commands whose target state already " "equals the light's known state. Minimizes network traffic and improves the " - "adaptation responsivity in some situations. " + "adaptation responsivity in some situations. 📉" "Disable if physical light states get out of sync with HA's recorded state." ) +CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT = ( + "multi_light_intercept", + True, +) +DOCS[CONF_MULTI_LIGHT_INTERCEPT] = ( + "Intercept and adapt `light.turn_on` calls that target multiple lights. ➗" + "⚠️ This might result in splitting up a single `light.turn_on` call " + "into multiple calls, e.g., when lights are in different switches." +) + SLEEP_MODE_SWITCH = "sleep_mode_switch" ADAPT_COLOR_SWITCH = "adapt_color_switch" ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch" @@ -290,6 +300,7 @@ VALIDATION_TUPLES = [ DEFAULT_SKIP_REDUNDANT_COMMANDS, bool, ), + (CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT, bool), ] diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index b6d62b0b..f60297c5 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -48,7 +48,8 @@ "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", - "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state." + "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.", + "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches." } } }, diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index fd0fb9d1..defe3b85 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -20,6 +20,7 @@ import ulid_transform import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_FLASH, @@ -119,6 +120,7 @@ from .const import ( CONF_MIN_BRIGHTNESS, CONF_MIN_COLOR_TEMP, CONF_MIN_SUNSET_TIME, + CONF_MULTI_LIGHT_INTERCEPT, CONF_ONLY_ONCE, CONF_PREFER_RGB_COLOR, CONF_SEND_SPLIT_DELAY, @@ -265,30 +267,39 @@ def create_context( return Context(id=context_id, parent_id=parent_id) -def is_our_context_id(context_id: str | None) -> bool: +def is_our_context_id(context_id: str | None, which: str | None = None) -> bool: """Check whether this integration created 'context_id'.""" if context_id is None: return False - return f":{_DOMAIN_SHORT}:" in context_id + + is_al = f":{_DOMAIN_SHORT}:" in context_id + if not is_al: + return False + if which is None: + return True + return f":{_remove_vowels(which)}:" in context_id -def is_our_context(context: Context | None) -> bool: +def is_our_context(context: Context | None, which: str | None = None) -> bool: """Check whether this integration created 'context'.""" if context is None: return False - return is_our_context_id(context.id) + return is_our_context_id(context.id, which) @bind_hass def _switches_with_lights( hass: HomeAssistant, lights: list[str], + expand_light_groups: bool = True, ) -> list[AdaptiveSwitch]: """Get all switches that control at least one of the lights passed.""" config_entries = hass.config_entries.async_entries(DOMAIN) data = hass.data[DOMAIN] switches = [] - all_check_lights = _expand_light_groups(hass, lights) + all_check_lights = ( + _expand_light_groups(hass, lights) if expand_light_groups else set(lights) + ) for config in config_entries: entry = data.get(config.entry_id) if entry is None: # entry might be disabled and therefore missing @@ -309,9 +320,10 @@ class NoSwitchFoundError(ValueError): def _switch_with_lights( hass: HomeAssistant, lights: list[str], + expand_light_groups: bool = True, ) -> AdaptiveSwitch: """Find the switch that controls the lights in 'lights'.""" - switches = _switches_with_lights(hass, lights) + switches = _switches_with_lights(hass, lights, expand_light_groups) if len(switches) == 1: return switches[0] if len(switches) > 1: @@ -643,7 +655,10 @@ def _is_state_event(event: Event, from_or_to_state: Iterable[str]): @bind_hass -def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]: +def _expand_light_groups( + hass: HomeAssistant, + lights: list[str], +) -> list[str]: all_lights = set() manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER] for light in lights: @@ -651,14 +666,18 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]: if state is None: _LOGGER.debug("State of %s is None", light) all_lights.add(light) - elif "entity_id" in state.attributes: # it's a light group + elif _is_light_group(state): group = state.attributes["entity_id"] manager.lights.discard(light) all_lights.update(group) _LOGGER.debug("Expanded %s to %s", light, group) else: all_lights.add(light) - return list(all_lights) + return sorted(all_lights) + + +def _is_light_group(state: State) -> bool: + return "entity_id" in state.attributes @bind_hass @@ -927,6 +946,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._take_over_control = True self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL] self._skip_redundant_commands = data[CONF_SKIP_REDUNDANT_COMMANDS] + self._multi_light_intercept = data[CONF_MULTI_LIGHT_INTERCEPT] self._expand_light_groups() # updates manual control timers location, _ = get_astral_location(self.hass) @@ -1272,11 +1292,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): prefer_rgb_color: bool | None = None, force: bool = False, ) -> None: - # This should never happen if it's been proactively adapted. - # The context.parent_id is the context.id of the service call that was intercepted - # and context.id here is from the resulting "light_event" event. - assert not self.manager.is_proactively_adapting(context.parent_id) - if (lock := self.manager.turn_off_locks.get(light)) and lock.locked(): _LOGGER.debug("%s: '%s' is locked", self._name, light) return @@ -1874,6 +1889,9 @@ class AdaptiveLightingManager: # Track light transitions self.transition_timers: dict[str, _AsyncSingleShotTimer] = {} + # Track _execute_cancellable_adaptation_calls tasks + self.adaptation_tasks = set() + # Setup listeners and its callbacks to remove them later self.listener_removers = [ self.hass.bus.async_listen( @@ -1958,65 +1976,235 @@ class AdaptiveLightingManager: for key in keys: self._proactively_adapting_contexts.pop(key) - async def _service_interceptor_turn_on_handler( # noqa: PLR0911 + async def _service_interceptor_turn_on_handler( # noqa: PLR0912, PLR0915 self, call: ServiceCall, data: ServiceData, - ): - # Don't adapt our own service calls - if is_our_context(call.context): + ) -> None: + """Intercept `light.turn_on` and `light.toggle` service calls and adapt them. + + It is possible that the calls are made for multiple lights at once, + which in turn might be in different switches or no switches at all. + If there are lights that are not all in a single switch, we need to + make multiple calls to `light.turn_on` with the correct entity IDs. + One of these calls can be intercepted and adapted, the others need to + be adapted by calling `_adapt_light` with the correct entity IDs or + by calling `light.turn_on` directly. + + We create a mapping from switch to entity IDs and keep a list + of skipped lights which are lights in no switches or in switches that + are off or lights that are already on. + + If there is only one switch and 0 skipped lights, we just intercept the + call directly. + + If there are multiple switches and skipped lights, we can adapt the call + for one of the switches to include only the lights in that switch and + need to call `_adapt_light` for the other switches with their + entity_ids. For skipped lights, we call light.turn_on directly with the + entity_ids and original service data. + + If there are only skipped lights, we can use the intercepted call + directly. + """ + is_skipped_hash = is_our_context(call.context, "skipped") + _LOGGER.debug( + "(0) _service_interceptor_turn_on_handler: call.context.id='%s', is_skipped_hash='%s'", + call.context.id, + is_skipped_hash, + ) + if is_our_context(call.context) and not is_skipped_hash: + # Don't adapt our own service calls, but do re-adapt calls that + # were skipped by us return if ATTR_EFFECT in data[CONF_PARAMS] or ATTR_FLASH in data[CONF_PARAMS]: return + _LOGGER.debug( + "(1) _service_interceptor_turn_on_handler: call='%s', data='%s'", + call, + data, + ) + entity_ids = self._get_entity_list(data) + # Note: we do not expand light groups anywhere in this method, instead + # we skip them and rely on the followup call that HA will make + # with the expanded entity IDs. - # For simplicity, only service calls affecting a single entity are currently handled. - # - # To add support for adapting multiple entities, the following properties - # need to hold for _all_ entities: - # - managed by this AL instance - # - not manually controlled - # - supporting the same relevant feature set - # - off state - if len(entity_ids) != 1: - return + # Create a mapping from switch to entity IDs + # AdaptiveSwitch.name → entity_ids mapping + switch_to_eids: dict[str, list[str]] = {} + # AdaptiveSwitch.name → AdaptiveSwitch mapping + switch_name_mapping: dict[str, AdaptiveSwitch] = {} + # Note: In HA≥2023.5, AdaptiveSwitch is hashable, so we can + # use dict[AdaptiveSwitch, list[str]] + skipped: list[str] = [] + for entity_id in entity_ids: + try: + switch = _switch_with_lights( + self.hass, + [entity_id], + # Do not expand light groups, because HA will make a separate light.turn_on + # call where the lights are expanded, and that call will be intercepted. + expand_light_groups=False, + ) + except NoSwitchFoundError: + # Needs to make the original call but without adaptation + skipped.append(entity_id) + _LOGGER.debug( + "No switch found for entity_id='%s', skipped='%s'", + entity_id, + skipped, + ) + else: + if ( + not switch.is_on + # Never adapt on light groups, because HA will make a separate light.turn_on + or _is_light_group(self.hass.states.get(entity_id)) + # Prevent adaptation of TURN_ON calls when light is already on, + # and of TOGGLE calls when toggling off. + or self.hass.states.is_state(entity_id, STATE_ON) + or self.manual_control.get(entity_id, False) + ): + _LOGGER.debug( + "Switch is off or light is already on for entity_id='%s', skipped='%s'" + " (is_on='%s', is_state='%s', manual_control='%s')", + entity_id, + skipped, + switch.is_on, + self.hass.states.is_state(entity_id, STATE_ON), + self.manual_control.get(entity_id, False), + ) + skipped.append(entity_id) + else: + switch_to_eids.setdefault(switch.name, []).append(entity_id) + switch_name_mapping[switch.name] = switch - entity_id = entity_ids[0] - - # Prevent adaptation of TURN_ON calls when light is already on, - # and of TOGGLE calls when toggling off. - if self.hass.states.is_state(entity_id, STATE_ON): - return - - if self.manual_control.get(entity_id, False): - return - - if data.get(ATTR_BRIGHTNESS) == 0: + # Check for `multi_light_intercept: true/false` + mli = [sw._multi_light_intercept for sw in switch_name_mapping.values()] + more_than_one_switch = len(switch_to_eids) > 1 + single_switch_with_multiple_lights = ( + len(switch_to_eids) == 1 and len(next(iter(switch_to_eids.values()))) > 1 + ) + switch_without_multi_light_intercept = not all(mli) + if more_than_one_switch and switch_without_multi_light_intercept: _LOGGER.warning( - "Turn-on call with zero brightness detected, Adaptive Lighting" - " intercepted this service_call and adjusted it. If you use this as" - " a brightness workaround, please remove it, it is no longer necessary", + "Multiple switches (%s) targeted, but not all have" + " `multi_light_intercept: true`, so skipping intercept" + " for all lights.", + switch_to_eids, ) + skipped = entity_ids + switch_to_eids = {} + elif ( + single_switch_with_multiple_lights and switch_without_multi_light_intercept + ): + _LOGGER.warning( + "Single switch with multiple lights targeted, but" + " `multi_light_intercept: true` is not set, so skipping intercept" + " for all lights.", + switch_to_eids, + ) + skipped = entity_ids + switch_to_eids = {} - try: - adaptive_switch = _switch_with_lights(self.hass, [entity_id]) - except NoSwitchFoundError: - # This might be a light that is not managed by this AL instance. + _LOGGER.debug( + "(2) _service_interceptor_turn_on_handler: switch_to_eids='%s', skipped='%s'", + switch_to_eids, + skipped, + ) + + def modify_service_data(service_data, entity_ids): + """Modify the service data to contain the entity IDs.""" + service_data.pop(ATTR_ENTITY_ID, None) + service_data.pop(ATTR_AREA_ID, None) + service_data[ATTR_ENTITY_ID] = entity_ids + return service_data + + # Intercept the call for first switch and call _adapt_light for the rest + has_intercepted = False # Can only intercept a turn_on call once + for adaptive_switch_name, _entity_ids in switch_to_eids.items(): + switch = switch_name_mapping[adaptive_switch_name] + transition = data[CONF_PARAMS].get( + ATTR_TRANSITION, + switch.initial_transition, + ) + if not has_intercepted: + _LOGGER.debug( + "(3) _service_interceptor_turn_on_handler: intercepting entity_ids='%s'", + _entity_ids, + ) + await self._service_interceptor_turn_on_single_light_handler( + entity_ids=_entity_ids, + switch=switch, + transition=transition, + call=call, + data=modify_service_data(data, _entity_ids), + ) + has_intercepted = True + continue + + for eid in _entity_ids: + # Must add a new context otherwise _adapt_light will bail out + context = switch.create_context("intercept") + self.clear_proactively_adapting(eid) + self.set_proactively_adapting(context.id, eid) + _LOGGER.debug( + "(4) _service_interceptor_turn_on_handler: calling `_adapt_light` with eid='%s', context='%s', transition='%s'", + eid, + context, + transition, + ) + await switch._adapt_light( + light=eid, + context=context, + transition=transition, + ) + + # Call light.turn_on service for skipped entities + if skipped: + if not has_intercepted: + assert set(skipped) == set(entity_ids) + return # The call will be intercepted with the original data + # Call light turn_on service for skipped entities + context = switch.create_context("skipped") _LOGGER.debug( - "No (or multiple) adaptive switch(es) found for entity %s," - " skipping adaptation by intercepting service call", - entity_id, + "(5) _service_interceptor_turn_on_handler: calling `light.turn_on` with skipped='%s', data: '%s', context='%s'", + skipped, + data, + context.id, + ) + # Need to expand light groups here because otherwise this interceptor loop will happen twice more + _LOGGER.debug( + "(6) _service_interceptor_turn_on_handler: calling `light.turn_on` with skipped='%s', data: '%s', context='%s'", + skipped, + data, + context.id, + ) + service_data = {ATTR_ENTITY_ID: skipped, **data[CONF_PARAMS]} + if ( + ATTR_COLOR_TEMP in service_data + and ATTR_COLOR_TEMP_KELVIN in service_data + ): + # ATTR_COLOR_TEMP and ATTR_COLOR_TEMP_KELVIN are mutually exclusive + del service_data[ATTR_COLOR_TEMP] + await self.hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + service_data, + blocking=True, + context=context, ) - return - - if not adaptive_switch.is_on: - return - - if entity_id not in adaptive_switch.lights: - return + async def _service_interceptor_turn_on_single_light_handler( + self, + entity_ids: list[str], + switch: AdaptiveSwitch, + transition: int, + call: ServiceCall, + data: ServiceData, + ): _LOGGER.debug( "Intercepted TURN_ON call with data %s (%s)", data, @@ -2024,18 +2212,13 @@ class AdaptiveLightingManager: ) # Reset because turning on the light, this also happens in - # `turn_on_off_event_listener`, however, this function is called + # `state_changed_event_listener`, however, this function is called # before that one. - self.reset(entity_id, reset_manual_control=False) + self.reset(*entity_ids, reset_manual_control=False) + for entity_id in entity_ids: + self.clear_proactively_adapting(entity_id) - self.clear_proactively_adapting(entity_id) - - transition = data[CONF_PARAMS].get( - ATTR_TRANSITION, - adaptive_switch.initial_transition, - ) - - adaptation_data = await adaptive_switch.prepare_adaptation_data( + adaptation_data = await switch.prepare_adaptation_data( entity_id, transition, ) @@ -2063,12 +2246,21 @@ class AdaptiveLightingManager: # We cannot know here whether there is another call to follow (since the # state can change until the next call), so we just schedule it and let # it sort out by itself. - self.set_proactively_adapting(call.context.id, entity_id) - self.set_proactively_adapting(adaptation_data.context.id, entity_id) + for entity_id in entity_ids: + self.set_proactively_adapting(call.context.id, entity_id) + self.set_proactively_adapting(adaptation_data.context.id, entity_id) adaptation_data.initial_sleep = True - _ = asyncio.create_task( # Don't await to avoid blocking the service call - adaptive_switch.execute_cancellable_adaptation_calls(adaptation_data), + + # Don't await to avoid blocking the service call. + # Assign to a variable only to await in tests. + self.adaptation_tasks.add( + asyncio.create_task( + switch.execute_cancellable_adaptation_calls(adaptation_data), + ), ) + # Remove tasks that are done + if done_tasks := [t for t in self.adaptation_tasks if t.done()]: + self.adaptation_tasks.difference_update(done_tasks) def _handle_timer( self, @@ -2092,14 +2284,22 @@ class AdaptiveLightingManager: def start_transition_timer(self, light: str) -> None: """Mark a light as manually controlled.""" - last_service_data = self.last_service_data[light] - last_transition = last_service_data.get(ATTR_TRANSITION) - if not last_transition: + last_service_data = self.last_service_data.get(light) + if last_service_data is None: _LOGGER.debug( - "No transition in last adapt for light %s, continuing...", + "No last service data for light %s, not starting timer.", light, ) return + + last_transition = last_service_data.get(ATTR_TRANSITION) + if not last_transition: + _LOGGER.debug( + "No transition in last adapt for light %s, not starting timer.", + light, + ) + return + _LOGGER.debug( "Start transition timer of %s seconds for light %s", last_transition, @@ -2198,8 +2398,7 @@ class AdaptiveLightingManager: for light in lights: if reset_manual_control: self.manual_control[light] = False - timer = self.auto_reset_manual_control_timers.pop(light, None) - if timer is not None: + if timer := self.auto_reset_manual_control_timers.pop(light, None): timer.cancel() self.our_last_state_on_change.pop(light, None) self.last_service_data.pop(light, None) @@ -2380,6 +2579,7 @@ class AdaptiveLightingManager: entity_id, event.context.id, ) + # Note: the reset below already happened in `_service_interceptor_turn_on_handler` return self.reset(entity_id, reset_manual_control=False) diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 358f7cbe..7e58be17 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -49,7 +49,8 @@ "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", - "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state." + "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.", + "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches." } } }, diff --git a/tests/test_switch.py b/tests/test_switch.py index 4963209b..e3bc9126 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1,6 +1,7 @@ """Tests for Adaptive Lighting switches.""" # pylint: disable=protected-access import asyncio +import itertools from copy import deepcopy import datetime import logging @@ -55,6 +56,7 @@ from custom_components.adaptive_lighting.adaptation_utils import ( from custom_components.adaptive_lighting.const import ( ADAPT_BRIGHTNESS_SWITCH, ADAPT_COLOR_SWITCH, + CONF_TAKE_OVER_CONTROL, ATTR_ADAPTIVE_LIGHTING_MANAGER, CONF_ADAPT_UNTIL_SLEEP, CONF_AUTORESET_CONTROL, @@ -64,6 +66,7 @@ from custom_components.adaptive_lighting.const import ( CONF_MAX_BRIGHTNESS, CONF_MIN_COLOR_TEMP, CONF_PREFER_RGB_COLOR, + CONF_MULTI_LIGHT_INTERCEPT, CONF_SEPARATE_TURN_ON_COMMANDS, CONF_SLEEP_RGB_OR_COLOR_TEMP, CONF_SUNRISE_OFFSET, @@ -90,6 +93,7 @@ from custom_components.adaptive_lighting.switch import ( _attributes_have_changed, color_difference_redmean, create_context, + AdaptiveLightingManager, is_our_context, is_our_context_id, lerp_color_hsv, @@ -141,6 +145,18 @@ def reset_time_zone(): dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE +@pytest.fixture +async def cleanup(hass): + yield + manager: AdaptiveLightingManager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER] + for timer in manager.auto_reset_manual_control_timers.values(): + timer.cancel() + for timer in manager.transition_timers.values(): + timer.cancel() + for task in manager.adaptation_tasks: + task.cancel() + + async def setup_switch(hass, extra_data) -> tuple[MockConfigEntry, AdaptiveSwitch]: """Create the switch entry.""" entry = MockConfigEntry( @@ -159,51 +175,46 @@ async def setup_switch(hass, extra_data) -> tuple[MockConfigEntry, AdaptiveSwitc return entry, switch -async def setup_lights(hass: HomeAssistant): +async def setup_lights(hass: HomeAssistant, with_group: bool = False): """Set up 3 light entities using the 'template' platform.""" + n = 3 if not with_group else 5 # last 2 will be put in a group + template_lights = { + f"light_{i}": { + "unique_id": f"light_{i}", + "friendly_name": f"light_{i}", + "turn_on": None, + "turn_off": None, + "set_level": None, + "set_temperature": None, + "set_color": None, + } + for i in range(1, n + 1) + } + template_lights["light_3"]["supports_transition_template"] = True + platforms = [{"platform": "template", "lights": template_lights}] + + if with_group: + platforms.append( + { + "platform": "group", + "entities": ["light.light_4", "light.light_5"], + "name": "Light Group", + "unique_id": "light_group", + "all": "false", + } + ) + await async_setup_component( hass, LIGHT_DOMAIN, - { - LIGHT_DOMAIN: [ - { - "platform": "template", - "lights": { - "light_1": { - "friendly_name": "light_1", - "unique_id": "light_1", - "turn_on": None, - "turn_off": None, - "set_level": None, - "set_temperature": None, - "set_color": None, - }, - "light_2": { - "friendly_name": "light_2", - "unique_id": "light_2", - "turn_on": None, - "turn_off": None, - "set_level": None, - "set_temperature": None, - "set_color": None, - }, - "light_3": { - "friendly_name": "light_3", - "unique_id": "light_3", - "turn_on": None, - "turn_off": None, - "set_level": None, - "set_temperature": None, - "set_color": None, - "supports_transition_template": True, - }, - }, - }, - ] - }, + {LIGHT_DOMAIN: platforms}, ) - await hass.async_block_till_done() + + if with_group: + state = hass.states.get("light.light_group") + assert state.attributes["entity_id"] == ["light.light_4", "light.light_5"] + platform = async_get_platforms(hass, "template") lights = list(platform[0].entities.values()) @@ -1374,13 +1385,15 @@ async def test_service_calls_task_cancellation(hass): async def _turn_on_and_track_event_contexts( - hass: HomeAssistant, context_id: str, entity_id + hass: HomeAssistant, context_id: str, entity_id, return_full_events: bool = False ): context = Context(id=context_id) event_context_ids = [] + events = [] async def turn_on_off_event_listener(event: Event) -> None: event_context_ids.append(event.context.id) + events.append(event) hass.bus.async_listen(EVENT_CALL_SERVICE, turn_on_off_event_listener) @@ -1392,7 +1405,8 @@ async def _turn_on_and_track_event_contexts( context=context, ) await hass.async_block_till_done() - + if return_full_events: + return events return event_context_ids @@ -1540,6 +1554,148 @@ async def test_proactive_adaptation_transition_override(hass): switch.manager.cancel_ongoing_adaptation_calls(ENTITY_LIGHT_3) +async def setup_proactive_multiple_lights_two_switches(hass): + lights_instances = await setup_lights(hass) + # Setup switches + lights = [ + ENTITY_LIGHT_1, + ENTITY_LIGHT_2, + ENTITY_LIGHT_3, + ] + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: lights}, + blocking=True, + ) + defaults = { + CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour), + CONF_SUNSET_TIME: datetime.time(SUNSET.hour), + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_PREFER_RGB_COLOR: False, + CONF_MIN_COLOR_TEMP: 2500, # to not coincide with sleep_color_temp} + INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True, + } + _, switch1 = await setup_switch( + hass, {CONF_NAME: "switch1", CONF_LIGHTS: [ENTITY_LIGHT_1], **defaults} + ) + _, switch2 = await setup_switch( + hass, {CONF_NAME: "switch2", CONF_LIGHTS: [ENTITY_LIGHT_2], **defaults} + ) + assert hass.states.get(switch1.entity_id).state == STATE_ON + assert hass.states.get(switch2.entity_id).state == STATE_ON + assert all(hass.states.get(light).state == STATE_OFF for light in lights) + return lights, switch1, switch2 + + +async def test_proactive_multiple_lights_all_at_once(hass): + """Create switch and demo lights.""" + lights, switch1, switch2 = await setup_proactive_multiple_lights_two_switches(hass) + _LOGGER.debug("Start test_proactive_multiple_lights_all_at_once") + # Setup demo lights and turn on + events = await _turn_on_and_track_event_contexts( + hass, "test1", lights, return_full_events=True + ) + assert len(events) == 3, events + + # Original turn_on call that is intercepted + assert events[0].context.id == "test1" + assert events[0].data["service_data"][ATTR_ENTITY_ID] == lights + + # The `has_intercepted` path + assert events[1].data["service_data"][ATTR_ENTITY_ID] == ENTITY_LIGHT_2 + assert ":ntrc:" in events[1].context.id + + # The skipped lights, the one not in a switch + assert events[2].data["service_data"][ATTR_ENTITY_ID] == [ENTITY_LIGHT_3] + assert ":skpp:" in events[2].context.id + + assert switch1.manager.is_proactively_adapting("test1") + assert switch2.manager.is_proactively_adapting("test1") + + await hass.async_block_till_done() + + assert all(hass.states.get(light).state == STATE_ON for light in lights) + + # Turn on second time even though already on + events = await _turn_on_and_track_event_contexts( + hass, "test2", lights, return_full_events=True + ) + assert len(events) == 1, events + assert events[0].context.id == "test2" + + +async def test_proactive_multiple_lights_turn_on_non_managed_light(hass): + """Create switch and demo lights.""" + lights, switch1, switch2 = await setup_proactive_multiple_lights_two_switches(hass) + turn_ons = await _turn_on_and_track_event_contexts(hass, "test1", lights) + assert len(turn_ons) == 3, turn_ons + await hass.async_block_till_done() + assert all(hass.states.get(light).state == STATE_ON for light in lights) + + # Turn off ENTITY_LIGHT_3 (which is not in a switch), leaving the other two on + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_3}, + blocking=True, + context=Context(id="test2"), + ) + + # Now turn on all lights again, which means the code gets to "if skipped: if not has_intercepted:" + turn_ons = await _turn_on_and_track_event_contexts(hass, "test2", ENTITY_LIGHT_3) + assert len(turn_ons) == 1, turn_ons + + +async def test_proactive_multiple_lights_turn_on_managed_lights_only(hass): + """Create switch and demo lights.""" + lights, switch1, switch2 = await setup_proactive_multiple_lights_two_switches(hass) + _LOGGER.debug("Start test_proactive_multiple_lights_all_at_once") + # Setup demo lights and turn on + events = await _turn_on_and_track_event_contexts( + hass, "test1", lights[:-1], return_full_events=True + ) + assert len(events) == 2, events + + # Original turn_on call that is intercepted + assert events[0].context.id == "test1" + assert events[0].data["service_data"][ATTR_ENTITY_ID] == lights[:-1] + + # The `has_intercepted` path + assert events[1].data["service_data"][ATTR_ENTITY_ID] == ENTITY_LIGHT_2 + assert ":ntrc:" in events[1].context.id + assert ATTR_BRIGHTNESS in events[1].data["service_data"] + + +async def test_proactive_multiple_lights_one_switch_and_one_skipped(hass): + """Create switch and demo lights.""" + lights, switch1, switch2 = await setup_proactive_multiple_lights_two_switches(hass) + two_lights = [lights[0], lights[-1]] + _LOGGER.debug("Start test_proactive_multiple_lights_all_at_once") + # Setup demo lights and turn on + events = await _turn_on_and_track_event_contexts( + hass, "test1", two_lights, return_full_events=True + ) + assert len(events) == 2, events + + # Original turn_on call that is intercepted + assert events[0].context.id == "test1" + assert events[0].data["service_data"][ATTR_ENTITY_ID] == two_lights + + # The skipped lights, the one not in a switch + assert events[1].data["service_data"][ATTR_ENTITY_ID] == [ENTITY_LIGHT_3] + assert ":skpp:" in events[1].context.id + + assert switch1.manager.is_proactively_adapting("test1") + assert switch2.manager.is_proactively_adapting("test1") + + await hass.async_block_till_done() + + assert all(hass.states.get(light).state == STATE_ON for light in two_lights) + + async def test_two_switches_for_single_light(hass): """Test the case where someone has two switches for a single light. @@ -1697,3 +1853,142 @@ def test_lerp_color_hsv(): with pytest.raises(AssertionError): lerp_color_hsv((255, 0, 0), (0, 255, 0), 1.1) + + +@pytest.mark.parametrize("proactive_service_call_adaptation", [True, False]) +@pytest.mark.parametrize("take_over_control", [True, False]) +@pytest.mark.parametrize("multi_light_intercept", [True, False]) +async def test_light_group( + hass, + proactive_service_call_adaptation, + take_over_control, + multi_light_intercept, + cleanup, +): + lights = await setup_lights(hass, with_group=True) + all_entity_ids = [light.entity_id for light in lights] + entity_ids = all_entity_ids[:3] # the last two are in the group + entity_ids.append("light.light_group") + _, switch = await setup_switch( + hass, + { + CONF_LIGHTS: entity_ids, + INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: proactive_service_call_adaptation, + CONF_TAKE_OVER_CONTROL: take_over_control, + CONF_MULTI_LIGHT_INTERCEPT: multi_light_intercept, + }, + ) + await hass.async_block_till_done() + assert switch.is_on + assert all(eid in switch.lights for eid in all_entity_ids) + + # Set the brightness of the group twice, once to turn it on and once to + # trigger manual control + for _ in range(2): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.light_group", ATTR_BRIGHTNESS_PCT: 50}, + blocking=True, + ) + await hass.async_block_till_done() + + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test") + ) + await hass.async_block_till_done() + + if take_over_control: + assert switch.manager.manual_control["light.light_4"] + assert switch.manager.manual_control["light.light_5"] + else: + assert not switch.manager.manual_control["light.light_4"] + assert not switch.manager.manual_control["light.light_5"] + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.light_group"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert not switch.manager.manual_control["light.light_4"] + assert not switch.manager.manual_control["light.light_5"] + events = await _turn_on_and_track_event_contexts( + hass, "testing", "light.light_group", return_full_events=True + ) + if proactive_service_call_adaptation and multi_light_intercept: + await asyncio.gather(*switch.manager.adaptation_tasks) + # Both lights should be adapted via interception, so with the original context + # [ + # "testing", # original call light 4 + # "testing", # original call light 5 + # ] + + assert events[0].data["service_data"][ATTR_ENTITY_ID] == "light.light_group" + assert events[0].context.id == "testing" + assert events[1].data["service_data"][ATTR_ENTITY_ID] == [ + "light.light_4", + "light.light_5", + ] + assert events[1].context.id == "testing" + else: + assert events[0].data["service_data"][ATTR_ENTITY_ID] == "light.light_group" + assert events[0].context.id == "testing" + assert events[1].data["service_data"][ATTR_ENTITY_ID] == [ + "light.light_4", + "light.light_5", + ] + assert events[1].context.id == "testing" + e1 = events[2].data["service_data"][ATTR_ENTITY_ID] + e2 = events[3].data["service_data"][ATTR_ENTITY_ID] + assert ( + e1 == "light.light_4" + and e2 == "light.light_5" + or e1 == "light.light_5" + and e2 == "light.light_4" + ) + assert ":lght:" in events[2].context.id + assert ":lght:" in events[3].context.id + assert len(events) == 4 + assert not switch.manager.is_proactively_adapting(events[0].context.id) + assert not switch.manager.is_proactively_adapting(events[1].context.id) + + # Turn off all lights, and then turn on all lights + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: all_entity_ids}, + blocking=True, + ) + await hass.async_block_till_done() + + # This turns on light_1, light_2, light_3, light_group (which is light_4 and light_5) + # This should result in the intercepted adaptation of light_1, light_2, light_3 + # and skip the light_group first. Then on a second light.turn_on where the + # light_group is expanded, with a :skpp: context_id, this goes trhough another iteration, + # and then the light_group is adapted. + events = await _turn_on_and_track_event_contexts( + hass, "testing", entity_ids, return_full_events=True + ) + if proactive_service_call_adaptation and multi_light_intercept: + await asyncio.gather(*switch.manager.adaptation_tasks) + # Original call + assert events[0].data["service_data"][ATTR_ENTITY_ID] == [ + "light.light_1", + "light.light_2", + "light.light_3", + "light.light_group", + ] + assert events[0].context.id == "testing" + # Skipped call with light_group + assert events[1].data["service_data"][ATTR_ENTITY_ID] == ["light.light_group"] + assert ":skpp:" in events[1].context.id + # HA automatically forwarded call with light_group expanded with same context + assert events[2].data["service_data"][ATTR_ENTITY_ID] == [ + "light.light_4", + "light.light_5", + ] + assert ":skpp:" in events[2].context.id + assert len(events) == 3 From 9a7cff9d6aca269c1db14307684547708bdc3eab Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 4 Aug 2023 14:45:12 -0700 Subject: [PATCH 0621/1077] feat: change the order of options in config flow (#700) * Change the order of options in config flow * Update README.md, strings.json, and services.yaml --------- Co-authored-by: github-actions[bot] --- README.md | 20 ++++++------- custom_components/adaptive_lighting/const.py | 29 +++++++++---------- .../adaptive_lighting/services.yaml | 4 +-- .../adaptive_lighting/strings.json | 26 ++++++++--------- .../adaptive_lighting/translations/en.json | 26 ++++++++--------- 5 files changed, 52 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index d48ee10d..963c4e9a 100644 --- a/README.md +++ b/README.md @@ -99,36 +99,36 @@ The YAML and frontend configuration methods support all of the options listed be | Variable name | Description | Default | Type | |:-------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------| | `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | -| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | -| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | -| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | -| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | -| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | -| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | | `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | | `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | | `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | | `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | | `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | | `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | | `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | | `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | | `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | | `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | -| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | | `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | | `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | -| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | | `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | | `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | | `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | | `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | | `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | | `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | -| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | | `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | | `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. | `True` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index d2b1dc1e..f204e1ac 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -125,7 +125,7 @@ DOCS[CONF_SUNRISE_TIME] = "Set a fixed time (HH:MM:SS) for sunrise. 🌅" CONF_MAX_SUNRISE_TIME = "max_sunrise_time" DOCS[CONF_MAX_SUNRISE_TIME] = ( "Set the latest virtual sunrise time (HH:MM:SS), allowing" - " for earlier real sunrises. 🌅" + " for earlier sunrises. 🌅" ) CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 @@ -137,10 +137,9 @@ CONF_SUNSET_TIME = "sunset_time" DOCS[CONF_SUNSET_TIME] = "Set a fixed time (HH:MM:SS) for sunset. 🌇" CONF_MIN_SUNSET_TIME = "min_sunset_time" -DOCS[CONF_MIN_SUNSET_TIME] = ( - "Set the earliest virtual sunset time (HH:MM:SS), allowing" - " for later real sunsets. 🌇" -) +DOCS[ + CONF_MIN_SUNSET_TIME +] = "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇" CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True DOCS[CONF_TAKE_OVER_CONTROL] = ( @@ -249,17 +248,14 @@ def int_between(min_int, max_int): VALIDATION_TUPLES = [ (CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), - (CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool), - (CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES, bool), - (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), - (CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION, VALID_TRANSITION), - (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), - (CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP, bool), (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), + (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), + (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), (CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS, int_between(1, 100)), (CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)), (CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP, int_between(1000, 10000)), (CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP, int_between(1000, 10000)), + (CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool), (CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)), ( CONF_SLEEP_RGB_OR_COLOR_TEMP, @@ -278,29 +274,32 @@ VALIDATION_TUPLES = [ DEFAULT_SLEEP_RGB_COLOR, selector.ColorRGBSelector(selector.ColorRGBSelectorConfig()), ), + (CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION, VALID_TRANSITION), + (CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP, bool), (CONF_SUNRISE_TIME, NONE_STR, str), (CONF_MAX_SUNRISE_TIME, NONE_STR, str), (CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int), (CONF_SUNSET_TIME, NONE_STR, str), (CONF_MIN_SUNSET_TIME, NONE_STR, str), (CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int), - (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), (CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool), (CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool), - (CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool), - (CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY, int_between(0, 10000)), - (CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, cv.positive_float), ( CONF_AUTORESET_CONTROL, DEFAULT_AUTORESET_CONTROL, int_between(0, 365 * 24 * 60 * 60), # 1 year max ), + (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), + (CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool), + (CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY, int_between(0, 10000)), + (CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, cv.positive_float), ( CONF_SKIP_REDUNDANT_COMMANDS, DEFAULT_SKIP_REDUNDANT_COMMANDS, bool, ), (CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT, bool), + (CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES, bool), ] diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 5b857b32..4b79f88c 100644 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -208,13 +208,13 @@ change_switch_settings: selector: time: null max_sunrise_time: - description: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 + description: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 example: '' required: false selector: time: null min_sunset_time: - description: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 + description: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 example: '' required: false selector: diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index f60297c5..a942e66b 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -20,36 +20,36 @@ "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", "data": { "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟", - "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", - "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝", - "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", - "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴", - "transition": "transition: Duration of transition when lights change, in seconds. 🕑", - "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", "interval": "interval: Frequency to adapt the lights, in seconds. 🔄", + "transition": "transition: Duration of transition when lights change, in seconds. 🕑", + "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", "min_brightness": "min_brightness: Minimum brightness percentage. 💡", "max_brightness": "max_brightness: Maximum brightness percentage. 💡", "min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥", "max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️", + "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", "sleep_brightness": "sleep_brightness: Brightness percentage of lights in sleep mode. 😴", "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", "sleep_color_temp": "sleep_color_temp: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", "sleep_rgb_color": "sleep_rgb_color: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", "sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅", - "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅", + "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", "sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰", "sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇", - "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇", + "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰", - "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", + "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", - "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.", - "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches." + "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches.", + "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝" } } }, @@ -204,11 +204,11 @@ "name": "sunset_time" }, "max_sunrise_time": { - "description": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅", + "description": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", "name": "max_sunrise_time" }, "min_sunset_time": { - "description": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇", + "description": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", "name": "min_sunset_time" }, "take_over_control": { diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 7e58be17..b27ee3c4 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -21,36 +21,36 @@ "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have the adaptive_lighting entry defined in your YAML configuration.", "data": { "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟", - "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", - "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝", - "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", - "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴", - "transition": "transition: Duration of transition when lights change, in seconds. 🕑", - "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", "interval": "interval: Frequency to adapt the lights, in seconds. 🔄", + "transition": "transition: Duration of transition when lights change, in seconds. 🕑", + "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", "min_brightness": "min_brightness: Minimum brightness percentage. 💡", "max_brightness": "max_brightness: Maximum brightness percentage. 💡", "min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥", "max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️", + "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", "sleep_brightness": "sleep_brightness: Brightness percentage of lights in sleep mode. 😴", "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", "sleep_color_temp": "sleep_color_temp: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", "sleep_rgb_color": "sleep_rgb_color: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", "sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅", - "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅", + "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", "sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰", "sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇", - "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇", + "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰", - "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", + "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", - "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.", - "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches." + "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches.", + "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝" } } }, @@ -205,11 +205,11 @@ "name": "sunset_time" }, "max_sunrise_time": { - "description": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅", + "description": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", "name": "max_sunrise_time" }, "min_sunset_time": { - "description": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇", + "description": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", "name": "min_sunset_time" }, "take_over_control": { From a1cec19351426d42a8ba32c87ef5f4791bd6cbfa Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 5 Aug 2023 13:14:21 -0700 Subject: [PATCH 0622/1077] feat: add different brightness ramping mechanisms (#699) * feat: add different brightness ramping mechanisms * Rephrase * Fix curves * update images * link to other graphs * update images --- README.md | 97 ++++--- custom_components/adaptive_lighting/const.py | 38 +++ .../adaptive_lighting/helpers.py | 206 ++++++++++++++ .../adaptive_lighting/strings.json | 5 +- custom_components/adaptive_lighting/switch.py | 269 +++++++++--------- .../adaptive_lighting/translations/en.json | 5 +- tests/test_switch.py | 89 +++++- 7 files changed, 534 insertions(+), 175 deletions(-) create mode 100644 custom_components/adaptive_lighting/helpers.py diff --git a/README.md b/README.md index 963c4e9a..71500862 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ The `adaptive_lighting.manual_control` event is fired when a light is marked as - [:thermometer: Color Temperature](#thermometer-color-temperature) - [:high_brightness: Brightness](#high_brightness-brightness) - [While using `transition_until_sleep: true`](#while-using-transition_until_sleep-true) + - [Custom brightness ramps using `brightness_mode` with `"linear"` and `"tanh"`](#custom-brightness-ramps-using-brightness_mode-with-linear-and-tanh) - [:eyes: See also](#eyes-see-also) - [:busts_in_silhouette: Contributors](#busts_in_silhouette-contributors) @@ -96,39 +97,43 @@ The YAML and frontend configuration methods support all of the options listed be -| Variable name | Description | Default | Type | -|:-------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------| -| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | -| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | -| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | -| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | -| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | -| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | -| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | -| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | -| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | -| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | -| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | -| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | -| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | -| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | -| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | -| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | -| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | -| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | -| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | -| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | -| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` | -| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | -| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | -| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | -| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | -| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | -| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | -| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. | `True` | `bool` | -| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| Variable name | Description | Default | Type | +|:-------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:---------------------------------------| +| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | +| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | +| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | +| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | +| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | +| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | +| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | +| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | +| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | +| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. | `True` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | @@ -400,6 +405,32 @@ These graphs were generated using the values calculated by the Adaptive Lighting ### While using `transition_until_sleep: true` ![image](https://user-images.githubusercontent.com/2219836/228949675-f9699624-8abc-466c-bb04-250ce0f495b8.png) +### Custom brightness ramps using `brightness_mode` with `"linear"` and `"tanh"` + +
+Enhance your control over brightness transitions during sunrise and sunset with brightness_mode (click here to learn more 🧠). + +With Adaptive Lighting, you can set a `brightness_mode` to specify how the brightness changes during sunrise and sunset. The `brightness_mode` can be set to `"default"` ([as illustrated in other graphs above](#high_brightness-brightness)), `"linear"`, or `"tanh"`. If you choose to deviate from the `"default"` mode, you can adjust `brightness_mode_time_dark` and `brightness_mode_time_light` to further customize the lighting transitions. + +When `brightness_mode` is set to `"linear"`: + +- During **_sunset_**, the brightness begins to gradually decrease from `max_brightness` starting at `time=sunset_time - brightness_mode_time_light`, until it reaches `min_brightness` at `time=sunset_time + brightness_mode_time_dark`. +- During **_sunrise_**, the brightness begins to gradually increase from `min_brightness` starting at `time=sunrise_time - brightness_mode_time_dark`, until it reaches `max_brightness` at `time=sunrise_time + brightness_mode_time_light`. + +When `brightness_mode` is set to `"tanh"`, it uses the smooth transition of a [hyperbolic tangent function](https://mathworld.wolfram.com/HyperbolicTangent.html): + +- During **_sunset_**, the brightness starts to decrease from 95% of `max_brightness` starting at `time=sunset_time - brightness_mode_time_light`, until it reaches 5% of `min_brightness` at `time=sunset_time + brightness_mode_time_dark`. +- During **_sunrise_**, the brightness starts to increase from 5% of `min_brightness` starting at `time=sunrise_time - brightness_mode_time_dark`, until it reaches 95% of `max_brightness` at `time=sunrise_time + brightness_mode_time_light`. +
+ +Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark` in the text box. +![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/15143580-13cd-4ab2-a603-89f2b7830afd) +![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/f61fdac9-6d47-48c9-84ed-cbb451d5de5d) +![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/e5fc5d27-3c37-4e3d-93d1-6e7cf4b48e7c) +![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/3dcbdc42-63c4-49df-8651-d2fae53dd08d) + +> [*Code to make the plots*](https://github.com/basnijholt/adaptive-lighting/pull/699#issuecomment-1666232555) + ## :eyes: See also - [*Sleep better with Adaptive Lighting in Home Assistant*](https://wartner.io/sleep-better-with-adaptive-lightning-in-home-assistant/) by Florian Wartner on 2023-02-23 (blog post 📜) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index f204e1ac..a8b53afd 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -141,6 +141,28 @@ DOCS[ CONF_MIN_SUNSET_TIME ] = "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇" +CONF_BRIGHTNESS_MODE, DEFAULT_BRIGHTNESS_MODE = "brightness_mode", "default" +DOCS[CONF_BRIGHTNESS_MODE] = ( + "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` " + "(uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈" +) +CONF_BRIGHTNESS_MODE_TIME_DARK, DEFAULT_BRIGHTNESS_MODE_TIME_DARK = ( + "brightness_mode_time_dark", + 900, +) +DOCS[CONF_BRIGHTNESS_MODE_TIME_DARK] = ( + "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down " + "the brightness before/after sunrise/sunset. 📈📉" +) +CONF_BRIGHTNESS_MODE_TIME_LIGHT, DEFAULT_BRIGHTNESS_MODE_TIME_LIGHT = ( + "brightness_mode_time_light", + 3600, +) +DOCS[CONF_BRIGHTNESS_MODE_TIME_LIGHT] = ( + "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down " + "the brightness after/before sunrise/sunset. 📈📉." +) + CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True DOCS[CONF_TAKE_OVER_CONTROL] = ( "Disable Adaptive Lighting if another source calls `light.turn_on` while lights " @@ -282,6 +304,20 @@ VALIDATION_TUPLES = [ (CONF_SUNSET_TIME, NONE_STR, str), (CONF_MIN_SUNSET_TIME, NONE_STR, str), (CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int), + ( + CONF_BRIGHTNESS_MODE, + DEFAULT_BRIGHTNESS_MODE, + selector.SelectSelector( + selector.SelectSelectorConfig( + options=["default", "linear", "tanh"], + multiple=False, + mode=selector.SelectSelectorMode.DROPDOWN, + ), + ), + ), + (CONF_BRIGHTNESS_MODE_TIME_DARK, DEFAULT_BRIGHTNESS_MODE_TIME_DARK, int), + (CONF_BRIGHTNESS_MODE_TIME_LIGHT, DEFAULT_BRIGHTNESS_MODE_TIME_LIGHT, int), + (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), (CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool), (CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool), ( @@ -321,6 +357,8 @@ EXTRA_VALIDATION = { CONF_SUNSET_OFFSET: (cv.time_period, timedelta_as_int), CONF_SUNSET_TIME: (cv.time, str), CONF_MIN_SUNSET_TIME: (cv.time, str), + CONF_BRIGHTNESS_MODE_TIME_LIGHT: (cv.time_period, timedelta_as_int), + CONF_BRIGHTNESS_MODE_TIME_DARK: (cv.time_period, timedelta_as_int), } diff --git a/custom_components/adaptive_lighting/helpers.py b/custom_components/adaptive_lighting/helpers.py new file mode 100644 index 00000000..95c87735 --- /dev/null +++ b/custom_components/adaptive_lighting/helpers.py @@ -0,0 +1,206 @@ +"""Helper functions for the Adaptive Lighting custom components.""" + +from __future__ import annotations + +import base64 +import colorsys +import logging +import math +from typing import cast + +_LOGGER = logging.getLogger(__name__) + + +def clamp(value: float, minimum: float, maximum: float) -> float: + """Clamp value between minimum and maximum.""" + return max(minimum, min(value, maximum)) + + +def find_a_b(x1: float, x2: float, y1: float, y2: float) -> tuple[float, float]: + """Compute the values of 'a' and 'b' for a scaled and shifted tanh function. + + Given two points (x1, y1) and (x2, y2), this function calculates the coefficients 'a' and 'b' + for a tanh function of the form y = 0.5 * (tanh(a * (x - b)) + 1) that passes through these points. + + The derivation is as follows: + + 1. Start with the equation of the tanh function: + y = 0.5 * (tanh(a * (x - b)) + 1) + + 2. Rearrange the equation to isolate tanh: + tanh(a * (x - b)) = 2*y - 1 + + 3. Take the inverse tanh (or artanh) on both sides to solve for 'a' and 'b': + a * (x - b) = artanh(2*y - 1) + + 4. Plug in the points (x1, y1) and (x2, y2) to get two equations. + Using these, we can solve for 'a' and 'b' as: + a = (artanh(2*y2 - 1) - artanh(2*y1 - 1)) / (x2 - x1) + b = x1 - (artanh(2*y1 - 1) / a) + + Parameters + ---------- + x1 + x-coordinate of the first point. + x2 + x-coordinate of the second point. + y1 + y-coordinate of the first point (should be between 0 and 1). + y2 + y-coordinate of the second point (should be between 0 and 1). + + Returns + ------- + a + Coefficient 'a' for the tanh function. + b + Coefficient 'b' for the tanh function. + + Notes + ----- + The values of y1 and y2 should lie between 0 and 1, inclusive. + """ + a = (math.atanh(2 * y2 - 1) - math.atanh(2 * y1 - 1)) / (x2 - x1) + b = x1 - (math.atanh(2 * y1 - 1) / a) + return a, b + + +def scaled_tanh( + x: float, + a: float, + b: float, + y_min: float = 0.0, + y_max: float = 100.0, +) -> float: + """Apply a scaled and shifted tanh function to a given input. + + This function represents a transformation of the tanh function that scales and shifts + the output to lie between y_min and y_max. For values of 'x' close to 'x1' and 'x2' + (used to calculate 'a' and 'b'), the output of this function will be close to 'y_min' + and 'y_max', respectively. + + The equation of the function is as follows: + y = y_min + (y_max - y_min) * 0.5 * (tanh(a * (x - b)) + 1) + + Parameters + ---------- + x + The input to the function. + a + The scale factor for the tanh function, found using 'find_a_b' function. + b + The shift factor for the tanh function, found using 'find_a_b' function. + y_min + The minimum value of the output range. Defaults to 0. + y_max + The maximum value of the output range. Defaults to 100. + + Returns + ------- + float: The output of the function, which lies in the range [y_min, y_max]. + """ + return y_min + (y_max - y_min) * 0.5 * (math.tanh(a * (x - b)) + 1) + + +def lerp_color_hsv( + rgb1: tuple[float, float, float], + rgb2: tuple[float, float, float], + t: float, +) -> tuple[int, int, int]: + """Linearly interpolate between two RGB colors in HSV color space.""" + t = abs(t) + assert 0 <= t <= 1 + + # Convert RGB to HSV + hsv1 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb1]) + hsv2 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb2]) + + # Linear interpolation in HSV space + hsv = ( + hsv1[0] + t * (hsv2[0] - hsv1[0]), + hsv1[1] + t * (hsv2[1] - hsv1[1]), + hsv1[2] + t * (hsv2[2] - hsv1[2]), + ) + + # Convert back to RGB + rgb = tuple(int(round(x * 255)) for x in colorsys.hsv_to_rgb(*hsv)) + assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}" + return cast(tuple[int, int, int], rgb) + + +def lerp(x, x1, x2, y1, y2): + """Linearly interpolate between two values.""" + return y1 + (x - x1) * (y2 - y1) / (x2 - x1) + + +def int_to_base36(num: int) -> str: + """Convert an integer to its base-36 representation using numbers and uppercase letters. + + Base-36 encoding uses digits 0-9 and uppercase letters A-Z, providing a case-insensitive + alphanumeric representation. The function takes an integer `num` as input and returns + its base-36 representation as a string. + + Parameters + ---------- + num + The integer to convert to base-36. + + Returns + ------- + str + The base-36 representation of the input integer. + + Examples + -------- + >>> num = 123456 + >>> base36_num = int_to_base36(num) + >>> print(base36_num) + '2N9' + """ + alphanumeric_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + if num == 0: + return alphanumeric_chars[0] + + base36_str = "" + base = len(alphanumeric_chars) + + while num: + num, remainder = divmod(num, base) + base36_str = alphanumeric_chars[remainder] + base36_str + + return base36_str + + +def short_hash(string: str, length: int = 4) -> str: + """Create a hash of 'string' with length 'length'.""" + return base64.b32encode(string.encode()).decode("utf-8").zfill(length)[:length] + + +def remove_vowels(input_str: str, length: int = 4) -> str: + """Remove vowels from a string and return a string of length 'length'.""" + vowels = "aeiouAEIOU" + output_str = "".join([char for char in input_str if char not in vowels]) + return output_str.zfill(length)[:length] + + +def color_difference_redmean( + rgb1: tuple[float, float, float], + rgb2: tuple[float, float, float], +) -> float: + """Distance between colors in RGB space (redmean metric). + + The maximal distance between (255, 255, 255) and (0, 0, 0) ≈ 765. + + Sources: + - https://en.wikipedia.org/wiki/Color_difference#Euclidean + - https://www.compuphase.com/cmetric.htm + """ + r_hat = (rgb1[0] + rgb2[0]) / 2 + delta_r, delta_g, delta_b = ( + (col1 - col2) for col1, col2 in zip(rgb1, rgb2, strict=True) + ) + red_term = (2 + r_hat / 256) * delta_r**2 + green_term = 4 * delta_g**2 + blue_term = (2 + (255 - r_hat) / 256) * delta_b**2 + return math.sqrt(red_term + green_term + blue_term) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index a942e66b..0751500b 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -40,10 +40,13 @@ "sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇", "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰", + "brightness_mode": "brightness_mode: Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "brightness_mode_time_dark: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", + "brightness_mode_time_light": "brightness_mode_time_light: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.", + "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", - "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index defe3b85..a32406d5 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2,9 +2,7 @@ from __future__ import annotations import asyncio -import base64 import bisect -import colorsys import datetime import functools import logging @@ -12,7 +10,7 @@ import math from copy import deepcopy from dataclasses import dataclass from datetime import timedelta -from typing import TYPE_CHECKING, Any, Literal, cast +from typing import TYPE_CHECKING, Any, Literal import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -108,6 +106,9 @@ from .const import ( CONF_ADAPT_DELAY, CONF_ADAPT_UNTIL_SLEEP, CONF_AUTORESET_CONTROL, + CONF_BRIGHTNESS_MODE, + CONF_BRIGHTNESS_MODE_TIME_DARK, + CONF_BRIGHTNESS_MODE_TIME_LIGHT, CONF_DETECT_NON_HA_CHANGES, CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, CONF_INITIAL_TRANSITION, @@ -158,6 +159,17 @@ from .const import ( replace_none_str, ) from .hass_utils import setup_service_call_interceptor +from .helpers import ( + clamp, + color_difference_redmean, + find_a_b, + int_to_base36, + lerp, + lerp_color_hsv, + remove_vowels, + scaled_tanh, + short_hash, +) if TYPE_CHECKING: from collections.abc import Callable, Coroutine, Iterable @@ -195,56 +207,6 @@ RGB_REDMEAN_CHANGE = 80 # ≈10% of total range _DOMAIN_SHORT = "al" -def _int_to_base36(num: int) -> str: - """Convert an integer to its base-36 representation using numbers and uppercase letters. - - Base-36 encoding uses digits 0-9 and uppercase letters A-Z, providing a case-insensitive - alphanumeric representation. The function takes an integer `num` as input and returns - its base-36 representation as a string. - - Parameters - ---------- - num - The integer to convert to base-36. - - Returns - ------- - str - The base-36 representation of the input integer. - - Examples - -------- - >>> num = 123456 - >>> base36_num = int_to_base36(num) - >>> print(base36_num) - '2N9' - """ - alphanumeric_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" - - if num == 0: - return alphanumeric_chars[0] - - base36_str = "" - base = len(alphanumeric_chars) - - while num: - num, remainder = divmod(num, base) - base36_str = alphanumeric_chars[remainder] + base36_str - - return base36_str - - -def _short_hash(string: str, length: int = 4) -> str: - """Create a hash of 'string' with length 'length'.""" - return base64.b32encode(string.encode()).decode("utf-8").zfill(length)[:length] - - -def _remove_vowels(input_str: str, length: int = 4) -> str: - vowels = "aeiouAEIOU" - output_str = "".join([char for char in input_str if char not in vowels]) - return output_str.zfill(length)[:length] - - def create_context( name: str, which: str, @@ -257,11 +219,11 @@ def create_context( # Pack index with base85 to maximize the number of contexts we can create # before we exceed the 26-character limit and are forced to wrap. time_stamp = ulid_transform.ulid_now()[:10] # time part of a ULID - name_hash = _short_hash(name) - which_short = _remove_vowels(which) + name_hash = short_hash(name) + which_short = remove_vowels(which) context_id_start = f"{time_stamp}:{_DOMAIN_SHORT}:{name_hash}:{which_short}:" chars_left = 26 - len(context_id_start) - index_packed = _int_to_base36(index).zfill(chars_left)[-chars_left:] + index_packed = int_to_base36(index).zfill(chars_left)[-chars_left:] context_id = context_id_start + index_packed parent_id = parent.id if parent else None return Context(id=context_id, parent_id=parent_id) @@ -277,7 +239,7 @@ def is_our_context_id(context_id: str | None, which: str | None = None) -> bool: return False if which is None: return True - return f":{_remove_vowels(which)}:" in context_id + return f":{remove_vowels(which)}:" in context_id def is_our_context(context: Context | None, which: str | None = None) -> bool: @@ -716,28 +678,6 @@ def _supported_features(hass: HomeAssistant, light: str) -> set[str]: return supported -def color_difference_redmean( - rgb1: tuple[float, float, float], - rgb2: tuple[float, float, float], -) -> float: - """Distance between colors in RGB space (redmean metric). - - The maximal distance between (255, 255, 255) and (0, 0, 0) ≈ 765. - - Sources: - - https://en.wikipedia.org/wiki/Color_difference#Euclidean - - https://www.compuphase.com/cmetric.htm - """ - r_hat = (rgb1[0] + rgb2[0]) / 2 - delta_r, delta_g, delta_b = ( - (col1 - col2) for col1, col2 in zip(rgb1, rgb2, strict=True) - ) - red_term = (2 + r_hat / 256) * delta_r**2 - green_term = 4 * delta_g**2 - blue_term = (2 + (255 - r_hat) / 256) * delta_b**2 - return math.sqrt(red_term + green_term + blue_term) - - # All comparisons should be done with RGB since # converting anything to color temp is inaccurate. def _convert_attributes(attributes: dict[str, Any]) -> dict[str, Any]: @@ -968,6 +908,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): sunset_offset=data[CONF_SUNSET_OFFSET], sunset_time=data[CONF_SUNSET_TIME], min_sunset_time=data[CONF_MIN_SUNSET_TIME], + brightness_mode=data[CONF_BRIGHTNESS_MODE], + brightness_mode_time_dark=data[CONF_BRIGHTNESS_MODE_TIME_DARK], + brightness_mode_time_light=data[CONF_BRIGHTNESS_MODE_TIME_LIGHT], transition=data[CONF_TRANSITION], ) _LOGGER.debug( @@ -1251,7 +1194,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): min_kelvin = attributes["min_color_temp_kelvin"] max_kelvin = attributes["max_color_temp_kelvin"] color_temp_kelvin = self._settings["color_temp_kelvin"] - color_temp_kelvin = max(min(color_temp_kelvin, max_kelvin), min_kelvin) + color_temp_kelvin = clamp(color_temp_kelvin, min_kelvin, max_kelvin) service_data[ATTR_COLOR_TEMP_KELVIN] = color_temp_kelvin elif "color" in features and adapt_color: _LOGGER.debug("%s: Setting rgb_color of light %s", self._name, light) @@ -1614,32 +1557,6 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): self._state = False -def lerp_color_hsv( - rgb1: tuple[float, float, float], - rgb2: tuple[float, float, float], - t: float, -) -> tuple[int, int, int]: - """Linearly interpolate between two RGB colors in HSV color space.""" - t = abs(t) - assert 0 <= t <= 1 - - # Convert RGB to HSV - hsv1 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb1]) - hsv2 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb2]) - - # Linear interpolation in HSV space - hsv = ( - hsv1[0] + t * (hsv2[0] - hsv1[0]), - hsv1[1] + t * (hsv2[1] - hsv1[1]), - hsv1[2] + t * (hsv2[2] - hsv1[2]), - ) - - # Convert back to RGB - rgb = tuple(int(round(x * 255)) for x in colorsys.hsv_to_rgb(*hsv)) - assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}" - return cast(tuple[int, int, int], rgb) - - @dataclass(frozen=True) class SunLightSettings: """Track the state of the sun and associated light settings.""" @@ -1661,18 +1578,47 @@ class SunLightSettings: sunset_offset: datetime.timedelta | None sunset_time: datetime.time | None min_sunset_time: datetime.time | None + brightness_mode: Literal["default", "linear", "tanh"] + brightness_mode_time_dark: datetime.timedelta | None + brightness_mode_time_light: datetime.timedelta | None transition: int + def sunrise(self, date: datetime.datetime) -> datetime.datetime: + """Return the (adjusted) sunrise time for the given date.""" + sunrise = ( + self.astral_location.sunrise(date, local=False) + if self.sunrise_time is None + else self._replace_time(date, "sunrise") + ) + self.sunrise_offset + if self.max_sunrise_time is not None: + max_sunrise = self._replace_time(date, "max_sunrise") + if max_sunrise < sunrise: + sunrise = max_sunrise + return sunrise + + def sunset(self, date: datetime.datetime) -> datetime.datetime: + """Return the (adjusted) sunset time for the given date.""" + sunset = ( + self.astral_location.sunset(date, local=False) + if self.sunset_time is None + else self._replace_time(date, "sunset") + ) + self.sunset_offset + if self.min_sunset_time is not None: + min_sunset = self._replace_time(date, "min_sunset") + if min_sunset > sunset: + sunset = min_sunset + return sunset + + def _replace_time(self, date: datetime.datetime, key: str) -> datetime.datetime: + time = getattr(self, f"{key}_time") + date_time = datetime.datetime.combine(date, time) + return date_time.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone( + dt_util.UTC, + ) + def get_sun_events(self, date: datetime.datetime) -> list[tuple[str, float]]: """Get the four sun event's timestamps at 'date'.""" - def _replace_time(date: datetime.datetime, key: str) -> datetime.datetime: - time = getattr(self, f"{key}_time") - date_time = datetime.datetime.combine(date, time) - return date_time.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone( - dt_util.UTC, - ) - def calculate_noon_and_midnight( sunset: datetime.datetime, sunrise: datetime.datetime, @@ -1689,27 +1635,8 @@ class SunLightSettings: return noon, midnight location = self.astral_location - - sunrise = ( - location.sunrise(date, local=False) - if self.sunrise_time is None - else _replace_time(date, "sunrise") - ) + self.sunrise_offset - sunset = ( - location.sunset(date, local=False) - if self.sunset_time is None - else _replace_time(date, "sunset") - ) + self.sunset_offset - - if self.max_sunrise_time is not None: - max_sunrise = _replace_time(date, "max_sunrise") - if max_sunrise < sunrise: - sunrise = max_sunrise - - if self.min_sunset_time is not None: - min_sunset = _replace_time(date, "min_sunset") - if min_sunset > sunset: - sunset = min_sunset + sunrise = self.sunrise(date) + sunset = self.sunset(date) if ( self.sunrise_time is None @@ -1774,11 +1701,75 @@ class SunLightSettings: """Calculate the brightness in %.""" if is_sleep: return self.sleep_brightness - if percent > 0: - return self.max_brightness - delta_brightness = self.max_brightness - self.min_brightness - percent = 1 + percent - return (delta_brightness * percent) + self.min_brightness + assert self.brightness_mode in ("default", "linear", "tanh") + + if self.brightness_mode == "default": + if percent > 0: + return self.max_brightness + delta_brightness = self.max_brightness - self.min_brightness + percent = 1 + percent + return (delta_brightness * percent) + self.min_brightness + + now = dt_util.utcnow() + (prev_event, prev_ts), (next_event, next_ts) = self.relevant_events(now) + + # at ts_event - dt_start, brightness == start_brightness + # at ts_event + dt_end, brightness == end_brightness + dark = (self.brightness_mode_time_dark or timedelta()).total_seconds() + light = (self.brightness_mode_time_light or timedelta()).total_seconds() + # Handle sunrise + if prev_event == SUN_EVENT_SUNRISE or next_event == SUN_EVENT_SUNRISE: + ts_event = prev_ts if prev_event == SUN_EVENT_SUNRISE else next_ts + if self.brightness_mode == "linear": + brightness = lerp( + now.timestamp(), + x1=ts_event - dark, + x2=ts_event + light, + y1=self.min_brightness, + y2=self.max_brightness, + ) + else: + assert self.brightness_mode == "tanh" + a, b = find_a_b( + x1=-dark, + x2=+light, + y1=0.05, # be at 5% of range at x1 + y2=0.95, # be at 95% of range at x2 + ) + brightness = scaled_tanh( + now.timestamp() - ts_event, + a=a, + b=b, + y_min=self.min_brightness, + y_max=self.max_brightness, + ) + # Handle sunset + elif prev_event == SUN_EVENT_SUNSET or next_event == SUN_EVENT_SUNSET: + ts_event = prev_ts if prev_event == SUN_EVENT_SUNSET else next_ts + if self.brightness_mode == "linear": + brightness = lerp( + now.timestamp(), + x1=ts_event - light, + x2=ts_event + dark, + y1=self.max_brightness, + y2=self.min_brightness, + ) + else: + assert self.brightness_mode == "tanh" + a, b = find_a_b( + x1=-light, # shifted timestamp for the start of sunset + x2=+dark, # shifted timestamp for the end of sunset + y1=0.95, # be at 95% of range at the start of sunset + y2=0.05, # be at 5% of range at the end of sunset + ) + brightness = scaled_tanh( + now.timestamp() - ts_event, + a=a, + b=b, + y_min=self.min_brightness, + y_max=self.max_brightness, + ) + return clamp(brightness, self.min_brightness, self.max_brightness) def calc_color_temp_kelvin(self, percent: float) -> int: """Calculate the color temperature in Kelvin.""" diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index b27ee3c4..511a11c8 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -41,10 +41,13 @@ "sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇", "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰", + "brightness_mode": "brightness_mode: Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "brightness_mode_time_dark: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", + "brightness_mode_time_light": "brightness_mode_time_light: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.", + "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", - "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", diff --git a/tests/test_switch.py b/tests/test_switch.py index e3bc9126..1a25c7a1 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -60,6 +60,9 @@ from custom_components.adaptive_lighting.const import ( ATTR_ADAPTIVE_LIGHTING_MANAGER, CONF_ADAPT_UNTIL_SLEEP, CONF_AUTORESET_CONTROL, + CONF_BRIGHTNESS_MODE, + CONF_BRIGHTNESS_MODE_TIME_DARK, + CONF_BRIGHTNESS_MODE_TIME_LIGHT, CONF_DETECT_NON_HA_CHANGES, CONF_INITIAL_TRANSITION, CONF_MANUAL_CONTROL, @@ -364,7 +367,11 @@ async def test_adaptive_lighting_time_zones_with_default_settings( @pytest.mark.parametrize("lat,long,timezone", LAT_LONG_TZS) async def test_adaptive_lighting_time_zones_and_sun_settings( - hass, lat, long, timezone, reset_time_zone # pylint: disable=redefined-outer-name + hass, + lat, + long, + timezone, + reset_time_zone, # pylint: disable=redefined-outer-name ): """Test setting up the Adaptive Lighting switches with different timezones. @@ -1992,3 +1999,83 @@ async def test_light_group( ] assert ":skpp:" in events[2].context.id assert len(events) == 3 + + +@pytest.mark.parametrize("brightness_mode", ["linear", "tanh"]) +@pytest.mark.parametrize("dark,light", ([900, 1800], [1800, 900], [1800, 1800])) +async def test_brightness_mode(hass, brightness_mode, dark, light): + """Test brightness mode. + + We are not testing the "default" mode because that is tested in all other tests. + """ + is_symmetric = dark == light + _, switch = await setup_switch( + hass, + { + CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour), + CONF_SUNSET_TIME: datetime.time(SUNSET.hour), + CONF_BRIGHTNESS_MODE: brightness_mode, + CONF_BRIGHTNESS_MODE_TIME_DARK: datetime.timedelta(seconds=dark), + CONF_BRIGHTNESS_MODE_TIME_LIGHT: datetime.timedelta(seconds=light), + }, + ) + + context = switch.create_context("test") # needs to be passed to update method + min_brightness = switch._sun_light_settings.min_brightness + max_brightness = switch._sun_light_settings.max_brightness + brightness_range = max_brightness - min_brightness + brightness_event = min_brightness + brightness_range / 2 + dark = switch._sun_light_settings.brightness_mode_time_dark + light = switch._sun_light_settings.brightness_mode_time_light + + sunset = SUNSET.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + before_sunset = sunset - light + after_sunset = sunset + dark + sunrise = SUNRISE.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + before_sunrise = sunrise - dark + after_sunrise = sunrise + light + + light_brightness = ( + max_brightness + if brightness_mode == "linear" + else 0.95 * brightness_range + min_brightness + ) + dark_brightness = ( + min_brightness + if brightness_mode == "linear" + else 0.05 * brightness_range + min_brightness + ) + + def is_approx_equal(a, b): + return abs(a - b) < 0.01 + + async def patch_time_and_update(time): + with patch("homeassistant.util.dt.utcnow", return_value=time): + await switch._update_attrs_and_maybe_adapt_lights(context=context) + await hass.async_block_till_done() + + if is_symmetric: + # At sunset the brightness should be 50% + await patch_time_and_update(sunset) + assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], brightness_event) + + # Before sunset the brightness should be max + await patch_time_and_update(before_sunset) + assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], light_brightness) + + # After sunset the brightness should be dark_brightness + await patch_time_and_update(after_sunset) + assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], dark_brightness) + + if is_symmetric: + # At sunrise the brightness should be 50% + await patch_time_and_update(sunrise) + assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], brightness_event) + + # Before sunrise the brightness should be min + await patch_time_and_update(before_sunrise) + assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], dark_brightness) + + # After sunrise the brightness should be light_brightness + await patch_time_and_update(after_sunrise) + assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], light_brightness) From e3a84ffd9668386806ac4582f89bb182154d4de1 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 5 Aug 2023 13:52:54 -0700 Subject: [PATCH 0623/1077] Logging fixes in significant_changes (#704) --- custom_components/adaptive_lighting/switch.py | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index a32406d5..85619460 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio import bisect import datetime -import functools import logging import math from copy import deepcopy @@ -2330,6 +2329,12 @@ class AdaptiveLightingManager: delay = self.auto_reset_manual_control_times.get(light) async def reset(): + _LOGGER.debug( + "Auto resetting 'manual_control' status of '%s' because" + " it was not manually controlled for %s seconds.", + light, + delay, + ) self.reset(light) switches = _switches_with_lights(self.hass, [light]) for switch in switches: @@ -2341,12 +2346,6 @@ class AdaptiveLightingManager: transition=switch.initial_transition, force=True, ) - _LOGGER.debug( - "Auto resetting 'manual_control' status of '%s' because" - " it was not manually controlled for %s seconds.", - light, - delay, - ) assert not self.manual_control[light] self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset) @@ -2384,7 +2383,7 @@ class AdaptiveLightingManager: # color_task might be the same as brightness_task color_task.cancel() - def reset(self, *lights, reset_manual_control=True) -> None: + def reset(self, *lights, reset_manual_control: bool = True) -> None: """Reset the 'manual_control' status of the lights.""" for light in lights: if reset_manual_control: @@ -2653,13 +2652,6 @@ class AdaptiveLightingManager: last_service_data = self.last_service_data.get(light) if last_service_data is None: return False - compare_to = functools.partial( - _attributes_have_changed, - light=light, - adapt_brightness=adapt_brightness, - adapt_color=adapt_color, - context=context, - ) # Update state and check for a manual change not done in HA. # Ensure HASS is correctly updating your light's state with # light.turn_on calls if any problems arise. This @@ -2668,13 +2660,17 @@ class AdaptiveLightingManager: refreshed_state = self.hass.states.get(light) assert refreshed_state is not None - changed = compare_to( + changed = _attributes_have_changed( old_attributes=last_service_data, new_attributes=refreshed_state.attributes, + light=light, + adapt_brightness=adapt_brightness, + adapt_color=adapt_color, + context=context, ) if changed: _LOGGER.debug( - "%s: State attributes of '%s' (%s) didn't change wrt 'last_service_data' (%s) (context.id=%s)", + "%s: State attributes of '%s' changed (%s) wrt 'last_service_data' (%s) (context.id=%s)", switch._name, light, refreshed_state.attributes, @@ -2683,7 +2679,7 @@ class AdaptiveLightingManager: ) return True _LOGGER.debug( - "%s: State attributes of '%s' (%s) changed wrt 'last_service_data' (%s) (context.id=%s)", + "%s: State attributes of '%s' did not change (%s) wrt 'last_service_data' (%s) (context.id=%s)", switch._name, light, refreshed_state.attributes, From 51a18f2afb445f9938cded66b952bf1035d37faa Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 5 Aug 2023 13:59:58 -0700 Subject: [PATCH 0624/1077] Allow adaptive_lighting.apply to turn on lights and do not issue log message (#705) --- custom_components/adaptive_lighting/switch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 85619460..afeb7fa7 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2694,7 +2694,10 @@ class AdaptiveLightingManager: off_to_on_event: Event, ) -> bool: # Adaptive Lighting should never turn on lights itself - if is_our_context(off_to_on_event.context): + if is_our_context(off_to_on_event.context) and not is_our_context( + off_to_on_event.context, + "service", # adaptive_lighting.apply is allowed to turn on lights + ): _LOGGER.warning( "Detected an 'off' → 'on' event for '%s' with context.id='%s' and" " event='%s', triggered by the adaptive_lighting integration itself," From 11a4489cb5b996e924966eba145833161641aac7 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 5 Aug 2023 14:42:40 -0700 Subject: [PATCH 0625/1077] Bump to 1.19.0b1 manifest.json (#706) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 7806e04a..6202a765 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.18.3" + "version": "1.19.0b1" } From 72419cb60f983a38a8b3ad16973b401b6cbc62b2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 5 Aug 2023 15:08:45 -0700 Subject: [PATCH 0626/1077] Add a min_sunrise_time and max_sunset_time (#707) --- README.md | 2 ++ custom_components/adaptive_lighting/const.py | 12 ++++++++++++ .../adaptive_lighting/strings.json | 2 ++ custom_components/adaptive_lighting/switch.py | 18 ++++++++++++++++-- .../adaptive_lighting/translations/en.json | 2 ++ 5 files changed, 34 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 71500862..b83d880f 100644 --- a/README.md +++ b/README.md @@ -115,10 +115,12 @@ The YAML and frontend configuration methods support all of the options listed be | `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | | `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | | `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | +| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | | `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | | `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | | `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | | `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | +| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | | `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | | `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | | `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index a8b53afd..1d060c56 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -122,6 +122,11 @@ DOCS[ CONF_SUNRISE_TIME = "sunrise_time" DOCS[CONF_SUNRISE_TIME] = "Set a fixed time (HH:MM:SS) for sunrise. 🌅" +CONF_MIN_SUNRISE_TIME = "min_sunrise_time" +DOCS[ + CONF_MIN_SUNRISE_TIME +] = "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅" + CONF_MAX_SUNRISE_TIME = "max_sunrise_time" DOCS[CONF_MAX_SUNRISE_TIME] = ( "Set the latest virtual sunrise time (HH:MM:SS), allowing" @@ -141,6 +146,11 @@ DOCS[ CONF_MIN_SUNSET_TIME ] = "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇" +CONF_MAX_SUNSET_TIME = "max_sunset_time" +DOCS[ + CONF_MAX_SUNSET_TIME +] = "Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇" + CONF_BRIGHTNESS_MODE, DEFAULT_BRIGHTNESS_MODE = "brightness_mode", "default" DOCS[CONF_BRIGHTNESS_MODE] = ( "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` " @@ -299,10 +309,12 @@ VALIDATION_TUPLES = [ (CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION, VALID_TRANSITION), (CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP, bool), (CONF_SUNRISE_TIME, NONE_STR, str), + (CONF_MIN_SUNRISE_TIME, NONE_STR, str), (CONF_MAX_SUNRISE_TIME, NONE_STR, str), (CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int), (CONF_SUNSET_TIME, NONE_STR, str), (CONF_MIN_SUNSET_TIME, NONE_STR, str), + (CONF_MAX_SUNSET_TIME, NONE_STR, str), (CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int), ( CONF_BRIGHTNESS_MODE, diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 0751500b..cece3da7 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -35,10 +35,12 @@ "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴", "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", "sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "min_sunrise_time": "min_sunrise_time: Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅", "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", "sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰", "sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇", "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", + "max_sunset_time": "max_sunset_time: Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇", "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰", "brightness_mode": "brightness_mode: Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", "brightness_mode_time_dark": "brightness_mode_time_dark: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index afeb7fa7..b5bf65de 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -117,8 +117,10 @@ from .const import ( CONF_MAX_BRIGHTNESS, CONF_MAX_COLOR_TEMP, CONF_MAX_SUNRISE_TIME, + CONF_MAX_SUNSET_TIME, CONF_MIN_BRIGHTNESS, CONF_MIN_COLOR_TEMP, + CONF_MIN_SUNRISE_TIME, CONF_MIN_SUNSET_TIME, CONF_MULTI_LIGHT_INTERCEPT, CONF_ONLY_ONCE, @@ -903,10 +905,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): sleep_rgb_or_color_temp=data[CONF_SLEEP_RGB_OR_COLOR_TEMP], sunrise_offset=data[CONF_SUNRISE_OFFSET], sunrise_time=data[CONF_SUNRISE_TIME], + min_sunrise_time=data[CONF_MIN_SUNRISE_TIME], max_sunrise_time=data[CONF_MAX_SUNRISE_TIME], sunset_offset=data[CONF_SUNSET_OFFSET], sunset_time=data[CONF_SUNSET_TIME], min_sunset_time=data[CONF_MIN_SUNSET_TIME], + max_sunset_time=data[CONF_MAX_SUNSET_TIME], brightness_mode=data[CONF_BRIGHTNESS_MODE], brightness_mode_time_dark=data[CONF_BRIGHTNESS_MODE_TIME_DARK], brightness_mode_time_light=data[CONF_BRIGHTNESS_MODE_TIME_LIGHT], @@ -1571,12 +1575,14 @@ class SunLightSettings: sleep_rgb_or_color_temp: Literal["color_temp", "rgb_color"] sleep_color_temp: int sleep_rgb_color: tuple[int, int, int] - sunrise_offset: datetime.timedelta | None sunrise_time: datetime.time | None + sunrise_offset: datetime.timedelta | None + min_sunrise_time: datetime.time | None max_sunrise_time: datetime.time | None - sunset_offset: datetime.timedelta | None sunset_time: datetime.time | None + sunset_offset: datetime.timedelta | None min_sunset_time: datetime.time | None + max_sunset_time: datetime.time | None brightness_mode: Literal["default", "linear", "tanh"] brightness_mode_time_dark: datetime.timedelta | None brightness_mode_time_light: datetime.timedelta | None @@ -1589,6 +1595,10 @@ class SunLightSettings: if self.sunrise_time is None else self._replace_time(date, "sunrise") ) + self.sunrise_offset + if self.min_sunrise_time is not None: + min_sunrise = self._replace_time(date, "min_sunrise") + if min_sunrise > sunrise: + sunrise = min_sunrise if self.max_sunrise_time is not None: max_sunrise = self._replace_time(date, "max_sunrise") if max_sunrise < sunrise: @@ -1606,6 +1616,10 @@ class SunLightSettings: min_sunset = self._replace_time(date, "min_sunset") if min_sunset > sunset: sunset = min_sunset + if self.max_sunset_time is not None: + max_sunset = self._replace_time(date, "max_sunset") + if max_sunset < sunset: + sunset = max_sunset return sunset def _replace_time(self, date: datetime.datetime, key: str) -> datetime.datetime: diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 511a11c8..c8e2c413 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -36,10 +36,12 @@ "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴", "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", "sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "min_sunrise_time": "min_sunrise_time: Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅", "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", "sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰", "sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇", "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", + "max_sunset_time": "max_sunset_time: Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇", "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰", "brightness_mode": "brightness_mode: Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", "brightness_mode_time_dark": "brightness_mode_time_dark: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", From 3f17738bd16fa49a92ea87f9690311965e967d95 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 5 Aug 2023 16:48:35 -0700 Subject: [PATCH 0627/1077] Add warning about Zigbee groups (#708) --- README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b83d880f..be753a9f 100644 --- a/README.md +++ b/README.md @@ -366,11 +366,21 @@ Ensure your light bulbs have a strong WiFi connection. If the signal strength is #### :spider_web: Zigbee, Z-Wave, and Other Mesh Networks -Mesh networks typically require powered devices to act as routers, relaying messages back to the central coordinator (the radio connected to Home Assistant). Philips lights usually function as routers, while Ikea, Sengled, and generic Tuya bulbs often do not. If devices become unresponsive or fail to respond to commands, Adaptive Lighting can exacerbate the issue. Use network maps (available in ZHA, zigbee2mqtt, deCONZ, and ZWaveJS UI) to evaluate your network health. Smart plugs can be an affordable way to add more routers to your network. +Mesh networks typically require powered devices to act as routers, relaying messages back to the central coordinator (the radio connected to Home Assistant). +Philips lights usually function as routers, while Ikea, Sengled, and generic Tuya bulbs often do not. +If devices become unresponsive or fail to respond to commands, Adaptive Lighting can exacerbate the issue. +Use network maps (available in ZHA, zigbee2mqtt, deCONZ, and ZWaveJS UI) to evaluate your network health. +Smart plugs can be an affordable way to add more routers to your network. -For most Zigbee networks, **using groups is essential for optimal performance**. For example, if you want to use Adaptive Lighting in a hallway with six bulbs, adding each bulb individually to the Adaptive Lighting configuration could overwhelm the network with commands. Instead, create a group in your Zigbee software (not a regular Home Assistant group) and add that single group to the Adaptive Lighting configuration. This sends a single broadcast command to adjust all bulbs, improving response times and keeping the bulbs in sync. +For most Zigbee networks, **using groups is essential for optimal performance**. +For example, if you want to use Adaptive Lighting in a hallway with six bulbs, adding each bulb individually to the Adaptive Lighting configuration could overwhelm the network with commands. +Instead, create a group in your Zigbee software (not a regular Home Assistant group) and add that single group to the Adaptive Lighting configuration. +This sends a single broadcast command to adjust all bulbs, improving response times and keeping the bulbs in sync. -As a rule of thumb, if you always control lights together (e.g., bulbs in a ceiling fixture), they should be in a Zigbee group. Expose only the group (not individual bulbs) in Home Assistant Dashboards and external systems like Google Home or Apple HomeKit. +As a rule of thumb, if you always control lights together (e.g., bulbs in a ceiling fixture), they should be in a Zigbee group. +Expose only the group (not individual bulbs) in Home Assistant Dashboards and external systems like Google Home or Apple HomeKit. + +> :warning: **If you control lights individually, `manual_control` cannot behave correctly! If you need to control lights individually as well, use a [Home Assistant Light Group](https://www.home-assistant.io/integrations/group/).** #### :rainbow: Light Colors Not Matching From d44cb66148af86a328e085db58d5d2b3fa03b6a3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 5 Aug 2023 20:56:32 -0700 Subject: [PATCH 0628/1077] Add `adapt_only_on_bare_turn_on` which instantly triggers `manual_control` when turning on with brightness or color (#709) --- README.md | 78 +++++++++---------- custom_components/adaptive_lighting/const.py | 14 +++- .../adaptive_lighting/strings.json | 3 +- custom_components/adaptive_lighting/switch.py | 64 +++++++++++++-- .../adaptive_lighting/translations/en.json | 3 +- tests/test_switch.py | 22 +++++- 6 files changed, 131 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index be753a9f..56d9f05d 100644 --- a/README.md +++ b/README.md @@ -97,45 +97,45 @@ The YAML and frontend configuration methods support all of the options listed be -| Variable name | Description | Default | Type | -|:-------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:---------------------------------------| -| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | -| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | -| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | -| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | -| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | -| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | -| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | -| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | -| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | -| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | -| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | -| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | -| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | -| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | -| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | -| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | -| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | -| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | -| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | -| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | -| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | -| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | -| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | -| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | -| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | -| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | -| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` | -| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | -| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | -| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | -| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | -| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | -| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | -| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. | `True` | `bool` | -| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| Variable name | Description | Default | Type | +|:-------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:---------------------------------------| +| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | +| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | +| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | +| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | +| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | +| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | +| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | +| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | +| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | +| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | +| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | +| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | +| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. | `True` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 1d060c56..f3dc628d 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -77,6 +77,18 @@ DOCS[CONF_ONLY_ONCE] = ( "(`false`). 🔄" ) +CONF_ADAPT_ONLY_ON_BARE_TURN_ON, DEFAULT_ADAPT_ONLY_ON_BARE_TURN_ON = ( + "adapt_only_on_bare_turn_on", + False, +) +DOCS[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] = ( + "When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is " + "invoked without specifying color or brightness. ❌🌈 " + "This e.g., prevents adaptation when activating a scene. " + "If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. " + "Needs `take_over_control` enabled. 🕵️ " +) + CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False DOCS[CONF_PREFER_RGB_COLOR] = ( "Whether to prefer RGB color adjustment over " @@ -329,7 +341,6 @@ VALIDATION_TUPLES = [ ), (CONF_BRIGHTNESS_MODE_TIME_DARK, DEFAULT_BRIGHTNESS_MODE_TIME_DARK, int), (CONF_BRIGHTNESS_MODE_TIME_LIGHT, DEFAULT_BRIGHTNESS_MODE_TIME_LIGHT, int), - (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), (CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool), (CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool), ( @@ -338,6 +349,7 @@ VALIDATION_TUPLES = [ int_between(0, 365 * 24 * 60 * 60), # 1 year max ), (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), + (CONF_ADAPT_ONLY_ON_BARE_TURN_ON, DEFAULT_ADAPT_ONLY_ON_BARE_TURN_ON, bool), (CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool), (CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY, int_between(0, 10000)), (CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, cv.positive_float), diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index cece3da7..5e60ed79 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -45,10 +45,11 @@ "brightness_mode": "brightness_mode: Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", "brightness_mode_time_dark": "brightness_mode_time_dark: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", "brightness_mode_time_light": "brightness_mode_time_light: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.", - "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ ", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index b5bf65de..49d886fa 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -103,6 +103,7 @@ from .const import ( ATTR_ADAPT_COLOR, ATTR_ADAPTIVE_LIGHTING_MANAGER, CONF_ADAPT_DELAY, + CONF_ADAPT_ONLY_ON_BARE_TURN_ON, CONF_ADAPT_UNTIL_SLEEP, CONF_AUTORESET_CONTROL, CONF_BRIGHTNESS_MODE, @@ -876,15 +877,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._adapt_delay = data[CONF_ADAPT_DELAY] self._send_split_delay = data[CONF_SEND_SPLIT_DELAY] self._take_over_control = data[CONF_TAKE_OVER_CONTROL] - self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] - if not data[CONF_TAKE_OVER_CONTROL] and data[CONF_DETECT_NON_HA_CHANGES]: + if not data[CONF_TAKE_OVER_CONTROL] and ( + data[CONF_DETECT_NON_HA_CHANGES] or data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] + ): _LOGGER.warning( - "%s: Config mismatch: 'detect_non_ha_changes: true' " - "requires 'take_over_control' to be enabled. Adjusting config " + "%s: Config mismatch: `detect_non_ha_changes` or `adapt_only_on_bare_turn_on` " + "set to `true` requires `take_over_control` to be enabled. Adjusting config " "and continuing setup with `take_over_control: true`.", self._name, ) self._take_over_control = True + self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] + self._adapt_only_on_bare_turn_on = data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL] self._skip_redundant_commands = data[CONF_SKIP_REDUNDANT_COMMANDS] self._multi_light_intercept = data[CONF_MULTI_LIGHT_INTERCEPT] @@ -1446,13 +1450,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def _respond_to_off_to_on_event(self, entity_id: str, event: Event) -> None: assert not self.manager.is_proactively_adapting(event.context.id) + from_turn_on = self.manager._off_to_on_state_event_is_from_turn_on( + entity_id, + event, + ) if ( self._take_over_control and not self._detect_non_ha_changes - and not self.manager._off_to_on_state_event_is_from_turn_on( - entity_id, - event, - ) + and not from_turn_on ): # There is an edge case where 2 switches control the same light, e.g., # one for brightness and one for color. Now we will mark both switches @@ -1468,6 +1473,25 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self.manager.mark_as_manual_control(entity_id) return + if ( + self._take_over_control + and self._adapt_only_on_bare_turn_on + and from_turn_on + ): + service_data = self.manager.turn_on_event[entity_id].data[ATTR_SERVICE_DATA] + if self.manager._mark_manual_control_if_non_bare_turn_on( + entity_id, + service_data, + ): + _LOGGER.debug( + "Skipping responding to 'off' → 'on' event for '%s' with context.id='%s' because" + " we only adapt on bare `light.turn_on` events and not on service_data: '%s'", + entity_id, + event.context.id, + service_data, + ) + return + if self._adapt_delay > 0: await asyncio.sleep(self._adapt_delay) @@ -2070,6 +2094,14 @@ class AdaptiveLightingManager: # and of TOGGLE calls when toggling off. or self.hass.states.is_state(entity_id, STATE_ON) or self.manual_control.get(entity_id, False) + or ( + switch._take_over_control + and switch._adapt_only_on_bare_turn_on + and self._mark_manual_control_if_non_bare_turn_on( + entity_id, + data[CONF_PARAMS], + ) + ) ): _LOGGER.debug( "Switch is off or light is already on for entity_id='%s', skipped='%s'" @@ -2622,6 +2654,7 @@ class AdaptiveLightingManager: turn_on_event = self.turn_on_event.get(light) if ( turn_on_event is not None + and not self.is_proactively_adapting(turn_on_event.context.id) and not is_our_context(turn_on_event.context) and not force ): @@ -2855,6 +2888,21 @@ class AdaptiveLightingManager: ) return False + def _mark_manual_control_if_non_bare_turn_on( + self, + entity_id: str, + service_data: ServiceData, + ) -> bool: + _LOGGER.debug( + "_mark_manual_control_if_non_bare_turn_on: entity_id='%s', service_data='%s'", + entity_id, + service_data, + ) + if any(attr in service_data for attr in COLOR_ATTRS | BRIGHTNESS_ATTRS): + self.mark_as_manual_control(entity_id) + return True + return False + class _AsyncSingleShotTimer: def __init__(self, delay, callback) -> None: diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index c8e2c413..cbf87f96 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -46,10 +46,11 @@ "brightness_mode": "brightness_mode: Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", "brightness_mode_time_dark": "brightness_mode_time_dark: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", "brightness_mode_time_light": "brightness_mode_time_light: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.", - "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ ", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", diff --git a/tests/test_switch.py b/tests/test_switch.py index 1a25c7a1..692f0fde 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -88,6 +88,7 @@ from custom_components.adaptive_lighting.const import ( SERVICE_CHANGE_SWITCH_SETTINGS, SERVICE_SET_MANUAL_CONTROL, SLEEP_MODE_SWITCH, + CONF_ADAPT_ONLY_ON_BARE_TURN_ON, UNDO_UPDATE_LISTENER, ) from custom_components.adaptive_lighting.switch import ( @@ -561,9 +562,19 @@ async def test_manager_not_tracking_untracked_lights(hass): assert light not in switch.manager.lights -async def test_manual_control(hass): +@pytest.mark.parametrize("adapt_only_on_bare_turn_on", [True, False]) +@pytest.mark.parametrize("proactive_service_call_adaptation", [True, False]) +async def test_manual_control( + hass, adapt_only_on_bare_turn_on, proactive_service_call_adaptation +): """Test the 'manual control' tracking.""" - switch, (light, *_) = await setup_lights_and_switch(hass) + switch, (light, *_) = await setup_lights_and_switch( + hass, + { + CONF_ADAPT_ONLY_ON_BARE_TURN_ON: adapt_only_on_bare_turn_on, + INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: proactive_service_call_adaptation, + }, + ) assert switch._take_over_control assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON @@ -639,9 +650,14 @@ async def test_manual_control(hass): await change_manual_control(True) assert manual_control[ENTITY_LIGHT_1] await turn_light(False) + assert not manual_control[ENTITY_LIGHT_1], manual_control await turn_light(True, brightness=increased_brightness()) assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON - assert not manual_control[ENTITY_LIGHT_1], manual_control + if adapt_only_on_bare_turn_on: + # Marks as manually controlled beacuse we turned it on with brightness + assert manual_control[ENTITY_LIGHT_1], manual_control + else: + assert not manual_control[ENTITY_LIGHT_1], manual_control # Check that toggling (sleep mode) switch resets manual control for entity_id in [ENTITY_SWITCH, ENTITY_SLEEP_MODE_SWITCH]: From fec50ad5265064545dad62dddbb434d422b0da92 Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Sat, 5 Aug 2023 23:19:18 -0500 Subject: [PATCH 0629/1077] Adapt lights simultaneously instead of one by one (#529) * Update switch.py * Update test_switch.py * Revert "Update test_switch.py" This reverts commit 87ea5243b97bfa064c282382b29e09ed61917892. * remove unnecessary create_task * remove unneeded len() * Revert "remove unnecessary create_task" This reverts commit 2c5da6d73954e0ff3808b61092adbe55cdec1bb4. * use `hass.async_create_task` --------- Co-authored-by: Bas Nijholt Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/switch.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 49d886fa..b0aa7f8c 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1399,7 +1399,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): adapt_color = self.adapt_color_switch.is_on assert isinstance(adapt_brightness, bool) assert isinstance(adapt_color, bool) - + tasks = [] for light in filtered_lights: manually_controlled = ( self._take_over_control @@ -1446,7 +1446,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): transition, context.id, ) - await self._adapt_light(light, context, transition, force=force) + coro = self._adapt_light(light, context, transition, force=force) + task = self.hass.async_create_task( + coro, + ) + tasks.append(task) + if tasks: + await asyncio.gather(*tasks) async def _respond_to_off_to_on_event(self, entity_id: str, event: Event) -> None: assert not self.manager.is_proactively_adapting(event.context.id) From d2c6811e63af43135fc1bc4a5a3afea53c411461 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 5 Aug 2023 23:06:27 -0700 Subject: [PATCH 0630/1077] Bump to 1.19.0b2 in manifest.json (#710) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 6202a765..cc91f7e2 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.19.0b1" + "version": "1.19.0b2" } From cb9ae39ee4693a82deeb5e2b2bc80300fada7401 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Aug 2023 11:15:14 -0700 Subject: [PATCH 0631/1077] Fix adaptive_lighting.change_switch_settings service (#712) * Fix adaptive_lighting.change_switch_settings service Closes #623 * filter defaults * fix mutable * Revert debugging logs --- custom_components/adaptive_lighting/switch.py | 21 ++++++++++++------- tests/test_switch.py | 6 ++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index b0aa7f8c..88d94009 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -363,19 +363,19 @@ async def handle_change_switch_settings( ) -> None: """Allows HASS to change config values via a service call.""" data = service_call.data - which = data.get(CONF_USE_DEFAULTS, "current") if which == "current": # use whatever we're already using. defaults = switch._current_settings # pylint: disable=protected-access elif which == "factory": # use actual defaults listed in the documentation - defaults = {key: default for key, default, _ in VALIDATION_TUPLES} + defaults = None elif which == "configuration": # use whatever's in the config flow or configuration.yaml - defaults = switch._config_backup # pylint: disable=protected-access + defaults = switch._config_backup else: defaults = None - switch._set_changeable_settings(data=data, defaults=defaults) + # deep copy the defaults so we don't modify the original dicts + switch._set_changeable_settings(data=data, defaults=deepcopy(defaults)) switch._update_time_interval_listener() _LOGGER.debug( @@ -589,7 +589,7 @@ def validate( if defaults is None: data = {key: default for key, default, _ in VALIDATION_TUPLES} else: - data = defaults + data = deepcopy(defaults) if config_entry is not None: assert service_data is None @@ -598,7 +598,12 @@ def validate( data.update(config_entry.data) # all yaml settings come from data else: assert service_data is not None - data.update(service_data) + changed_settings = { + key: value + for key, value in service_data.items() + if key not in (CONF_USE_DEFAULTS, ATTR_ENTITY_ID) + } + data.update(changed_settings) data = {key: replace_none_str(value) for key, value in data.items()} for key, (validate_value, _) in EXTRA_VALIDATION.items(): value = data.get(key) @@ -843,8 +848,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): def _set_changeable_settings( self, - data: dict, - defaults: dict | None = None, + data: dict[str, Any], + defaults: dict[str, Any] | None = None, ): # Only pass settings users can change during runtime data = validate( diff --git a/tests/test_switch.py b/tests/test_switch.py index 692f0fde..79dc9b20 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1353,6 +1353,12 @@ async def test_change_switch_settings_service(hass): await change_switch_settings(**{CONF_USE_DEFAULTS: "current"}) assert switch._sun_light_settings.min_color_temp == 2000 + # testing with "configuration" and setting a new value + await change_switch_settings( + **{CONF_USE_DEFAULTS: "configuration", CONF_MIN_COLOR_TEMP: 3000} + ) + assert switch._sun_light_settings.min_color_temp == 3000 + # testing with "configuration" should revert back to 2500 await change_switch_settings(**{CONF_USE_DEFAULTS: "configuration"}) assert switch._sun_light_settings.min_color_temp == 2500 From 104664d3588442730f7ea9e811dd1986aca0c2e8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Aug 2023 11:21:54 -0700 Subject: [PATCH 0632/1077] Bump to 1.19.0b3 in manifest.json (#713) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index cc91f7e2..7a561884 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.19.0b2" + "version": "1.19.0b3" } From 943a6360c798dfbf59a348e21bb68b0316c078c3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Aug 2023 16:42:56 -0700 Subject: [PATCH 0633/1077] Add simple webapp to play with different parameters (#715) * Add simple webapp to play with different parameters * ignore --- .github/workflows/deploy-webapp.yml | 61 +++++++ .ruff.toml | 1 + webapp/app.py | 268 ++++++++++++++++++++++++++++ webapp/requirements.txt | 1 + 4 files changed, 331 insertions(+) create mode 100644 .github/workflows/deploy-webapp.yml create mode 100644 webapp/app.py create mode 100644 webapp/requirements.txt diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml new file mode 100644 index 00000000..7c4ac13e --- /dev/null +++ b/.github/workflows/deploy-webapp.yml @@ -0,0 +1,61 @@ +# Simple workflow for deploying WebAssembly app to GitHub Pages +name: Deploy WebAssembly app to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + pull_request: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set Up Python + uses: actions/setup-python@v2 + with: + python-version: 3.x + + - name: Install Dependencies + run: | + pip install -r webapp/requirements.txt + + - name: Build the WebAssembly app + run: | + shinylive export webapp site + + - name: Setup Pages + uses: actions/configure-pages@v3 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v2 + with: + # Upload the 'site' directory, where your app has been built + path: "site" + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 diff --git a/.ruff.toml b/.ruff.toml index 1cfeeb74..ead4d843 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -26,6 +26,7 @@ ignore = [ [per-file-ignores] "tests/*.py" = ["ALL"] ".github/*py" = ["INP001"] +"webapp/*py" = ["ALL"] [flake8-pytest-style] fixture-parentheses = false diff --git a/webapp/app.py b/webapp/app.py new file mode 100644 index 00000000..f99369cb --- /dev/null +++ b/webapp/app.py @@ -0,0 +1,268 @@ +"""Simple web app to visualize brightness over time.""" + +import math + +import matplotlib.pyplot as plt +import numpy as np +from shiny import App, render, ui + + +def lerp(x, x1, x2, y1, y2): + """Linearly interpolate between two values.""" + return y1 + (x - x1) * (y2 - y1) / (x2 - x1) + + +def clamp(value: float, minimum: float, maximum: float) -> float: + """Clamp value between minimum and maximum.""" + return max(minimum, min(value, maximum)) + + +def find_a_b(x1: float, x2: float, y1: float, y2: float) -> tuple[float, float]: + a = (math.atanh(2 * y2 - 1) - math.atanh(2 * y1 - 1)) / (x2 - x1) + b = x1 - (math.atanh(2 * y1 - 1) / a) + return a, b + + +def scaled_tanh( + x: float, + a: float, + b: float, + y_min: float = 0.0, + y_max: float = 1.0, +) -> float: + """Apply a scaled and shifted tanh function to a given input.""" + return y_min + (y_max - y_min) * 0.5 * (math.tanh(a * (x - b)) + 1) + + +def is_closer_to_sunrise_than_sunset(time, sunrise_time, sunset_time): + """Return True if the time is closer to sunrise than sunset.""" + return abs(time - sunrise_time) < abs(time - sunset_time) + + +def brightness_linear( + time, + sunrise_time, + sunset_time, + time_light, + time_dark, + max_brightness, + min_brightness, +): + """Calculate the brightness for the 'linear' mode.""" + closer_to_sunrise = is_closer_to_sunrise_than_sunset( + time, + sunrise_time, + sunset_time, + ) + if closer_to_sunrise: + brightness = lerp( + time, + x1=sunrise_time - time_dark, + x2=sunrise_time + time_light, + y1=min_brightness, + y2=max_brightness, + ) + else: + brightness = lerp( + time, + x1=sunset_time - time_light, + x2=sunset_time + time_dark, + y1=max_brightness, + y2=min_brightness, + ) + return clamp(brightness, min_brightness, max_brightness) + + +def brightness_tanh( + time, + sunrise_time, + sunset_time, + time_light, + time_dark, + max_brightness, + min_brightness, +): + """Calculate the brightness for the 'tanh' mode.""" + closer_to_sunrise = is_closer_to_sunrise_than_sunset( + time, + sunrise_time, + sunset_time, + ) + if closer_to_sunrise: + a, b = find_a_b( + x1=-time_dark, + x2=time_light, + y1=0.05, # be at 5% of range at x1 + y2=0.95, # be at 95% of range at x2 + ) + brightness = scaled_tanh( + time - sunrise_time, + a=a, + b=b, + y_min=min_brightness, + y_max=max_brightness, + ) + else: + a, b = find_a_b( + x1=-time_light, # shifted timestamp for the start of sunset + x2=time_dark, # shifted timestamp for the end of sunset + y1=0.95, # be at 95% of range at the start of sunset + y2=0.05, # be at 5% of range at the end of sunset + ) + brightness = scaled_tanh( + time - sunset_time, + a=a, + b=b, + y_min=min_brightness, + y_max=max_brightness, + ) + return clamp(brightness, min_brightness, max_brightness) + + +SEC_PER_HR = 60 * 60 + +# Shiny UI +app_ui = ui.page_fluid( + ui.layout_sidebar( + ui.panel_sidebar( + ui.input_slider("min_brightness", "min_brightness", 0, 100, 30, post="%"), + ui.input_slider("max_brightness", "max_brightness", 0, 100, 100, post="%"), + ui.input_slider( + "dark_time", + "brightness_mode_time_dark", + 0, + 5 * SEC_PER_HR, + 3 * SEC_PER_HR, + post=" sec", + ), + ui.input_slider( + "light_time", + "brightness_mode_time_light", + 0, + 5 * SEC_PER_HR, + 0.5 * SEC_PER_HR, + post=" sec", + ), + ui.input_slider( + "sunrise_time", + "sunrise_time", + 0, + 24, + 6, + step=0.5, + post=" hr", + ), + ui.input_slider( + "sunset_time", + "sunset_time", + 0, + 24, + 18, + step=0.5, + post=" hr", + ), + ), + ui.panel_main(ui.output_plot(id="brightness_plot")), + ), +) + + +def server(input, output, session): + @output + @render.plot + def brightness_plot(): + return plot_brightness( + min_brightness=input.min_brightness() / 100, + max_brightness=input.max_brightness() / 100, + brightness_mode_time_dark=input.dark_time() / SEC_PER_HR, + brightness_mode_time_light=input.light_time() / SEC_PER_HR, + sunrise_time=input.sunrise_time(), + sunset_time=input.sunset_time(), + ) + + +def plot_brightness( + min_brightness, + max_brightness, + brightness_mode_time_dark, + brightness_mode_time_light, + sunrise_time=6, # 6 AM + sunset_time=18, # 6 PM +): + # Define the time range for our simulation + time_range = np.linspace(0, 24, 1000) # From 0 to 24 hours + + # Calculate the brightness for each time in the time range for both modes + brightness_linear_values = [ + brightness_linear( + time, + sunrise_time, + sunset_time, + brightness_mode_time_light, + brightness_mode_time_dark, + max_brightness, + min_brightness, + ) + for time in time_range + ] + brightness_tanh_values = [ + brightness_tanh( + time, + sunrise_time, + sunset_time, + brightness_mode_time_light, + brightness_mode_time_dark, + max_brightness, + min_brightness, + ) + for time in time_range + ] + + # Plot the brightness over time for both modes + plt.figure(figsize=(10, 6)) + plt.plot(time_range, brightness_linear_values, label="Linear Mode") + plt.plot(time_range, brightness_tanh_values, label="Tanh Mode") + plt.vlines(sunrise_time, 0, 1, color="C2", label="Sunrise", linestyles="dashed") + plt.vlines(sunset_time, 0, 1, color="C3", label="Sunset", linestyles="dashed") + plt.xlim(0, 24) + plt.xticks(np.arange(0, 25, 1)) + yticks = np.arange(0, 1.05, 0.05) + ytick_labels = [f"{100*label:.0f}%" for label in yticks] + plt.yticks(yticks, ytick_labels) + plt.xlabel("Time (hours)") + plt.ylabel("Brightness") + plt.title("Brightness over Time for Different Modes") + + # Add text box + textstr = "\n".join( + ( + f"Sunrise Time = {sunrise_time}:00:00", + f"Sunset Time = {sunset_time}:00:00", + f"Max Brightness = {max_brightness*100:.0f}%", + f"Min Brightness = {min_brightness*100:.0f}%", + f"Time Light = {brightness_mode_time_light:.1f} hours", + f"Time Dark = {brightness_mode_time_dark:.1f} hours", + ), + ) + + # these are matplotlib.patch.Patch properties + props = {"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5} + + plt.legend() + plt.grid(True) + + # place a text box in upper left in axes coords + plt.gca().text( + 0.4, + 0.55, + textstr, + transform=plt.gca().transAxes, + fontsize=10, + verticalalignment="center", + bbox=props, + ) + + return plt.gcf() + + +app = App(app_ui, server) diff --git a/webapp/requirements.txt b/webapp/requirements.txt new file mode 100644 index 00000000..f832a57a --- /dev/null +++ b/webapp/requirements.txt @@ -0,0 +1 @@ +shinylive From 69355b8c3a50ebf00d0b02924f162d81d9562a75 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Aug 2023 17:20:58 -0700 Subject: [PATCH 0634/1077] Small WebApp improvements (#716) * Small WebApp improvements * Add link --- webapp/app.py | 140 ++++++++++++++++++++++++++++---------------------- 1 file changed, 78 insertions(+), 62 deletions(-) diff --git a/webapp/app.py b/webapp/app.py index f99369cb..208ea242 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -119,68 +119,6 @@ def brightness_tanh( return clamp(brightness, min_brightness, max_brightness) -SEC_PER_HR = 60 * 60 - -# Shiny UI -app_ui = ui.page_fluid( - ui.layout_sidebar( - ui.panel_sidebar( - ui.input_slider("min_brightness", "min_brightness", 0, 100, 30, post="%"), - ui.input_slider("max_brightness", "max_brightness", 0, 100, 100, post="%"), - ui.input_slider( - "dark_time", - "brightness_mode_time_dark", - 0, - 5 * SEC_PER_HR, - 3 * SEC_PER_HR, - post=" sec", - ), - ui.input_slider( - "light_time", - "brightness_mode_time_light", - 0, - 5 * SEC_PER_HR, - 0.5 * SEC_PER_HR, - post=" sec", - ), - ui.input_slider( - "sunrise_time", - "sunrise_time", - 0, - 24, - 6, - step=0.5, - post=" hr", - ), - ui.input_slider( - "sunset_time", - "sunset_time", - 0, - 24, - 18, - step=0.5, - post=" hr", - ), - ), - ui.panel_main(ui.output_plot(id="brightness_plot")), - ), -) - - -def server(input, output, session): - @output - @render.plot - def brightness_plot(): - return plot_brightness( - min_brightness=input.min_brightness() / 100, - max_brightness=input.max_brightness() / 100, - brightness_mode_time_dark=input.dark_time() / SEC_PER_HR, - brightness_mode_time_light=input.light_time() / SEC_PER_HR, - sunrise_time=input.sunrise_time(), - sunset_time=input.sunset_time(), - ) - - def plot_brightness( min_brightness, max_brightness, @@ -265,4 +203,82 @@ def plot_brightness( return plt.gcf() +SEC_PER_HR = 60 * 60 +desc = """ +**Experience the Dynamics of [Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) in Real-Time.** + +Have you ever wondered how the intricate settings of [Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) impact your home ambiance? The Adaptive Lighting Simulator WebApp is here to demystify just that. + +Harnessing the technology of the popular Adaptive Lighting integration for Home Assistant, this webapp provides a hands-on, visual platform to explore, tweak, and understand the myriad of parameters that dictate the behavior of your smart lights. Whether you're aiming for a subtle morning glow or a cozy evening warmth, observe firsthand how each tweak changes the ambiance. + +**Why Use the Simulator?** +- **Interactive Exploration**: No more guesswork. See in real-time how changes to settings influence the lighting dynamics. +- **Circadian Cycle Preview**: Understand how Adaptive Lighting adjusts throughout the day based on specific parameters, ensuring your lighting aligns with your circadian rhythms. +- **Tailored Testing**: Play with parameters and find the perfect combination that suits your personal or family's needs. +- **Educational Experience**: For both newbies and experts, delve deep into the intricacies of Adaptive Lighting's logic and potential. + +Dive into the simulator, experiment with different settings, and fine-tune the behavior of Adaptive Lighting to perfection. Whether you're setting it up for the first time or optimizing an existing setup, this tool ensures you get the most out of your smart lighting experience. +""" + +# Shiny UI +app_ui = ui.page_fluid( + ui.panel_title("🌞 Adaptive Lighting Simulator WebApp 🌛"), + ui.layout_sidebar( + ui.panel_sidebar( + ui.input_slider("min_brightness", "min_brightness", 0, 100, 30, post="%"), + ui.input_slider("max_brightness", "max_brightness", 0, 100, 100, post="%"), + ui.input_slider( + "dark_time", + "brightness_mode_time_dark", + 0, + 5 * SEC_PER_HR, + 3 * SEC_PER_HR, + post=" sec", + ), + ui.input_slider( + "light_time", + "brightness_mode_time_light", + 0, + 5 * SEC_PER_HR, + 0.5 * SEC_PER_HR, + post=" sec", + ), + ui.input_slider( + "sunrise_time", + "sunrise_time", + 0, + 24, + 6, + step=0.5, + post=" hr", + ), + ui.input_slider( + "sunset_time", + "sunset_time", + 0, + 24, + 18, + step=0.5, + post=" hr", + ), + ), + ui.panel_main(ui.markdown(desc), ui.output_plot(id="brightness_plot")), + ), +) + + +def server(input, output, session): + @output + @render.plot + def brightness_plot(): + return plot_brightness( + min_brightness=input.min_brightness() / 100, + max_brightness=input.max_brightness() / 100, + brightness_mode_time_dark=input.dark_time() / SEC_PER_HR, + brightness_mode_time_light=input.light_time() / SEC_PER_HR, + sunrise_time=input.sunrise_time(), + sunset_time=input.sunset_time(), + ) + + app = App(app_ui, server) From ef1546b6b93a47b51aa6f1379b938cbc071ad3dd Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Aug 2023 19:21:16 -0700 Subject: [PATCH 0635/1077] Fix min_sunrise_time and max_sunset_time (#717) Closes #714 --- custom_components/adaptive_lighting/const.py | 2 ++ custom_components/adaptive_lighting/switch.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index f3dc628d..0608d3da 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -377,10 +377,12 @@ EXTRA_VALIDATION = { CONF_INTERVAL: (cv.time_period, timedelta_as_int), CONF_SUNRISE_OFFSET: (cv.time_period, timedelta_as_int), CONF_SUNRISE_TIME: (cv.time, str), + CONF_MIN_SUNRISE_TIME: (cv.time, str), CONF_MAX_SUNRISE_TIME: (cv.time, str), CONF_SUNSET_OFFSET: (cv.time_period, timedelta_as_int), CONF_SUNSET_TIME: (cv.time, str), CONF_MIN_SUNSET_TIME: (cv.time, str), + CONF_MAX_SUNSET_TIME: (cv.time, str), CONF_BRIGHTNESS_MODE_TIME_LIGHT: (cv.time_period, timedelta_as_int), CONF_BRIGHTNESS_MODE_TIME_DARK: (cv.time_period, timedelta_as_int), } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 88d94009..07eb0a97 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1689,8 +1689,10 @@ class SunLightSettings: if ( self.sunrise_time is None and self.sunset_time is None + and self.min_sunrise_time is None and self.max_sunrise_time is None and self.min_sunset_time is None + and self.max_sunset_time is None ): solar_noon = location.noon(date, local=False) solar_midnight = location.midnight(date, local=False) From 00ec76a4678ba94cbb8855a06d7418954dc7530e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Aug 2023 19:21:39 -0700 Subject: [PATCH 0636/1077] Bump to 1.19.0b4 in manifest.json (#718) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 7a561884..3b6c7cf5 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.19.0b3" + "version": "1.19.0b4" } From 61d51cbf1e6486029b66253906248d27c473fbd6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 22:20:28 -0700 Subject: [PATCH 0637/1077] [pre-commit.ci] pre-commit autoupdate (#722) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.0.281 → v0.0.282](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.281...v0.0.282) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6e27d970..31fdf86c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.281 + rev: v0.0.282 hooks: - id: ruff args: ["--fix"] From f8e7880a968651e5d4bbede8dacfec8e20230052 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Aug 2023 14:31:17 -0700 Subject: [PATCH 0638/1077] Split up SunLightSettings and improve https://basnijholt.github.io/adaptive-lighting/ (#719) * Split up SunLightSettings * Renames * factor out SunEvents * more renames * rewrite * rewrite more * simpler * refactor * refact * raise * refact * rename * move method * clean * Move to new module 'sun.py' * make sun independent of HA * rename * Move to webapp/homeassistant_util_color.py * Rework app * Add link * new plotting * app changes * fix tests * test clean * tz fixes * fix * use sed * verbose * fix tz * fix * tiem --- .github/workflows/deploy-webapp.yml | 3 + .ruff.toml | 1 + README.md | 2 +- .../adaptive_lighting/color_and_brightness.py | 518 ++++++++++++ custom_components/adaptive_lighting/const.py | 2 - .../adaptive_lighting/helpers.py | 122 --- custom_components/adaptive_lighting/switch.py | 314 +------ tests/test_color_and_brightness.py | 209 +++++ tests/test_switch.py | 23 +- webapp/app.py | 400 ++++----- webapp/homeassistant_util_color.py | 773 ++++++++++++++++++ webapp/requirements.txt | 1 + 12 files changed, 1744 insertions(+), 624 deletions(-) create mode 100644 custom_components/adaptive_lighting/color_and_brightness.py create mode 100644 tests/test_color_and_brightness.py create mode 100644 webapp/homeassistant_util_color.py diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml index 7c4ac13e..04504522 100644 --- a/.github/workflows/deploy-webapp.yml +++ b/.github/workflows/deploy-webapp.yml @@ -45,6 +45,9 @@ jobs: - name: Build the WebAssembly app run: | + set -ex + cp custom_components/adaptive_lighting/color_and_brightness.py webapp/color_and_brightness.py + sed -i 's/homeassistant.util.color/homeassistant_util_color/g' "webapp/color_and_brightness.py" shinylive export webapp site - name: Setup Pages diff --git a/.ruff.toml b/.ruff.toml index ead4d843..8ece9b9c 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -27,6 +27,7 @@ ignore = [ "tests/*.py" = ["ALL"] ".github/*py" = ["INP001"] "webapp/*py" = ["ALL"] +"custom_components/adaptive_lighting/homeassistant_util_color.py" = ["ALL"] [flake8-pytest-style] fixture-parentheses = false diff --git a/README.md b/README.md index 56d9f05d..faa43e9b 100644 --- a/README.md +++ b/README.md @@ -441,7 +441,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark ![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/e5fc5d27-3c37-4e3d-93d1-6e7cf4b48e7c) ![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/3dcbdc42-63c4-49df-8651-d2fae53dd08d) -> [*Code to make the plots*](https://github.com/basnijholt/adaptive-lighting/pull/699#issuecomment-1666232555) +> Check out the interactive webapp on https://basnijholt.github.io/adaptive-lighting/ to play with the parameters and see how the brightness changes! ## :eyes: See also diff --git a/custom_components/adaptive_lighting/color_and_brightness.py b/custom_components/adaptive_lighting/color_and_brightness.py new file mode 100644 index 00000000..2968fcc0 --- /dev/null +++ b/custom_components/adaptive_lighting/color_and_brightness.py @@ -0,0 +1,518 @@ +"""Switch for the Adaptive Lighting integration.""" +from __future__ import annotations + +import bisect +import colorsys +import datetime +import logging +import math +from dataclasses import dataclass +from datetime import timedelta +from functools import cached_property, partial +from typing import TYPE_CHECKING, Any, Literal, cast + +from homeassistant.util.color import ( + color_RGB_to_xy, + color_temperature_to_rgb, + color_xy_to_hs, +) + +if TYPE_CHECKING: + import astral + +# Same as homeassistant.const.SUN_EVENT_SUNRISE and homeassistant.const.SUN_EVENT_SUNSET +# We re-define them here to not depend on homeassistant in this file. +SUN_EVENT_SUNRISE = "sunrise" +SUN_EVENT_SUNSET = "sunset" + +SUN_EVENT_NOON = "solar_noon" +SUN_EVENT_MIDNIGHT = "solar_midnight" + +_ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT) +_ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))} + +UTC = datetime.timezone.utc +utcnow: partial[datetime.datetime] = partial(datetime.datetime.now, UTC) +utcnow.__doc__ = "Get now in UTC time." + +_LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class SunEvents: + """Track the state of the sun and associated light settings.""" + + name: str + astral_location: astral.Location + sunrise_time: datetime.time | None + min_sunrise_time: datetime.time | None + max_sunrise_time: datetime.time | None + sunset_time: datetime.time | None + min_sunset_time: datetime.time | None + max_sunset_time: datetime.time | None + sunrise_offset: datetime.timedelta = datetime.timedelta() + sunset_offset: datetime.timedelta = datetime.timedelta() + timezone: datetime.tzinfo = UTC + + def sunrise(self, dt: datetime.date) -> datetime.datetime: + """Return the (adjusted) sunrise time for the given datetime.""" + sunrise = ( + self.astral_location.sunrise(dt, local=False) + if self.sunrise_time is None + else self._replace_time(dt, self.sunrise_time) + ) + self.sunrise_offset + if self.min_sunrise_time is not None: + min_sunrise = self._replace_time(dt, self.min_sunrise_time) + if min_sunrise > sunrise: + sunrise = min_sunrise + if self.max_sunrise_time is not None: + max_sunrise = self._replace_time(dt, self.max_sunrise_time) + if max_sunrise < sunrise: + sunrise = max_sunrise + return sunrise + + def sunset(self, dt: datetime.date) -> datetime.datetime: + """Return the (adjusted) sunset time for the given datetime.""" + sunset = ( + self.astral_location.sunset(dt, local=False) + if self.sunset_time is None + else self._replace_time(dt, self.sunset_time) + ) + self.sunset_offset + if self.min_sunset_time is not None: + min_sunset = self._replace_time(dt, self.min_sunset_time) + if min_sunset > sunset: + sunset = min_sunset + if self.max_sunset_time is not None: + max_sunset = self._replace_time(dt, self.max_sunset_time) + if max_sunset < sunset: + sunset = max_sunset + return sunset + + def _replace_time( + self, + dt: datetime.date, + time: datetime.time, + ) -> datetime.datetime: + date_time = datetime.datetime.combine(dt, time) + dt_with_tz = date_time.replace(tzinfo=self.timezone) + return dt_with_tz.astimezone(UTC) + + def noon_and_midnight( + self, + dt: datetime.datetime, + sunset: datetime.datetime | None = None, + sunrise: datetime.datetime | None = None, + ) -> tuple[datetime.datetime, datetime.datetime]: + """Return the (adjusted) noon and midnight times for the given datetime.""" + if ( + self.sunrise_time is None + and self.sunset_time is None + and self.min_sunrise_time is None + and self.max_sunrise_time is None + and self.min_sunset_time is None + and self.max_sunset_time is None + ): + solar_noon = self.astral_location.noon(dt, local=False) + solar_midnight = self.astral_location.midnight(dt, local=False) + return solar_noon, solar_midnight + + if sunset is None: + sunset = self.sunset(dt) + if sunrise is None: + sunrise = self.sunrise(dt) + + middle = abs(sunset - sunrise) / 2 + if sunset > sunrise: + noon = sunrise + middle + midnight = noon + timedelta(hours=12) * (1 if noon.hour < 12 else -1) + else: + midnight = sunset + middle + noon = midnight + timedelta(hours=12) * (1 if midnight.hour < 12 else -1) + return noon, midnight + + def sun_events(self, dt: datetime.datetime) -> list[tuple[str, float]]: + """Get the four sun event's timestamps at 'dt'.""" + sunrise = self.sunrise(dt) + sunset = self.sunset(dt) + solar_noon, solar_midnight = self.noon_and_midnight(dt, sunset, sunrise) + events = [ + (SUN_EVENT_SUNRISE, sunrise.timestamp()), + (SUN_EVENT_SUNSET, sunset.timestamp()), + (SUN_EVENT_NOON, solar_noon.timestamp()), + (SUN_EVENT_MIDNIGHT, solar_midnight.timestamp()), + ] + self._validate_sun_event_order(events) + return events + + def _validate_sun_event_order(self, events: list[tuple[str, float]]) -> None: + """Check if the sun events are in the expected order.""" + events = sorted(events, key=lambda x: x[1]) + events_names, _ = zip(*events, strict=True) + if events_names not in _ALLOWED_ORDERS: + msg = ( + f"{self.name}: The sun events {events_names} are not in the expected" + " order. The Adaptive Lighting integration will not work!" + " This might happen if your sunrise/sunset offset is too large or" + " your manually set sunrise/sunset time is past/before noon/midnight." + ) + _LOGGER.error(msg) + raise ValueError(msg) + + def prev_and_next_events(self, dt: datetime.datetime) -> list[tuple[str, float]]: + """Get the previous and next sun event.""" + events = [ + event + for days in [-1, 0, 1] + for event in self.sun_events(dt + timedelta(days=days)) + ] + events = sorted(events, key=lambda x: x[1]) + i_now = bisect.bisect([ts for _, ts in events], dt.timestamp()) + return events[i_now - 1 : i_now + 1] + + def sun_position(self, dt: datetime.datetime) -> float: + """Calculate the position of the sun, between [-1, 1].""" + target_ts = dt.timestamp() + (_, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt) + h, x = ( + (prev_ts, next_ts) + if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE) + else (next_ts, prev_ts) + ) + # k = -1 between sunset and sunrise (sun below horizon) + # k = 1 between sunrise and sunset (sun above horizon) + k = 1 if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_NOON) else -1 + return k * (1 - ((target_ts - h) / (h - x)) ** 2) + + def closest_event(self, dt: datetime.datetime) -> tuple[str, float]: + """Get the closest sunset or sunrise event.""" + (prev_event, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt) + if prev_event == SUN_EVENT_SUNRISE or next_event == SUN_EVENT_SUNRISE: + ts_event = prev_ts if prev_event == SUN_EVENT_SUNRISE else next_ts + return SUN_EVENT_SUNRISE, ts_event + if prev_event == SUN_EVENT_SUNSET or next_event == SUN_EVENT_SUNSET: + ts_event = prev_ts if prev_event == SUN_EVENT_SUNSET else next_ts + return SUN_EVENT_SUNSET, ts_event + msg = "No sunrise or sunset event found." + raise ValueError(msg) + + +@dataclass(frozen=True) +class SunLightSettings: + """Track the state of the sun and associated light settings.""" + + name: str + astral_location: astral.Location + adapt_until_sleep: bool + max_brightness: int + max_color_temp: int + min_brightness: int + min_color_temp: int + sleep_brightness: int + sleep_rgb_or_color_temp: Literal["color_temp", "rgb_color"] + sleep_color_temp: int + sleep_rgb_color: tuple[int, int, int] + sunrise_time: datetime.time | None + min_sunrise_time: datetime.time | None + max_sunrise_time: datetime.time | None + sunset_time: datetime.time | None + min_sunset_time: datetime.time | None + max_sunset_time: datetime.time | None + brightness_mode_time_dark: datetime.timedelta + brightness_mode_time_light: datetime.timedelta + brightness_mode: Literal["default", "linear", "tanh"] = "default" + sunrise_offset: datetime.timedelta = datetime.timedelta() + sunset_offset: datetime.timedelta = datetime.timedelta() + timezone: datetime.tzinfo = UTC + + @cached_property + def sun(self) -> SunEvents: + """Return the SunEvents object.""" + return SunEvents( + name=self.name, + astral_location=self.astral_location, + sunrise_time=self.sunrise_time, + sunrise_offset=self.sunrise_offset, + min_sunrise_time=self.min_sunrise_time, + max_sunrise_time=self.max_sunrise_time, + sunset_time=self.sunset_time, + sunset_offset=self.sunset_offset, + min_sunset_time=self.min_sunset_time, + max_sunset_time=self.max_sunset_time, + timezone=self.timezone, + ) + + def _brightness_pct_default(self, dt: datetime.datetime) -> float: + """Calculate the brightness percentage using the default method.""" + sun_position = self.sun.sun_position(dt) + if sun_position > 0: + return self.max_brightness + delta_brightness = self.max_brightness - self.min_brightness + return (delta_brightness * (1 + sun_position)) + self.min_brightness + + def _brightness_pct_tanh(self, dt: datetime.datetime) -> float: + event, ts_event = self.sun.closest_event(dt) + dark = self.brightness_mode_time_dark.total_seconds() + light = self.brightness_mode_time_light.total_seconds() + if event == SUN_EVENT_SUNRISE: + brightness = scaled_tanh( + dt.timestamp() - ts_event, + x1=-dark, + x2=+light, + y1=0.05, # be at 5% of range at x1 + y2=0.95, # be at 95% of range at x2 + y_min=self.min_brightness, + y_max=self.max_brightness, + ) + elif event == SUN_EVENT_SUNSET: + brightness = scaled_tanh( + dt.timestamp() - ts_event, + x1=-light, # shifted timestamp for the start of sunset + x2=+dark, # shifted timestamp for the end of sunset + y1=0.95, # be at 95% of range at the start of sunset + y2=0.05, # be at 5% of range at the end of sunset + y_min=self.min_brightness, + y_max=self.max_brightness, + ) + return clamp(brightness, self.min_brightness, self.max_brightness) + + def _brightness_pct_linear(self, dt: datetime.datetime) -> float: + event, ts_event = self.sun.closest_event(dt) + # at ts_event - dt_start, brightness == start_brightness + # at ts_event + dt_end, brightness == end_brightness + dark = self.brightness_mode_time_dark.total_seconds() + light = self.brightness_mode_time_light.total_seconds() + if event == SUN_EVENT_SUNRISE: + brightness = lerp( + dt.timestamp() - ts_event, + x1=-dark, + x2=+light, + y1=self.min_brightness, + y2=self.max_brightness, + ) + elif event == SUN_EVENT_SUNSET: + brightness = lerp( + dt.timestamp() - ts_event, + x1=-light, + x2=+dark, + y1=self.max_brightness, + y2=self.min_brightness, + ) + return clamp(brightness, self.min_brightness, self.max_brightness) + + def brightness_pct(self, dt: datetime.datetime, is_sleep: bool) -> float: + """Calculate the brightness in %.""" + if is_sleep: + return self.sleep_brightness + assert self.brightness_mode in ("default", "linear", "tanh") + if self.brightness_mode == "default": + return self._brightness_pct_default(dt) + if self.brightness_mode == "linear": + return self._brightness_pct_linear(dt) + if self.brightness_mode == "tanh": + return self._brightness_pct_tanh(dt) + return None + + def color_temp_kelvin(self, sun_position: float) -> int: + """Calculate the color temperature in Kelvin.""" + if sun_position > 0: + delta = self.max_color_temp - self.min_color_temp + ct = (delta * sun_position) + self.min_color_temp + return 5 * round(ct / 5) # round to nearest 5 + if sun_position == 0 or not self.adapt_until_sleep: + return self.min_color_temp + if self.adapt_until_sleep and sun_position < 0: + delta = abs(self.min_color_temp - self.sleep_color_temp) + ct = (delta * abs(1 + sun_position)) + self.sleep_color_temp + return 5 * round(ct / 5) # round to nearest 5 + msg = "Should not happen" + raise ValueError(msg) + + def brightness_and_color( + self, + dt: datetime.datetime, + is_sleep: bool, + ) -> dict[str, Any]: + """Calculate the brightness and color.""" + sun_position = self.sun.sun_position(dt) + rgb_color: tuple[float, float, float] + # Variable `force_rgb_color` is needed for RGB color after sunset (if enabled) + force_rgb_color = False + brightness_pct = self.brightness_pct(dt, is_sleep) + if is_sleep: + color_temp_kelvin = self.sleep_color_temp + rgb_color = self.sleep_rgb_color + elif ( + self.sleep_rgb_or_color_temp == "rgb_color" + and self.adapt_until_sleep + and sun_position < 0 + ): + # Feature requested in + # https://github.com/basnijholt/adaptive-lighting/issues/624 + # This will result in a perceptible jump in color at sunset and sunrise + # because the `color_temperature_to_rgb` function is not 100% accurate. + min_color_rgb = color_temperature_to_rgb(self.min_color_temp) + rgb_color = lerp_color_hsv( + min_color_rgb, + self.sleep_rgb_color, + sun_position, + ) + color_temp_kelvin = self.color_temp_kelvin(sun_position) + force_rgb_color = True + else: + color_temp_kelvin = self.color_temp_kelvin(sun_position) + rgb_color = color_temperature_to_rgb(color_temp_kelvin) + # backwards compatibility for versions < 1.3.1 - see #403 + color_temp_mired: float = math.floor(1000000 / color_temp_kelvin) + xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color) + hs_color: tuple[float, float] = color_xy_to_hs(*xy_color) + return { + "brightness_pct": brightness_pct, + "color_temp_kelvin": color_temp_kelvin, + "color_temp_mired": color_temp_mired, + "rgb_color": rgb_color, + "xy_color": xy_color, + "hs_color": hs_color, + "sun_position": sun_position, + "force_rgb_color": force_rgb_color, + } + + def get_settings( + self, + is_sleep, + transition, + ) -> dict[str, float | int | tuple[float, float] | tuple[float, float, float]]: + """Get all light settings. + + Calculating all values takes <0.5ms. + """ + dt = utcnow() + timedelta(seconds=transition or 0) + return self.brightness_and_color(dt, is_sleep) + + +def find_a_b(x1: float, x2: float, y1: float, y2: float) -> tuple[float, float]: + """Compute the values of 'a' and 'b' for a scaled and shifted tanh function. + + Given two points (x1, y1) and (x2, y2), this function calculates the coefficients 'a' and 'b' + for a tanh function of the form y = 0.5 * (tanh(a * (x - b)) + 1) that passes through these points. + + The derivation is as follows: + + 1. Start with the equation of the tanh function: + y = 0.5 * (tanh(a * (x - b)) + 1) + + 2. Rearrange the equation to isolate tanh: + tanh(a * (x - b)) = 2*y - 1 + + 3. Take the inverse tanh (or artanh) on both sides to solve for 'a' and 'b': + a * (x - b) = artanh(2*y - 1) + + 4. Plug in the points (x1, y1) and (x2, y2) to get two equations. + Using these, we can solve for 'a' and 'b' as: + a = (artanh(2*y2 - 1) - artanh(2*y1 - 1)) / (x2 - x1) + b = x1 - (artanh(2*y1 - 1) / a) + + Parameters + ---------- + x1 + x-coordinate of the first point. + x2 + x-coordinate of the second point. + y1 + y-coordinate of the first point (should be between 0 and 1). + y2 + y-coordinate of the second point (should be between 0 and 1). + + Returns + ------- + a + Coefficient 'a' for the tanh function. + b + Coefficient 'b' for the tanh function. + + Notes + ----- + The values of y1 and y2 should lie between 0 and 1, inclusive. + """ + a = (math.atanh(2 * y2 - 1) - math.atanh(2 * y1 - 1)) / (x2 - x1) + b = x1 - (math.atanh(2 * y1 - 1) / a) + return a, b + + +def scaled_tanh( + x: float, + x1: float, + x2: float, + y1: float = 0.05, + y2: float = 0.95, + y_min: float = 0.0, + y_max: float = 100.0, +) -> float: + """Apply a scaled and shifted tanh function to a given input. + + This function represents a transformation of the tanh function that scales and shifts + the output to lie between y_min and y_max. For values of 'x' close to 'x1' and 'x2' + (used to calculate 'a' and 'b'), the output of this function will be close to 'y_min' + and 'y_max', respectively. + + The equation of the function is as follows: + y = y_min + (y_max - y_min) * 0.5 * (tanh(a * (x - b)) + 1) + + Parameters + ---------- + x + The input to the function. + x1 + x-coordinate of the first point. + x2 + x-coordinate of the second point. + y1 + y-coordinate of the first point (should be between 0 and 1). Defaults to 0.05. + y2 + y-coordinate of the second point (should be between 0 and 1). Defaults to 0.95. + y_min + The minimum value of the output range. Defaults to 0. + y_max + The maximum value of the output range. Defaults to 100. + + Returns + ------- + float: The output of the function, which lies in the range [y_min, y_max]. + """ + a, b = find_a_b(x1, x2, y1, y2) + return y_min + (y_max - y_min) * 0.5 * (math.tanh(a * (x - b)) + 1) + + +def lerp_color_hsv( + rgb1: tuple[float, float, float], + rgb2: tuple[float, float, float], + t: float, +) -> tuple[int, int, int]: + """Linearly interpolate between two RGB colors in HSV color space.""" + t = abs(t) + assert 0 <= t <= 1 + + # Convert RGB to HSV + hsv1 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb1]) + hsv2 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb2]) + + # Linear interpolation in HSV space + hsv = ( + hsv1[0] + t * (hsv2[0] - hsv1[0]), + hsv1[1] + t * (hsv2[1] - hsv1[1]), + hsv1[2] + t * (hsv2[2] - hsv1[2]), + ) + + # Convert back to RGB + rgb = tuple(int(round(x * 255)) for x in colorsys.hsv_to_rgb(*hsv)) + assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}" + return cast(tuple[int, int, int], rgb) + + +def lerp(x, x1, x2, y1, y2): + """Linearly interpolate between two values.""" + return y1 + (x - x1) * (y2 - y1) / (x2 - x1) + + +def clamp(value: float, minimum: float, maximum: float) -> float: + """Clamp value between minimum and maximum.""" + return max(minimum, min(value, maximum)) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 0608d3da..c747528d 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -12,8 +12,6 @@ ICON_COLOR_TEMP = "mdi:sun-thermometer" ICON_SLEEP = "mdi:sleep" DOMAIN = "adaptive_lighting" -SUN_EVENT_NOON = "solar_noon" -SUN_EVENT_MIDNIGHT = "solar_midnight" DOCS = {CONF_ENTITY_ID: "Entity ID of the switch. 📝"} diff --git a/custom_components/adaptive_lighting/helpers.py b/custom_components/adaptive_lighting/helpers.py index 95c87735..2e7ba23e 100644 --- a/custom_components/adaptive_lighting/helpers.py +++ b/custom_components/adaptive_lighting/helpers.py @@ -3,12 +3,7 @@ from __future__ import annotations import base64 -import colorsys -import logging import math -from typing import cast - -_LOGGER = logging.getLogger(__name__) def clamp(value: float, minimum: float, maximum: float) -> float: @@ -16,123 +11,6 @@ def clamp(value: float, minimum: float, maximum: float) -> float: return max(minimum, min(value, maximum)) -def find_a_b(x1: float, x2: float, y1: float, y2: float) -> tuple[float, float]: - """Compute the values of 'a' and 'b' for a scaled and shifted tanh function. - - Given two points (x1, y1) and (x2, y2), this function calculates the coefficients 'a' and 'b' - for a tanh function of the form y = 0.5 * (tanh(a * (x - b)) + 1) that passes through these points. - - The derivation is as follows: - - 1. Start with the equation of the tanh function: - y = 0.5 * (tanh(a * (x - b)) + 1) - - 2. Rearrange the equation to isolate tanh: - tanh(a * (x - b)) = 2*y - 1 - - 3. Take the inverse tanh (or artanh) on both sides to solve for 'a' and 'b': - a * (x - b) = artanh(2*y - 1) - - 4. Plug in the points (x1, y1) and (x2, y2) to get two equations. - Using these, we can solve for 'a' and 'b' as: - a = (artanh(2*y2 - 1) - artanh(2*y1 - 1)) / (x2 - x1) - b = x1 - (artanh(2*y1 - 1) / a) - - Parameters - ---------- - x1 - x-coordinate of the first point. - x2 - x-coordinate of the second point. - y1 - y-coordinate of the first point (should be between 0 and 1). - y2 - y-coordinate of the second point (should be between 0 and 1). - - Returns - ------- - a - Coefficient 'a' for the tanh function. - b - Coefficient 'b' for the tanh function. - - Notes - ----- - The values of y1 and y2 should lie between 0 and 1, inclusive. - """ - a = (math.atanh(2 * y2 - 1) - math.atanh(2 * y1 - 1)) / (x2 - x1) - b = x1 - (math.atanh(2 * y1 - 1) / a) - return a, b - - -def scaled_tanh( - x: float, - a: float, - b: float, - y_min: float = 0.0, - y_max: float = 100.0, -) -> float: - """Apply a scaled and shifted tanh function to a given input. - - This function represents a transformation of the tanh function that scales and shifts - the output to lie between y_min and y_max. For values of 'x' close to 'x1' and 'x2' - (used to calculate 'a' and 'b'), the output of this function will be close to 'y_min' - and 'y_max', respectively. - - The equation of the function is as follows: - y = y_min + (y_max - y_min) * 0.5 * (tanh(a * (x - b)) + 1) - - Parameters - ---------- - x - The input to the function. - a - The scale factor for the tanh function, found using 'find_a_b' function. - b - The shift factor for the tanh function, found using 'find_a_b' function. - y_min - The minimum value of the output range. Defaults to 0. - y_max - The maximum value of the output range. Defaults to 100. - - Returns - ------- - float: The output of the function, which lies in the range [y_min, y_max]. - """ - return y_min + (y_max - y_min) * 0.5 * (math.tanh(a * (x - b)) + 1) - - -def lerp_color_hsv( - rgb1: tuple[float, float, float], - rgb2: tuple[float, float, float], - t: float, -) -> tuple[int, int, int]: - """Linearly interpolate between two RGB colors in HSV color space.""" - t = abs(t) - assert 0 <= t <= 1 - - # Convert RGB to HSV - hsv1 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb1]) - hsv2 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb2]) - - # Linear interpolation in HSV space - hsv = ( - hsv1[0] + t * (hsv2[0] - hsv1[0]), - hsv1[1] + t * (hsv2[1] - hsv1[1]), - hsv1[2] + t * (hsv2[2] - hsv1[2]), - ) - - # Convert back to RGB - rgb = tuple(int(round(x * 255)) for x in colorsys.hsv_to_rgb(*hsv)) - assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}" - return cast(tuple[int, int, int], rgb) - - -def lerp(x, x1, x2, y1, y2): - """Linearly interpolate between two values.""" - return y1 + (x - x1) * (y2 - y1) / (x2 - x1) - - def int_to_base36(num: int) -> str: """Convert an integer to its base-36 representation using numbers and uppercase letters. diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 07eb0a97..35b1d7e5 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2,12 +2,10 @@ from __future__ import annotations import asyncio -import bisect import datetime import logging -import math +import zoneinfo from copy import deepcopy -from dataclasses import dataclass from datetime import timedelta from typing import TYPE_CHECKING, Any, Literal @@ -60,8 +58,6 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_OFF, STATE_ON, - SUN_EVENT_SUNRISE, - SUN_EVENT_SUNSET, ) from homeassistant.core import ( CALLBACK_TYPE, @@ -83,9 +79,7 @@ from homeassistant.helpers.template import area_entities from homeassistant.loader import bind_hass from homeassistant.util import slugify from homeassistant.util.color import ( - color_RGB_to_xy, color_temperature_to_rgb, - color_xy_to_hs, color_xy_to_RGB, ) @@ -96,6 +90,7 @@ from .adaptation_utils import ( ServiceData, prepare_adaptation_data, ) +from .color_and_brightness import SunLightSettings from .const import ( ADAPT_BRIGHTNESS_SWITCH, ADAPT_COLOR_SWITCH, @@ -153,8 +148,6 @@ from .const import ( SERVICE_SET_MANUAL_CONTROL, SET_MANUAL_CONTROL_SCHEMA, SLEEP_MODE_SWITCH, - SUN_EVENT_MIDNIGHT, - SUN_EVENT_NOON, TURNING_OFF_DELAY, VALIDATION_TUPLES, apply_service_schema, @@ -164,19 +157,14 @@ from .hass_utils import setup_service_call_interceptor from .helpers import ( clamp, color_difference_redmean, - find_a_b, int_to_base36, - lerp, - lerp_color_hsv, remove_vowels, - scaled_tanh, short_hash, ) if TYPE_CHECKING: from collections.abc import Callable, Coroutine, Iterable - import astral from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -187,8 +175,6 @@ _SUPPORT_OPTS = { "transition": SUPPORT_TRANSITION, } -_ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT) -_ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))} _LOGGER = logging.getLogger(__name__) @@ -923,7 +909,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): brightness_mode=data[CONF_BRIGHTNESS_MODE], brightness_mode_time_dark=data[CONF_BRIGHTNESS_MODE_TIME_DARK], brightness_mode_time_light=data[CONF_BRIGHTNESS_MODE_TIME_LIGHT], - transition=data[CONF_TRANSITION], + timezone=zoneinfo.ZoneInfo(self.hass.config.time_zone), ) _LOGGER.debug( "%s: Set switch settings for lights '%s'. now using data: '%s'", @@ -1595,300 +1581,6 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): self._state = False -@dataclass(frozen=True) -class SunLightSettings: - """Track the state of the sun and associated light settings.""" - - name: str - astral_location: astral.Location - adapt_until_sleep: bool - max_brightness: int - max_color_temp: int - min_brightness: int - min_color_temp: int - sleep_brightness: int - sleep_rgb_or_color_temp: Literal["color_temp", "rgb_color"] - sleep_color_temp: int - sleep_rgb_color: tuple[int, int, int] - sunrise_time: datetime.time | None - sunrise_offset: datetime.timedelta | None - min_sunrise_time: datetime.time | None - max_sunrise_time: datetime.time | None - sunset_time: datetime.time | None - sunset_offset: datetime.timedelta | None - min_sunset_time: datetime.time | None - max_sunset_time: datetime.time | None - brightness_mode: Literal["default", "linear", "tanh"] - brightness_mode_time_dark: datetime.timedelta | None - brightness_mode_time_light: datetime.timedelta | None - transition: int - - def sunrise(self, date: datetime.datetime) -> datetime.datetime: - """Return the (adjusted) sunrise time for the given date.""" - sunrise = ( - self.astral_location.sunrise(date, local=False) - if self.sunrise_time is None - else self._replace_time(date, "sunrise") - ) + self.sunrise_offset - if self.min_sunrise_time is not None: - min_sunrise = self._replace_time(date, "min_sunrise") - if min_sunrise > sunrise: - sunrise = min_sunrise - if self.max_sunrise_time is not None: - max_sunrise = self._replace_time(date, "max_sunrise") - if max_sunrise < sunrise: - sunrise = max_sunrise - return sunrise - - def sunset(self, date: datetime.datetime) -> datetime.datetime: - """Return the (adjusted) sunset time for the given date.""" - sunset = ( - self.astral_location.sunset(date, local=False) - if self.sunset_time is None - else self._replace_time(date, "sunset") - ) + self.sunset_offset - if self.min_sunset_time is not None: - min_sunset = self._replace_time(date, "min_sunset") - if min_sunset > sunset: - sunset = min_sunset - if self.max_sunset_time is not None: - max_sunset = self._replace_time(date, "max_sunset") - if max_sunset < sunset: - sunset = max_sunset - return sunset - - def _replace_time(self, date: datetime.datetime, key: str) -> datetime.datetime: - time = getattr(self, f"{key}_time") - date_time = datetime.datetime.combine(date, time) - return date_time.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone( - dt_util.UTC, - ) - - def get_sun_events(self, date: datetime.datetime) -> list[tuple[str, float]]: - """Get the four sun event's timestamps at 'date'.""" - - def calculate_noon_and_midnight( - sunset: datetime.datetime, - sunrise: datetime.datetime, - ) -> tuple[datetime.datetime, datetime.datetime]: - middle = abs(sunset - sunrise) / 2 - if sunset > sunrise: - noon = sunrise + middle - midnight = noon + timedelta(hours=12) * (1 if noon.hour < 12 else -1) - else: - midnight = sunset + middle - noon = midnight + timedelta(hours=12) * ( - 1 if midnight.hour < 12 else -1 - ) - return noon, midnight - - location = self.astral_location - sunrise = self.sunrise(date) - sunset = self.sunset(date) - - if ( - self.sunrise_time is None - and self.sunset_time is None - and self.min_sunrise_time is None - and self.max_sunrise_time is None - and self.min_sunset_time is None - and self.max_sunset_time is None - ): - solar_noon = location.noon(date, local=False) - solar_midnight = location.midnight(date, local=False) - else: - solar_noon, solar_midnight = calculate_noon_and_midnight(sunset, sunrise) - - events = [ - (SUN_EVENT_SUNRISE, sunrise.timestamp()), - (SUN_EVENT_SUNSET, sunset.timestamp()), - (SUN_EVENT_NOON, solar_noon.timestamp()), - (SUN_EVENT_MIDNIGHT, solar_midnight.timestamp()), - ] - # Check whether order is correct - events = sorted(events, key=lambda x: x[1]) - events_names, _ = zip(*events, strict=True) - if events_names not in _ALLOWED_ORDERS: - msg = ( - f"{self.name}: The sun events {events_names} are not in the expected" - " order. The Adaptive Lighting integration will not work!" - " This might happen if your sunrise/sunset offset is too large or" - " your manually set sunrise/sunset time is past/before noon/midnight." - ) - _LOGGER.error(msg) - raise ValueError(msg) - - return events - - def relevant_events(self, now: datetime.datetime) -> list[tuple[str, float]]: - """Get the previous and next sun event.""" - events = [ - event - for days in [-1, 0, 1] - for event in self.get_sun_events(now + timedelta(days=days)) - ] - events = sorted(events, key=lambda x: x[1]) - i_now = bisect.bisect([ts for _, ts in events], now.timestamp()) - return events[i_now - 1 : i_now + 1] - - def calc_percent(self, transition: int) -> float: - """Calculate the position of the sun in %.""" - now = dt_util.utcnow() - - target_time = now + timedelta(seconds=transition) - target_ts = target_time.timestamp() - today = self.relevant_events(target_time) - (_, prev_ts), (next_event, next_ts) = today - h, x = ( # pylint: disable=invalid-name - (prev_ts, next_ts) - if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE) - else (next_ts, prev_ts) - ) - k = 1 if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_NOON) else -1 - return (0 - k) * ((target_ts - h) / (h - x)) ** 2 + k - - def calc_brightness_pct(self, percent: float, is_sleep: bool) -> float: - """Calculate the brightness in %.""" - if is_sleep: - return self.sleep_brightness - assert self.brightness_mode in ("default", "linear", "tanh") - - if self.brightness_mode == "default": - if percent > 0: - return self.max_brightness - delta_brightness = self.max_brightness - self.min_brightness - percent = 1 + percent - return (delta_brightness * percent) + self.min_brightness - - now = dt_util.utcnow() - (prev_event, prev_ts), (next_event, next_ts) = self.relevant_events(now) - - # at ts_event - dt_start, brightness == start_brightness - # at ts_event + dt_end, brightness == end_brightness - dark = (self.brightness_mode_time_dark or timedelta()).total_seconds() - light = (self.brightness_mode_time_light or timedelta()).total_seconds() - # Handle sunrise - if prev_event == SUN_EVENT_SUNRISE or next_event == SUN_EVENT_SUNRISE: - ts_event = prev_ts if prev_event == SUN_EVENT_SUNRISE else next_ts - if self.brightness_mode == "linear": - brightness = lerp( - now.timestamp(), - x1=ts_event - dark, - x2=ts_event + light, - y1=self.min_brightness, - y2=self.max_brightness, - ) - else: - assert self.brightness_mode == "tanh" - a, b = find_a_b( - x1=-dark, - x2=+light, - y1=0.05, # be at 5% of range at x1 - y2=0.95, # be at 95% of range at x2 - ) - brightness = scaled_tanh( - now.timestamp() - ts_event, - a=a, - b=b, - y_min=self.min_brightness, - y_max=self.max_brightness, - ) - # Handle sunset - elif prev_event == SUN_EVENT_SUNSET or next_event == SUN_EVENT_SUNSET: - ts_event = prev_ts if prev_event == SUN_EVENT_SUNSET else next_ts - if self.brightness_mode == "linear": - brightness = lerp( - now.timestamp(), - x1=ts_event - light, - x2=ts_event + dark, - y1=self.max_brightness, - y2=self.min_brightness, - ) - else: - assert self.brightness_mode == "tanh" - a, b = find_a_b( - x1=-light, # shifted timestamp for the start of sunset - x2=+dark, # shifted timestamp for the end of sunset - y1=0.95, # be at 95% of range at the start of sunset - y2=0.05, # be at 5% of range at the end of sunset - ) - brightness = scaled_tanh( - now.timestamp() - ts_event, - a=a, - b=b, - y_min=self.min_brightness, - y_max=self.max_brightness, - ) - return clamp(brightness, self.min_brightness, self.max_brightness) - - def calc_color_temp_kelvin(self, percent: float) -> int: - """Calculate the color temperature in Kelvin.""" - if percent > 0: - delta = self.max_color_temp - self.min_color_temp - ct = (delta * percent) + self.min_color_temp - return 5 * round(ct / 5) # round to nearest 5 - if percent == 0 or not self.adapt_until_sleep: - return self.min_color_temp - if self.adapt_until_sleep and percent < 0: - delta = abs(self.min_color_temp - self.sleep_color_temp) - ct = (delta * abs(1 + percent)) + self.sleep_color_temp - return 5 * round(ct / 5) # round to nearest 5 - msg = "Should not happen" - raise ValueError(msg) - - def get_settings( - self, - is_sleep, - transition, - ) -> dict[str, float | int | tuple[float, float] | tuple[float, float, float]]: - """Get all light settings. - - Calculating all values takes <0.5ms. - """ - percent = ( - self.calc_percent(transition) - if transition is not None - else self.calc_percent(0) - ) - rgb_color: tuple[float, float, float] - # Variable `force_rgb_color` is needed for RGB color after sunset (if enabled) - force_rgb_color = False - brightness_pct = self.calc_brightness_pct(percent, is_sleep) - if is_sleep: - color_temp_kelvin = self.sleep_color_temp - rgb_color = self.sleep_rgb_color - elif ( - self.sleep_rgb_or_color_temp == "rgb_color" - and self.adapt_until_sleep - and percent < 0 - ): - # Feature requested in - # https://github.com/basnijholt/adaptive-lighting/issues/624 - # This will result in a perceptible jump in color at sunset and sunrise - # because the `color_temperature_to_rgb` function is not 100% accurate. - min_color_rgb = color_temperature_to_rgb(self.min_color_temp) - rgb_color = lerp_color_hsv(min_color_rgb, self.sleep_rgb_color, percent) - color_temp_kelvin = self.calc_color_temp_kelvin(percent) - force_rgb_color = True - else: - color_temp_kelvin = self.calc_color_temp_kelvin(percent) - rgb_color = color_temperature_to_rgb(color_temp_kelvin) - # backwards compatibility for versions < 1.3.1 - see #403 - color_temp_mired: float = math.floor(1000000 / color_temp_kelvin) - xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color) - hs_color: tuple[float, float] = color_xy_to_hs(*xy_color) - return { - "brightness_pct": brightness_pct, - "color_temp_kelvin": color_temp_kelvin, - "color_temp_mired": color_temp_mired, - "rgb_color": rgb_color, - "xy_color": xy_color, - "hs_color": hs_color, - "sun_position": percent, - "force_rgb_color": force_rgb_color, - } - - class AdaptiveLightingManager: """Track 'light.turn_off' and 'light.turn_on' service calls.""" diff --git a/tests/test_color_and_brightness.py b/tests/test_color_and_brightness.py new file mode 100644 index 00000000..a199f939 --- /dev/null +++ b/tests/test_color_and_brightness.py @@ -0,0 +1,209 @@ +import pytest +from custom_components.adaptive_lighting.color_and_brightness import ( + SunEvents, + SUN_EVENT_SUNRISE, + SUN_EVENT_NOON, +) +import datetime as dt +from astral import LocationInfo +from astral.location import Location +import zoneinfo + +# Create a mock astral_location object +location = Location(LocationInfo()) + +LAT_LONG_TZS = [ + (52.379189, 4.899431, "Europe/Amsterdam"), + (32.87336, -117.22743, "US/Pacific"), + (60, 50, "GMT"), + (60, 50, "UTC"), +] + + +@pytest.fixture(params=LAT_LONG_TZS) +def tzinfo_and_location(request): + lat, long, timezone = request.param + tzinfo = zoneinfo.ZoneInfo(timezone) + location = Location( + LocationInfo( + name="name", + region="region", + timezone=timezone, + latitude=lat, + longitude=long, + ) + ) + return tzinfo, location + + +def test_replace_time(tzinfo_and_location): + tzinfo, location = tzinfo_and_location + sun_events = SunEvents( + name="test", + astral_location=location, + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + timezone=tzinfo, + ) + + new_time = dt.time(5, 30) + datetime = dt.datetime(2022, 1, 1) + replaced_time_utc = sun_events._replace_time(datetime.date(), new_time) + assert replaced_time_utc.astimezone(tzinfo).time() == new_time + + +def test_sunrise_without_offset(tzinfo_and_location): + tzinfo, location = tzinfo_and_location + + sun_events = SunEvents( + name="test", + astral_location=location, + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + timezone=tzinfo, + ) + date = dt.datetime(2022, 1, 1).date() + result = sun_events.sunrise(date) + assert result == location.sunrise(date) + + +def test_sun_position_no_fixed_sunset_and_sunrise(tzinfo_and_location): + tzinfo, location = tzinfo_and_location + sun_events = SunEvents( + name="test", + astral_location=location, + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + timezone=tzinfo, + ) + date = dt.datetime(2022, 1, 1).date() + sunset = location.sunset(date) + position = sun_events.sun_position(sunset) + assert position == 0 + sunrise = location.sunrise(date) + position = sun_events.sun_position(sunrise) + assert position == 0 + noon = location.noon(date) + position = sun_events.sun_position(noon) + assert position == 1 + midnight = location.midnight(date) + position = sun_events.sun_position(midnight) + assert position == -1 + + +def test_sun_position_fixed_sunset_and_sunrise(tzinfo_and_location): + tzinfo, location = tzinfo_and_location + sun_events = SunEvents( + name="test", + astral_location=location, + sunrise_time=dt.time(6, 0), + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=dt.time(18, 0), + min_sunset_time=None, + max_sunset_time=None, + timezone=tzinfo, + ) + date = dt.datetime(2022, 1, 1).date() + sunset = sun_events.sunset(date) + position = sun_events.sun_position(sunset) + assert position == 0 + sunrise = sun_events.sunrise(date) + position = sun_events.sun_position(sunrise) + assert position == 0 + noon, midnight = sun_events.noon_and_midnight(date) + position = sun_events.sun_position(noon) + assert position == 1 + position = sun_events.sun_position(midnight) + assert position == -1 + + +def test_noon_and_midnight(tzinfo_and_location): + tzinfo, location = tzinfo_and_location + sun_events = SunEvents( + name="test", + astral_location=location, + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + timezone=tzinfo, + ) + date = dt.datetime(2022, 1, 1) + noon, midnight = sun_events.noon_and_midnight(date) + assert noon == location.noon(date) + assert midnight == location.midnight(date) + + +def test_sun_events(tzinfo_and_location): + tzinfo, location = tzinfo_and_location + sun_events = SunEvents( + name="test", + astral_location=location, + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + timezone=tzinfo, + ) + + date = dt.datetime(2022, 1, 1) + events = sun_events.sun_events(date) + assert len(events) == 4 + assert (SUN_EVENT_SUNRISE, location.sunrise(date).timestamp()) in events + + +def test_prev_and_next_events(tzinfo_and_location): + tzinfo, location = tzinfo_and_location + sun_events = SunEvents( + name="test", + astral_location=location, + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + timezone=tzinfo, + ) + datetime = dt.datetime(2022, 1, 1, 10, 0) + after_sunrise = sun_events.sunrise(datetime.date()) + dt.timedelta(hours=1) + prev_event, next_event = sun_events.prev_and_next_events(after_sunrise) + assert prev_event[0] == SUN_EVENT_SUNRISE + assert next_event[0] == SUN_EVENT_NOON + + +def test_closest_event(tzinfo_and_location): + tzinfo, location = tzinfo_and_location + sun_events = SunEvents( + name="test", + astral_location=location, + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + timezone=tzinfo, + ) + datetime = dt.datetime(2022, 1, 1, 6, 0) + sunrise = sun_events.sunrise(datetime.date()) + event_name, ts = sun_events.closest_event(sunrise) + assert event_name == SUN_EVENT_SUNRISE + assert ts == location.sunrise(sunrise.date()).timestamp() diff --git a/tests/test_switch.py b/tests/test_switch.py index 79dc9b20..6ffd1a21 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -100,8 +100,8 @@ from custom_components.adaptive_lighting.switch import ( AdaptiveLightingManager, is_our_context, is_our_context_id, - lerp_color_hsv, ) +from custom_components.adaptive_lighting.color_and_brightness import lerp_color_hsv _LOGGER = logging.getLogger(__name__) @@ -394,6 +394,7 @@ async def test_adaptive_lighting_time_zones_and_sun_settings( min_color_temp = switch._sun_light_settings.min_color_temp sunset = SUNSET.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) + before_sunset = sunset - datetime.timedelta(hours=1) after_sunset = sunset + datetime.timedelta(hours=1) sunrise = SUNRISE.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC) @@ -401,7 +402,10 @@ async def test_adaptive_lighting_time_zones_and_sun_settings( after_sunrise = sunrise + datetime.timedelta(hours=1) async def patch_time_and_update(time): - with patch("homeassistant.util.dt.utcnow", return_value=time): + with patch( + "custom_components.adaptive_lighting.color_and_brightness.utcnow", + return_value=time, + ): await switch._update_attrs_and_maybe_adapt_lights(context=context) await hass.async_block_till_done() @@ -490,7 +494,10 @@ async def test_light_settings(hass): context = switch.create_context("test") # needs to be passed to update method async def patch_time_and_get_updated_states(time): - with patch("homeassistant.util.dt.utcnow", return_value=time): + with patch( + "custom_components.adaptive_lighting.color_and_brightness.utcnow", + return_value=time, + ): await switch._update_attrs_and_maybe_adapt_lights( context=context, transition=0, force=True ) @@ -1818,7 +1825,10 @@ async def test_adapt_until_sleep_and_rgb_colors(hass): after_sunrise = sunrise + datetime.timedelta(hours=1) async def patch_time_and_update(time): - with patch("homeassistant.util.dt.utcnow", return_value=time): + with patch( + "custom_components.adaptive_lighting.color_and_brightness.utcnow", + return_value=time, + ): await switch._update_attrs_and_maybe_adapt_lights(context=context) await hass.async_block_till_done() @@ -2072,7 +2082,10 @@ async def test_brightness_mode(hass, brightness_mode, dark, light): return abs(a - b) < 0.01 async def patch_time_and_update(time): - with patch("homeassistant.util.dt.utcnow", return_value=time): + with patch( + "custom_components.adaptive_lighting.color_and_brightness.utcnow", + return_value=time, + ): await switch._update_attrs_and_maybe_adapt_lights(context=context) await hass.async_block_till_done() diff --git a/webapp/app.py b/webapp/app.py index 208ea242..91a33d0c 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -1,206 +1,181 @@ """Simple web app to visualize brightness over time.""" -import math - import matplotlib.pyplot as plt import numpy as np from shiny import App, render, ui +from pathlib import Path +from contextlib import suppress +import datetime as dt +from astral import LocationInfo +from astral.location import Location -def lerp(x, x1, x2, y1, y2): - """Linearly interpolate between two values.""" - return y1 + (x - x1) * (y2 - y1) / (x2 - x1) - - -def clamp(value: float, minimum: float, maximum: float) -> float: - """Clamp value between minimum and maximum.""" - return max(minimum, min(value, maximum)) - - -def find_a_b(x1: float, x2: float, y1: float, y2: float) -> tuple[float, float]: - a = (math.atanh(2 * y2 - 1) - math.atanh(2 * y1 - 1)) / (x2 - x1) - b = x1 - (math.atanh(2 * y1 - 1) / a) - return a, b - - -def scaled_tanh( - x: float, - a: float, - b: float, - y_min: float = 0.0, - y_max: float = 1.0, -) -> float: - """Apply a scaled and shifted tanh function to a given input.""" - return y_min + (y_max - y_min) * 0.5 * (math.tanh(a * (x - b)) + 1) - - -def is_closer_to_sunrise_than_sunset(time, sunrise_time, sunset_time): - """Return True if the time is closer to sunrise than sunset.""" - return abs(time - sunrise_time) < abs(time - sunset_time) - - -def brightness_linear( - time, - sunrise_time, - sunset_time, - time_light, - time_dark, - max_brightness, - min_brightness, -): - """Calculate the brightness for the 'linear' mode.""" - closer_to_sunrise = is_closer_to_sunrise_than_sunset( - time, - sunrise_time, - sunset_time, +def date_range(tzinfo): + start_of_day = dt.datetime.now(tzinfo).replace( + hour=0, minute=0, second=0, microsecond=0 ) - if closer_to_sunrise: - brightness = lerp( - time, - x1=sunrise_time - time_dark, - x2=sunrise_time + time_light, - y1=min_brightness, - y2=max_brightness, - ) - else: - brightness = lerp( - time, - x1=sunset_time - time_light, - x2=sunset_time + time_dark, - y1=max_brightness, - y2=min_brightness, - ) - return clamp(brightness, min_brightness, max_brightness) + # one second before the next day + end_of_day = start_of_day + dt.timedelta(days=1) - dt.timedelta(seconds=1) + hours_range = [start_of_day] + while hours_range[-1] < end_of_day: + hours_range.append(hours_range[-1] + dt.timedelta(minutes=5)) + return hours_range[:-1] -def brightness_tanh( - time, - sunrise_time, - sunset_time, - time_light, - time_dark, - max_brightness, - min_brightness, -): - """Calculate the brightness for the 'tanh' mode.""" - closer_to_sunrise = is_closer_to_sunrise_than_sunset( - time, - sunrise_time, - sunset_time, - ) - if closer_to_sunrise: - a, b = find_a_b( - x1=-time_dark, - x2=time_light, - y1=0.05, # be at 5% of range at x1 - y2=0.95, # be at 95% of range at x2 +def copy_color_and_brightness_module(): + with suppress(Exception): + webapp_folder = Path(__file__).parent.absolute() + module = ( + webapp_folder.parent + / "custom_components" + / "adaptive_lighting" + / "color_and_brightness.py" ) - brightness = scaled_tanh( - time - sunrise_time, - a=a, - b=b, - y_min=min_brightness, - y_max=max_brightness, - ) - else: - a, b = find_a_b( - x1=-time_light, # shifted timestamp for the start of sunset - x2=time_dark, # shifted timestamp for the end of sunset - y1=0.95, # be at 95% of range at the start of sunset - y2=0.05, # be at 5% of range at the end of sunset - ) - brightness = scaled_tanh( - time - sunset_time, - a=a, - b=b, - y_min=min_brightness, - y_max=max_brightness, - ) - return clamp(brightness, min_brightness, max_brightness) + new_module = webapp_folder / module.name + with module.open() as f: + lines = [ + line.replace("homeassistant.util.color", "homeassistant_util_color") + for line in f.readlines() + ] + with new_module.open("r") as f: + existing_lines = f.readlines() + if existing_lines != lines: + with new_module.open("w") as f: + f.writelines(lines) -def plot_brightness( - min_brightness, - max_brightness, - brightness_mode_time_dark, - brightness_mode_time_light, - sunrise_time=6, # 6 AM - sunset_time=18, # 6 PM -): +copy_color_and_brightness_module() + +from color_and_brightness import SunLightSettings + + +def plot_brightness(kw, sleep_mode: bool): # Define the time range for our simulation - time_range = np.linspace(0, 24, 1000) # From 0 to 24 hours - - # Calculate the brightness for each time in the time range for both modes + sun_linear = SunLightSettings(**kw, brightness_mode="linear") + sun_tanh = SunLightSettings(**kw, brightness_mode="tanh") + sun = SunLightSettings(**kw, brightness_mode="default") + # Calculate the brightness for each time in the time range for all modes + dt_range = date_range(sun.timezone) + time_range = [time_to_float(dt) for dt in dt_range] brightness_linear_values = [ - brightness_linear( - time, - sunrise_time, - sunset_time, - brightness_mode_time_light, - brightness_mode_time_dark, - max_brightness, - min_brightness, - ) - for time in time_range + sun_linear.brightness_pct(dt, sleep_mode) for dt in dt_range ] brightness_tanh_values = [ - brightness_tanh( - time, - sunrise_time, - sunset_time, - brightness_mode_time_light, - brightness_mode_time_dark, - max_brightness, - min_brightness, - ) - for time in time_range + sun_tanh.brightness_pct(dt, sleep_mode) for dt in dt_range ] + brightness_default_values = [sun.brightness_pct(dt, sleep_mode) for dt in dt_range] # Plot the brightness over time for both modes - plt.figure(figsize=(10, 6)) - plt.plot(time_range, brightness_linear_values, label="Linear Mode") - plt.plot(time_range, brightness_tanh_values, label="Tanh Mode") - plt.vlines(sunrise_time, 0, 1, color="C2", label="Sunrise", linestyles="dashed") - plt.vlines(sunset_time, 0, 1, color="C3", label="Sunset", linestyles="dashed") - plt.xlim(0, 24) - plt.xticks(np.arange(0, 25, 1)) - yticks = np.arange(0, 1.05, 0.05) - ytick_labels = [f"{100*label:.0f}%" for label in yticks] - plt.yticks(yticks, ytick_labels) - plt.xlabel("Time (hours)") - plt.ylabel("Brightness") - plt.title("Brightness over Time for Different Modes") + fig, ax = plt.subplots(figsize=(10, 6)) + ax.plot(time_range, brightness_linear_values, label="Linear Mode") + ax.plot(time_range, brightness_tanh_values, label="Tanh Mode") + ax.plot(time_range, brightness_default_values, label="Default Mode") + sunrise_time = sun.sun.sunrise(dt.date.today()) + sunset_time = sun.sun.sunset(dt.date.today()) + ax.vlines( + time_to_float(sunrise_time), + 0, + 100, + color="C2", + label="Sunrise", + linestyles="dashed", + ) + ax.vlines( + time_to_float(sunset_time), + 0, + 100, + color="C3", + label="Sunset", + linestyles="dashed", + ) + ax.set_xlim(0, 24) + ax.set_xticks(np.arange(0, 25, 1)) + yticks = np.arange(0, 105, 5) + ytick_labels = [f"{label:.0f}%" for label in yticks] + ax.set_yticks(yticks, ytick_labels) + ax.set_xlabel("Time (hours)") + ax.set_ylabel("Brightness") + ax.set_title("Brightness over Time for Different Modes") # Add text box textstr = "\n".join( ( - f"Sunrise Time = {sunrise_time}:00:00", - f"Sunset Time = {sunset_time}:00:00", - f"Max Brightness = {max_brightness*100:.0f}%", - f"Min Brightness = {min_brightness*100:.0f}%", - f"Time Light = {brightness_mode_time_light:.1f} hours", - f"Time Dark = {brightness_mode_time_dark:.1f} hours", + f"Sunrise Time = {sunrise_time.time()}", + f"Sunset Time = {sunset_time.time()}", + f"Max Brightness = {sun.max_brightness:.0f}%", + f"Min Brightness = {sun.min_brightness:.0f}%", + f"Time Light = {sun.brightness_mode_time_light}", + f"Time Dark = {sun.brightness_mode_time_dark}", ), ) - # these are matplotlib.patch.Patch properties - props = {"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5} + ax.legend() + ax.grid(True) - plt.legend() - plt.grid(True) - - # place a text box in upper left in axes coords - plt.gca().text( + ax.text( 0.4, 0.55, textstr, - transform=plt.gca().transAxes, + transform=ax.transAxes, fontsize=10, verticalalignment="center", - bbox=props, + bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5}, ) - return plt.gcf() + return fig + + +def plot_color_temp(kw, sleep_mode: bool): + sun = SunLightSettings(**kw, brightness_mode="default") + dt_range = date_range(tzinfo=sun.timezone) + time_range = [time_to_float(dt) for dt in dt_range] + settings = [sun.brightness_and_color(dt, sleep_mode) for dt in dt_range] + color_temp_values = ( + np.array([(*setting["rgb_color"], 255) for setting in settings]) / 255 + ) + color_temp_values = color_temp_values.reshape(-1, 1, 4) + sun_position = [setting["sun_position"] for setting in settings] + fig, ax = plt.subplots(figsize=(10, 6)) + + # Display as a horizontal bar + ax.imshow( + np.rot90(color_temp_values)[:, ::1], + aspect="auto", + extent=[0, 24, -1, 1], + origin="upper", + ) + # Plot a curve on top of the imshow + ax.plot(time_range, sun_position, color="k", label="Sun Position") + + sunrise_time = sun.sun.sunrise(dt.date.today()) + sunset_time = sun.sun.sunset(dt.date.today()) + ax.vlines( + time_to_float(sunrise_time), + -1, + 1, + color="C2", + label="Sunrise", + linestyles="dashed", + ) + ax.vlines( + time_to_float(sunset_time), + -1, + 1, + color="C3", + label="Sunset", + linestyles="dashed", + ) + + ax.set_xlim(0, 24) + ax.set_xticks(np.arange(0, 25, 1)) + yticks = np.arange(-1, 1.1, 0.1) + ax.set_yticks(yticks, [f"{label*100:.0f}%" for label in yticks]) + ax.set_xlabel("Time (hours)") + ax.legend() + ax.set_ylabel("Sun position (%)") + ax.set_title("RGB Color Intensity over Time") + + return fig SEC_PER_HR = 60 * 60 @@ -225,20 +200,34 @@ app_ui = ui.page_fluid( ui.panel_title("🌞 Adaptive Lighting Simulator WebApp 🌛"), ui.layout_sidebar( ui.panel_sidebar( - ui.input_slider("min_brightness", "min_brightness", 0, 100, 30, post="%"), - ui.input_slider("max_brightness", "max_brightness", 0, 100, 100, post="%"), + ui.input_switch("adapt_until_sleep", "adapt_until_sleep", False), + ui.input_switch("sleep_mode", "sleep_mode", False), + ui.input_slider("min_brightness", "min_brightness", 1, 100, 30, post="%"), + ui.input_slider("max_brightness", "max_brightness", 1, 100, 100, post="%"), + ui.input_numeric("min_color_temp", "min_color_temp", 2000), + ui.input_numeric("max_color_temp", "max_color_temp", 6666), + ui.input_slider( + "sleep_brightness", "sleep_brightness", 1, 100, 1, post="%" + ), + ui.input_radio_buttons( + "sleep_rgb_or_color_temp", + "sleep_rgb_or_color_temp", + ["rgb_color", "color_temp"], + ), + ui.input_numeric("sleep_color_temp", "sleep_color_temp", 2000), + ui.input_text("sleep_rgb_color", "sleep_rgb_color", "255,0,0"), ui.input_slider( - "dark_time", "brightness_mode_time_dark", - 0, + "brightness_mode_time_dark", + 1, 5 * SEC_PER_HR, 3 * SEC_PER_HR, post=" sec", ), ui.input_slider( - "light_time", "brightness_mode_time_light", - 0, + "brightness_mode_time_light", + 1, 5 * SEC_PER_HR, 0.5 * SEC_PER_HR, post=" sec", @@ -262,23 +251,68 @@ app_ui = ui.page_fluid( post=" hr", ), ), - ui.panel_main(ui.markdown(desc), ui.output_plot(id="brightness_plot")), + ui.panel_main( + ui.markdown(desc), + ui.output_plot(id="brightness_plot"), + ui.output_plot(id="color_temp_plot"), + ), ), ) +def float_to_time(value: float) -> dt.time: + hours = int(value) + minutes = int((value - hours) * 60) + time = dt.time(hours, minutes) + return time + + +def time_to_float(time: dt.time | dt.datetime) -> float: + return time.hour + time.minute / 60 + + +def _kw(input): + location = Location(LocationInfo(timezone=dt.timezone.utc)) + return dict( + name="Adaptive Lighting Simulator", + adapt_until_sleep=input.adapt_until_sleep(), + max_brightness=input.max_brightness(), + min_brightness=input.min_brightness(), + min_color_temp=input.min_color_temp(), + max_color_temp=input.max_color_temp(), + sleep_brightness=input.sleep_brightness(), + sleep_rgb_or_color_temp=input.sleep_rgb_or_color_temp(), + sleep_color_temp=input.sleep_color_temp(), + sleep_rgb_color=[int(x) for x in input.sleep_rgb_color().split(",")], + sunrise_time=float_to_time(input.sunrise_time()), + sunset_time=float_to_time(input.sunset_time()), + brightness_mode_time_dark=dt.timedelta( + seconds=input.brightness_mode_time_dark() + ), + brightness_mode_time_light=dt.timedelta( + seconds=input.brightness_mode_time_light() + ), + sunrise_offset=dt.timedelta(0), + sunset_offset=dt.timedelta(0), + min_sunrise_time=None, + max_sunrise_time=None, + min_sunset_time=None, + max_sunset_time=None, + astral_location=location, + timezone=location.timezone, + ) + + def server(input, output, session): @output @render.plot def brightness_plot(): - return plot_brightness( - min_brightness=input.min_brightness() / 100, - max_brightness=input.max_brightness() / 100, - brightness_mode_time_dark=input.dark_time() / SEC_PER_HR, - brightness_mode_time_light=input.light_time() / SEC_PER_HR, - sunrise_time=input.sunrise_time(), - sunset_time=input.sunset_time(), - ) + return plot_brightness(_kw(input), sleep_mode=input.sleep_mode()) + + @output + @render.plot + def color_temp_plot(): + return plot_color_temp(_kw(input), sleep_mode=input.sleep_mode()) app = App(app_ui, server) diff --git a/webapp/homeassistant_util_color.py b/webapp/homeassistant_util_color.py new file mode 100644 index 00000000..33df5cf3 --- /dev/null +++ b/webapp/homeassistant_util_color.py @@ -0,0 +1,773 @@ +"""Color util methods.""" +# Slightly modified from homeassistant.util.color at +# https://github.com/home-assistant/core/blob/798fb3e31a6ba87358adc93a4c5b772b64451712/homeassistant/util/color.py#L14 +# to remove the dependency on homeassistant.util.color in sun.py +from __future__ import annotations + +import colorsys +import math +from dataclasses import dataclass +from typing import NamedTuple + + +class RGBColor(NamedTuple): + """RGB hex values.""" + + r: int + g: int + b: int + + +# Official CSS3 colors from w3.org: +# https://www.w3.org/TR/2010/PR-css3-color-20101028/#html4 +# names do not have spaces in them so that we can compare against +# requests more easily (by removing spaces from the requests as well). +# This lets "dark seagreen" and "dark sea green" both match the same +# color "darkseagreen". +COLORS = { + "aliceblue": RGBColor(240, 248, 255), + "antiquewhite": RGBColor(250, 235, 215), + "aqua": RGBColor(0, 255, 255), + "aquamarine": RGBColor(127, 255, 212), + "azure": RGBColor(240, 255, 255), + "beige": RGBColor(245, 245, 220), + "bisque": RGBColor(255, 228, 196), + "black": RGBColor(0, 0, 0), + "blanchedalmond": RGBColor(255, 235, 205), + "blue": RGBColor(0, 0, 255), + "blueviolet": RGBColor(138, 43, 226), + "brown": RGBColor(165, 42, 42), + "burlywood": RGBColor(222, 184, 135), + "cadetblue": RGBColor(95, 158, 160), + "chartreuse": RGBColor(127, 255, 0), + "chocolate": RGBColor(210, 105, 30), + "coral": RGBColor(255, 127, 80), + "cornflowerblue": RGBColor(100, 149, 237), + "cornsilk": RGBColor(255, 248, 220), + "crimson": RGBColor(220, 20, 60), + "cyan": RGBColor(0, 255, 255), + "darkblue": RGBColor(0, 0, 139), + "darkcyan": RGBColor(0, 139, 139), + "darkgoldenrod": RGBColor(184, 134, 11), + "darkgray": RGBColor(169, 169, 169), + "darkgreen": RGBColor(0, 100, 0), + "darkgrey": RGBColor(169, 169, 169), + "darkkhaki": RGBColor(189, 183, 107), + "darkmagenta": RGBColor(139, 0, 139), + "darkolivegreen": RGBColor(85, 107, 47), + "darkorange": RGBColor(255, 140, 0), + "darkorchid": RGBColor(153, 50, 204), + "darkred": RGBColor(139, 0, 0), + "darksalmon": RGBColor(233, 150, 122), + "darkseagreen": RGBColor(143, 188, 143), + "darkslateblue": RGBColor(72, 61, 139), + "darkslategray": RGBColor(47, 79, 79), + "darkslategrey": RGBColor(47, 79, 79), + "darkturquoise": RGBColor(0, 206, 209), + "darkviolet": RGBColor(148, 0, 211), + "deeppink": RGBColor(255, 20, 147), + "deepskyblue": RGBColor(0, 191, 255), + "dimgray": RGBColor(105, 105, 105), + "dimgrey": RGBColor(105, 105, 105), + "dodgerblue": RGBColor(30, 144, 255), + "firebrick": RGBColor(178, 34, 34), + "floralwhite": RGBColor(255, 250, 240), + "forestgreen": RGBColor(34, 139, 34), + "fuchsia": RGBColor(255, 0, 255), + "gainsboro": RGBColor(220, 220, 220), + "ghostwhite": RGBColor(248, 248, 255), + "gold": RGBColor(255, 215, 0), + "goldenrod": RGBColor(218, 165, 32), + "gray": RGBColor(128, 128, 128), + "green": RGBColor(0, 128, 0), + "greenyellow": RGBColor(173, 255, 47), + "grey": RGBColor(128, 128, 128), + "honeydew": RGBColor(240, 255, 240), + "hotpink": RGBColor(255, 105, 180), + "indianred": RGBColor(205, 92, 92), + "indigo": RGBColor(75, 0, 130), + "ivory": RGBColor(255, 255, 240), + "khaki": RGBColor(240, 230, 140), + "lavender": RGBColor(230, 230, 250), + "lavenderblush": RGBColor(255, 240, 245), + "lawngreen": RGBColor(124, 252, 0), + "lemonchiffon": RGBColor(255, 250, 205), + "lightblue": RGBColor(173, 216, 230), + "lightcoral": RGBColor(240, 128, 128), + "lightcyan": RGBColor(224, 255, 255), + "lightgoldenrodyellow": RGBColor(250, 250, 210), + "lightgray": RGBColor(211, 211, 211), + "lightgreen": RGBColor(144, 238, 144), + "lightgrey": RGBColor(211, 211, 211), + "lightpink": RGBColor(255, 182, 193), + "lightsalmon": RGBColor(255, 160, 122), + "lightseagreen": RGBColor(32, 178, 170), + "lightskyblue": RGBColor(135, 206, 250), + "lightslategray": RGBColor(119, 136, 153), + "lightslategrey": RGBColor(119, 136, 153), + "lightsteelblue": RGBColor(176, 196, 222), + "lightyellow": RGBColor(255, 255, 224), + "lime": RGBColor(0, 255, 0), + "limegreen": RGBColor(50, 205, 50), + "linen": RGBColor(250, 240, 230), + "magenta": RGBColor(255, 0, 255), + "maroon": RGBColor(128, 0, 0), + "mediumaquamarine": RGBColor(102, 205, 170), + "mediumblue": RGBColor(0, 0, 205), + "mediumorchid": RGBColor(186, 85, 211), + "mediumpurple": RGBColor(147, 112, 219), + "mediumseagreen": RGBColor(60, 179, 113), + "mediumslateblue": RGBColor(123, 104, 238), + "mediumspringgreen": RGBColor(0, 250, 154), + "mediumturquoise": RGBColor(72, 209, 204), + "mediumvioletred": RGBColor(199, 21, 133), + "midnightblue": RGBColor(25, 25, 112), + "mintcream": RGBColor(245, 255, 250), + "mistyrose": RGBColor(255, 228, 225), + "moccasin": RGBColor(255, 228, 181), + "navajowhite": RGBColor(255, 222, 173), + "navy": RGBColor(0, 0, 128), + "navyblue": RGBColor(0, 0, 128), + "oldlace": RGBColor(253, 245, 230), + "olive": RGBColor(128, 128, 0), + "olivedrab": RGBColor(107, 142, 35), + "orange": RGBColor(255, 165, 0), + "orangered": RGBColor(255, 69, 0), + "orchid": RGBColor(218, 112, 214), + "palegoldenrod": RGBColor(238, 232, 170), + "palegreen": RGBColor(152, 251, 152), + "paleturquoise": RGBColor(175, 238, 238), + "palevioletred": RGBColor(219, 112, 147), + "papayawhip": RGBColor(255, 239, 213), + "peachpuff": RGBColor(255, 218, 185), + "peru": RGBColor(205, 133, 63), + "pink": RGBColor(255, 192, 203), + "plum": RGBColor(221, 160, 221), + "powderblue": RGBColor(176, 224, 230), + "purple": RGBColor(128, 0, 128), + "red": RGBColor(255, 0, 0), + "rosybrown": RGBColor(188, 143, 143), + "royalblue": RGBColor(65, 105, 225), + "saddlebrown": RGBColor(139, 69, 19), + "salmon": RGBColor(250, 128, 114), + "sandybrown": RGBColor(244, 164, 96), + "seagreen": RGBColor(46, 139, 87), + "seashell": RGBColor(255, 245, 238), + "sienna": RGBColor(160, 82, 45), + "silver": RGBColor(192, 192, 192), + "skyblue": RGBColor(135, 206, 235), + "slateblue": RGBColor(106, 90, 205), + "slategray": RGBColor(112, 128, 144), + "slategrey": RGBColor(112, 128, 144), + "snow": RGBColor(255, 250, 250), + "springgreen": RGBColor(0, 255, 127), + "steelblue": RGBColor(70, 130, 180), + "tan": RGBColor(210, 180, 140), + "teal": RGBColor(0, 128, 128), + "thistle": RGBColor(216, 191, 216), + "tomato": RGBColor(255, 99, 71), + "turquoise": RGBColor(64, 224, 208), + "violet": RGBColor(238, 130, 238), + "wheat": RGBColor(245, 222, 179), + "white": RGBColor(255, 255, 255), + "whitesmoke": RGBColor(245, 245, 245), + "yellow": RGBColor(255, 255, 0), + "yellowgreen": RGBColor(154, 205, 50), + # And... + "homeassistant": RGBColor(3, 169, 244), +} + + +@dataclass +class XYPoint: + """Represents a CIE 1931 XY coordinate pair.""" + + x: float + y: float + + +@dataclass +class GamutType: + """Represents the Gamut of a light.""" + + red: XYPoint + green: XYPoint + blue: XYPoint + + +def color_name_to_rgb(color_name: str) -> RGBColor: + """Convert color name to RGB hex value.""" + # COLORS map has no spaces in it, so make the color_name have no + # spaces in it as well for matching purposes + hex_value = COLORS.get(color_name.replace(" ", "").lower()) + if not hex_value: + msg = "Unknown color" + raise ValueError(msg) + + return hex_value + + +# pylint: disable=invalid-name + + +def color_RGB_to_xy( + iR: int, + iG: int, + iB: int, + Gamut: GamutType | None = None, +) -> tuple[float, float]: + """Convert from RGB color to XY color.""" + return color_RGB_to_xy_brightness(iR, iG, iB, Gamut)[:2] + + +# Taken from: +# https://github.com/PhilipsHue/PhilipsHueSDK-iOS-OSX/blob/00187a3/ApplicationDesignNotes/RGB%20to%20xy%20Color%20conversion.md +# License: Code is given as is. Use at your own risk and discretion. +def color_RGB_to_xy_brightness( + iR: int, + iG: int, + iB: int, + Gamut: GamutType | None = None, +) -> tuple[float, float, int]: + """Convert from RGB color to XY color.""" + if iR + iG + iB == 0: + return 0.0, 0.0, 0 + + R = iR / 255 + B = iB / 255 + G = iG / 255 + + # Gamma correction + R = pow((R + 0.055) / (1.0 + 0.055), 2.4) if (R > 0.04045) else (R / 12.92) + G = pow((G + 0.055) / (1.0 + 0.055), 2.4) if (G > 0.04045) else (G / 12.92) + B = pow((B + 0.055) / (1.0 + 0.055), 2.4) if (B > 0.04045) else (B / 12.92) + + # Wide RGB D65 conversion formula + X = R * 0.664511 + G * 0.154324 + B * 0.162028 + Y = R * 0.283881 + G * 0.668433 + B * 0.047685 + Z = R * 0.000088 + G * 0.072310 + B * 0.986039 + + # Convert XYZ to xy + x = X / (X + Y + Z) + y = Y / (X + Y + Z) + + # Brightness + Y = 1 if Y > 1 else Y + brightness = round(Y * 255) + + # Check if the given xy value is within the color-reach of the lamp. + if Gamut: + in_reach = check_point_in_lamps_reach((x, y), Gamut) + if not in_reach: + xy_closest = get_closest_point_to_point((x, y), Gamut) + x = xy_closest[0] + y = xy_closest[1] + + return round(x, 3), round(y, 3), brightness + + +def color_xy_to_RGB( + vX: float, + vY: float, + Gamut: GamutType | None = None, +) -> tuple[int, int, int]: + """Convert from XY to a normalized RGB.""" + return color_xy_brightness_to_RGB(vX, vY, 255, Gamut) + + +# Converted to Python from Obj-C, original source from: +# https://github.com/PhilipsHue/PhilipsHueSDK-iOS-OSX/blob/00187a3/ApplicationDesignNotes/RGB%20to%20xy%20Color%20conversion.md +def color_xy_brightness_to_RGB( + vX: float, + vY: float, + ibrightness: int, + Gamut: GamutType | None = None, +) -> tuple[int, int, int]: + """Convert from XYZ to RGB.""" + if Gamut and not check_point_in_lamps_reach((vX, vY), Gamut): + xy_closest = get_closest_point_to_point((vX, vY), Gamut) + vX = xy_closest[0] + vY = xy_closest[1] + + brightness = ibrightness / 255.0 + if brightness == 0.0: + return (0, 0, 0) + + Y = brightness + + if vY == 0.0: + vY += 0.00000000001 + + X = (Y / vY) * vX + Z = (Y / vY) * (1 - vX - vY) + + # Convert to RGB using Wide RGB D65 conversion. + r = X * 1.656492 - Y * 0.354851 - Z * 0.255038 + g = -X * 0.707196 + Y * 1.655397 + Z * 0.036152 + b = X * 0.051713 - Y * 0.121364 + Z * 1.011530 + + # Apply reverse gamma correction. + r, g, b = ( + 12.92 * x if (x <= 0.0031308) else ((1.0 + 0.055) * pow(x, (1.0 / 2.4)) - 0.055) + for x in (r, g, b) + ) + + # Bring all negative components to zero. + r, g, b = (max(0, x) for x in (r, g, b)) + + # If one component is greater than 1, weight components by that value. + max_component = max(r, g, b) + if max_component > 1: + r, g, b = (x / max_component for x in (r, g, b)) + + ir, ig, ib = (int(x * 255) for x in (r, g, b)) + + return (ir, ig, ib) + + +def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> tuple[int, int, int]: + """Convert a hsb into its rgb representation.""" + if fS == 0.0: + fV = int(fB * 255) + return fV, fV, fV + + r = g = b = 0 + h = fH / 60 + f = h - float(math.floor(h)) + p = fB * (1 - fS) + q = fB * (1 - fS * f) + t = fB * (1 - (fS * (1 - f))) + + if int(h) == 0: + r = int(fB * 255) + g = int(t * 255) + b = int(p * 255) + elif int(h) == 1: + r = int(q * 255) + g = int(fB * 255) + b = int(p * 255) + elif int(h) == 2: + r = int(p * 255) + g = int(fB * 255) + b = int(t * 255) + elif int(h) == 3: + r = int(p * 255) + g = int(q * 255) + b = int(fB * 255) + elif int(h) == 4: + r = int(t * 255) + g = int(p * 255) + b = int(fB * 255) + elif int(h) == 5: + r = int(fB * 255) + g = int(p * 255) + b = int(q * 255) + + return (r, g, b) + + +def color_RGB_to_hsv(iR: float, iG: float, iB: float) -> tuple[float, float, float]: + """Convert an rgb color to its hsv representation. + + Hue is scaled 0-360 + Sat is scaled 0-100 + Val is scaled 0-100 + """ + fHSV = colorsys.rgb_to_hsv(iR / 255.0, iG / 255.0, iB / 255.0) + return round(fHSV[0] * 360, 3), round(fHSV[1] * 100, 3), round(fHSV[2] * 100, 3) + + +def color_RGB_to_hs(iR: float, iG: float, iB: float) -> tuple[float, float]: + """Convert an rgb color to its hs representation.""" + return color_RGB_to_hsv(iR, iG, iB)[:2] + + +def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> tuple[int, int, int]: + """Convert an hsv color into its rgb representation. + + Hue is scaled 0-360 + Sat is scaled 0-100 + Val is scaled 0-100 + """ + fRGB = colorsys.hsv_to_rgb(iH / 360, iS / 100, iV / 100) + return (int(fRGB[0] * 255), int(fRGB[1] * 255), int(fRGB[2] * 255)) + + +def color_hs_to_RGB(iH: float, iS: float) -> tuple[int, int, int]: + """Convert an hsv color into its rgb representation.""" + return color_hsv_to_RGB(iH, iS, 100) + + +def color_xy_to_hs( + vX: float, + vY: float, + Gamut: GamutType | None = None, +) -> tuple[float, float]: + """Convert an xy color to its hs representation.""" + h, s, _ = color_RGB_to_hsv(*color_xy_to_RGB(vX, vY, Gamut)) + return h, s + + +def color_hs_to_xy( + iH: float, + iS: float, + Gamut: GamutType | None = None, +) -> tuple[float, float]: + """Convert an hs color to its xy representation.""" + return color_RGB_to_xy(*color_hs_to_RGB(iH, iS), Gamut) + + +def match_max_scale( + input_colors: tuple[int, ...], + output_colors: tuple[float, ...], +) -> tuple[int, ...]: + """Match the maximum value of the output to the input.""" + max_in = max(input_colors) + max_out = max(output_colors) + factor = 0.0 if max_out == 0 else max_in / max_out + return tuple(int(round(i * factor)) for i in output_colors) + + +def color_rgb_to_rgbw(r: int, g: int, b: int) -> tuple[int, int, int, int]: + """Convert an rgb color to an rgbw representation.""" + # Calculate the white channel as the minimum of input rgb channels. + # Subtract the white portion from the remaining rgb channels. + w = min(r, g, b) + rgbw = (r - w, g - w, b - w, w) + + # Match the output maximum value to the input. This ensures the full + # channel range is used. + return match_max_scale((r, g, b), rgbw) # type: ignore[return-value] + + +def color_rgbw_to_rgb(r: int, g: int, b: int, w: int) -> tuple[int, int, int]: + """Convert an rgbw color to an rgb representation.""" + # Add the white channel to the rgb channels. + rgb = (r + w, g + w, b + w) + + # Match the output maximum value to the input. This ensures the + # output doesn't overflow. + return match_max_scale((r, g, b, w), rgb) # type: ignore[return-value] + + +def color_rgb_to_rgbww( + r: int, + g: int, + b: int, + min_kelvin: int, + max_kelvin: int, +) -> tuple[int, int, int, int, int]: + """Convert an rgb color to an rgbww representation.""" + # Find the color temperature when both white channels have equal brightness + max_mireds = color_temperature_kelvin_to_mired(min_kelvin) + min_mireds = color_temperature_kelvin_to_mired(max_kelvin) + mired_range = max_mireds - min_mireds + mired_midpoint = min_mireds + mired_range / 2 + color_temp_kelvin = color_temperature_mired_to_kelvin(mired_midpoint) + w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin) + + # Find the ratio of the midpoint white in the input rgb channels + white_level = min( + r / w_r if w_r else 0, + g / w_g if w_g else 0, + b / w_b if w_b else 0, + ) + + # Subtract the white portion from the rgb channels. + rgb = (r - w_r * white_level, g - w_g * white_level, b - w_b * white_level) + rgbww = (*rgb, round(white_level * 255), round(white_level * 255)) + + # Match the output maximum value to the input. This ensures the full + # channel range is used. + return match_max_scale((r, g, b), rgbww) # type: ignore[return-value] + + +def color_rgbww_to_rgb( + r: int, + g: int, + b: int, + cw: int, + ww: int, + min_kelvin: int, + max_kelvin: int, +) -> tuple[int, int, int]: + """Convert an rgbww color to an rgb representation.""" + # Calculate color temperature of the white channels + max_mireds = color_temperature_kelvin_to_mired(min_kelvin) + min_mireds = color_temperature_kelvin_to_mired(max_kelvin) + mired_range = max_mireds - min_mireds + try: + ct_ratio = ww / (cw + ww) + except ZeroDivisionError: + ct_ratio = 0.5 + color_temp_mired = min_mireds + ct_ratio * mired_range + if color_temp_mired: + color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired) + else: + color_temp_kelvin = 0 + w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin) + white_level = max(cw, ww) / 255 + + # Add the white channels to the rgb channels. + rgb = (r + w_r * white_level, g + w_g * white_level, b + w_b * white_level) + + # Match the output maximum value to the input. This ensures the + # output doesn't overflow. + return match_max_scale((r, g, b, cw, ww), rgb) # type: ignore[return-value] + + +def color_rgb_to_hex(r: int, g: int, b: int) -> str: + """Return a RGB color from a hex color string.""" + return f"{round(r):02x}{round(g):02x}{round(b):02x}" + + +def rgb_hex_to_rgb_list(hex_string: str) -> list[int]: + """Return an RGB color value list from a hex color string.""" + return [ + int(hex_string[i : i + len(hex_string) // 3], 16) + for i in range(0, len(hex_string), len(hex_string) // 3) + ] + + +def color_temperature_to_hs(color_temperature_kelvin: float) -> tuple[float, float]: + """Return an hs color from a color temperature in Kelvin.""" + return color_RGB_to_hs(*color_temperature_to_rgb(color_temperature_kelvin)) + + +def color_temperature_to_rgb( + color_temperature_kelvin: float, +) -> tuple[float, float, float]: + """Return an RGB color from a color temperature in Kelvin. + + This is a rough approximation based on the formula provided by T. Helland + http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/ + """ + # range check + if color_temperature_kelvin < 1000: + color_temperature_kelvin = 1000 + elif color_temperature_kelvin > 40000: + color_temperature_kelvin = 40000 + + tmp_internal = color_temperature_kelvin / 100.0 + + red = _get_red(tmp_internal) + + green = _get_green(tmp_internal) + + blue = _get_blue(tmp_internal) + + return red, green, blue + + +def color_temperature_to_rgbww( + temperature: int, + brightness: int, + min_kelvin: int, + max_kelvin: int, +) -> tuple[int, int, int, int, int]: + """Convert color temperature in kelvin to rgbcw. + + Returns a (r, g, b, cw, ww) tuple. + """ + max_mireds = color_temperature_kelvin_to_mired(min_kelvin) + min_mireds = color_temperature_kelvin_to_mired(max_kelvin) + temperature = color_temperature_kelvin_to_mired(temperature) + mired_range = max_mireds - min_mireds + cold = ((max_mireds - temperature) / mired_range) * brightness + warm = brightness - cold + return (0, 0, 0, round(cold), round(warm)) + + +def rgbww_to_color_temperature( + rgbww: tuple[int, int, int, int, int], + min_kelvin: int, + max_kelvin: int, +) -> tuple[int, int]: + """Convert rgbcw to color temperature in kelvin. + + Returns a tuple (color_temperature, brightness). + """ + _, _, _, cold, warm = rgbww + return _white_levels_to_color_temperature(cold, warm, min_kelvin, max_kelvin) + + +def _white_levels_to_color_temperature( + cold: int, + warm: int, + min_kelvin: int, + max_kelvin: int, +) -> tuple[int, int]: + """Convert whites to color temperature in kelvin. + + Returns a tuple (color_temperature, brightness). + """ + max_mireds = color_temperature_kelvin_to_mired(min_kelvin) + min_mireds = color_temperature_kelvin_to_mired(max_kelvin) + brightness = warm / 255 + cold / 255 + if brightness == 0: + # Return the warmest color if brightness is 0 + return (min_kelvin, 0) + return round( + color_temperature_mired_to_kelvin( + ((cold / 255 / brightness) * (min_mireds - max_mireds)) + max_mireds, + ), + ), min(255, round(brightness * 255)) + + +def _clamp(color_component: float, minimum: float = 0, maximum: float = 255) -> float: + """Clamp the given color component value between the given min and max values. + + The range defined by the minimum and maximum values is inclusive, i.e. given a + color_component of 0 and a minimum of 10, the returned value is 10. + """ + color_component_out = max(color_component, minimum) + return min(color_component_out, maximum) + + +def _get_red(temperature: float) -> float: + """Get the red component of the temperature in RGB space.""" + if temperature <= 66: + return 255 + tmp_red = 329.698727446 * math.pow(temperature - 60, -0.1332047592) + return _clamp(tmp_red) + + +def _get_green(temperature: float) -> float: + """Get the green component of the given color temp in RGB space.""" + if temperature <= 66: + green = 99.4708025861 * math.log(temperature) - 161.1195681661 + else: + green = 288.1221695283 * math.pow(temperature - 60, -0.0755148492) + return _clamp(green) + + +def _get_blue(temperature: float) -> float: + """Get the blue component of the given color temperature in RGB space.""" + if temperature >= 66: + return 255 + if temperature <= 19: + return 0 + blue = 138.5177312231 * math.log(temperature - 10) - 305.0447927307 + return _clamp(blue) + + +def color_temperature_mired_to_kelvin(mired_temperature: float) -> int: + """Convert absolute mired shift to degrees kelvin.""" + return math.floor(1000000 / mired_temperature) + + +def color_temperature_kelvin_to_mired(kelvin_temperature: float) -> int: + """Convert degrees kelvin to mired shift.""" + return math.floor(1000000 / kelvin_temperature) + + +# The following 5 functions are adapted from rgbxy provided by Benjamin Knight +# License: The MIT License (MIT), 2014. +# https://github.com/benknight/hue-python-rgb-converter +def cross_product(p1: XYPoint, p2: XYPoint) -> float: + """Calculate the cross product of two XYPoints.""" + return float(p1.x * p2.y - p1.y * p2.x) + + +def get_distance_between_two_points(one: XYPoint, two: XYPoint) -> float: + """Calculate the distance between two XYPoints.""" + dx = one.x - two.x + dy = one.y - two.y + return math.sqrt(dx * dx + dy * dy) + + +def get_closest_point_to_line(A: XYPoint, B: XYPoint, P: XYPoint) -> XYPoint: + """Find the closest point from P to a line defined by A and B. + + This point will be reproducible by the lamp + as it is on the edge of the gamut. + """ + AP = XYPoint(P.x - A.x, P.y - A.y) + AB = XYPoint(B.x - A.x, B.y - A.y) + ab2 = AB.x * AB.x + AB.y * AB.y + ap_ab = AP.x * AB.x + AP.y * AB.y + t = ap_ab / ab2 + + if t < 0.0: + t = 0.0 + elif t > 1.0: + t = 1.0 + + return XYPoint(A.x + AB.x * t, A.y + AB.y * t) + + +def get_closest_point_to_point( + xy_tuple: tuple[float, float], + Gamut: GamutType, +) -> tuple[float, float]: + """Get the closest matching color within the gamut of the light. + + Should only be used if the supplied color is outside of the color gamut. + """ + xy_point = XYPoint(xy_tuple[0], xy_tuple[1]) + + # find the closest point on each line in the CIE 1931 'triangle'. + pAB = get_closest_point_to_line(Gamut.red, Gamut.green, xy_point) + pAC = get_closest_point_to_line(Gamut.blue, Gamut.red, xy_point) + pBC = get_closest_point_to_line(Gamut.green, Gamut.blue, xy_point) + + # Get the distances per point and see which point is closer to our Point. + dAB = get_distance_between_two_points(xy_point, pAB) + dAC = get_distance_between_two_points(xy_point, pAC) + dBC = get_distance_between_two_points(xy_point, pBC) + + lowest = dAB + closest_point = pAB + + if dAC < lowest: + lowest = dAC + closest_point = pAC + + if dBC < lowest: + lowest = dBC + closest_point = pBC + + # Change the xy value to a value which is within the reach of the lamp. + cx = closest_point.x + cy = closest_point.y + + return (cx, cy) + + +def check_point_in_lamps_reach(p: tuple[float, float], Gamut: GamutType) -> bool: + """Check if the provided XYPoint can be recreated by a Hue lamp.""" + v1 = XYPoint(Gamut.green.x - Gamut.red.x, Gamut.green.y - Gamut.red.y) + v2 = XYPoint(Gamut.blue.x - Gamut.red.x, Gamut.blue.y - Gamut.red.y) + + q = XYPoint(p[0] - Gamut.red.x, p[1] - Gamut.red.y) + s = cross_product(q, v2) / cross_product(v1, v2) + t = cross_product(v1, q) / cross_product(v1, v2) + + return (s >= 0.0) and (t >= 0.0) and (s + t <= 1.0) + + +def check_valid_gamut(Gamut: GamutType) -> bool: + """Check if the supplied gamut is valid.""" + # Check if the three points of the supplied gamut are not on the same line. + v1 = XYPoint(Gamut.green.x - Gamut.red.x, Gamut.green.y - Gamut.red.y) + v2 = XYPoint(Gamut.blue.x - Gamut.red.x, Gamut.blue.y - Gamut.red.y) + not_on_line = cross_product(v1, v2) > 0.0001 + + # Check if all six coordinates of the gamut lie between 0 and 1. + red_valid = ( + Gamut.red.x >= 0 and Gamut.red.x <= 1 and Gamut.red.y >= 0 and Gamut.red.y <= 1 + ) + green_valid = ( + Gamut.green.x >= 0 + and Gamut.green.x <= 1 + and Gamut.green.y >= 0 + and Gamut.green.y <= 1 + ) + blue_valid = ( + Gamut.blue.x >= 0 + and Gamut.blue.x <= 1 + and Gamut.blue.y >= 0 + and Gamut.blue.y <= 1 + ) + + return not_on_line and red_valid and green_valid and blue_valid diff --git a/webapp/requirements.txt b/webapp/requirements.txt index f832a57a..ee8c6966 100644 --- a/webapp/requirements.txt +++ b/webapp/requirements.txt @@ -1 +1,2 @@ shinylive +astral==2.2 From db5f9d623c25281c7bf39b767fb9043273c1a768 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Aug 2023 17:34:35 -0700 Subject: [PATCH 0639/1077] Mention app more prominently and improve style (#725) * Mention app more prominently * Use dark-mode * add setuptools * no cyber * no not run on PR * Ruff fixes * Add link * remove unused deps --- .github/workflows/deploy-webapp.yml | 2 - .ruff.toml | 3 +- README.md | 2 + webapp/app.py | 128 +++++++++++++++++----------- webapp/requirements.txt | 1 + 5 files changed, 81 insertions(+), 55 deletions(-) diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml index 04504522..629902b5 100644 --- a/.github/workflows/deploy-webapp.yml +++ b/.github/workflows/deploy-webapp.yml @@ -5,8 +5,6 @@ on: # Runs on pushes targeting the default branch push: branches: ["main"] - pull_request: - branches: ["main"] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: diff --git a/.ruff.toml b/.ruff.toml index 8ece9b9c..6ebfec62 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -26,7 +26,8 @@ ignore = [ [per-file-ignores] "tests/*.py" = ["ALL"] ".github/*py" = ["INP001"] -"webapp/*py" = ["ALL"] +"webapp/homeassistant_util_color.py" = ["ALL"] +"webapp/app.py" = ["INP001", "DTZ011", "A002"] "custom_components/adaptive_lighting/homeassistant_util_color.py" = ["ALL"] [flake8-pytest-style] diff --git a/README.md b/README.md index faa43e9b..3f9d4bc2 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ By automatically adapting the settings of your lights throughout the day, Adapti In addition to its regular mode, Adaptive Lighting also offers a "sleep mode" 🌜 which sets your lights to minimal brightness and a very warm color, perfect for winding down at night. +> 🌈 Visualize Adaptive Lighting's settings with the [_🌞 Adaptive Lighting Simulator WebApp 🌛_](https://basnijholt.github.io/adaptive-lighting) + [[ToC](#books-table-of-contents)] ## :bulb: Features diff --git a/webapp/app.py b/webapp/app.py index 91a33d0c..8cc9b99f 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -1,28 +1,36 @@ """Simple web app to visualize brightness over time.""" +import datetime as dt +from contextlib import suppress +from pathlib import Path +from typing import Any + import matplotlib.pyplot as plt import numpy as np -from shiny import App, render, ui -from pathlib import Path -from contextlib import suppress -import datetime as dt +import shinyswatch from astral import LocationInfo from astral.location import Location +from shiny import App, render, ui -def date_range(tzinfo): +def date_range(tzinfo: dt.tzinfo) -> list[dt.datetime]: + """Return a list of datetimes for the current day.""" start_of_day = dt.datetime.now(tzinfo).replace( - hour=0, minute=0, second=0, microsecond=0 + hour=0, + minute=0, + second=0, + microsecond=0, ) # one second before the next day end_of_day = start_of_day + dt.timedelta(days=1) - dt.timedelta(seconds=1) hours_range = [start_of_day] while hours_range[-1] < end_of_day: - hours_range.append(hours_range[-1] + dt.timedelta(minutes=5)) + hours_range.append(hours_range[-1] + dt.timedelta(minutes=1)) return hours_range[:-1] -def copy_color_and_brightness_module(): +def copy_color_and_brightness_module() -> None: + """Copy the color_and_brightness module to the webapp folder.""" with suppress(Exception): webapp_folder = Path(__file__).parent.absolute() module = ( @@ -46,14 +54,15 @@ def copy_color_and_brightness_module(): copy_color_and_brightness_module() -from color_and_brightness import SunLightSettings +from color_and_brightness import SunLightSettings # noqa: E402 -def plot_brightness(kw, sleep_mode: bool): +def plot_brightness(inputs: dict[str, Any], sleep_mode: bool): + """Plot the brightness over time for different modes.""" # Define the time range for our simulation - sun_linear = SunLightSettings(**kw, brightness_mode="linear") - sun_tanh = SunLightSettings(**kw, brightness_mode="tanh") - sun = SunLightSettings(**kw, brightness_mode="default") + sun_linear = SunLightSettings(**inputs, brightness_mode="linear") + sun_tanh = SunLightSettings(**inputs, brightness_mode="tanh") + sun = SunLightSettings(**inputs, brightness_mode="default") # Calculate the brightness for each time in the time range for all modes dt_range = date_range(sun.timezone) time_range = [time_to_float(dt) for dt in dt_range] @@ -69,7 +78,7 @@ def plot_brightness(kw, sleep_mode: bool): fig, ax = plt.subplots(figsize=(10, 6)) ax.plot(time_range, brightness_linear_values, label="Linear Mode") ax.plot(time_range, brightness_tanh_values, label="Tanh Mode") - ax.plot(time_range, brightness_default_values, label="Default Mode") + ax.plot(time_range, brightness_default_values, label="Default Mode", c="C5") sunrise_time = sun.sun.sunrise(dt.date.today()) sunset_time = sun.sun.sunset(dt.date.today()) ax.vlines( @@ -110,7 +119,6 @@ def plot_brightness(kw, sleep_mode: bool): ) ax.legend() - ax.grid(True) ax.text( 0.4, @@ -121,12 +129,14 @@ def plot_brightness(kw, sleep_mode: bool): verticalalignment="center", bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5}, ) + ax.grid(visible=True) return fig -def plot_color_temp(kw, sleep_mode: bool): - sun = SunLightSettings(**kw, brightness_mode="default") +def plot_color_temp(inputs: dict[str, Any], sleep_mode: bool) -> plt.Figure: + """Plot the color temperature over time for different modes.""" + sun = SunLightSettings(**inputs, brightness_mode="default") dt_range = date_range(tzinfo=sun.timezone) time_range = [time_to_float(dt) for dt in dt_range] settings = [sun.brightness_and_color(dt, sleep_mode) for dt in dt_range] @@ -174,16 +184,20 @@ def plot_color_temp(kw, sleep_mode: bool): ax.legend() ax.set_ylabel("Sun position (%)") ax.set_title("RGB Color Intensity over Time") - + ax.grid(visible=False) return fig SEC_PER_HR = 60 * 60 -desc = """ +desc_top = """ **Experience the Dynamics of [Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) in Real-Time.** Have you ever wondered how the intricate settings of [Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) impact your home ambiance? The Adaptive Lighting Simulator WebApp is here to demystify just that. +(More text below the plots) +""" + +desc_bottom = """ Harnessing the technology of the popular Adaptive Lighting integration for Home Assistant, this webapp provides a hands-on, visual platform to explore, tweak, and understand the myriad of parameters that dictate the behavior of your smart lights. Whether you're aiming for a subtle morning glow or a cozy evening warmth, observe firsthand how each tweak changes the ambiance. **Why Use the Simulator?** @@ -197,17 +211,23 @@ Dive into the simulator, experiment with different settings, and fine-tune the b # Shiny UI app_ui = ui.page_fluid( + shinyswatch.theme.sandstone(), ui.panel_title("🌞 Adaptive Lighting Simulator WebApp 🌛"), ui.layout_sidebar( ui.panel_sidebar( - ui.input_switch("adapt_until_sleep", "adapt_until_sleep", False), - ui.input_switch("sleep_mode", "sleep_mode", False), + ui.input_switch("adapt_until_sleep", "adapt_until_sleep", value=False), + ui.input_switch("sleep_mode", "sleep_mode", value=False), ui.input_slider("min_brightness", "min_brightness", 1, 100, 30, post="%"), ui.input_slider("max_brightness", "max_brightness", 1, 100, 100, post="%"), ui.input_numeric("min_color_temp", "min_color_temp", 2000), ui.input_numeric("max_color_temp", "max_color_temp", 6666), ui.input_slider( - "sleep_brightness", "sleep_brightness", 1, 100, 1, post="%" + "sleep_brightness", + "sleep_brightness", + 1, + 100, + 1, + post="%", ), ui.input_radio_buttons( "sleep_rgb_or_color_temp", @@ -252,58 +272,62 @@ app_ui = ui.page_fluid( ), ), ui.panel_main( - ui.markdown(desc), + ui.markdown(desc_top), ui.output_plot(id="brightness_plot"), ui.output_plot(id="color_temp_plot"), + ui.markdown(desc_bottom), ), ), ) def float_to_time(value: float) -> dt.time: + """Convert a float to a time object.""" hours = int(value) minutes = int((value - hours) * 60) - time = dt.time(hours, minutes) - return time + return dt.time(hours, minutes) def time_to_float(time: dt.time | dt.datetime) -> float: + """Convert a time object to a float.""" return time.hour + time.minute / 60 def _kw(input): location = Location(LocationInfo(timezone=dt.timezone.utc)) - return dict( - name="Adaptive Lighting Simulator", - adapt_until_sleep=input.adapt_until_sleep(), - max_brightness=input.max_brightness(), - min_brightness=input.min_brightness(), - min_color_temp=input.min_color_temp(), - max_color_temp=input.max_color_temp(), - sleep_brightness=input.sleep_brightness(), - sleep_rgb_or_color_temp=input.sleep_rgb_or_color_temp(), - sleep_color_temp=input.sleep_color_temp(), - sleep_rgb_color=[int(x) for x in input.sleep_rgb_color().split(",")], - sunrise_time=float_to_time(input.sunrise_time()), - sunset_time=float_to_time(input.sunset_time()), - brightness_mode_time_dark=dt.timedelta( - seconds=input.brightness_mode_time_dark() + return { + "name": "Adaptive Lighting Simulator", + "adapt_until_sleep": input.adapt_until_sleep(), + "max_brightness": input.max_brightness(), + "min_brightness": input.min_brightness(), + "min_color_temp": input.min_color_temp(), + "max_color_temp": input.max_color_temp(), + "sleep_brightness": input.sleep_brightness(), + "sleep_rgb_or_color_temp": input.sleep_rgb_or_color_temp(), + "sleep_color_temp": input.sleep_color_temp(), + "sleep_rgb_color": [int(x) for x in input.sleep_rgb_color().split(",")], + "sunrise_time": float_to_time(input.sunrise_time()), + "sunset_time": float_to_time(input.sunset_time()), + "brightness_mode_time_dark": dt.timedelta( + seconds=input.brightness_mode_time_dark(), ), - brightness_mode_time_light=dt.timedelta( - seconds=input.brightness_mode_time_light() + "brightness_mode_time_light": dt.timedelta( + seconds=input.brightness_mode_time_light(), ), - sunrise_offset=dt.timedelta(0), - sunset_offset=dt.timedelta(0), - min_sunrise_time=None, - max_sunrise_time=None, - min_sunset_time=None, - max_sunset_time=None, - astral_location=location, - timezone=location.timezone, - ) + "sunrise_offset": dt.timedelta(0), + "sunset_offset": dt.timedelta(0), + "min_sunrise_time": None, + "max_sunrise_time": None, + "min_sunset_time": None, + "max_sunset_time": None, + "astral_location": location, + "timezone": location.timezone, + } -def server(input, output, session): +def server(input, output, session): # noqa: ARG001 + """Shiny server.""" + @output @render.plot def brightness_plot(): diff --git a/webapp/requirements.txt b/webapp/requirements.txt index ee8c6966..4a287a69 100644 --- a/webapp/requirements.txt +++ b/webapp/requirements.txt @@ -1,2 +1,3 @@ shinylive astral==2.2 +shinyswatch From b06d38eb24c50fcb18747cfe7c0e6226e47cc32c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Aug 2023 18:03:06 -0700 Subject: [PATCH 0640/1077] Add mp4 link in README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3f9d4bc2..9c7fb3b2 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ In addition to its regular mode, Adaptive Lighting also offers a "sleep mode" > 🌈 Visualize Adaptive Lighting's settings with the [_🌞 Adaptive Lighting Simulator WebApp 🌛_](https://basnijholt.github.io/adaptive-lighting) +https://github.com/basnijholt/adaptive-lighting/assets/6897215/68908f7d-fbf1-4991-98ce-3f2af6df996f + [[ToC](#books-table-of-contents)] ## :bulb: Features From 597e050ab2a769cb9ef5fed5df9403f8d36cdb85 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 9 Aug 2023 13:54:18 -0700 Subject: [PATCH 0641/1077] Prevent light.turn_on of light that was just turned off (#727) Closes #726 --- custom_components/adaptive_lighting/switch.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 35b1d7e5..89106b45 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1120,6 +1120,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self.manager.reset(*self.lights) async def _async_update_at_interval_action(self, now=None) -> None: # noqa: ARG002 + """Update the attributes and maybe adapt the lights.""" await self._update_attrs_and_maybe_adapt_lights( context=self.create_context("interval"), transition=self._transition, @@ -1331,7 +1332,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): data, ) - async def _update_attrs_and_maybe_adapt_lights( + async def _update_attrs_and_maybe_adapt_lights( # noqa: PLR0912 self, *, context: Context, @@ -1379,6 +1380,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._name, light, ) + elif ( + # This is to prevent lights immediately turning on after + # being turned off in 'interval' update, see #726 + not self._detect_non_ha_changes + and is_our_context(context, "interval") + and (turn_on := self.manager.turn_on_event.get(light)) + and (turn_off := self.manager.turn_off_event.get(light)) + and turn_off.time_fired > turn_on.time_fired + ): + _LOGGER.debug( + "%s: Light '%s' was turned just turned off", + self._name, + light, + ) else: filtered_lights.append(light) From da5147a58367fe278bdd0ae1e67f9ef80de3996e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 9 Aug 2023 13:56:00 -0700 Subject: [PATCH 0642/1077] Bump to 1.19.0b5 in manifest.json (#728) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 3b6c7cf5..bfbe994b 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.19.0b4" + "version": "1.19.0b5" } From 435b2ce5d0c82b24659bf295999cc76b2f48a2c4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 9 Aug 2023 14:48:18 -0700 Subject: [PATCH 0643/1077] Fix adapt_only_on_bare_turn_on and apply does not result in manual_control (#729) * Fix adapt_only_on_bare_turn_on and apply does not result in manual_control Closes #723 * fix style --- custom_components/adaptive_lighting/switch.py | 8 ++++++-- tests/test_switch.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 89106b45..150f2ef4 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1376,9 +1376,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): timer = self.manager.transition_timers.get(light) if timer is not None and timer.is_running(): _LOGGER.debug( - "%s: Light '%s' is still transitioning", + "%s: Light '%s' is still transitioning, context.id='%s'", self._name, light, + context.id, ) elif ( # This is to prevent lights immediately turning on after @@ -1390,9 +1391,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): and turn_off.time_fired > turn_on.time_fired ): _LOGGER.debug( - "%s: Light '%s' was turned just turned off", + "%s: Light '%s' was turned just turned off, context.id='%s'", self._name, light, + context.id, ) else: filtered_lights.append(light) @@ -1489,6 +1491,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._take_over_control and self._adapt_only_on_bare_turn_on and from_turn_on + # adaptive_lighting.apply can turn on light, so check this is not our context + and not is_our_context(event.context) ): service_data = self.manager.turn_on_event[entity_id].data[ATTR_SERVICE_DATA] if self.manager._mark_manual_control_if_non_bare_turn_on( diff --git a/tests/test_switch.py b/tests/test_switch.py index 6ffd1a21..4744f878 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -740,6 +740,22 @@ async def test_manual_control( await change_manual_control(False, {}) assert all([not manual_control[eid] for eid in switch.lights]) + # Turn off light and turn on using adaptive_lighting.apply + await turn_light(False) + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: ENTITY_SWITCH, + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_TURN_ON_LIGHTS: True, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + assert not manual_control[ENTITY_LIGHT_1] + async def test_auto_reset_manual_control(hass): switch, (light, *_) = await setup_lights_and_switch( From 0d387d4bcd4709d73d5640a40e24fd321a831f7d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 9 Aug 2023 14:56:08 -0700 Subject: [PATCH 0644/1077] Add release-drafter GitHub Action (#731) --- .github/workflows/release-drafter.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/release-drafter.yml diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 00000000..12b1f6c2 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,22 @@ +name: Release Drafter + +on: + push: + branches: + - main + pull_request: + types: [opened, reopened, synchronize] + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 750acc978423dbcc41c998ee18c167c452ff9426 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 9 Aug 2023 14:57:34 -0700 Subject: [PATCH 0645/1077] Add release-drafter config --- .github/release-drafter.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/release-drafter.yml diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..27bcee3f --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,4 @@ +template: | + ## What’s Changed + + $CHANGES From 487756b345b4be286fc2118eb0eaf78d885e4ea2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 9 Aug 2023 15:01:00 -0700 Subject: [PATCH 0646/1077] Add full changelog to releases --- .github/release-drafter.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 27bcee3f..ad38c0ea 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -2,3 +2,5 @@ template: | ## What’s Changed $CHANGES + + **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION From 5b7ee446295a5ff54dcfc54ba7e34cce0d271237 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 9 Aug 2023 17:17:25 -0700 Subject: [PATCH 0647/1077] Fix length of descriptions in UI (config flow) (#732) * Fix length of strings in UI * Update README.md, strings.json, and services.yaml * fix * add desc * Update README.md, strings.json, and services.yaml * mention webapp * Add desc * link --------- Co-authored-by: github-actions[bot] --- .github/update-strings.py | 13 +++- .../adaptive_lighting/strings.json | 70 +++++++++++++------ .../adaptive_lighting/translations/en.json | 70 +++++++++++++------ 3 files changed, 106 insertions(+), 47 deletions(-) diff --git a/.github/update-strings.py b/.github/update-strings.py index 44dc6bd6..f90ac437 100644 --- a/.github/update-strings.py +++ b/.github/update-strings.py @@ -3,6 +3,7 @@ import json import sys from pathlib import Path +import homeassistant.helpers.config_validation as cv import yaml sys.path.append(str(Path(__file__).parent.parent)) @@ -16,8 +17,17 @@ with strings_fname.open() as f: strings = json.load(f) # Set "options" -data = {k: f"{k}: {const.DOCS[k]}" for k, _, _ in const.VALIDATION_TUPLES} +data = {} +data_description = {} +for k, _, typ in const.VALIDATION_TUPLES: + desc = const.DOCS[k] + if len(desc) > 40 and typ != bool and typ != cv.entity_ids: + data[k] = k + data_description[k] = desc + else: + data[k] = f"{k}: {desc}" strings["options"]["step"]["init"]["data"] = data +strings["options"]["step"]["init"]["data_description"] = data_description # Set "services" services_filename = Path("custom_components") / "adaptive_lighting" / "services.yaml" @@ -48,6 +58,7 @@ with en_fname.open() as f: en["config"]["step"]["user"] = strings["config"]["step"]["user"] en["options"]["step"]["init"]["data"] = data +en["options"]["step"]["init"]["data_description"] = data_description en["services"] = services_json with en_fname.open("w") as f: diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 5e60ed79..5733bfd5 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -17,45 +17,69 @@ "step": { "init": { "title": "Adaptive Lighting options", - "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.", + "description": "Configure an Adaptive Lighting component. Option names align with the YAML settings. If you've defined this entry in YAML, no options will appear here. For interactive graphs that demonstrate parameter effects, visit [this web app](https://basnijholt.github.io/adaptive-lighting). For further details, see the [official documentation](https://github.com/basnijholt/adaptive-lighting#readme).", "data": { "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟", - "interval": "interval: Frequency to adapt the lights, in seconds. 🔄", - "transition": "transition: Duration of transition when lights change, in seconds. 🕑", - "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "interval": "interval", + "transition": "transition", + "initial_transition": "initial_transition", "min_brightness": "min_brightness: Minimum brightness percentage. 💡", "max_brightness": "max_brightness: Maximum brightness percentage. 💡", "min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥", "max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️", "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", - "sleep_brightness": "sleep_brightness: Brightness percentage of lights in sleep mode. 😴", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", - "sleep_color_temp": "sleep_color_temp: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", - "sleep_rgb_color": "sleep_rgb_color: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", - "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "sleep_brightness": "sleep_brightness", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp", + "sleep_color_temp": "sleep_color_temp", + "sleep_rgb_color": "sleep_rgb_color", + "sleep_transition": "sleep_transition", "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", - "sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅", - "min_sunrise_time": "min_sunrise_time: Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅", - "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", - "sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰", - "sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇", - "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", - "max_sunset_time": "max_sunset_time: Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇", - "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰", - "brightness_mode": "brightness_mode: Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", - "brightness_mode_time_dark": "brightness_mode_time_dark: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", - "brightness_mode_time_light": "brightness_mode_time_light: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.", + "sunrise_time": "sunrise_time", + "min_sunrise_time": "min_sunrise_time", + "max_sunrise_time": "max_sunrise_time", + "sunrise_offset": "sunrise_offset", + "sunset_time": "sunset_time", + "min_sunset_time": "min_sunset_time", + "max_sunset_time": "max_sunset_time", + "sunset_offset": "sunset_offset", + "brightness_mode": "brightness_mode", + "brightness_mode_time_dark": "brightness_mode_time_dark", + "brightness_mode_time_light": "brightness_mode_time_light", "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", - "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ ", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", - "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", - "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", + "send_split_delay": "send_split_delay", + "adapt_delay": "adapt_delay", "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.", "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches.", "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝" + }, + "data_description": { + "interval": "Frequency to adapt the lights, in seconds. 🔄", + "transition": "Duration of transition when lights change, in seconds. 🕑", + "initial_transition": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "sleep_brightness": "Brightness percentage of lights in sleep mode. 😴", + "sleep_rgb_or_color_temp": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", + "sleep_color_temp": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", + "sleep_rgb_color": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "sunrise_time": "Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "min_sunrise_time": "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅", + "max_sunrise_time": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", + "sunrise_offset": "Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "sunset_time": "Set a fixed time (HH:MM:SS) for sunset. 🌇", + "min_sunset_time": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", + "max_sunset_time": "Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇", + "sunset_offset": "Adjust sunset time with a positive or negative offset in seconds. ⏰", + "brightness_mode": "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", + "brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.", + "autoreset_control_seconds": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "send_split_delay": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", + "adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️" } } }, diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index cbf87f96..eaf28798 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -18,45 +18,69 @@ "step": { "init": { "title": "Adaptive Lighting options", - "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have the adaptive_lighting entry defined in your YAML configuration.", + "description": "Configure an Adaptive Lighting component. Option names align with the YAML settings. If you've defined this entry in YAML, no options will appear here. For interactive graphs that demonstrate parameter effects, visit [this web app](https://basnijholt.github.io/adaptive-lighting). For further details, see the [official documentation](https://github.com/basnijholt/adaptive-lighting#readme).", "data": { "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟", - "interval": "interval: Frequency to adapt the lights, in seconds. 🔄", - "transition": "transition: Duration of transition when lights change, in seconds. 🕑", - "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "interval": "interval", + "transition": "transition", + "initial_transition": "initial_transition", "min_brightness": "min_brightness: Minimum brightness percentage. 💡", "max_brightness": "max_brightness: Maximum brightness percentage. 💡", "min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥", "max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️", "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", - "sleep_brightness": "sleep_brightness: Brightness percentage of lights in sleep mode. 😴", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", - "sleep_color_temp": "sleep_color_temp: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", - "sleep_rgb_color": "sleep_rgb_color: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", - "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "sleep_brightness": "sleep_brightness", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp", + "sleep_color_temp": "sleep_color_temp", + "sleep_rgb_color": "sleep_rgb_color", + "sleep_transition": "sleep_transition", "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", - "sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅", - "min_sunrise_time": "min_sunrise_time: Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅", - "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", - "sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰", - "sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇", - "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", - "max_sunset_time": "max_sunset_time: Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇", - "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰", - "brightness_mode": "brightness_mode: Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", - "brightness_mode_time_dark": "brightness_mode_time_dark: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", - "brightness_mode_time_light": "brightness_mode_time_light: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.", + "sunrise_time": "sunrise_time", + "min_sunrise_time": "min_sunrise_time", + "max_sunrise_time": "max_sunrise_time", + "sunrise_offset": "sunrise_offset", + "sunset_time": "sunset_time", + "min_sunset_time": "min_sunset_time", + "max_sunset_time": "max_sunset_time", + "sunset_offset": "sunset_offset", + "brightness_mode": "brightness_mode", + "brightness_mode_time_dark": "brightness_mode_time_dark", + "brightness_mode_time_light": "brightness_mode_time_light", "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", - "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ ", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", - "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", - "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", + "send_split_delay": "send_split_delay", + "adapt_delay": "adapt_delay", "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.", "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches.", "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝" + }, + "data_description": { + "interval": "Frequency to adapt the lights, in seconds. 🔄", + "transition": "Duration of transition when lights change, in seconds. 🕑", + "initial_transition": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "sleep_brightness": "Brightness percentage of lights in sleep mode. 😴", + "sleep_rgb_or_color_temp": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", + "sleep_color_temp": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", + "sleep_rgb_color": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "sunrise_time": "Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "min_sunrise_time": "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅", + "max_sunrise_time": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", + "sunrise_offset": "Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "sunset_time": "Set a fixed time (HH:MM:SS) for sunset. 🌇", + "min_sunset_time": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", + "max_sunset_time": "Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇", + "sunset_offset": "Adjust sunset time with a positive or negative offset in seconds. ⏰", + "brightness_mode": "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", + "brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.", + "autoreset_control_seconds": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "send_split_delay": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", + "adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️" } } }, From 9112bb9f7329f8aee995ac33b9502ce83b8177f8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 9 Aug 2023 19:06:48 -0700 Subject: [PATCH 0648/1077] Bump to 1.19.0 in manifest.json (#734) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index bfbe994b..eb7f0155 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.19.0b5" + "version": "1.19.0" } From 0723280a89678834f3b4c0afc2112e74cdf593d0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 10 Aug 2023 17:00:16 -0700 Subject: [PATCH 0649/1077] Fix sleep_mode + sleep_rgb_or_color_temp == "color_temp" in webapp (#740) Thanks to @danielbrunt57 for reporting here --- webapp/app.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/webapp/app.py b/webapp/app.py index 8cc9b99f..ab896687 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -10,6 +10,7 @@ import numpy as np import shinyswatch from astral import LocationInfo from astral.location import Location +from homeassistant_util_color import color_temperature_to_rgb from shiny import App, render, ui @@ -140,9 +141,14 @@ def plot_color_temp(inputs: dict[str, Any], sleep_mode: bool) -> plt.Figure: dt_range = date_range(tzinfo=sun.timezone) time_range = [time_to_float(dt) for dt in dt_range] settings = [sun.brightness_and_color(dt, sleep_mode) for dt in dt_range] - color_temp_values = ( - np.array([(*setting["rgb_color"], 255) for setting in settings]) / 255 - ) + if sleep_mode and sun.sleep_rgb_or_color_temp == "color_temp": + colors = [ + color_temperature_to_rgb(setting["color_temp_kelvin"]) + for setting in settings + ] + else: + colors = [setting["rgb_color"] for setting in settings] + color_temp_values = np.array([(*col, 255) for col in colors]) / 255 color_temp_values = color_temp_values.reshape(-1, 1, 4) sun_position = [setting["sun_position"] for setting in settings] fig, ax = plt.subplots(figsize=(10, 6)) From f881b4b32d1eb2913a133e4476181b36900a9d3f Mon Sep 17 00:00:00 2001 From: Kendell R Date: Mon, 14 Aug 2023 16:41:34 -0400 Subject: [PATCH 0650/1077] Use new logo (#746) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c7fb3b2..39eafa88 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 -![](https://github.com/home-assistant/brands/raw/b4a168b9af282ef916e120d31091ecd5e3c35e66/core_integrations/adaptive_lighting/icon.png) +logo [Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) is a custom component for [Home Assistant](https://www.home-assistant.io/) that intelligently adjusts the brightness and color of your lights 💡 based on the sun's position, while still allowing for manual control. From f540057093cff6fa74dcfda00037f9c793e34cac Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 13:49:15 -0700 Subject: [PATCH 0651/1077] docs: add KTibow as a contributor for design (#747) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 12 +++++++++++- README.md | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 33b875f2..c264a714 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -449,8 +449,18 @@ "contributions": [ "code" ] + }, + { + "login": "KTibow", + "name": "Kendell R", + "avatar_url": "https://avatars.githubusercontent.com/u/10727862?v=4", + "profile": "https://ktibow.github.io/", + "contributions": [ + "design" + ] } ], "contributorsPerLine": 7, - "linkToUsage": true + "linkToUsage": true, + "commitType": "docs" } diff --git a/README.md b/README.md index 39eafa88..dff1f89c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-48-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-49-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -521,6 +521,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
+ From 68feb3d93164876ce4829dd2bdc53907ffddee5e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 15 Aug 2023 12:23:56 -0700 Subject: [PATCH 0652/1077] Make instantaneous `light.turn_on` adaptation configurable with `intercept` (#750) * Make intercept configurable * Update README.md, strings.json, and services.yaml * import * skip * split * do not pass config_entry * add to conf * Update README.md, strings.json, and services.yaml * spacing --------- Co-authored-by: github-actions[bot] --- README.md | 3 +- custom_components/adaptive_lighting/const.py | 11 ++- .../adaptive_lighting/strings.json | 3 +- custom_components/adaptive_lighting/switch.py | 78 +++++++++---------- .../adaptive_lighting/translations/en.json | 3 +- tests/test_switch.py | 24 +++--- 6 files changed, 65 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index dff1f89c..1f2065c7 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,8 @@ The YAML and frontend configuration methods support all of the options listed be | `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | | `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | | `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | -| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. | `True` | `bool` | +| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | +| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | | `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index c747528d..cf93cee6 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -231,6 +231,13 @@ DOCS[CONF_SKIP_REDUNDANT_COMMANDS] = ( "Disable if physical light states get out of sync with HA's recorded state." ) +CONF_INTERCEPT, DEFAULT_INTERCEPT = "intercept", True +DOCS[CONF_INTERCEPT] = ( + "Intercept and adapt `light.turn_on` calls to enabling instantaneous color " + "and brightness adaptation. 🏎️ Disable for lights that do not " + "support `light.turn_on` with color and brightness." +) + CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT = ( "multi_light_intercept", True, @@ -238,7 +245,8 @@ CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT = ( DOCS[CONF_MULTI_LIGHT_INTERCEPT] = ( "Intercept and adapt `light.turn_on` calls that target multiple lights. ➗" "⚠️ This might result in splitting up a single `light.turn_on` call " - "into multiple calls, e.g., when lights are in different switches." + "into multiple calls, e.g., when lights are in different switches. " + "Requires `intercept` to be enabled." ) SLEEP_MODE_SWITCH = "sleep_mode_switch" @@ -356,6 +364,7 @@ VALIDATION_TUPLES = [ DEFAULT_SKIP_REDUNDANT_COMMANDS, bool, ), + (CONF_INTERCEPT, DEFAULT_INTERCEPT, bool), (CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT, bool), (CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES, bool), ] diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 5733bfd5..f6f6890b 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -54,7 +54,8 @@ "send_split_delay": "send_split_delay", "adapt_delay": "adapt_delay", "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.", - "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches.", + "intercept": "intercept: Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness.", + "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled.", "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝" }, "data_description": { diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 150f2ef4..4d66cc3d 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -107,6 +107,7 @@ from .const import ( CONF_DETECT_NON_HA_CHANGES, CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, CONF_INITIAL_TRANSITION, + CONF_INTERCEPT, CONF_INTERVAL, CONF_LIGHTS, CONF_MANUAL_CONTROL, @@ -180,11 +181,6 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=10) -# A (non-user-configurable, thus internal) flag to control the proactive adaptation mode. -# This exists to disable the proactive adaptation in the unit tests and enable it -# only for specific unit tests and when running as integration.""" -INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION = "proactive_adaptation" - # Consider it a significant change when attribute changes more than BRIGHTNESS_CHANGE = 25 # ≈10% of total range COLOR_TEMP_CHANGE = 100 # ≈3% of total range (2000-6500) @@ -427,7 +423,7 @@ async def async_setup_entry( # noqa: PLR0915 return if (manager := data.get(ATTR_ADAPTIVE_LIGHTING_MANAGER)) is None: - manager = AdaptiveLightingManager(hass, config_entry) + manager = AdaptiveLightingManager(hass) data[ATTR_ADAPTIVE_LIGHTING_MANAGER] = manager sleep_mode_switch = SimpleSwitch( @@ -882,7 +878,16 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._adapt_only_on_bare_turn_on = data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL] self._skip_redundant_commands = data[CONF_SKIP_REDUNDANT_COMMANDS] + self._intercept = data[CONF_INTERCEPT] self._multi_light_intercept = data[CONF_MULTI_LIGHT_INTERCEPT] + if not data[CONF_INTERCEPT] and data[CONF_MULTI_LIGHT_INTERCEPT]: + _LOGGER.warning( + "%s: Config mismatch: `multi_light_intercept` set to `true` requires `intercept`" + " to be enabled. Adjusting config and continuing setup with" + " `multi_light_intercept: false`.", + self._name, + ) + self._multi_light_intercept = False self._expand_light_groups() # updates manual control timers location, _ = get_astral_location(self.hass) @@ -1603,11 +1608,10 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): class AdaptiveLightingManager: """Track 'light.turn_off' and 'light.turn_on' service calls.""" - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize the AdaptiveLightingManager that is shared among all switches.""" assert hass is not None self.hass = hass - data = validate(config_entry) self.lights: set[str] = set() # Tracks 'light.turn_off' service calls @@ -1658,38 +1662,32 @@ class AdaptiveLightingManager: self._proactively_adapting_contexts: dict[str, str] = {} - is_proactive_adaptation_enabled = data.get( - INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION, - True, - ) + try: + self.listener_removers.append( + setup_service_call_interceptor( + hass, + LIGHT_DOMAIN, + SERVICE_TURN_ON, + self._service_interceptor_turn_on_handler, + ), + ) - if is_proactive_adaptation_enabled: - try: - self.listener_removers.append( - setup_service_call_interceptor( - hass, - LIGHT_DOMAIN, - SERVICE_TURN_ON, - self._service_interceptor_turn_on_handler, - ), - ) + self.listener_removers.append( + setup_service_call_interceptor( + hass, + LIGHT_DOMAIN, + SERVICE_TOGGLE, + self._service_interceptor_turn_on_handler, + ), + ) - self.listener_removers.append( - setup_service_call_interceptor( - hass, - LIGHT_DOMAIN, - SERVICE_TOGGLE, - self._service_interceptor_turn_on_handler, - ), - ) - - _LOGGER.debug("Proactive adaptation enabled") - except RuntimeError: - _LOGGER.warning( - "Failed to set up service call interceptors, " - "falling back to event-reactive mode", - exc_info=True, - ) + _LOGGER.debug("Proactive adaptation enabled") + except RuntimeError: + _LOGGER.warning( + "Failed to set up service call interceptors, " + "falling back to event-reactive mode", + exc_info=True, + ) def disable(self): """Disable the listener by removing all subscribed handlers.""" @@ -1812,6 +1810,7 @@ class AdaptiveLightingManager: else: if ( not switch.is_on + or not switch._intercept # Never adapt on light groups, because HA will make a separate light.turn_on or _is_light_group(self.hass.states.get(entity_id)) # Prevent adaptation of TURN_ON calls when light is already on, @@ -1829,12 +1828,13 @@ class AdaptiveLightingManager: ): _LOGGER.debug( "Switch is off or light is already on for entity_id='%s', skipped='%s'" - " (is_on='%s', is_state='%s', manual_control='%s')", + " (is_on='%s', is_state='%s', manual_control='%s', switch._intercept='%s')", entity_id, skipped, switch.is_on, self.hass.states.is_state(entity_id, STATE_ON), self.manual_control.get(entity_id, False), + switch._intercept, ) skipped.append(entity_id) else: diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index eaf28798..f55a6d88 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -55,7 +55,8 @@ "send_split_delay": "send_split_delay", "adapt_delay": "adapt_delay", "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.", - "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches.", + "intercept": "intercept: Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness.", + "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled.", "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝" }, "data_description": { diff --git a/tests/test_switch.py b/tests/test_switch.py index 4744f878..018c8965 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -92,7 +92,7 @@ from custom_components.adaptive_lighting.const import ( UNDO_UPDATE_LISTENER, ) from custom_components.adaptive_lighting.switch import ( - INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION, + CONF_INTERCEPT, AdaptiveSwitch, _attributes_have_changed, color_difference_redmean, @@ -167,7 +167,7 @@ async def setup_switch(hass, extra_data) -> tuple[MockConfigEntry, AdaptiveSwitc domain=DOMAIN, data={ CONF_NAME: DEFAULT_NAME, - INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: False, + CONF_INTERCEPT: False, **extra_data, }, ) @@ -579,7 +579,7 @@ async def test_manual_control( hass, { CONF_ADAPT_ONLY_ON_BARE_TURN_ON: adapt_only_on_bare_turn_on, - INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: proactive_service_call_adaptation, + CONF_INTERCEPT: proactive_service_call_adaptation, }, ) assert switch._take_over_control @@ -1470,9 +1470,7 @@ def _mock_sun_light_settings(switch: AdaptiveSwitch, settings: dict[str, Any]): async def test_proactive_adaptation(hass): """Validate that a proactive adaptation updates the original service call.""" - switch, _ = await setup_lights_and_switch( - hass, {INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True}, True - ) + switch, _ = await setup_lights_and_switch(hass, {CONF_INTERCEPT: True}, True) _mock_sun_light_settings( switch, @@ -1503,7 +1501,7 @@ async def test_proactive_adaptation_with_separate_commands(hass): switch, _ = await setup_lights_and_switch( hass, { - INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True, + CONF_INTERCEPT: True, CONF_SEPARATE_TURN_ON_COMMANDS: True, }, True, @@ -1539,9 +1537,7 @@ async def test_proactive_adaptation_toggle(hass): This test is based on the fact that contexts of proactive adaptations are recorded. """ - switch, _ = await setup_lights_and_switch( - hass, {INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True}, True - ) + switch, _ = await setup_lights_and_switch(hass, {CONF_INTERCEPT: True}, True) # Toggle ON await hass.services.async_call( @@ -1571,7 +1567,7 @@ async def test_proactive_adaptation_transition_override(hass): switch, (_, _, light3) = await setup_lights_and_switch( hass, { - INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True, + CONF_INTERCEPT: True, CONF_INITIAL_TRANSITION: 123, }, True, @@ -1628,7 +1624,7 @@ async def setup_proactive_multiple_lights_two_switches(hass): CONF_DETECT_NON_HA_CHANGES: True, CONF_PREFER_RGB_COLOR: False, CONF_MIN_COLOR_TEMP: 2500, # to not coincide with sleep_color_temp} - INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True, + CONF_INTERCEPT: True, } _, switch1 = await setup_switch( hass, {CONF_NAME: "switch1", CONF_LIGHTS: [ENTITY_LIGHT_1], **defaults} @@ -1753,7 +1749,7 @@ async def test_two_switches_for_single_light(hass): One switch for brightness and another for color. """ - extra_conf = {INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True} + extra_conf = {CONF_INTERCEPT: True} switch1, (light1, *_) = await setup_lights_and_switch( hass, extra_conf | {CONF_NAME: "switch1"}, all_lights=True ) @@ -1928,7 +1924,7 @@ async def test_light_group( hass, { CONF_LIGHTS: entity_ids, - INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: proactive_service_call_adaptation, + CONF_INTERCEPT: proactive_service_call_adaptation, CONF_TAKE_OVER_CONTROL: take_over_control, CONF_MULTI_LIGHT_INTERCEPT: multi_light_intercept, }, From e9b7988868e2c9e0b35e02816a35707ba0f3e520 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 15 Aug 2023 12:52:39 -0700 Subject: [PATCH 0653/1077] Fix skipped path in multi light intercept (#751) * Fix skipped path in multi light intercept * Deepcopy to make sure that _service_interceptor_turn_on_single_light_handler doesn't moddify * fix --- custom_components/adaptive_lighting/switch.py | 163 +++++++++++------- 1 file changed, 96 insertions(+), 67 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 4d66cc3d..5f69dd5f 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1726,62 +1726,11 @@ class AdaptiveLightingManager: for key in keys: self._proactively_adapting_contexts.pop(key) - async def _service_interceptor_turn_on_handler( # noqa: PLR0912, PLR0915 + def _separate_entity_ids( self, - call: ServiceCall, - data: ServiceData, - ) -> None: - """Intercept `light.turn_on` and `light.toggle` service calls and adapt them. - - It is possible that the calls are made for multiple lights at once, - which in turn might be in different switches or no switches at all. - If there are lights that are not all in a single switch, we need to - make multiple calls to `light.turn_on` with the correct entity IDs. - One of these calls can be intercepted and adapted, the others need to - be adapted by calling `_adapt_light` with the correct entity IDs or - by calling `light.turn_on` directly. - - We create a mapping from switch to entity IDs and keep a list - of skipped lights which are lights in no switches or in switches that - are off or lights that are already on. - - If there is only one switch and 0 skipped lights, we just intercept the - call directly. - - If there are multiple switches and skipped lights, we can adapt the call - for one of the switches to include only the lights in that switch and - need to call `_adapt_light` for the other switches with their - entity_ids. For skipped lights, we call light.turn_on directly with the - entity_ids and original service data. - - If there are only skipped lights, we can use the intercepted call - directly. - """ - is_skipped_hash = is_our_context(call.context, "skipped") - _LOGGER.debug( - "(0) _service_interceptor_turn_on_handler: call.context.id='%s', is_skipped_hash='%s'", - call.context.id, - is_skipped_hash, - ) - if is_our_context(call.context) and not is_skipped_hash: - # Don't adapt our own service calls, but do re-adapt calls that - # were skipped by us - return - - if ATTR_EFFECT in data[CONF_PARAMS] or ATTR_FLASH in data[CONF_PARAMS]: - return - - _LOGGER.debug( - "(1) _service_interceptor_turn_on_handler: call='%s', data='%s'", - call, - data, - ) - - entity_ids = self._get_entity_list(data) - # Note: we do not expand light groups anywhere in this method, instead - # we skip them and rely on the followup call that HA will make - # with the expanded entity IDs. - + entity_ids: list[str], + data, + ) -> tuple[list[str], list[str]]: # Create a mapping from switch to entity IDs # AdaptiveSwitch.name → entity_ids mapping switch_to_eids: dict[str, list[str]] = {} @@ -1840,7 +1789,15 @@ class AdaptiveLightingManager: else: switch_to_eids.setdefault(switch.name, []).append(entity_id) switch_name_mapping[switch.name] = switch + return switch_to_eids, switch_name_mapping, skipped + def _correct_for_multi_light_intercept( + self, + entity_ids, + switch_to_eids, + switch_name_mapping, + skipped, + ): # Check for `multi_light_intercept: true/false` mli = [sw._multi_light_intercept for sw in switch_name_mapping.values()] more_than_one_switch = len(switch_to_eids) > 1 @@ -1868,7 +1825,86 @@ class AdaptiveLightingManager: ) skipped = entity_ids switch_to_eids = {} + return switch_to_eids, switch_name_mapping, skipped + async def _service_interceptor_turn_on_handler( + self, + call: ServiceCall, + service_data: ServiceData, + ) -> None: + """Intercept `light.turn_on` and `light.toggle` service calls and adapt them. + + It is possible that the calls are made for multiple lights at once, + which in turn might be in different switches or no switches at all. + If there are lights that are not all in a single switch, we need to + make multiple calls to `light.turn_on` with the correct entity IDs. + One of these calls can be intercepted and adapted, the others need to + be adapted by calling `_adapt_light` with the correct entity IDs or + by calling `light.turn_on` directly. + + We create a mapping from switch to entity IDs and keep a list + of skipped lights which are lights in no switches or in switches that + are off or lights that are already on. + + If there is only one switch and 0 skipped lights, we just intercept the + call directly. + + If there are multiple switches and skipped lights, we can adapt the call + for one of the switches to include only the lights in that switch and + need to call `_adapt_light` for the other switches with their + entity_ids. For skipped lights, we call light.turn_on directly with the + entity_ids and original service data. + + If there are only skipped lights, we can use the intercepted call + directly. + """ + is_skipped_hash = is_our_context(call.context, "skipped") + _LOGGER.debug( + "(0) _service_interceptor_turn_on_handler: call.context.id='%s', is_skipped_hash='%s'", + call.context.id, + is_skipped_hash, + ) + if is_our_context(call.context) and not is_skipped_hash: + # Don't adapt our own service calls, but do re-adapt calls that + # were skipped by us + return + + if ( + ATTR_EFFECT in service_data[CONF_PARAMS] + or ATTR_FLASH in service_data[CONF_PARAMS] + ): + return + + _LOGGER.debug( + "(1) _service_interceptor_turn_on_handler: call='%s', service_data='%s'", + call, + service_data, + ) + + # Because `_service_interceptor_turn_on_single_light_handler` modifies the + # original service data, we need to make a copy of it to use in the `skipped` call + service_data_copy = deepcopy(service_data) + + entity_ids = self._get_entity_list(service_data) + # Note: we do not expand light groups anywhere in this method, instead + # we skip them and rely on the followup call that HA will make + # with the expanded entity IDs. + + switch_to_eids, switch_name_mapping, skipped = self._separate_entity_ids( + entity_ids, + service_data, + ) + + ( + switch_to_eids, + switch_name_mapping, + skipped, + ) = self._correct_for_multi_light_intercept( + entity_ids, + switch_to_eids, + switch_name_mapping, + skipped, + ) _LOGGER.debug( "(2) _service_interceptor_turn_on_handler: switch_to_eids='%s', skipped='%s'", switch_to_eids, @@ -1886,7 +1922,7 @@ class AdaptiveLightingManager: has_intercepted = False # Can only intercept a turn_on call once for adaptive_switch_name, _entity_ids in switch_to_eids.items(): switch = switch_name_mapping[adaptive_switch_name] - transition = data[CONF_PARAMS].get( + transition = service_data[CONF_PARAMS].get( ATTR_TRANSITION, switch.initial_transition, ) @@ -1900,7 +1936,7 @@ class AdaptiveLightingManager: switch=switch, transition=transition, call=call, - data=modify_service_data(data, _entity_ids), + data=modify_service_data(service_data, _entity_ids), ) has_intercepted = True continue @@ -1930,19 +1966,12 @@ class AdaptiveLightingManager: # Call light turn_on service for skipped entities context = switch.create_context("skipped") _LOGGER.debug( - "(5) _service_interceptor_turn_on_handler: calling `light.turn_on` with skipped='%s', data: '%s', context='%s'", + "(5) _service_interceptor_turn_on_handler: calling `light.turn_on` with skipped='%s', service_data: '%s', context='%s'", skipped, - data, + service_data_copy, # This is the original service data context.id, ) - # Need to expand light groups here because otherwise this interceptor loop will happen twice more - _LOGGER.debug( - "(6) _service_interceptor_turn_on_handler: calling `light.turn_on` with skipped='%s', data: '%s', context='%s'", - skipped, - data, - context.id, - ) - service_data = {ATTR_ENTITY_ID: skipped, **data[CONF_PARAMS]} + service_data = {ATTR_ENTITY_ID: skipped, **service_data_copy[CONF_PARAMS]} if ( ATTR_COLOR_TEMP in service_data and ATTR_COLOR_TEMP_KELVIN in service_data From 0e23e906dadd44127b2eebc51d1fd632549f4986 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 15 Aug 2023 14:47:43 -0700 Subject: [PATCH 0654/1077] Fix #745, first time YAML setup (#752) * Check whether this is the first time setup * Keep list * simplify * no UI tracking --- custom_components/adaptive_lighting/config_flow.py | 8 +++++--- custom_components/adaptive_lighting/switch.py | 6 ++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 3dd89fd4..8f82582a 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -42,13 +42,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input=None): """Handle configuration by YAML file.""" await self.async_set_unique_id(user_input[CONF_NAME]) + # Keep a list of switches that are configured via YAML + data = self.hass.data.setdefault(DOMAIN, {}) + data.setdefault("__yaml__", set()).add(self.unique_id) + for entry in self._async_current_entries(): if entry.unique_id == self.unique_id: - # Keep a list of switches that are configured via YAML - data = self.hass.data.setdefault(DOMAIN, {}) - data.setdefault("__yaml__", []).append(self.unique_id) self.hass.config_entries.async_update_entry(entry, data=user_input) self._abort_if_unique_id_configured() + return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) @staticmethod diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 5f69dd5f..ff951d15 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -410,9 +410,9 @@ async def async_setup_entry( # noqa: PLR0915 data, config_entry, ) - if ( # Skip deleted YAML config entries + if ( # Skip deleted YAML config entries or first time YAML config entries config_entry.source == SOURCE_IMPORT - and config_entry.unique_id not in data.get("__yaml__", []) + and config_entry.unique_id not in data.get("__yaml__", set()) ): _LOGGER.warning( "Deleting AdaptiveLighting switch '%s' because YAML" @@ -1680,8 +1680,6 @@ class AdaptiveLightingManager: self._service_interceptor_turn_on_handler, ), ) - - _LOGGER.debug("Proactive adaptation enabled") except RuntimeError: _LOGGER.warning( "Failed to set up service call interceptors, " From 7a7baafcb32a453588b95a0897f332eec2e185a1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Aug 2023 15:25:19 -0700 Subject: [PATCH 0655/1077] [pre-commit.ci] pre-commit autoupdate (#748) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.0.282 → v0.0.284](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.282...v0.0.284) * Ruff fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .pre-commit-config.yaml | 2 +- custom_components/adaptive_lighting/hass_utils.py | 7 +++---- custom_components/adaptive_lighting/switch.py | 11 +++++------ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 31fdf86c..e7b8b253 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.282 + rev: v0.0.284 hooks: - id: ruff args: ["--fix"] diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py index ba5bb8b0..cc3257ce 100644 --- a/custom_components/adaptive_lighting/hass_utils.py +++ b/custom_components/adaptive_lighting/hass_utils.py @@ -51,12 +51,11 @@ def setup_service_call_interceptor( # Convert data back to read-only call.data = ReadOnlyDict(data) - except Exception as e: # noqa: BLE001 + except Exception: # Blindly catch all exceptions to avoid breaking light.turn_on - _LOGGER.error( - "Error for call '%s' in service_func_proxy: '%s'", + _LOGGER.exception( + "Error for call '%s' in service_func_proxy", call.data, - e, ) # Call original service handler with processed data await existing_service.job.target(call) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index ff951d15..7e910525 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -681,7 +681,7 @@ def _convert_attributes(attributes: dict[str, Any]) -> dict[str, Any]: if rgb is not None: attributes[ATTR_RGB_COLOR] = rgb - _LOGGER.debug(f"Converted {attributes} to rgb {rgb}") + _LOGGER.debug("Converted attributes %s to rgb %s", attributes, rgb) else: _LOGGER.debug("No suitable color conversion found for %s", attributes) @@ -1041,11 +1041,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): try: # HACK: this is a private method in `Entity` which can change super()._call_on_remove_callbacks() - except AttributeError as err: - _LOGGER.error( - "%s: Caught AttributeError in `_call_on_remove_callbacks`: %s", + except AttributeError: + _LOGGER.exception( + "%s: Caught AttributeError in `_call_on_remove_callbacks`", self._name, - err, ) def _remove_interval_listener(self) -> None: @@ -1816,7 +1815,7 @@ class AdaptiveLightingManager: single_switch_with_multiple_lights and switch_without_multi_light_intercept ): _LOGGER.warning( - "Single switch with multiple lights targeted, but" + "Single switch with multiple lights targeted (%s), but" " `multi_light_intercept: true` is not set, so skipping intercept" " for all lights.", switch_to_eids, From 792bed8cf6682056c5d5b651d1557f307e6102c9 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 15 Aug 2023 15:25:29 -0700 Subject: [PATCH 0656/1077] Bump to 1.19.1 in manifest.json --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index eb7f0155..e93d3897 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.19.0" + "version": "1.19.1" } From 7e84ed0a8433d39b339afa4762392294c3a50886 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 15 Aug 2023 17:09:18 -0700 Subject: [PATCH 0657/1077] Use WebLate for translations (#753) * Add license * lunk * chore(docs): update TOC * fix --------- Co-authored-by: basnijholt --- README.md | 12 +++++ .../adaptive_lighting/translations/LICENSE.md | 44 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/LICENSE.md diff --git a/README.md b/README.md index 1f2065c7..1997c351 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ The `adaptive_lighting.manual_control` event is fired when a light is marked as - [Custom brightness ramps using `brightness_mode` with `"linear"` and `"tanh"`](#custom-brightness-ramps-using-brightness_mode-with-linear-and-tanh) - [:eyes: See also](#eyes-see-also) - [:busts_in_silhouette: Contributors](#busts_in_silhouette-contributors) +- [Translating Adaptive Lighting](#translating-adaptive-lighting) @@ -540,3 +541,14 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + +## Translating Adaptive Lighting + +Help to translate Adaptive Lighting into your language on [Hosted Weblate](https://hosted.weblate.org/engage/adaptive-lighting/)! + +Translating can be done from your webbrowser, no programming knowledge +is needed! + + +Translation status + diff --git a/custom_components/adaptive_lighting/translations/LICENSE.md b/custom_components/adaptive_lighting/translations/LICENSE.md new file mode 100644 index 00000000..5916442d --- /dev/null +++ b/custom_components/adaptive_lighting/translations/LICENSE.md @@ -0,0 +1,44 @@ +This license **only** applies to all files in `custom_components/adaptive_lighting/translations/` in the Adaptive Lighting repository. +For these translations we wave copyright and related rights through the CC0 1.0 Universal license. + +# Creative Commons CC0 1.0 Universal + +CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. + +### Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. + +1. **Copyright and Related Rights.** A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. + +2. **Waiver.** To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. + +3. **Public License Fallback.** Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. + +4. **Limitations and Disclaimers.** + + a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. From 93cfda427a084972252925fc4af154fa0b5e8e6a Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Wed, 16 Aug 2023 04:09:55 +0200 Subject: [PATCH 0658/1077] Translations update from Hosted Weblate (#754) * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Adaptive Lighting/Adaptive Lighting Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ * Translated using Weblate (Dutch) Currently translated at 24.1% (37 of 153 strings) Translation: Adaptive Lighting/Adaptive Lighting Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/ --------- Co-authored-by: Bas Nijholt --- .../adaptive_lighting/translations/nl.json | 110 ++++++++++-------- .../adaptive_lighting/translations/sv.json | 91 +++++++-------- 2 files changed, 104 insertions(+), 97 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json index a0b6b082..cc269685 100644 --- a/custom_components/adaptive_lighting/translations/nl.json +++ b/custom_components/adaptive_lighting/translations/nl.json @@ -1,57 +1,67 @@ { - "title": "Adaptieve verlichting", - "config": { - "step": { - "user": { - "title": "Kies een naam voor de adaptieve verlichting integratie", - "description": "Kies een naam voor deze integratie. U kunt verschillende integratie van Adaptieve verlichting uitvoeren, elk van deze kan meerdere lichten bevatten!", - "data": { - "name": "Naam" + "title": "Adaptieve verlichting", + "config": { + "step": { + "user": { + "title": "Kies een naam voor de adaptieve verlichting integratie", + "description": "Kies een naam voor deze integratie. U kunt verschillende integratie van Adaptieve verlichting uitvoeren, elk van deze kan meerdere lichten bevatten!", + "data": { + "name": "Naam" + } + } + }, + "abort": { + "already_configured": "Dit apparaat is al geconfigureerd" } - } }, - "abort": { - "already_configured": "Dit apparaat is al geconfigureerd" - } - }, - "options": { - "step": { - "init": { - "title": "Adaptieve verlichting instellingen", - "description": "Alle instellingen voor een Adaptieve verlichting component. De optienamen komen overeen met de YAML-instellingen. Er worden geen opties weergegeven als u het item adaptive_lighting hebt gedefinieerd in uw YAML-configuratie.", - "data": { - "lights": "Lichten", - "initial_transition": "initial_transition: Wanneer lichten van 'uit' naar 'aan' gaan. (seconden)", - "sleep_transition": "sleep_transition: Wanneer 'sleep_state' verandert. (seconden)", - "interval": "interval: Tijd tussen switch-updates. (seconden)", - "max_brightness": "max_brightness: Hoogste helderheid van lichten tijdens een cyclus. (%)", - "max_color_temp": "max_color_temp: Koudste tint van de kleurtemperatuurcyclus. (kelvin)", - "min_brightness": "min_brightness: Laagste helderheid van lichten tijdens een cyclus. (%)", - "min_color_temp": "min_color_temp, Warmste tint van de kleurtemperatuurcyclus. (kelvin)", - "only_once": "only_once: pas de verlichting alleen aan wanneer u ze aanzet.", - "prefer_rgb_color": "prefer_rgb_color: Gebruik waar mogelijk 'rgb_color' in plaats van 'color_temp'.", - "separate_turn_on_commands": "separate_turn_on_commands: Scheid de commando's voor elk attribuut (kleur, helderheid, enz.) in 'light.turn_on' (vereist voor sommige lampen).", - "send_split_delay": "send_split_delay: wacht tussen commando's (milliseconden), wanneer separate_turn_on_commands wordt gebruikt. Kan ervoor zorgen dat beide commando's correct door de lamp worden afgehandeld.", - "sleep_brightness": "sleep_brightness, helderheidsinstelling voor slaapstand. (%)", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, gebruik 'rgb_color' of 'color_temp'", - "sleep_rgb_color": "sleep_rgb_color, in RGB", - "sleep_color_temp": "sleep_color_temp: Kleurtemperatuurinstelling voor slaapstand. (kelvin)", - "sunrise_offset": "sunrise_offset: Hoe lang voor(-) of na(+) zonsopgang uitvoeren (+/- seconden)", - "sunrise_time": "sunrise_time: Handmatige wijziging van de zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", - "max_sunrise_time": "max_sunrise_time: handmatige aanpassing van de maximale zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", - "sunset_offset": "sunset_offset: Hoe lang voor(-) of na(+) zonsondergang uitvoeren (+/- seconden)", - "sunset_time": "sunset_time: handmatige onderdrukking van de zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", - "min_sunset_time": "min_sunset_time: handmatige onderdrukking van de minimale zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsondergangstijd op uw locatie gebruikt (UU:MM:SS)", - "take_over_control": "take_over_control: Als iets anders dan Adaptive Lighting 'light.turn_on' roept wanneer een lamp al aan is, stop dan met het aanpassen van het licht totdat het (of de schakelaar) uit -> aan gaat.", - "detect_non_ha_changes": "detect_non_ha_changes: detecteert alle >10% wijzigingen aan de lichten (ook buiten HA), vereist dat 'take_over_control' is ingeschakeld (heet 'homeassistant.update_entity' elke 'interval'!)", - "transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)", - "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen." + "options": { + "step": { + "init": { + "title": "Adaptieve verlichting instellingen", + "description": "Alle instellingen voor een Adaptieve verlichting component. De optienamen komen overeen met de YAML-instellingen. Er worden geen opties weergegeven als u het item adaptive_lighting hebt gedefinieerd in uw YAML-configuratie.", + "data": { + "lights": "Lichten", + "initial_transition": "initial_transition: Wanneer lichten van 'uit' naar 'aan' gaan. (seconden)", + "sleep_transition": "sleep_transition: Wanneer 'sleep_state' verandert. (seconden)", + "interval": "interval: Tijd tussen switch-updates. (seconden)", + "max_brightness": "max_brightness: Hoogste helderheid van lichten tijdens een cyclus. (%)", + "max_color_temp": "max_color_temp: Koudste tint van de kleurtemperatuurcyclus. (kelvin)", + "min_brightness": "min_brightness: Laagste helderheid van lichten tijdens een cyclus. (%)", + "min_color_temp": "min_color_temp, Warmste tint van de kleurtemperatuurcyclus. (kelvin)", + "only_once": "only_once: pas de verlichting alleen aan wanneer u ze aanzet.", + "prefer_rgb_color": "prefer_rgb_color: Gebruik waar mogelijk 'rgb_color' in plaats van 'color_temp'.", + "separate_turn_on_commands": "separate_turn_on_commands: Scheid de commando's voor elk attribuut (kleur, helderheid, enz.) in 'light.turn_on' (vereist voor sommige lampen).", + "send_split_delay": "send_split_delay: wacht tussen commando's (milliseconden), wanneer separate_turn_on_commands wordt gebruikt. Kan ervoor zorgen dat beide commando's correct door de lamp worden afgehandeld.", + "sleep_brightness": "sleep_brightness, helderheidsinstelling voor slaapstand. (%)", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, gebruik 'rgb_color' of 'color_temp'", + "sleep_rgb_color": "sleep_rgb_color, in RGB", + "sleep_color_temp": "sleep_color_temp: Kleurtemperatuurinstelling voor slaapstand. (kelvin)", + "sunrise_offset": "sunrise_offset: Hoe lang voor(-) of na(+) zonsopgang uitvoeren (+/- seconden)", + "sunrise_time": "sunrise_time: Handmatige wijziging van de zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", + "max_sunrise_time": "max_sunrise_time: handmatige aanpassing van de maximale zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", + "sunset_offset": "sunset_offset: Hoe lang voor(-) of na(+) zonsondergang uitvoeren (+/- seconden)", + "sunset_time": "sunset_time: handmatige onderdrukking van de zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", + "min_sunset_time": "min_sunset_time: handmatige onderdrukking van de minimale zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsondergangstijd op uw locatie gebruikt (UU:MM:SS)", + "take_over_control": "take_over_control: Als iets anders dan Adaptive Lighting 'light.turn_on' roept wanneer een lamp al aan is, stop dan met het aanpassen van het licht totdat het (of de schakelaar) uit -> aan gaat.", + "detect_non_ha_changes": "detect_non_ha_changes: detecteert alle >10% wijzigingen aan de lichten (ook buiten HA), vereist dat 'take_over_control' is ingeschakeld (heet 'homeassistant.update_entity' elke 'interval'!)", + "transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)", + "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Wanneer je de lichten in eerste instantie aanzet. Indien ingesteld op `true`, past AL zich alleen aan als `light.turn_on` wordt aangeroepen zonder kleur of helderheid op te geven. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Indien `false`, past AL zich aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. Moet 'take_over_control' ingeschakeld zijn. 🕵️ " + } + } + }, + "error": { + "option_error": "Ongeldige optie", + "entity_missing": "Een of meer geselecteerde lichtentiteiten ontbreken in Home Assistant" } - } }, - "error": { - "option_error": "Ongeldige optie", - "entity_missing": "Een of meer geselecteerde lichtentiteiten ontbreken in Home Assistant" + "services": { + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄" + } + } + } } - } } diff --git a/custom_components/adaptive_lighting/translations/sv.json b/custom_components/adaptive_lighting/translations/sv.json index 2239ad3b..fe5fb653 100644 --- a/custom_components/adaptive_lighting/translations/sv.json +++ b/custom_components/adaptive_lighting/translations/sv.json @@ -1,53 +1,50 @@ { - "title": "Adaptiv Ljussättning", - "config": { - "step": { - "user": { - "title": "Välj ett namn för Adaptiv Ljussättning", - "description": "Varje konfiguration kan innehålla flera ljuskällor!", - "data": { - "name": "Namn" + "title": "Adaptiv Ljussättning", + "config": { + "step": { + "user": { + "title": "Välj ett namn för Adaptiv Ljussättning", + "description": "Varje konfiguration kan innehålla flera ljuskällor!", + "data": { + "name": "Namn" + } + } + }, + "abort": { + "already_configured": "Enheten är redan konfiguerad" } - } }, - "abort": { - "already_configured": "Enheten är redan konfiguerad" - } - }, - "options": { - "step": { - "init": { - "title": "Adaptiv Ljussättning Inställningar", - "description": "Alla inställningar för en Adaptiv Ljussättning komponent. Titeln på inställningarna är desamma som i YAML konfigurationen. Inga inställningar visas om enheten redan är konfigurerad i YAML.", - "data": { - "lights": "lights, ljuskällor", - "adapt_brightness": "adapt_brightness, Adaptiv ljusstyrka", - "adapt_color_temp": "adapt_color_temp, Justera färgtemperatur genom att använda 'color_temp' om möjligt", - "adapt_rgb_color": "adapt_rgb_color, Justera färgtemperatur genom att använda RGB/XY om möjligt", - "initial_transition": "initial_transition, när ljuskällorna går från 'av' till 'på' eller när 'sleep_state' ändras", - "interval": "interval, Tid mellan uppdateringar i sekunder", - "max_brightness": "max_brightness, i procent %", - "max_color_temp": "max_color_temp, i Kelvin", - "min_brightness": "min_brightness, i %", - "min_color_temp": "min_color_temp, i Kelvin", - "only_once": "only_once, Adaptivt justera endast ljuskällorna när de sätts från 'av' till 'på'", - "prefer_rgb_color": "prefer_rgb_color, Använd 'rgb_color' över 'color_temp' om möjligt", - "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.", - "sleep_brightness": "sleep_brightness, i %", - "sleep_color_temp": "sleep_color_temp, i Kelvin", - "sunrise_offset": "sunrise_offset, i +/- sekunder", - "sunrise_time": "sunrise_time, i 'HH:MM:SS' format (om 'None', används den faktiskta soluppgången för din position)", - "sunset_offset": "sunset_offset, i +/- sekunder", - "sunset_time": "sunset_time, i 'HH:MM:SS' format (om 'None', används den faktiskta solnedgången för din position)", - "take_over_control": "take_over_control, om något utöver 'Adaptiv Ljussättning' komponenten kallar på 'light.turn_on' när en ljuskälla redan är på, stängs den adaptiva justeringen av tills ljuskällan stängs av -> på igen, alternativt switchen för konfigurationen", - "detect_non_ha_changes": "detect_non_ha_changes, Upptäcker alla ändringar större än 5% gjorda på ljuskällorna som inte kommer från HA. Kräver att 'take_over_control' är påslaget.(Kallar på 'homeassistant.update_entity' vid varje 'interval'!)", - "transition": "transition, i sekunder" + "options": { + "step": { + "init": { + "title": "Adaptiv Ljussättning Inställningar", + "description": "Alla inställningar för en Adaptiv Ljussättning komponent. Titeln på inställningarna är desamma som i YAML konfigurationen. Inga inställningar visas om enheten redan är konfigurerad i YAML.", + "data": { + "lights": "lights, ljuskällor", + "initial_transition": "initial_transition, när ljuskällorna går från 'av' till 'på' eller när 'sleep_state' ändras", + "interval": "interval, Tid mellan uppdateringar i sekunder", + "max_brightness": "max_brightness, i procent %", + "max_color_temp": "max_color_temp, i Kelvin", + "min_brightness": "min_brightness, i %", + "min_color_temp": "min_color_temp, i Kelvin", + "only_once": "only_once, Adaptivt justera endast ljuskällorna när de sätts från 'av' till 'på'", + "prefer_rgb_color": "prefer_rgb_color, Använd 'rgb_color' över 'color_temp' om möjligt", + "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.", + "sleep_brightness": "sleep_brightness, i %", + "sleep_color_temp": "sleep_color_temp, i Kelvin", + "sunrise_offset": "sunrise_offset, i +/- sekunder", + "sunrise_time": "sunrise_time, i 'HH:MM:SS' format (om 'None', används den faktiskta soluppgången för din position)", + "sunset_offset": "sunset_offset, i +/- sekunder", + "sunset_time": "sunset_time, i 'HH:MM:SS' format (om 'None', används den faktiskta solnedgången för din position)", + "take_over_control": "take_over_control, om något utöver 'Adaptiv Ljussättning' komponenten kallar på 'light.turn_on' när en ljuskälla redan är på, stängs den adaptiva justeringen av tills ljuskällan stängs av -> på igen, alternativt switchen för konfigurationen", + "detect_non_ha_changes": "detect_non_ha_changes, Upptäcker alla ändringar större än 5% gjorda på ljuskällorna som inte kommer från HA. Kräver att 'take_over_control' är påslaget.(Kallar på 'homeassistant.update_entity' vid varje 'interval'!)", + "transition": "transition, i sekunder" + } + } + }, + "error": { + "option_error": "Ogiltlig inställning", + "entity_missing": "Ett valt ljus hittades inte" } - } - }, - "error": { - "option_error": "Ogiltlig inställning", - "entity_missing": "Ett valt ljus hittades inte" } - } } From 627c7a120c0ea54a9de6d881dd8ec896cb9dc145 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Wed, 16 Aug 2023 05:02:47 +0200 Subject: [PATCH 0659/1077] Translations update from Hosted Weblate (de, nl, fr) (#755) * Translated using Weblate (German) Currently translated at 0.0% (0 of 153 strings) Translation: Adaptive Lighting/Adaptive Lighting Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/de/ * Translated using Weblate (French) Currently translated at 0.0% (0 of 153 strings) Translation: Adaptive Lighting/Adaptive Lighting Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/fr/ * Translated using Weblate (Dutch) Currently translated at 56.8% (87 of 153 strings) Translation: Adaptive Lighting/Adaptive Lighting Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/ --------- Co-authored-by: Bas Nijholt --- .../adaptive_lighting/translations/de.json | 313 +++++++++++++++--- .../adaptive_lighting/translations/fr.json | 306 ++++++++++++++--- .../adaptive_lighting/translations/nl.json | 6 + 3 files changed, 530 insertions(+), 95 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json index 79eb0df8..abb12101 100644 --- a/custom_components/adaptive_lighting/translations/de.json +++ b/custom_components/adaptive_lighting/translations/de.json @@ -1,58 +1,269 @@ { - "title": "Adaptive Lighting", - "config": { - "step": { - "user": { - "title": "Benenne das Adaptive Lighting", - "description": "Jede Instanz kann mehrere Licht Entitäten beinhalten", - "data": { - "name": "Name" + "title": "Verwendbare Beleuchtung", + "config": { + "step": { + "user": { + "title": "Wähl aus einen Namen für die Verwendbare Beleuchtung Instanz", + "description": "Jede Instanz kann mehrfache Lichter zügeln!", + "data": { + "name": "Name" + } + } + }, + "abort": { + "already_configured": "Diese Vorrichtung ist schon konfiguriert" } - } }, - "abort": { - "already_configured": "Gerät ist bereits konfiguriert!" - } - }, - "options": { - "step": { - "init": { - "title": "Adaptive Lighting Optionen", - "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde.", - "data": { - "lights": "Lichter", - "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt", - "sleep_transition": "sleep_transition: Wenn 'sleep_state' sich ändert. (Sekunden)", - "interval": "interval, Zeit zwischen Updates des Switches", - "max_brightness": "max_brightness, maximale Helligkeit in %", - "max_color_temp": "max_color_temp, maximale Farbtemperatur in Kelvin", - "min_brightness": "min_brightness, minimale Helligkeit in %", - "min_color_temp": "min_color_temp, minimale Farbtemperatur in Kelvin", - "only_once": "only_once, passe die Lichter nur beim Einschalten an", - "prefer_rgb_color": "prefer_rgb_color, nutze 'rgb_color' vor 'color_temp', wenn möglich", - "separate_turn_on_commands": "separate_turn_on_commands, für jedes Attribut (Farbe, Helligkeit usw.) in 'light.turn_on' werden separate Befehle gesendet. Wird für manche Leuchtmittel benötigt.", - "send_split_delay": "send_split_delay: Wartezeit zwischen dem Senden der Befehle (Millisekunden), wenn separate_turn_on_commands genutzt wird. Kann helfen, wenn die Leuchtmittel die separaten Befehle nicht korrekt umsetzen.", - "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, nutze 'rgb_color' oder 'color_temp'", - "sleep_rgb_color": "sleep_rgb_color, in RGB", - "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperatur in Kelvin", - "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- Sekunden", - "sunrise_time": "sunrise_time, Sonnenaufgangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenaufgangs an deiner Position verwendet)", - "max_sunrise_time": "max_sunrise_time: Manuelles Überschreiben der max. sunrise_time. Falls 'None', wird die tatsächliche sunrise_time an deiner Position verwendet (HH:MM:SS)", - "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- Sekunden", - "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)", - "min_sunset_time": "min_sunset_time: Manuelles Überschreiben der min. sunset_time. Falls 'None', wird die tatsächliche sunset_time an deiner Position verwendet (HH:MM:SS)", - "take_over_control": "take_over_control, wenn irgendetwas während ein Licht an ist außer Adaptive Lighting den Service 'light.turn_on' aufruft, stoppe die Anpassung des Lichtes (oder des Schalters) bis dieser wieder von off -> on geschaltet wird.", - "detect_non_ha_changes": "detect_non_ha_changes, entdeckt alle Änderungen über 10% am Licht (auch außerhalb von HA gemacht), 'take_over_control' muss aktiviert sein (ruft 'homeassistant.update_entity' jede 'interval' auf!)", - "transition": "transition, Wechselzeit in Sekunden", - "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.", - "skip_redundant_commands": "Keine Adaptierungsbefehle senden, deren erwünschter Status schon dem bekanntes Status von Lichtern entspricht. Minimiert die Netzwerkbelastung und verbessert die Adaptierung in manchen Situationen. Deaktiviert lassen falls der pysikalische Status der Lichter und der erkannte Status in HA nicht synchron bleiben." + "options": { + "step": { + "init": { + "title": "Verwendbare Beleuchtung Optionen", + "description": "Konfigurier eine Verwendbare Beleuchtung Komponente. Option Namen stimmen ab mit die YAML Lagen. Ob du hast diesen Eintrag definiert herein YAML, keine Optionen wollen fungieren hier. Für interaktiv Graphen #der demonstrieren Parameter Effekte, besuchen [dieses Web #App](https://basnijholt.github.io/Verwendbar-Beleuchtung). Für ferner Details, seht die [offizielle Dokumentation](https://github.com/basnijholt/Verwendbar-Beleuchtung#readme).", + "data": { + "lights": "Lichter: Liste von licht Entität_ids zu sein reguliert (darf sein leer). 🌟", + "initial_transition": "Einleitender_Wechsel", + "sleep_transition": "Schlaf_Wechsel", + "interval": "Intervall", + "max_brightness": "max_Helligkeit: #Höchster Helligkeit Prozentsatz. 💡", + "max_color_temp": "max_Farbe_Aushilfskraft: #Kalt Farbe Temperatur in #Kelvin. ❄️", + "min_brightness": "min_Helligkeit: Minimaler Helligkeit Prozentsatz. 💡", + "min_color_temp": "min_Farbe_Aushilfskraft: #Warm Farbe Temperatur in #Kelvin. 🔥", + "only_once": "Nur_einmal: Adaptier nur Lichter als sind angemacht sie (`wahr`) oder Unterhalt adaptieren ihnen (`falsch`). 🔄", + "prefer_rgb_color": "Bevorzug_rgb_Farbe: Ob zu bevorzugen #RGB Farbe Anpassung über licht Farbe Temperatur als möglich. 🌈", + "separate_turn_on_commands": "Getrennte_Drehung_auf_Befehle: Nutzung trennen `Licht.Drehung_holt ab` weiter Farbe und Helligkeit, gebraucht für einige Licht Typen. 🔀", + "send_split_delay": "#Senden_geteilt_Aufschub", + "sleep_brightness": "Schlaf_Helligkeit", + "sleep_rgb_or_color_temp": "Schlaf_rgb_oder_Farbe_Aushilfskraft", + "sleep_rgb_color": "Schlaf_rgb_Farbe", + "sleep_color_temp": "Schlaf_Farbe_Aushilfskraft", + "sunrise_offset": "Sonnenaufgang_Verschiebung", + "sunrise_time": "Sonnenaufgang_Zeit", + "max_sunrise_time": "max_Sonnenaufgang_Zeit", + "sunset_offset": "Abendrot_Verschiebung", + "sunset_time": "Abendrot_Zeit", + "min_sunset_time": "min_Abendrot_Zeit", + "take_over_control": "Nimm_über_Aufsicht: Schalte aus Verwendbare Beleuchtung ob anderen Quelle Anrufe `hell.Drehung_auf` während sind Lichter weiter und sein adaptiert. Note dass diese Anrufe `homeassistant.Update_Entität` jedes `Intervall`! 🔒", + "detect_non_ha_changes": "Finde heraus_nicht_ha_Änderungen: Findet heraus und hält an Bearbeitungen für nicht-`Licht.Drehung_auf` staatlich Änderungen. Notwendigkeiten `nehmen_über_Aufsicht` aktiviert. 🕵️ Vorsicht: ⚠️ Einige Lichter dürfen hindeuten #kein 'weiter' Staat, #welche konnte resultieren in Lichter anmachen unerwartet. Schalte aus diesen Charakterzug ob du begegnest solche Sachverhalte.", + "transition": "Wechsel", + "adapt_delay": "Adaptier_Aufschub", + "skip_redundant_commands": "Sprung_redundant_Befehle: Sprung senden Bearbeitung gebietet wessen anrichten schon staatlich #angleichen das Lichts gekannt Staat. Minimiert Netzwerk Verkehr und verbessert die Bearbeitung responsivity in einigen Situationen. 📉Schalte aus ob physische Licht Staaten steigen aus von synchronisieren mit HAaufgezeichnet haben Staat.", + "autoreset_control_seconds": "autoreset_Aufsicht_Sekunden", + "brightness_mode": "Helligkeit_Verfahren", + "brightness_mode_time_dark": "Helligkeit_Verfahren_Zeit_Finsternis", + "brightness_mode_time_light": "Helligkeit_Verfahren_Zeit_Licht", + "max_sunset_time": "max_Abendrot_Zeit", + "transition_until_sleep": "Wechsel_bis_Schlaf: Wann aktiviert, Verwendbare Beleuchtung will spendieren schlafen Lagen da das Minimum, übergehen zu diesen Werten nach Abendrot. 🌙", + "min_sunrise_time": "min_Sonnenaufgang_Zeit", + "adapt_only_on_bare_turn_on": "Adaptier_nur_auf_kahl_Drehung_weiter: Wann drehen weiter Lichter anfänglich. Ob gesetzt zu `wahr`, #AL adaptiert nur ob `Licht.Drehung_ist aufgerufen` weiter ohne präzisieren Farbe oder Helligkeit. ❌🌈 Dies #z.B., verhindert als Bearbeitung aktivierend eine Szene. Ob `falsch`, #AL adaptiert #ohne Rücksicht auf von dem Vorliegen von Farbe oder Helligkeit in der einleitenden `Bedienung_Daten`. Notwendigkeiten `nehmen_über_Aufsicht` aktiviert. 🕵️ ", + "intercept": "Unterbrich: Unterbrich und adaptier `Licht.Drehung_auf` Anrufe zu aktivieren momentane Farbe und Helligkeit Bearbeitung. 🏎️ Schalte aus für Lichter #der unterstützen nicht `Licht.Drehung_weiter` mit Farbe und Helligkeit.", + "multi_light_intercept": "multi_Licht_unterbricht: Unterbrich und adaptier `Licht.Drehung_auf` Anrufe jenes Soll mehrfache Lichter. ➗⚠️ Dies darf resultieren in #aufspalten ein lediges `Licht.Drehung_auf` Anruf hinein mehrfach Anrufe, #z.B., als sind Lichter in verschieden Schalter. Bedürft `unterbrechen` zu sein aktiviert.", + "include_config_in_attributes": "Schließ ein_config_in_Attribute: Vorstellung jede Optionen da Attribute auf dem Schalter in Heim Stellvertretend als gesetzt zu `wahr`. 📝" + }, + "data_description": { + "initial_transition": "Dauer von dem ersten Wechsel zündet an als Drehung von `ab` zu `weiter` in Sekunden. ⏲️", + "sleep_brightness": "Helligkeit Prozentsatz von Lichter in schlafen Verfahren. 😴", + "sleep_rgb_color": "#RGB Farbe in Schlaf Verfahren (benutzt als `schlafen_rgb_oder_Farbe_Aushilfskraft` ist \"rgb_Farbe\"). 🌈", + "sleep_transition": "Dauer von Wechsel schläft \"als Verfahren\" ist #festknebeln in Sekunden. 😴", + "sunrise_time": "Gesetzt eine feste Zeit (HH:MM:SS) Für Sonnenaufgang. 🌅", + "min_sunrise_time": "Setzte den frühesten virtuellen Sonnenaufgang Zeit (HH:MM:SS), Erlauben für später Sonnenaufgänge. 🌅", + "interval": "Häufigkeit zu adaptieren die Lichter, in Sekunden. 🔄", + "transition": "Dauer von Wechsel zündet an als Änderung, in Sekunden. 🕑", + "sleep_rgb_or_color_temp": "Nutzung entweder `\"rgb_Farbe\"` oder `\"Farbe_Aushilfskraft\"` in Schlaf Verfahren. 🌙", + "sleep_color_temp": "Farbe Temperatur in Schlaf Verfahren (benutzt als `schlafen_rgb_oder_Farbe_Aushilfskraft` ist `Farbe_Aushilfskraft`) in #Kelvin. 😴", + "max_sunrise_time": "Setzte den spätesten virtuellen Sonnenaufgang Zeit (HH:MM:SS), Erlauben für früher Sonnenaufgänge. 🌅", + "sunrise_offset": "Pass an Sonnenaufgang Zeit mit ein bejahendes oder verneinte Verschiebung in Sekunden. ⏰", + "sunset_time": "Gesetzt eine feste Zeit (HH:MM:SS) Für Abendrot. 🌇", + "min_sunset_time": "Setzte das früheste virtuelle Abendrot Zeit (HH:MM:SS), Erlauben für später Abendrot. 🌇", + "max_sunset_time": "Setzte das späteste virtuelle Abendrot Zeit (HH:MM:SS), Erlauben für früher Abendrot. 🌇", + "sunset_offset": "Pass an Abendrot Zeit mit ein bejahendes oder verneinte Verschiebung in Sekunden. ⏰", + "brightness_mode": "Helligkeit Verfahren zu benutzen. Mögliche Werte sind `#voreingestellt`, `geradlinig`, und `tanh` (Nutzungen `Helligkeit_Verfahren_Zeit_Finsternis` und `Helligkeit_Verfahren_Zeit_hell`). 📈", + "brightness_mode_time_dark": "(Überhört ob `Helligkeit_Verfahren='#voreingestellt'`) Die Dauer in Sekunden zu #Nepp oben/#vor herunter die Helligkeit/nach Sonnenaufgang/Abendrot. 📈📉", + "brightness_mode_time_light": "(Überhört ob `Helligkeit_Verfahren='#voreingestellt'`) Die Dauer in Sekunden zu #Nepp oben/#nachdem herunter die Helligkeit/vor Sonnenaufgang/Abendrot. 📈📉.", + "autoreset_control_seconds": "Automatisch #rücksetzen die manuelle Aufsicht nach einer Nummer von Sekunden. Apparat zu 0 zu ausschalten. ⏲️", + "send_split_delay": "Aufschub (ms) zwischen `getrennt_Drehung_auf_Befehle` für Lichter #der unterstützen nicht zeitgleiche Helligkeit und Farbe Lage. ⏲️", + "adapt_delay": "Wartezeit Zeit (Sekunden) zwischen Licht macht an und Verwendbare Beleuchtung bewerbend Änderungen. Dürfen helfen zu vermeiden flackern. ⏲️" + } + } + }, + "error": { + "option_error": "Kränkliche Option", + "entity_missing": "#Man oder #mehr ausgewählt Licht Entitäten fehlen von Heim Assistenten" } - } }, - "error": { - "option_error": "Fehlerhafte Option", - "entity_missing": "Ein ausgewähltes Licht wurde nicht gefunden" + "services": { + "change_switch_settings": { + "fields": { + "use_defaults": { + "description": "Setzt nicht die #voreingestellt Werte präzisiert in diesen Bedienung Anruf. Optionen: \"Lauf\" (#voreingestellt, hält fest gängige Werte), \"Fabrik\" (#Nachstellung zu dokumentieren Vorgaben), oder \"Konfiguration\" (fällt zurück zu schalten config Vorgaben). ⚙️", + "name": "Benutzen_Vorgaben" + }, + "include_config_in_attributes": { + "description": "Vorstellung jede Optionen da Attribute auf dem Schalter in Heim Stellvertretend als gesetzt zu `wahr`. 📝", + "name": "Schließ ein_config_in_Attribute" + }, + "turn_on_lights": { + "description": "Ob zu anmachen Lichter jener ist zurzeit ab. 🔆", + "name": "Drehung_auf_Lichter" + }, + "initial_transition": { + "description": "Dauer von dem ersten Wechsel zündet an als Drehung von `ab` zu `weiter` in Sekunden. ⏲️", + "name": "Einleitender_Wechsel" + }, + "sleep_transition": { + "description": "Dauer von Wechsel schläft \"als Verfahren\" ist #festknebeln in Sekunden. 😴", + "name": "Schlaf_Wechsel" + }, + "max_brightness": { + "description": "#Höchster Helligkeit Prozentsatz. 💡", + "name": "max_Helligkeit" + }, + "max_color_temp": { + "description": "#Kalt Farbe Temperatur in #Kelvin. ❄️", + "name": "max_Farbe_Aushilfskraft" + }, + "min_brightness": { + "description": "Minimaler Helligkeit Prozentsatz. 💡", + "name": "min_Helligkeit" + }, + "only_once": { + "description": "Adaptier nur Lichter als sind angemacht sie (`wahr`) oder Unterhalt adaptieren ihnen (`falsch`). 🔄", + "name": "Nur_einmal" + }, + "prefer_rgb_color": { + "description": "Ob zu bevorzugen #RGB Farbe Anpassung über licht Farbe Temperatur als möglich. 🌈", + "name": "Bevorzug_rgb_Farbe" + }, + "send_split_delay": { + "description": "Aufschub (ms) zwischen `getrennt_Drehung_auf_Befehle` für Lichter #der unterstützen nicht zeitgleiche Helligkeit und Farbe Lage. ⏲️", + "name": "#Senden_geteilt_Aufschub" + }, + "sleep_brightness": { + "description": "Helligkeit Prozentsatz von Lichter in schlafen Verfahren. 😴", + "name": "Schlaf_Helligkeit" + }, + "sleep_rgb_or_color_temp": { + "description": "Nutzung entweder `\"rgb_Farbe\"` oder `\"Farbe_Aushilfskraft\"` in Schlaf Verfahren. 🌙", + "name": "Schlaf_rgb_oder_Farbe_Aushilfskraft" + }, + "sleep_rgb_color": { + "description": "#RGB Farbe in Schlaf Verfahren (benutzt als `schlafen_rgb_oder_Farbe_Aushilfskraft` ist \"rgb_Farbe\"). 🌈", + "name": "Schlaf_rgb_Farbe" + }, + "sleep_color_temp": { + "description": "Farbe Temperatur in Schlaf Verfahren (benutzt als `schlafen_rgb_oder_Farbe_Aushilfskraft` ist `Farbe_Aushilfskraft`) in #Kelvin. 😴", + "name": "Schlaf_Farbe_Aushilfskraft" + }, + "sunrise_offset": { + "description": "Pass an Sonnenaufgang Zeit mit ein bejahendes oder verneinte Verschiebung in Sekunden. ⏰", + "name": "Sonnenaufgang_Verschiebung" + }, + "sunrise_time": { + "description": "Gesetzt eine feste Zeit (HH:MM:SS) Für Sonnenaufgang. 🌅", + "name": "Sonnenaufgang_Zeit" + }, + "sunset_offset": { + "description": "Pass an Abendrot Zeit mit ein bejahendes oder verneinte Verschiebung in Sekunden. ⏰", + "name": "Abendrot_Verschiebung" + }, + "take_over_control": { + "description": "Schalte aus Verwendbare Beleuchtung ob anderen Quelle Anrufe `hell.Drehung_auf` während sind Lichter weiter und sein adaptiert. Note dass diese Anrufe `homeassistant.Update_Entität` jedes `Intervall`! 🔒", + "name": "Nimm_über_Aufsicht" + }, + "detect_non_ha_changes": { + "description": "Findet heraus und hält an Bearbeitungen für nicht-`Licht.Drehung_auf` staatlich Änderungen. Notwendigkeiten `nehmen_über_Aufsicht` aktiviert. 🕵️ Vorsicht: ⚠️ Einige Lichter dürfen hindeuten #kein 'weiter' Staat, #welche konnte resultieren in Lichter anmachen unerwartet. Schalte aus diesen Charakterzug ob du begegnest solche Sachverhalte.", + "name": "Finde heraus_nicht_ha_Änderungen" + }, + "transition": { + "description": "Dauer von Wechsel zündet an als Änderung, in Sekunden. 🕑", + "name": "Wechsel" + }, + "adapt_delay": { + "description": "Wartezeit Zeit (Sekunden) zwischen Licht macht an und Verwendbare Beleuchtung bewerbend Änderungen. Dürfen helfen zu vermeiden flackern. ⏲️", + "name": "Adaptier_Aufschub" + }, + "autoreset_control_seconds": { + "description": "Automatisch #rücksetzen die manuelle Aufsicht nach einer Nummer von Sekunden. Apparat zu 0 zu ausschalten. ⏲️", + "name": "autoreset_Aufsicht_Sekunden" + }, + "min_color_temp": { + "description": "#Warm Farbe Temperatur in #Kelvin. 🔥", + "name": "min_Farbe_Aushilfskraft" + }, + "separate_turn_on_commands": { + "description": "Nutzung trennen `Licht.Drehung_holt ab` weiter Farbe und Helligkeit, gebraucht für einige Licht Typen. 🔀", + "name": "Getrennte_Drehung_auf_Befehle" + }, + "sunset_time": { + "description": "Gesetzt eine feste Zeit (HH:MM:SS) Für Abendrot. 🌇", + "name": "Abendrot_Zeit" + }, + "max_sunrise_time": { + "description": "Setzte den spätesten virtuellen Sonnenaufgang Zeit (HH:MM:SS), Erlauben für früher Sonnenaufgänge. 🌅", + "name": "max_Sonnenaufgang_Zeit" + }, + "min_sunset_time": { + "description": "Setzte das früheste virtuelle Abendrot Zeit (HH:MM:SS), Erlauben für später Abendrot. 🌇", + "name": "min_Abendrot_Zeit" + }, + "entity_id": { + "name": "Entität_id", + "description": "Entität ID von dem Schalter. 📝" + } + }, + "name": "Änderung_Schalter_Lagen", + "description": "Änderung irgendwelche Lagen wolltest du mögen in dem Schalter. Alle Optionen sind hier #dieselbe wie herein die config Strömung." + }, + "set_manual_control": { + "fields": { + "manual_control": { + "name": "Manuelle_Aufsicht", + "description": "Ob zu zufügen (\"wahr\") oder entfernen (\"falsch\") das Licht von der \"manuellen_Aufsicht\" Liste. 🔒" + }, + "lights": { + "name": "Lichter", + "description": "Entität_id(s) von Lichter, ob nicht präzisiert, alle Lichter in dem Schalter ist ausgewählt. 💡" + }, + "entity_id": { + "description": "Die `Entität_id` von dem Schalter in #welche zu (un)zensiert das lichtes da sein `manuell reguliert`. 📝", + "name": "Entität_id" + } + }, + "name": "Gesetzt_manuelle_Aufsicht", + "description": "Mark ob ist ein Licht 'manuell reguliert'." + }, + "apply": { + "fields": { + "prefer_rgb_color": { + "name": "Bevorzug_rgb_Farbe", + "description": "Ob zu bevorzugen #RGB Farbe Anpassung über licht Farbe Temperatur als möglich. 🌈" + }, + "transition": { + "name": "Wechsel", + "description": "Dauer von Wechsel zündet an als Änderung, in Sekunden. 🕑" + }, + "adapt_brightness": { + "name": "Adaptier_Helligkeit", + "description": "Ob zu adaptieren die Helligkeit von dem Licht. 🌞" + }, + "entity_id": { + "name": "Entität_id", + "description": "Die `Entität_id` von dem Schalter mit den Lagen zu bewerben. 📝" + }, + "lights": { + "name": "Lichter", + "description": "Ein lichtes (oder Liste von Lichter) zu bewerben die Lagen zu. 💡" + }, + "turn_on_lights": { + "name": "Drehung_auf_Lichter", + "description": "Ob zu anmachen Lichter jener ist zurzeit ab. 🔆" + }, + "adapt_color": { + "description": "Ob zu adaptieren die Farbe auf #stützend Lichter. 🌈", + "name": "Adaptier_Farbe" + } + }, + "name": "Bewirb", + "description": "Bewirbt den Lauf Verwendbar Beleuchtung Lagen zu Lichter." + } } - } } diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json index a41d84a0..6e4c7a8f 100644 --- a/custom_components/adaptive_lighting/translations/fr.json +++ b/custom_components/adaptive_lighting/translations/fr.json @@ -1,51 +1,269 @@ { - "title": "Éclairage adaptatif", - "config": { - "step": { - "user": { - "title": "Choisissez un nom pour cette instance d'éclairage adaptatif", - "description": "Choisissez un nom pour cette instance. Vous pouvez configurer plusieurs instances d'éclairage adaptatif, chacune pouvant contrôler plusieurs lampes !", - "data": { - "name": "Nom" + "title": "Éclairage adaptatif", + "config": { + "step": { + "user": { + "title": "Choisissez un nom pour l'instance Adaptive Lighting", + "description": "Chaque instance peut contenir plusieurs lumières !", + "data": { + "name": "Nom" + } + } + }, + "abort": { + "already_configured": "Cet appareil est déjà configuré" } - } }, - "abort": { - "already_configured": "Cet appareil est déjà configuré" - } - }, - "options": { - "step": { - "init": { - "title": "Options d'éclairage adaptatif", - "description": "Tous les paramètres de l'instance d'éclairage adaptatif. Les noms des options correspondent aux paramètres YAML. Aucune option n'est affichée si l'entrée adaptive_lighting est définie dans votre configuration YAML.", - "data": { - "lights": "lights : Les lampes à contrôler", - "initial_transition": "initial_transition : Transition (en secondes) lorsque l'état d'une lampe passe d'« éteinte » à « allumée ».", - "sleep_transition": "sleep_transition : Transition (en secondes) lorsque « sleep_state » est commuté.", - "interval": "interval : Temps (en secondes) entre deux mises à jour du commutateur.", - "max_brightness": "max_brightness : Luminosité maximale des lampes (en pourcentage) au cours d'un cycle.", - "max_color_temp": "max_color_temp : Couleur la plus froide (en kelvins) du cycle de température de couleur.", - "min_brightness": "min_brightness : Luminosité minimale des lampes (en pourcentage) au cours d'un cycle.", - "min_color_temp": "min_color_temp : Couleur la plus chaude (en kelvins) du cycle de température de couleur.", - "only_once": "only_once : Adapter les lampes uniquement au moment où elles sont allumées.", - "prefer_rgb_color": "prefer_rgb_color : Utiliser « rgb_color » plutôt que « color_temp » lorsque cela est possible.", - "separate_turn_on_commands": "separate_turn_on_commands : Séparer les commandes pour chaque attribut (couleur, luminosité, etc.) de « light.turn_on » (nécessaire pour certaines lampes).", - "sleep_brightness": "sleep_brightness : Luminosité (en pourcentage) du mode nuit.", - "sleep_color_temp": "sleep_color_temp : Température de couleur (en kelvins) du mode nuit.", - "sunrise_offset": "sunrise_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au lever du soleil.", - "sunrise_time": "sunrise_time : Heure (HH:MM:SS) du lever du soleil. Si « None », utilise l'heure correspondant à votre emplacement.", - "sunset_offset": "sunset_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au coucher du soleil.", - "sunset_time": "sunset_time : Heure (HH:MM:SS) du coucher du soleil. Si « None », utilise l'heure correspondant à votre emplacement.", - "take_over_control": "take_over_control : Si quelque chose d'autre que l'éclairage adaptatif appelle « light.turn_on » alors qu'une lampe est déjà allumée, cesser d'adapter cette lampe jusqu'à ce qu'elle (ou le commutateur) soit éteinte puis rallumée.", - "detect_non_ha_changes": "detect_non_ha_changes : Détecter tout changement de plus de 10 % appliqué aux lampes (même en dehors de HA). Nécessite que « take_over_control » soit activé. (Appelle « homeassistant.update_entity » tous les « interval » !)", - "transition": "transition : Durée de la transition (en secondes) des changements appliqués aux lampes." + "options": { + "step": { + "init": { + "title": "Options d ' éclairage adaptatifs", + "description": "Configurer un adaptatif Élément d'éclairage. Les noms d'options correspondent aux réglages YAML. Si vous avez défini cette entrée dans YAML, aucune option n'apparaîtra ici. Pour les graphiques interactifs qui montrent les effets des paramètres, visitez [cette application Web](https://basnijholt.github.io/adaptive-lighting). Pour plus de détails, voir la [document officiel](https://github.com/basnijholt/adaptive-lighting#readme).", + "data": { + "lights": "lumières: List of light entity_ids to be controlled (may be empty). 🌟", + "initial_transition": "initial_transition", + "sleep_transition": "sleep_transition", + "interval": "intervalle", + "max_brightness": "max_brightness: pourcentage de luminosité maximum. personnalisation", + "max_color_temp": "max_color_temp: Température de couleur la plus froide en Kelvin. assemblage", + "min_brightness": "min_brightness: Pourcentage de luminosité minimum. personnalisation", + "min_color_temp": "min_color_temp: Température de couleur la plus chaude de Kelvin. 🔥", + "only_once": "seulement_une fois: Adaptez les lumières seulement lorsqu'elles sont allumées ( \" vrai \" ) ou continuez à les adapter ( \" faux \" ). 🔄", + "prefer_rgb_color": "prefer_rgb_color: Que ce soit pour préférer le réglage de couleur RGB sur la température de couleur claire si possible.", + "separate_turn_on_commands": "separate_turn_on_commands: Utilisez des appels séparés `light.turn_on` pour la couleur et la luminosité, nécessaires pour certains types de lumière. 🔀", + "sleep_brightness": "sleep_brightness", + "sleep_color_temp": "sleep_color_temp", + "sunrise_offset": "sunrise_offset", + "sunrise_time": "sunrise_time", + "sunset_offset": "coucher de soleil_offset", + "sunset_time": "coucher de soleil_time", + "take_over_control": "take_over_control: Éclairage adaptatif désactive si une autre source appelle `light.turn_on` alors que les lumières sont allumées et adaptées. Notez que cela appelle `homeassistant.update_entity` chaque `intervale`", + "detect_non_ha_changes": "detect_non_ha_changes: Détecte et arrête les adaptations pour non-lumière. changement d'état. Besoins `take_over_control` activé. Certaines lumières pourraient faussement indiquer un état « sur », ce qui pourrait donner lieu à des lumières s'allumer de façon inattendue. Désactivez cette fonctionnalité si vous rencontrez de tels problèmes.", + "transition": "transition", + "send_split_delay": "send_split_delay", + "brightness_mode": "luminosité_mode", + "brightness_mode_time_dark": "brightness_mode_time_dark", + "brightness_mode_time_light": "luminosité_mode_time_light", + "max_sunset_time": "max_sunset_time", + "min_sunset_time": "min_sunset_time", + "sleep_rgb_color": "sleep_rgb_color", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp", + "transition_until_sleep": "transition_until_sleep: Lorsque cela est activé, l'éclairage adaptatif traitera les paramètres de sommeil comme le minimum, en passant à ces valeurs après le coucher du soleil. 🌙", + "min_sunrise_time": "min_sunrise_time", + "max_sunrise_time": "max_sunrise_time", + "autoreset_control_seconds": "autoreset_control_seconds", + "adapt_delay": "adapt_delay", + "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimise le trafic réseau et améliore la réceptivité de l'adaptation dans certaines situations. 📉Disponible si les états de lumière physique sortent de synchronisation avec l'état enregistré de HA.", + "intercept": "intercept: Intercepter et adapter les appels `light.turn_on` pour permettre une adaptation instantanée de la couleur et de la luminosité. ACIA Désactiver les lumières qui ne supportent pas `light.turn_on` avec couleur et luminosité.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quand on allume les lumières au départ. Si le paramètre est < < vrai > > , AL s ' adapte uniquement si l ' on invoque < < light.turn_on > > sans préciser la couleur ou la luminosité. ❌ Ceci, par exemple, empêche l'adaptation lors de l'activation d'une scène. Si `false`, AL s'adapte indépendamment de la présence de couleur ou de luminosité dans le `service_data' initial. Besoins `take_over_control` activé. 🕵∫ ", + "multi_light_intercept": "multi_light_intercept: Interceptez et adaptez `light.turn_on` les appels qui ciblent plusieurs lumières. ➗gène Cela pourrait permettre de diviser un seul appel `light.turn_on` en plusieurs appels, par exemple lorsque les lumières sont dans différents interrupteurs. Nécessite un < < intercept > > pour être activé.", + "include_config_in_attributes": "include_config_in_attributes: Afficher toutes les options en tant qu'attributs sur le commutateur dans l'assistant d'accueil lorsqu'il s'agit de «true»" + }, + "data_description": { + "sleep_rgb_color": "Couleur RGB en mode sommeil (utilisé lorsque `sleep_rgb_or_color_temp` est \"rgb_color\").", + "sleep_transition": "Durée de la transition lorsque \"mode de repos\" est activé en quelques secondes. 😴", + "sunrise_time": "Réglez un temps fixe (HH:MM:SS) pour le lever du soleil. 🌅", + "min_sunrise_time": "Définir le premier temps de lever de soleil virtuel (HHH:MM:SS), permettant des levures de soleil ultérieures. 🌅", + "max_sunrise_time": "Définir le dernier temps de lever de soleil virtuel (HHH:MM:SS), permettant des levures de soleil antérieures. 🌅", + "sunrise_offset": "Réglez le lever de soleil avec un décalage positif ou négatif en quelques secondes. ⏰", + "sunset_time": "Réglez un temps fixe (HH:MM:SS) pour le coucher de soleil. 🌇", + "min_sunset_time": "Réglez le premier temps de coucher de soleil virtuel (HHH:MM:SS), permettant des couchers de soleil ultérieurs. 🌇", + "max_sunset_time": "Définir le dernier coucher de soleil virtuel (HHH:MM:SS), permettant des couchers de soleil antérieurs. 🌇", + "sunset_offset": "Réglez le temps de coucher avec un décalage positif ou négatif en quelques secondes. ⏰", + "brightness_mode": "Mode de luminosité à utiliser. Les valeurs possibles sont < < par défaut > > , < < linéaire > > et < < parois > > , < < par défaut > > , et < < par coup > > , < < par défaut > > et par > par > . 📈", + "brightness_mode_time_light": "(Ignoré si `brightness_mode='default'`) La durée en quelques secondes pour augmenter/verser la luminosité après/avant le lever du soleil/sunset. 📈📉.", + "autoreset_control_seconds": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲∫", + "send_split_delay": "Délai (ms) entre `separate_turn_on_commands` pour les lumières qui ne supportent pas la luminosité simultanée et le réglage de couleur. ⏲∫", + "adapt_delay": "Temps d'attente (secondes) entre allumage de la lumière et éclairage adaptatif en appliquant des changements. Ça pourrait aider à éviter de filmer. ⏲∫", + "interval": "Fréquence pour adapter les lumières, en quelques secondes. 🔄", + "transition": "Durée de la transition lorsque les lumières changent, en quelques secondes. 🕑", + "initial_transition": "Durée de la première transition lorsque les feux tournent de `off` à `on` en quelques secondes. ⏲∫", + "sleep_brightness": "Pourcentage de luminosité des lumières en mode sommeil. 😴", + "sleep_rgb_or_color_temp": "Utilisez soit `\"rgb_color\"` ou `\"color_temp\"` en mode sommeil. 🌙", + "sleep_color_temp": "Température de couleur en mode sommeil (utilisée lorsque `sleep_rgb_or_color_temp` est `color_temp`) en Kelvin. 😴", + "brightness_mode_time_dark": "(Ignoré si `brightness_mode='default'`) La durée en quelques secondes pour remonter/verser la luminosité avant/après le lever du soleil/sunset. 📈📉" + } + } + }, + "error": { + "option_error": "Option non valide", + "entity_missing": "Une ou plusieurs entités lumineuses sélectionnées sont absentes de Home Assistant" } - } }, - "error": { - "option_error": "Option non valide", - "entity_missing": "Une lumière sélectionnée n’a pas été trouvée" + "services": { + "change_switch_settings": { + "fields": { + "adapt_delay": { + "name": "adapt_delay", + "description": "Temps d'attente (secondes) entre allumage de la lumière et éclairage adaptatif en appliquant des changements. Ça pourrait aider à éviter de filmer. ⏲∫" + }, + "min_color_temp": { + "name": "min_color_temp", + "description": "Température de couleur la plus chaude de Kelvin. 🔥" + }, + "autoreset_control_seconds": { + "name": "autoreset_control_seconds", + "description": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲∫" + }, + "entity_id": { + "name": "entity_id", + "description": "ID d'entité du commutateur. 📝" + }, + "include_config_in_attributes": { + "name": "include_config_in_attributes", + "description": "Afficher toutes les options en tant qu'attributs sur le commutateur dans l'assistant d'accueil lorsqu'il s'agit de \" vérité \" . 📝" + }, + "max_color_temp": { + "name": "max_color_temp", + "description": "Température de couleur la plus froide en Kelvin. assemblage" + }, + "only_once": { + "name": "only_once", + "description": "Adaptez les lumières seulement lorsqu'elles sont allumées ( \" vrai \" ) ou continuez à les adapter ( \" faux \" ). 🔄" + }, + "prefer_rgb_color": { + "name": "prefer_rgb_color", + "description": "Que ce soit pour préférer le réglage de couleur RGB sur la température de couleur claire si possible." + }, + "send_split_delay": { + "name": "send_split_delay", + "description": "Délai (ms) entre `separate_turn_on_commands` pour les lumières qui ne supportent pas la luminosité simultanée et le réglage de couleur. ⏲∫" + }, + "separate_turn_on_commands": { + "name": "separate_turn_on_commands", + "description": "Utilisez des appels séparés `light.turn_on` pour la couleur et la luminosité, nécessaires pour certains types de lumière. 🔀" + }, + "sleep_brightness": { + "name": "sleep_brightness", + "description": "Pourcentage de luminosité des lumières en mode sommeil. 😴" + }, + "sunrise_time": { + "name": "sunrise_time", + "description": "Réglez un temps fixe (HH:MM:SS) pour le lever du soleil. 🌅" + }, + "sunset_time": { + "name": "coucher de soleil_time", + "description": "Réglez un temps fixe (HH:MM:SS) pour le coucher de soleil. 🌇" + }, + "sunset_offset": { + "name": "coucher de soleil_offset", + "description": "Réglez le temps de coucher avec un décalage positif ou négatif en quelques secondes. ⏰" + }, + "take_over_control": { + "name": "take_over_control", + "description": "Adaptable Éclairage si une autre source appelle `light.turn_on` alors que les lumières sont allumées et adaptées. Notez que cela appelle `homeassistant.update_entity` chaque `intervale`" + }, + "use_defaults": { + "name": "use_defaults", + "description": "Définit les valeurs par défaut non spécifiées dans cet appel de service. Options : \"current\" (par défaut, conserve les valeurs courantes), \"factory\" (réinitialisation des défauts documentés), ou \"configuration\" (revertissement des défauts de configuration). écrasement" + }, + "sleep_rgb_or_color_temp": { + "name": "sleep_rgb_or_color_temp", + "description": "Utilisez soit `\"rgb_color\"` ou `\"color_temp\"` en mode sommeil. 🌙" + }, + "turn_on_lights": { + "description": "Que ce soit pour allumer des lumières qui sont actuellement éteintes. 🔆", + "name": "turn_on_lights" + }, + "initial_transition": { + "description": "Durée de la première transition lorsque les feux tournent de `off` à `on` en quelques secondes. ⏲∫", + "name": "initial_transition" + }, + "sleep_transition": { + "description": "Durée de la transition lorsque \"mode de repos\" est activé en quelques secondes. 😴", + "name": "sleep_transition" + }, + "max_brightness": { + "description": "Pourcentage de luminosité maximum. personnalisation", + "name": "max_brightness" + }, + "min_brightness": { + "description": "Pourcentage de luminosité minimum. personnalisation", + "name": "min_brightness" + }, + "sleep_rgb_color": { + "description": "Couleur RGB en mode sommeil (utilisé lorsque `sleep_rgb_or_color_temp` est \"rgb_color\").", + "name": "sleep_rgb_color" + }, + "sleep_color_temp": { + "description": "Température de couleur en mode sommeil (utilisée lorsque `sleep_rgb_or_color_temp` est `color_temp`) en Kelvin. 😴", + "name": "sleep_color_temp" + }, + "sunrise_offset": { + "description": "Réglez le lever de soleil avec un décalage positif ou négatif en quelques secondes. ⏰", + "name": "sunrise_offset" + }, + "max_sunrise_time": { + "description": "Définir le dernier temps de lever de soleil virtuel (HHH:MM:SS), permettant des levures de soleil antérieures. 🌅", + "name": "max_sunrise_time" + }, + "min_sunset_time": { + "description": "Réglez le premier temps de coucher de soleil virtuel (HHH:MM:SS), permettant des couchers de soleil ultérieurs. 🌇", + "name": "min_sunset_time" + }, + "detect_non_ha_changes": { + "description": "Détecte et arrête les adaptations pour non-léger. changement d'état. Besoins `take_over_control` activé. Certaines lumières pourraient faussement indiquer un état « sur », ce qui pourrait donner lieu à des lumières s'allumer de façon inattendue. Désactivez cette fonctionnalité si vous rencontrez de tels problèmes.", + "name": "detect_non_ha_changes" + }, + "transition": { + "description": "Durée de la transition lorsque les lumières changent, en quelques secondes. 🕑", + "name": "transition" + } + }, + "name": "change_switch_settings", + "description": "Changez les réglages que vous souhaitez dans le commutateur. Toutes les options ici sont les mêmes que dans le flux de configuration." + }, + "apply": { + "fields": { + "entity_id": { + "name": "entity_id", + "description": "Le `entity_id` du commutateur avec les réglages à appliquer. 📝" + }, + "lights": { + "name": "lumières", + "description": "Une lumière (ou une liste de lumières) pour appliquer les réglages. personnalisation" + }, + "prefer_rgb_color": { + "name": "prefer_rgb_color", + "description": "Que ce soit pour préférer le réglage de couleur RGB sur la température de couleur claire si possible." + }, + "transition": { + "name": "transition", + "description": "Durée de la transition lorsque les lumières changent, en quelques secondes. 🕑" + }, + "turn_on_lights": { + "name": "turn_on_lights", + "description": "Que ce soit pour allumer des lumières qui sont actuellement éteintes. 🔆" + }, + "adapt_brightness": { + "description": "Que ce soit pour adapter la luminosité de la lumière. 🌞", + "name": "adapt_brightness" + }, + "adapt_color": { + "description": "Que ce soit pour adapter la couleur sur les feux support.", + "name": "adapt_color" + } + }, + "name": "applique", + "description": "Applique les réglages d'éclairage adaptatif actuels aux lumières." + }, + "set_manual_control": { + "fields": { + "lights": { + "name": "lumières", + "description": "entity_id(s) of lights, if not specified, all lights in the switch are selected. personnalisation" + }, + "manual_control": { + "name": "manual_control", + "description": "Que ce soit pour ajouter (« faux ») ou supprimer (« faux ») la lumière de la liste « manual_control ». 🔒" + }, + "entity_id": { + "description": "Le `entity_id` du commutateur dans lequel (un) marquer la lumière comme étant `manuellement contrôlé`. 📝", + "name": "entity_id" + } + }, + "description": "Marquer si une lumière est «manuellement contrôlée».", + "name": "set_manual_control" + } } - } } diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json index cc269685..39bee1e2 100644 --- a/custom_components/adaptive_lighting/translations/nl.json +++ b/custom_components/adaptive_lighting/translations/nl.json @@ -47,6 +47,9 @@ "transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)", "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Wanneer je de lichten in eerste instantie aanzet. Indien ingesteld op `true`, past AL zich alleen aan als `light.turn_on` wordt aangeroepen zonder kleur of helderheid op te geven. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Indien `false`, past AL zich aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. Moet 'take_over_control' ingeschakeld zijn. 🕵️ " + }, + "data_description": { + "sunrise_offset": "Een zonsopgang met een positief of negatief offset in seconden. _" } } }, @@ -60,6 +63,9 @@ "fields": { "only_once": { "description": "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄" + }, + "sunrise_offset": { + "description": "Een zonsopgang met een positief of negatief offset in seconden. _" } } } From 2211aa07b5400fc59f7b2122ee9bceff48a9f645 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 15 Aug 2023 23:16:17 -0700 Subject: [PATCH 0660/1077] Revert "Translations update from Hosted Weblate (de, nl, fr) (#755)" (#757) This reverts commit 627c7a120c0ea54a9de6d881dd8ec896cb9dc145. --- .../adaptive_lighting/translations/de.json | 317 +++--------------- .../adaptive_lighting/translations/fr.json | 310 +++-------------- .../adaptive_lighting/translations/nl.json | 6 - 3 files changed, 99 insertions(+), 534 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json index abb12101..79eb0df8 100644 --- a/custom_components/adaptive_lighting/translations/de.json +++ b/custom_components/adaptive_lighting/translations/de.json @@ -1,269 +1,58 @@ { - "title": "Verwendbare Beleuchtung", - "config": { - "step": { - "user": { - "title": "Wähl aus einen Namen für die Verwendbare Beleuchtung Instanz", - "description": "Jede Instanz kann mehrfache Lichter zügeln!", - "data": { - "name": "Name" - } - } - }, - "abort": { - "already_configured": "Diese Vorrichtung ist schon konfiguriert" + "title": "Adaptive Lighting", + "config": { + "step": { + "user": { + "title": "Benenne das Adaptive Lighting", + "description": "Jede Instanz kann mehrere Licht Entitäten beinhalten", + "data": { + "name": "Name" } + } }, - "options": { - "step": { - "init": { - "title": "Verwendbare Beleuchtung Optionen", - "description": "Konfigurier eine Verwendbare Beleuchtung Komponente. Option Namen stimmen ab mit die YAML Lagen. Ob du hast diesen Eintrag definiert herein YAML, keine Optionen wollen fungieren hier. Für interaktiv Graphen #der demonstrieren Parameter Effekte, besuchen [dieses Web #App](https://basnijholt.github.io/Verwendbar-Beleuchtung). Für ferner Details, seht die [offizielle Dokumentation](https://github.com/basnijholt/Verwendbar-Beleuchtung#readme).", - "data": { - "lights": "Lichter: Liste von licht Entität_ids zu sein reguliert (darf sein leer). 🌟", - "initial_transition": "Einleitender_Wechsel", - "sleep_transition": "Schlaf_Wechsel", - "interval": "Intervall", - "max_brightness": "max_Helligkeit: #Höchster Helligkeit Prozentsatz. 💡", - "max_color_temp": "max_Farbe_Aushilfskraft: #Kalt Farbe Temperatur in #Kelvin. ❄️", - "min_brightness": "min_Helligkeit: Minimaler Helligkeit Prozentsatz. 💡", - "min_color_temp": "min_Farbe_Aushilfskraft: #Warm Farbe Temperatur in #Kelvin. 🔥", - "only_once": "Nur_einmal: Adaptier nur Lichter als sind angemacht sie (`wahr`) oder Unterhalt adaptieren ihnen (`falsch`). 🔄", - "prefer_rgb_color": "Bevorzug_rgb_Farbe: Ob zu bevorzugen #RGB Farbe Anpassung über licht Farbe Temperatur als möglich. 🌈", - "separate_turn_on_commands": "Getrennte_Drehung_auf_Befehle: Nutzung trennen `Licht.Drehung_holt ab` weiter Farbe und Helligkeit, gebraucht für einige Licht Typen. 🔀", - "send_split_delay": "#Senden_geteilt_Aufschub", - "sleep_brightness": "Schlaf_Helligkeit", - "sleep_rgb_or_color_temp": "Schlaf_rgb_oder_Farbe_Aushilfskraft", - "sleep_rgb_color": "Schlaf_rgb_Farbe", - "sleep_color_temp": "Schlaf_Farbe_Aushilfskraft", - "sunrise_offset": "Sonnenaufgang_Verschiebung", - "sunrise_time": "Sonnenaufgang_Zeit", - "max_sunrise_time": "max_Sonnenaufgang_Zeit", - "sunset_offset": "Abendrot_Verschiebung", - "sunset_time": "Abendrot_Zeit", - "min_sunset_time": "min_Abendrot_Zeit", - "take_over_control": "Nimm_über_Aufsicht: Schalte aus Verwendbare Beleuchtung ob anderen Quelle Anrufe `hell.Drehung_auf` während sind Lichter weiter und sein adaptiert. Note dass diese Anrufe `homeassistant.Update_Entität` jedes `Intervall`! 🔒", - "detect_non_ha_changes": "Finde heraus_nicht_ha_Änderungen: Findet heraus und hält an Bearbeitungen für nicht-`Licht.Drehung_auf` staatlich Änderungen. Notwendigkeiten `nehmen_über_Aufsicht` aktiviert. 🕵️ Vorsicht: ⚠️ Einige Lichter dürfen hindeuten #kein 'weiter' Staat, #welche konnte resultieren in Lichter anmachen unerwartet. Schalte aus diesen Charakterzug ob du begegnest solche Sachverhalte.", - "transition": "Wechsel", - "adapt_delay": "Adaptier_Aufschub", - "skip_redundant_commands": "Sprung_redundant_Befehle: Sprung senden Bearbeitung gebietet wessen anrichten schon staatlich #angleichen das Lichts gekannt Staat. Minimiert Netzwerk Verkehr und verbessert die Bearbeitung responsivity in einigen Situationen. 📉Schalte aus ob physische Licht Staaten steigen aus von synchronisieren mit HAaufgezeichnet haben Staat.", - "autoreset_control_seconds": "autoreset_Aufsicht_Sekunden", - "brightness_mode": "Helligkeit_Verfahren", - "brightness_mode_time_dark": "Helligkeit_Verfahren_Zeit_Finsternis", - "brightness_mode_time_light": "Helligkeit_Verfahren_Zeit_Licht", - "max_sunset_time": "max_Abendrot_Zeit", - "transition_until_sleep": "Wechsel_bis_Schlaf: Wann aktiviert, Verwendbare Beleuchtung will spendieren schlafen Lagen da das Minimum, übergehen zu diesen Werten nach Abendrot. 🌙", - "min_sunrise_time": "min_Sonnenaufgang_Zeit", - "adapt_only_on_bare_turn_on": "Adaptier_nur_auf_kahl_Drehung_weiter: Wann drehen weiter Lichter anfänglich. Ob gesetzt zu `wahr`, #AL adaptiert nur ob `Licht.Drehung_ist aufgerufen` weiter ohne präzisieren Farbe oder Helligkeit. ❌🌈 Dies #z.B., verhindert als Bearbeitung aktivierend eine Szene. Ob `falsch`, #AL adaptiert #ohne Rücksicht auf von dem Vorliegen von Farbe oder Helligkeit in der einleitenden `Bedienung_Daten`. Notwendigkeiten `nehmen_über_Aufsicht` aktiviert. 🕵️ ", - "intercept": "Unterbrich: Unterbrich und adaptier `Licht.Drehung_auf` Anrufe zu aktivieren momentane Farbe und Helligkeit Bearbeitung. 🏎️ Schalte aus für Lichter #der unterstützen nicht `Licht.Drehung_weiter` mit Farbe und Helligkeit.", - "multi_light_intercept": "multi_Licht_unterbricht: Unterbrich und adaptier `Licht.Drehung_auf` Anrufe jenes Soll mehrfache Lichter. ➗⚠️ Dies darf resultieren in #aufspalten ein lediges `Licht.Drehung_auf` Anruf hinein mehrfach Anrufe, #z.B., als sind Lichter in verschieden Schalter. Bedürft `unterbrechen` zu sein aktiviert.", - "include_config_in_attributes": "Schließ ein_config_in_Attribute: Vorstellung jede Optionen da Attribute auf dem Schalter in Heim Stellvertretend als gesetzt zu `wahr`. 📝" - }, - "data_description": { - "initial_transition": "Dauer von dem ersten Wechsel zündet an als Drehung von `ab` zu `weiter` in Sekunden. ⏲️", - "sleep_brightness": "Helligkeit Prozentsatz von Lichter in schlafen Verfahren. 😴", - "sleep_rgb_color": "#RGB Farbe in Schlaf Verfahren (benutzt als `schlafen_rgb_oder_Farbe_Aushilfskraft` ist \"rgb_Farbe\"). 🌈", - "sleep_transition": "Dauer von Wechsel schläft \"als Verfahren\" ist #festknebeln in Sekunden. 😴", - "sunrise_time": "Gesetzt eine feste Zeit (HH:MM:SS) Für Sonnenaufgang. 🌅", - "min_sunrise_time": "Setzte den frühesten virtuellen Sonnenaufgang Zeit (HH:MM:SS), Erlauben für später Sonnenaufgänge. 🌅", - "interval": "Häufigkeit zu adaptieren die Lichter, in Sekunden. 🔄", - "transition": "Dauer von Wechsel zündet an als Änderung, in Sekunden. 🕑", - "sleep_rgb_or_color_temp": "Nutzung entweder `\"rgb_Farbe\"` oder `\"Farbe_Aushilfskraft\"` in Schlaf Verfahren. 🌙", - "sleep_color_temp": "Farbe Temperatur in Schlaf Verfahren (benutzt als `schlafen_rgb_oder_Farbe_Aushilfskraft` ist `Farbe_Aushilfskraft`) in #Kelvin. 😴", - "max_sunrise_time": "Setzte den spätesten virtuellen Sonnenaufgang Zeit (HH:MM:SS), Erlauben für früher Sonnenaufgänge. 🌅", - "sunrise_offset": "Pass an Sonnenaufgang Zeit mit ein bejahendes oder verneinte Verschiebung in Sekunden. ⏰", - "sunset_time": "Gesetzt eine feste Zeit (HH:MM:SS) Für Abendrot. 🌇", - "min_sunset_time": "Setzte das früheste virtuelle Abendrot Zeit (HH:MM:SS), Erlauben für später Abendrot. 🌇", - "max_sunset_time": "Setzte das späteste virtuelle Abendrot Zeit (HH:MM:SS), Erlauben für früher Abendrot. 🌇", - "sunset_offset": "Pass an Abendrot Zeit mit ein bejahendes oder verneinte Verschiebung in Sekunden. ⏰", - "brightness_mode": "Helligkeit Verfahren zu benutzen. Mögliche Werte sind `#voreingestellt`, `geradlinig`, und `tanh` (Nutzungen `Helligkeit_Verfahren_Zeit_Finsternis` und `Helligkeit_Verfahren_Zeit_hell`). 📈", - "brightness_mode_time_dark": "(Überhört ob `Helligkeit_Verfahren='#voreingestellt'`) Die Dauer in Sekunden zu #Nepp oben/#vor herunter die Helligkeit/nach Sonnenaufgang/Abendrot. 📈📉", - "brightness_mode_time_light": "(Überhört ob `Helligkeit_Verfahren='#voreingestellt'`) Die Dauer in Sekunden zu #Nepp oben/#nachdem herunter die Helligkeit/vor Sonnenaufgang/Abendrot. 📈📉.", - "autoreset_control_seconds": "Automatisch #rücksetzen die manuelle Aufsicht nach einer Nummer von Sekunden. Apparat zu 0 zu ausschalten. ⏲️", - "send_split_delay": "Aufschub (ms) zwischen `getrennt_Drehung_auf_Befehle` für Lichter #der unterstützen nicht zeitgleiche Helligkeit und Farbe Lage. ⏲️", - "adapt_delay": "Wartezeit Zeit (Sekunden) zwischen Licht macht an und Verwendbare Beleuchtung bewerbend Änderungen. Dürfen helfen zu vermeiden flackern. ⏲️" - } - } - }, - "error": { - "option_error": "Kränkliche Option", - "entity_missing": "#Man oder #mehr ausgewählt Licht Entitäten fehlen von Heim Assistenten" - } - }, - "services": { - "change_switch_settings": { - "fields": { - "use_defaults": { - "description": "Setzt nicht die #voreingestellt Werte präzisiert in diesen Bedienung Anruf. Optionen: \"Lauf\" (#voreingestellt, hält fest gängige Werte), \"Fabrik\" (#Nachstellung zu dokumentieren Vorgaben), oder \"Konfiguration\" (fällt zurück zu schalten config Vorgaben). ⚙️", - "name": "Benutzen_Vorgaben" - }, - "include_config_in_attributes": { - "description": "Vorstellung jede Optionen da Attribute auf dem Schalter in Heim Stellvertretend als gesetzt zu `wahr`. 📝", - "name": "Schließ ein_config_in_Attribute" - }, - "turn_on_lights": { - "description": "Ob zu anmachen Lichter jener ist zurzeit ab. 🔆", - "name": "Drehung_auf_Lichter" - }, - "initial_transition": { - "description": "Dauer von dem ersten Wechsel zündet an als Drehung von `ab` zu `weiter` in Sekunden. ⏲️", - "name": "Einleitender_Wechsel" - }, - "sleep_transition": { - "description": "Dauer von Wechsel schläft \"als Verfahren\" ist #festknebeln in Sekunden. 😴", - "name": "Schlaf_Wechsel" - }, - "max_brightness": { - "description": "#Höchster Helligkeit Prozentsatz. 💡", - "name": "max_Helligkeit" - }, - "max_color_temp": { - "description": "#Kalt Farbe Temperatur in #Kelvin. ❄️", - "name": "max_Farbe_Aushilfskraft" - }, - "min_brightness": { - "description": "Minimaler Helligkeit Prozentsatz. 💡", - "name": "min_Helligkeit" - }, - "only_once": { - "description": "Adaptier nur Lichter als sind angemacht sie (`wahr`) oder Unterhalt adaptieren ihnen (`falsch`). 🔄", - "name": "Nur_einmal" - }, - "prefer_rgb_color": { - "description": "Ob zu bevorzugen #RGB Farbe Anpassung über licht Farbe Temperatur als möglich. 🌈", - "name": "Bevorzug_rgb_Farbe" - }, - "send_split_delay": { - "description": "Aufschub (ms) zwischen `getrennt_Drehung_auf_Befehle` für Lichter #der unterstützen nicht zeitgleiche Helligkeit und Farbe Lage. ⏲️", - "name": "#Senden_geteilt_Aufschub" - }, - "sleep_brightness": { - "description": "Helligkeit Prozentsatz von Lichter in schlafen Verfahren. 😴", - "name": "Schlaf_Helligkeit" - }, - "sleep_rgb_or_color_temp": { - "description": "Nutzung entweder `\"rgb_Farbe\"` oder `\"Farbe_Aushilfskraft\"` in Schlaf Verfahren. 🌙", - "name": "Schlaf_rgb_oder_Farbe_Aushilfskraft" - }, - "sleep_rgb_color": { - "description": "#RGB Farbe in Schlaf Verfahren (benutzt als `schlafen_rgb_oder_Farbe_Aushilfskraft` ist \"rgb_Farbe\"). 🌈", - "name": "Schlaf_rgb_Farbe" - }, - "sleep_color_temp": { - "description": "Farbe Temperatur in Schlaf Verfahren (benutzt als `schlafen_rgb_oder_Farbe_Aushilfskraft` ist `Farbe_Aushilfskraft`) in #Kelvin. 😴", - "name": "Schlaf_Farbe_Aushilfskraft" - }, - "sunrise_offset": { - "description": "Pass an Sonnenaufgang Zeit mit ein bejahendes oder verneinte Verschiebung in Sekunden. ⏰", - "name": "Sonnenaufgang_Verschiebung" - }, - "sunrise_time": { - "description": "Gesetzt eine feste Zeit (HH:MM:SS) Für Sonnenaufgang. 🌅", - "name": "Sonnenaufgang_Zeit" - }, - "sunset_offset": { - "description": "Pass an Abendrot Zeit mit ein bejahendes oder verneinte Verschiebung in Sekunden. ⏰", - "name": "Abendrot_Verschiebung" - }, - "take_over_control": { - "description": "Schalte aus Verwendbare Beleuchtung ob anderen Quelle Anrufe `hell.Drehung_auf` während sind Lichter weiter und sein adaptiert. Note dass diese Anrufe `homeassistant.Update_Entität` jedes `Intervall`! 🔒", - "name": "Nimm_über_Aufsicht" - }, - "detect_non_ha_changes": { - "description": "Findet heraus und hält an Bearbeitungen für nicht-`Licht.Drehung_auf` staatlich Änderungen. Notwendigkeiten `nehmen_über_Aufsicht` aktiviert. 🕵️ Vorsicht: ⚠️ Einige Lichter dürfen hindeuten #kein 'weiter' Staat, #welche konnte resultieren in Lichter anmachen unerwartet. Schalte aus diesen Charakterzug ob du begegnest solche Sachverhalte.", - "name": "Finde heraus_nicht_ha_Änderungen" - }, - "transition": { - "description": "Dauer von Wechsel zündet an als Änderung, in Sekunden. 🕑", - "name": "Wechsel" - }, - "adapt_delay": { - "description": "Wartezeit Zeit (Sekunden) zwischen Licht macht an und Verwendbare Beleuchtung bewerbend Änderungen. Dürfen helfen zu vermeiden flackern. ⏲️", - "name": "Adaptier_Aufschub" - }, - "autoreset_control_seconds": { - "description": "Automatisch #rücksetzen die manuelle Aufsicht nach einer Nummer von Sekunden. Apparat zu 0 zu ausschalten. ⏲️", - "name": "autoreset_Aufsicht_Sekunden" - }, - "min_color_temp": { - "description": "#Warm Farbe Temperatur in #Kelvin. 🔥", - "name": "min_Farbe_Aushilfskraft" - }, - "separate_turn_on_commands": { - "description": "Nutzung trennen `Licht.Drehung_holt ab` weiter Farbe und Helligkeit, gebraucht für einige Licht Typen. 🔀", - "name": "Getrennte_Drehung_auf_Befehle" - }, - "sunset_time": { - "description": "Gesetzt eine feste Zeit (HH:MM:SS) Für Abendrot. 🌇", - "name": "Abendrot_Zeit" - }, - "max_sunrise_time": { - "description": "Setzte den spätesten virtuellen Sonnenaufgang Zeit (HH:MM:SS), Erlauben für früher Sonnenaufgänge. 🌅", - "name": "max_Sonnenaufgang_Zeit" - }, - "min_sunset_time": { - "description": "Setzte das früheste virtuelle Abendrot Zeit (HH:MM:SS), Erlauben für später Abendrot. 🌇", - "name": "min_Abendrot_Zeit" - }, - "entity_id": { - "name": "Entität_id", - "description": "Entität ID von dem Schalter. 📝" - } - }, - "name": "Änderung_Schalter_Lagen", - "description": "Änderung irgendwelche Lagen wolltest du mögen in dem Schalter. Alle Optionen sind hier #dieselbe wie herein die config Strömung." - }, - "set_manual_control": { - "fields": { - "manual_control": { - "name": "Manuelle_Aufsicht", - "description": "Ob zu zufügen (\"wahr\") oder entfernen (\"falsch\") das Licht von der \"manuellen_Aufsicht\" Liste. 🔒" - }, - "lights": { - "name": "Lichter", - "description": "Entität_id(s) von Lichter, ob nicht präzisiert, alle Lichter in dem Schalter ist ausgewählt. 💡" - }, - "entity_id": { - "description": "Die `Entität_id` von dem Schalter in #welche zu (un)zensiert das lichtes da sein `manuell reguliert`. 📝", - "name": "Entität_id" - } - }, - "name": "Gesetzt_manuelle_Aufsicht", - "description": "Mark ob ist ein Licht 'manuell reguliert'." - }, - "apply": { - "fields": { - "prefer_rgb_color": { - "name": "Bevorzug_rgb_Farbe", - "description": "Ob zu bevorzugen #RGB Farbe Anpassung über licht Farbe Temperatur als möglich. 🌈" - }, - "transition": { - "name": "Wechsel", - "description": "Dauer von Wechsel zündet an als Änderung, in Sekunden. 🕑" - }, - "adapt_brightness": { - "name": "Adaptier_Helligkeit", - "description": "Ob zu adaptieren die Helligkeit von dem Licht. 🌞" - }, - "entity_id": { - "name": "Entität_id", - "description": "Die `Entität_id` von dem Schalter mit den Lagen zu bewerben. 📝" - }, - "lights": { - "name": "Lichter", - "description": "Ein lichtes (oder Liste von Lichter) zu bewerben die Lagen zu. 💡" - }, - "turn_on_lights": { - "name": "Drehung_auf_Lichter", - "description": "Ob zu anmachen Lichter jener ist zurzeit ab. 🔆" - }, - "adapt_color": { - "description": "Ob zu adaptieren die Farbe auf #stützend Lichter. 🌈", - "name": "Adaptier_Farbe" - } - }, - "name": "Bewirb", - "description": "Bewirbt den Lauf Verwendbar Beleuchtung Lagen zu Lichter." - } + "abort": { + "already_configured": "Gerät ist bereits konfiguriert!" } + }, + "options": { + "step": { + "init": { + "title": "Adaptive Lighting Optionen", + "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde.", + "data": { + "lights": "Lichter", + "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt", + "sleep_transition": "sleep_transition: Wenn 'sleep_state' sich ändert. (Sekunden)", + "interval": "interval, Zeit zwischen Updates des Switches", + "max_brightness": "max_brightness, maximale Helligkeit in %", + "max_color_temp": "max_color_temp, maximale Farbtemperatur in Kelvin", + "min_brightness": "min_brightness, minimale Helligkeit in %", + "min_color_temp": "min_color_temp, minimale Farbtemperatur in Kelvin", + "only_once": "only_once, passe die Lichter nur beim Einschalten an", + "prefer_rgb_color": "prefer_rgb_color, nutze 'rgb_color' vor 'color_temp', wenn möglich", + "separate_turn_on_commands": "separate_turn_on_commands, für jedes Attribut (Farbe, Helligkeit usw.) in 'light.turn_on' werden separate Befehle gesendet. Wird für manche Leuchtmittel benötigt.", + "send_split_delay": "send_split_delay: Wartezeit zwischen dem Senden der Befehle (Millisekunden), wenn separate_turn_on_commands genutzt wird. Kann helfen, wenn die Leuchtmittel die separaten Befehle nicht korrekt umsetzen.", + "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, nutze 'rgb_color' oder 'color_temp'", + "sleep_rgb_color": "sleep_rgb_color, in RGB", + "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperatur in Kelvin", + "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- Sekunden", + "sunrise_time": "sunrise_time, Sonnenaufgangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenaufgangs an deiner Position verwendet)", + "max_sunrise_time": "max_sunrise_time: Manuelles Überschreiben der max. sunrise_time. Falls 'None', wird die tatsächliche sunrise_time an deiner Position verwendet (HH:MM:SS)", + "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- Sekunden", + "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)", + "min_sunset_time": "min_sunset_time: Manuelles Überschreiben der min. sunset_time. Falls 'None', wird die tatsächliche sunset_time an deiner Position verwendet (HH:MM:SS)", + "take_over_control": "take_over_control, wenn irgendetwas während ein Licht an ist außer Adaptive Lighting den Service 'light.turn_on' aufruft, stoppe die Anpassung des Lichtes (oder des Schalters) bis dieser wieder von off -> on geschaltet wird.", + "detect_non_ha_changes": "detect_non_ha_changes, entdeckt alle Änderungen über 10% am Licht (auch außerhalb von HA gemacht), 'take_over_control' muss aktiviert sein (ruft 'homeassistant.update_entity' jede 'interval' auf!)", + "transition": "transition, Wechselzeit in Sekunden", + "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.", + "skip_redundant_commands": "Keine Adaptierungsbefehle senden, deren erwünschter Status schon dem bekanntes Status von Lichtern entspricht. Minimiert die Netzwerkbelastung und verbessert die Adaptierung in manchen Situationen. Deaktiviert lassen falls der pysikalische Status der Lichter und der erkannte Status in HA nicht synchron bleiben." + } + } + }, + "error": { + "option_error": "Fehlerhafte Option", + "entity_missing": "Ein ausgewähltes Licht wurde nicht gefunden" + } + } } diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json index 6e4c7a8f..a41d84a0 100644 --- a/custom_components/adaptive_lighting/translations/fr.json +++ b/custom_components/adaptive_lighting/translations/fr.json @@ -1,269 +1,51 @@ { - "title": "Éclairage adaptatif", - "config": { - "step": { - "user": { - "title": "Choisissez un nom pour l'instance Adaptive Lighting", - "description": "Chaque instance peut contenir plusieurs lumières !", - "data": { - "name": "Nom" - } - } - }, - "abort": { - "already_configured": "Cet appareil est déjà configuré" + "title": "Éclairage adaptatif", + "config": { + "step": { + "user": { + "title": "Choisissez un nom pour cette instance d'éclairage adaptatif", + "description": "Choisissez un nom pour cette instance. Vous pouvez configurer plusieurs instances d'éclairage adaptatif, chacune pouvant contrôler plusieurs lampes !", + "data": { + "name": "Nom" } + } }, - "options": { - "step": { - "init": { - "title": "Options d ' éclairage adaptatifs", - "description": "Configurer un adaptatif Élément d'éclairage. Les noms d'options correspondent aux réglages YAML. Si vous avez défini cette entrée dans YAML, aucune option n'apparaîtra ici. Pour les graphiques interactifs qui montrent les effets des paramètres, visitez [cette application Web](https://basnijholt.github.io/adaptive-lighting). Pour plus de détails, voir la [document officiel](https://github.com/basnijholt/adaptive-lighting#readme).", - "data": { - "lights": "lumières: List of light entity_ids to be controlled (may be empty). 🌟", - "initial_transition": "initial_transition", - "sleep_transition": "sleep_transition", - "interval": "intervalle", - "max_brightness": "max_brightness: pourcentage de luminosité maximum. personnalisation", - "max_color_temp": "max_color_temp: Température de couleur la plus froide en Kelvin. assemblage", - "min_brightness": "min_brightness: Pourcentage de luminosité minimum. personnalisation", - "min_color_temp": "min_color_temp: Température de couleur la plus chaude de Kelvin. 🔥", - "only_once": "seulement_une fois: Adaptez les lumières seulement lorsqu'elles sont allumées ( \" vrai \" ) ou continuez à les adapter ( \" faux \" ). 🔄", - "prefer_rgb_color": "prefer_rgb_color: Que ce soit pour préférer le réglage de couleur RGB sur la température de couleur claire si possible.", - "separate_turn_on_commands": "separate_turn_on_commands: Utilisez des appels séparés `light.turn_on` pour la couleur et la luminosité, nécessaires pour certains types de lumière. 🔀", - "sleep_brightness": "sleep_brightness", - "sleep_color_temp": "sleep_color_temp", - "sunrise_offset": "sunrise_offset", - "sunrise_time": "sunrise_time", - "sunset_offset": "coucher de soleil_offset", - "sunset_time": "coucher de soleil_time", - "take_over_control": "take_over_control: Éclairage adaptatif désactive si une autre source appelle `light.turn_on` alors que les lumières sont allumées et adaptées. Notez que cela appelle `homeassistant.update_entity` chaque `intervale`", - "detect_non_ha_changes": "detect_non_ha_changes: Détecte et arrête les adaptations pour non-lumière. changement d'état. Besoins `take_over_control` activé. Certaines lumières pourraient faussement indiquer un état « sur », ce qui pourrait donner lieu à des lumières s'allumer de façon inattendue. Désactivez cette fonctionnalité si vous rencontrez de tels problèmes.", - "transition": "transition", - "send_split_delay": "send_split_delay", - "brightness_mode": "luminosité_mode", - "brightness_mode_time_dark": "brightness_mode_time_dark", - "brightness_mode_time_light": "luminosité_mode_time_light", - "max_sunset_time": "max_sunset_time", - "min_sunset_time": "min_sunset_time", - "sleep_rgb_color": "sleep_rgb_color", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp", - "transition_until_sleep": "transition_until_sleep: Lorsque cela est activé, l'éclairage adaptatif traitera les paramètres de sommeil comme le minimum, en passant à ces valeurs après le coucher du soleil. 🌙", - "min_sunrise_time": "min_sunrise_time", - "max_sunrise_time": "max_sunrise_time", - "autoreset_control_seconds": "autoreset_control_seconds", - "adapt_delay": "adapt_delay", - "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimise le trafic réseau et améliore la réceptivité de l'adaptation dans certaines situations. 📉Disponible si les états de lumière physique sortent de synchronisation avec l'état enregistré de HA.", - "intercept": "intercept: Intercepter et adapter les appels `light.turn_on` pour permettre une adaptation instantanée de la couleur et de la luminosité. ACIA Désactiver les lumières qui ne supportent pas `light.turn_on` avec couleur et luminosité.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quand on allume les lumières au départ. Si le paramètre est < < vrai > > , AL s ' adapte uniquement si l ' on invoque < < light.turn_on > > sans préciser la couleur ou la luminosité. ❌ Ceci, par exemple, empêche l'adaptation lors de l'activation d'une scène. Si `false`, AL s'adapte indépendamment de la présence de couleur ou de luminosité dans le `service_data' initial. Besoins `take_over_control` activé. 🕵∫ ", - "multi_light_intercept": "multi_light_intercept: Interceptez et adaptez `light.turn_on` les appels qui ciblent plusieurs lumières. ➗gène Cela pourrait permettre de diviser un seul appel `light.turn_on` en plusieurs appels, par exemple lorsque les lumières sont dans différents interrupteurs. Nécessite un < < intercept > > pour être activé.", - "include_config_in_attributes": "include_config_in_attributes: Afficher toutes les options en tant qu'attributs sur le commutateur dans l'assistant d'accueil lorsqu'il s'agit de «true»" - }, - "data_description": { - "sleep_rgb_color": "Couleur RGB en mode sommeil (utilisé lorsque `sleep_rgb_or_color_temp` est \"rgb_color\").", - "sleep_transition": "Durée de la transition lorsque \"mode de repos\" est activé en quelques secondes. 😴", - "sunrise_time": "Réglez un temps fixe (HH:MM:SS) pour le lever du soleil. 🌅", - "min_sunrise_time": "Définir le premier temps de lever de soleil virtuel (HHH:MM:SS), permettant des levures de soleil ultérieures. 🌅", - "max_sunrise_time": "Définir le dernier temps de lever de soleil virtuel (HHH:MM:SS), permettant des levures de soleil antérieures. 🌅", - "sunrise_offset": "Réglez le lever de soleil avec un décalage positif ou négatif en quelques secondes. ⏰", - "sunset_time": "Réglez un temps fixe (HH:MM:SS) pour le coucher de soleil. 🌇", - "min_sunset_time": "Réglez le premier temps de coucher de soleil virtuel (HHH:MM:SS), permettant des couchers de soleil ultérieurs. 🌇", - "max_sunset_time": "Définir le dernier coucher de soleil virtuel (HHH:MM:SS), permettant des couchers de soleil antérieurs. 🌇", - "sunset_offset": "Réglez le temps de coucher avec un décalage positif ou négatif en quelques secondes. ⏰", - "brightness_mode": "Mode de luminosité à utiliser. Les valeurs possibles sont < < par défaut > > , < < linéaire > > et < < parois > > , < < par défaut > > , et < < par coup > > , < < par défaut > > et par > par > . 📈", - "brightness_mode_time_light": "(Ignoré si `brightness_mode='default'`) La durée en quelques secondes pour augmenter/verser la luminosité après/avant le lever du soleil/sunset. 📈📉.", - "autoreset_control_seconds": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲∫", - "send_split_delay": "Délai (ms) entre `separate_turn_on_commands` pour les lumières qui ne supportent pas la luminosité simultanée et le réglage de couleur. ⏲∫", - "adapt_delay": "Temps d'attente (secondes) entre allumage de la lumière et éclairage adaptatif en appliquant des changements. Ça pourrait aider à éviter de filmer. ⏲∫", - "interval": "Fréquence pour adapter les lumières, en quelques secondes. 🔄", - "transition": "Durée de la transition lorsque les lumières changent, en quelques secondes. 🕑", - "initial_transition": "Durée de la première transition lorsque les feux tournent de `off` à `on` en quelques secondes. ⏲∫", - "sleep_brightness": "Pourcentage de luminosité des lumières en mode sommeil. 😴", - "sleep_rgb_or_color_temp": "Utilisez soit `\"rgb_color\"` ou `\"color_temp\"` en mode sommeil. 🌙", - "sleep_color_temp": "Température de couleur en mode sommeil (utilisée lorsque `sleep_rgb_or_color_temp` est `color_temp`) en Kelvin. 😴", - "brightness_mode_time_dark": "(Ignoré si `brightness_mode='default'`) La durée en quelques secondes pour remonter/verser la luminosité avant/après le lever du soleil/sunset. 📈📉" - } - } - }, - "error": { - "option_error": "Option non valide", - "entity_missing": "Une ou plusieurs entités lumineuses sélectionnées sont absentes de Home Assistant" - } - }, - "services": { - "change_switch_settings": { - "fields": { - "adapt_delay": { - "name": "adapt_delay", - "description": "Temps d'attente (secondes) entre allumage de la lumière et éclairage adaptatif en appliquant des changements. Ça pourrait aider à éviter de filmer. ⏲∫" - }, - "min_color_temp": { - "name": "min_color_temp", - "description": "Température de couleur la plus chaude de Kelvin. 🔥" - }, - "autoreset_control_seconds": { - "name": "autoreset_control_seconds", - "description": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲∫" - }, - "entity_id": { - "name": "entity_id", - "description": "ID d'entité du commutateur. 📝" - }, - "include_config_in_attributes": { - "name": "include_config_in_attributes", - "description": "Afficher toutes les options en tant qu'attributs sur le commutateur dans l'assistant d'accueil lorsqu'il s'agit de \" vérité \" . 📝" - }, - "max_color_temp": { - "name": "max_color_temp", - "description": "Température de couleur la plus froide en Kelvin. assemblage" - }, - "only_once": { - "name": "only_once", - "description": "Adaptez les lumières seulement lorsqu'elles sont allumées ( \" vrai \" ) ou continuez à les adapter ( \" faux \" ). 🔄" - }, - "prefer_rgb_color": { - "name": "prefer_rgb_color", - "description": "Que ce soit pour préférer le réglage de couleur RGB sur la température de couleur claire si possible." - }, - "send_split_delay": { - "name": "send_split_delay", - "description": "Délai (ms) entre `separate_turn_on_commands` pour les lumières qui ne supportent pas la luminosité simultanée et le réglage de couleur. ⏲∫" - }, - "separate_turn_on_commands": { - "name": "separate_turn_on_commands", - "description": "Utilisez des appels séparés `light.turn_on` pour la couleur et la luminosité, nécessaires pour certains types de lumière. 🔀" - }, - "sleep_brightness": { - "name": "sleep_brightness", - "description": "Pourcentage de luminosité des lumières en mode sommeil. 😴" - }, - "sunrise_time": { - "name": "sunrise_time", - "description": "Réglez un temps fixe (HH:MM:SS) pour le lever du soleil. 🌅" - }, - "sunset_time": { - "name": "coucher de soleil_time", - "description": "Réglez un temps fixe (HH:MM:SS) pour le coucher de soleil. 🌇" - }, - "sunset_offset": { - "name": "coucher de soleil_offset", - "description": "Réglez le temps de coucher avec un décalage positif ou négatif en quelques secondes. ⏰" - }, - "take_over_control": { - "name": "take_over_control", - "description": "Adaptable Éclairage si une autre source appelle `light.turn_on` alors que les lumières sont allumées et adaptées. Notez que cela appelle `homeassistant.update_entity` chaque `intervale`" - }, - "use_defaults": { - "name": "use_defaults", - "description": "Définit les valeurs par défaut non spécifiées dans cet appel de service. Options : \"current\" (par défaut, conserve les valeurs courantes), \"factory\" (réinitialisation des défauts documentés), ou \"configuration\" (revertissement des défauts de configuration). écrasement" - }, - "sleep_rgb_or_color_temp": { - "name": "sleep_rgb_or_color_temp", - "description": "Utilisez soit `\"rgb_color\"` ou `\"color_temp\"` en mode sommeil. 🌙" - }, - "turn_on_lights": { - "description": "Que ce soit pour allumer des lumières qui sont actuellement éteintes. 🔆", - "name": "turn_on_lights" - }, - "initial_transition": { - "description": "Durée de la première transition lorsque les feux tournent de `off` à `on` en quelques secondes. ⏲∫", - "name": "initial_transition" - }, - "sleep_transition": { - "description": "Durée de la transition lorsque \"mode de repos\" est activé en quelques secondes. 😴", - "name": "sleep_transition" - }, - "max_brightness": { - "description": "Pourcentage de luminosité maximum. personnalisation", - "name": "max_brightness" - }, - "min_brightness": { - "description": "Pourcentage de luminosité minimum. personnalisation", - "name": "min_brightness" - }, - "sleep_rgb_color": { - "description": "Couleur RGB en mode sommeil (utilisé lorsque `sleep_rgb_or_color_temp` est \"rgb_color\").", - "name": "sleep_rgb_color" - }, - "sleep_color_temp": { - "description": "Température de couleur en mode sommeil (utilisée lorsque `sleep_rgb_or_color_temp` est `color_temp`) en Kelvin. 😴", - "name": "sleep_color_temp" - }, - "sunrise_offset": { - "description": "Réglez le lever de soleil avec un décalage positif ou négatif en quelques secondes. ⏰", - "name": "sunrise_offset" - }, - "max_sunrise_time": { - "description": "Définir le dernier temps de lever de soleil virtuel (HHH:MM:SS), permettant des levures de soleil antérieures. 🌅", - "name": "max_sunrise_time" - }, - "min_sunset_time": { - "description": "Réglez le premier temps de coucher de soleil virtuel (HHH:MM:SS), permettant des couchers de soleil ultérieurs. 🌇", - "name": "min_sunset_time" - }, - "detect_non_ha_changes": { - "description": "Détecte et arrête les adaptations pour non-léger. changement d'état. Besoins `take_over_control` activé. Certaines lumières pourraient faussement indiquer un état « sur », ce qui pourrait donner lieu à des lumières s'allumer de façon inattendue. Désactivez cette fonctionnalité si vous rencontrez de tels problèmes.", - "name": "detect_non_ha_changes" - }, - "transition": { - "description": "Durée de la transition lorsque les lumières changent, en quelques secondes. 🕑", - "name": "transition" - } - }, - "name": "change_switch_settings", - "description": "Changez les réglages que vous souhaitez dans le commutateur. Toutes les options ici sont les mêmes que dans le flux de configuration." - }, - "apply": { - "fields": { - "entity_id": { - "name": "entity_id", - "description": "Le `entity_id` du commutateur avec les réglages à appliquer. 📝" - }, - "lights": { - "name": "lumières", - "description": "Une lumière (ou une liste de lumières) pour appliquer les réglages. personnalisation" - }, - "prefer_rgb_color": { - "name": "prefer_rgb_color", - "description": "Que ce soit pour préférer le réglage de couleur RGB sur la température de couleur claire si possible." - }, - "transition": { - "name": "transition", - "description": "Durée de la transition lorsque les lumières changent, en quelques secondes. 🕑" - }, - "turn_on_lights": { - "name": "turn_on_lights", - "description": "Que ce soit pour allumer des lumières qui sont actuellement éteintes. 🔆" - }, - "adapt_brightness": { - "description": "Que ce soit pour adapter la luminosité de la lumière. 🌞", - "name": "adapt_brightness" - }, - "adapt_color": { - "description": "Que ce soit pour adapter la couleur sur les feux support.", - "name": "adapt_color" - } - }, - "name": "applique", - "description": "Applique les réglages d'éclairage adaptatif actuels aux lumières." - }, - "set_manual_control": { - "fields": { - "lights": { - "name": "lumières", - "description": "entity_id(s) of lights, if not specified, all lights in the switch are selected. personnalisation" - }, - "manual_control": { - "name": "manual_control", - "description": "Que ce soit pour ajouter (« faux ») ou supprimer (« faux ») la lumière de la liste « manual_control ». 🔒" - }, - "entity_id": { - "description": "Le `entity_id` du commutateur dans lequel (un) marquer la lumière comme étant `manuellement contrôlé`. 📝", - "name": "entity_id" - } - }, - "description": "Marquer si une lumière est «manuellement contrôlée».", - "name": "set_manual_control" - } + "abort": { + "already_configured": "Cet appareil est déjà configuré" } + }, + "options": { + "step": { + "init": { + "title": "Options d'éclairage adaptatif", + "description": "Tous les paramètres de l'instance d'éclairage adaptatif. Les noms des options correspondent aux paramètres YAML. Aucune option n'est affichée si l'entrée adaptive_lighting est définie dans votre configuration YAML.", + "data": { + "lights": "lights : Les lampes à contrôler", + "initial_transition": "initial_transition : Transition (en secondes) lorsque l'état d'une lampe passe d'« éteinte » à « allumée ».", + "sleep_transition": "sleep_transition : Transition (en secondes) lorsque « sleep_state » est commuté.", + "interval": "interval : Temps (en secondes) entre deux mises à jour du commutateur.", + "max_brightness": "max_brightness : Luminosité maximale des lampes (en pourcentage) au cours d'un cycle.", + "max_color_temp": "max_color_temp : Couleur la plus froide (en kelvins) du cycle de température de couleur.", + "min_brightness": "min_brightness : Luminosité minimale des lampes (en pourcentage) au cours d'un cycle.", + "min_color_temp": "min_color_temp : Couleur la plus chaude (en kelvins) du cycle de température de couleur.", + "only_once": "only_once : Adapter les lampes uniquement au moment où elles sont allumées.", + "prefer_rgb_color": "prefer_rgb_color : Utiliser « rgb_color » plutôt que « color_temp » lorsque cela est possible.", + "separate_turn_on_commands": "separate_turn_on_commands : Séparer les commandes pour chaque attribut (couleur, luminosité, etc.) de « light.turn_on » (nécessaire pour certaines lampes).", + "sleep_brightness": "sleep_brightness : Luminosité (en pourcentage) du mode nuit.", + "sleep_color_temp": "sleep_color_temp : Température de couleur (en kelvins) du mode nuit.", + "sunrise_offset": "sunrise_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au lever du soleil.", + "sunrise_time": "sunrise_time : Heure (HH:MM:SS) du lever du soleil. Si « None », utilise l'heure correspondant à votre emplacement.", + "sunset_offset": "sunset_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au coucher du soleil.", + "sunset_time": "sunset_time : Heure (HH:MM:SS) du coucher du soleil. Si « None », utilise l'heure correspondant à votre emplacement.", + "take_over_control": "take_over_control : Si quelque chose d'autre que l'éclairage adaptatif appelle « light.turn_on » alors qu'une lampe est déjà allumée, cesser d'adapter cette lampe jusqu'à ce qu'elle (ou le commutateur) soit éteinte puis rallumée.", + "detect_non_ha_changes": "detect_non_ha_changes : Détecter tout changement de plus de 10 % appliqué aux lampes (même en dehors de HA). Nécessite que « take_over_control » soit activé. (Appelle « homeassistant.update_entity » tous les « interval » !)", + "transition": "transition : Durée de la transition (en secondes) des changements appliqués aux lampes." + } + } + }, + "error": { + "option_error": "Option non valide", + "entity_missing": "Une lumière sélectionnée n’a pas été trouvée" + } + } } diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json index 39bee1e2..cc269685 100644 --- a/custom_components/adaptive_lighting/translations/nl.json +++ b/custom_components/adaptive_lighting/translations/nl.json @@ -47,9 +47,6 @@ "transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)", "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Wanneer je de lichten in eerste instantie aanzet. Indien ingesteld op `true`, past AL zich alleen aan als `light.turn_on` wordt aangeroepen zonder kleur of helderheid op te geven. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Indien `false`, past AL zich aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. Moet 'take_over_control' ingeschakeld zijn. 🕵️ " - }, - "data_description": { - "sunrise_offset": "Een zonsopgang met een positief of negatief offset in seconden. _" } } }, @@ -63,9 +60,6 @@ "fields": { "only_once": { "description": "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄" - }, - "sunrise_offset": { - "description": "Een zonsopgang met een positief of negatief offset in seconden. _" } } } From 3757a668291a2058ace0731ea1f5e465ec1a345a Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 17 Aug 2023 06:04:59 +0200 Subject: [PATCH 0661/1077] Translated using Weblate (German) (#758) Currently translated at 55.5% (85 of 153 strings) Translation: Adaptive Lighting/Adaptive Lighting Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/de/ Co-authored-by: Michel Balzer --- .../adaptive_lighting/translations/de.json | 111 ++++++++++-------- 1 file changed, 60 insertions(+), 51 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json index 79eb0df8..154d1fe0 100644 --- a/custom_components/adaptive_lighting/translations/de.json +++ b/custom_components/adaptive_lighting/translations/de.json @@ -1,58 +1,67 @@ { - "title": "Adaptive Lighting", - "config": { - "step": { - "user": { - "title": "Benenne das Adaptive Lighting", - "description": "Jede Instanz kann mehrere Licht Entitäten beinhalten", - "data": { - "name": "Name" + "title": "Adaptive Lighting", + "config": { + "step": { + "user": { + "title": "Benenne das Adaptive Lighting", + "description": "Jede Instanz kann mehrere Licht Entitäten beinhalten", + "data": { + "name": "Name" + } + } + }, + "abort": { + "already_configured": "Gerät ist bereits konfiguriert!" } - } }, - "abort": { - "already_configured": "Gerät ist bereits konfiguriert!" - } - }, - "options": { - "step": { - "init": { - "title": "Adaptive Lighting Optionen", - "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde.", - "data": { - "lights": "Lichter", - "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt", - "sleep_transition": "sleep_transition: Wenn 'sleep_state' sich ändert. (Sekunden)", - "interval": "interval, Zeit zwischen Updates des Switches", - "max_brightness": "max_brightness, maximale Helligkeit in %", - "max_color_temp": "max_color_temp, maximale Farbtemperatur in Kelvin", - "min_brightness": "min_brightness, minimale Helligkeit in %", - "min_color_temp": "min_color_temp, minimale Farbtemperatur in Kelvin", - "only_once": "only_once, passe die Lichter nur beim Einschalten an", - "prefer_rgb_color": "prefer_rgb_color, nutze 'rgb_color' vor 'color_temp', wenn möglich", - "separate_turn_on_commands": "separate_turn_on_commands, für jedes Attribut (Farbe, Helligkeit usw.) in 'light.turn_on' werden separate Befehle gesendet. Wird für manche Leuchtmittel benötigt.", - "send_split_delay": "send_split_delay: Wartezeit zwischen dem Senden der Befehle (Millisekunden), wenn separate_turn_on_commands genutzt wird. Kann helfen, wenn die Leuchtmittel die separaten Befehle nicht korrekt umsetzen.", - "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, nutze 'rgb_color' oder 'color_temp'", - "sleep_rgb_color": "sleep_rgb_color, in RGB", - "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperatur in Kelvin", - "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- Sekunden", - "sunrise_time": "sunrise_time, Sonnenaufgangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenaufgangs an deiner Position verwendet)", - "max_sunrise_time": "max_sunrise_time: Manuelles Überschreiben der max. sunrise_time. Falls 'None', wird die tatsächliche sunrise_time an deiner Position verwendet (HH:MM:SS)", - "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- Sekunden", - "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)", - "min_sunset_time": "min_sunset_time: Manuelles Überschreiben der min. sunset_time. Falls 'None', wird die tatsächliche sunset_time an deiner Position verwendet (HH:MM:SS)", - "take_over_control": "take_over_control, wenn irgendetwas während ein Licht an ist außer Adaptive Lighting den Service 'light.turn_on' aufruft, stoppe die Anpassung des Lichtes (oder des Schalters) bis dieser wieder von off -> on geschaltet wird.", - "detect_non_ha_changes": "detect_non_ha_changes, entdeckt alle Änderungen über 10% am Licht (auch außerhalb von HA gemacht), 'take_over_control' muss aktiviert sein (ruft 'homeassistant.update_entity' jede 'interval' auf!)", - "transition": "transition, Wechselzeit in Sekunden", - "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.", - "skip_redundant_commands": "Keine Adaptierungsbefehle senden, deren erwünschter Status schon dem bekanntes Status von Lichtern entspricht. Minimiert die Netzwerkbelastung und verbessert die Adaptierung in manchen Situationen. Deaktiviert lassen falls der pysikalische Status der Lichter und der erkannte Status in HA nicht synchron bleiben." + "options": { + "step": { + "init": { + "title": "Adaptive Lighting Optionen", + "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde.", + "data": { + "lights": "Lichter", + "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt", + "sleep_transition": "sleep_transition: Wenn 'sleep_state' sich ändert. (Sekunden)", + "interval": "interval, Zeit zwischen Updates des Switches", + "max_brightness": "max_brightness, maximale Helligkeit in %", + "max_color_temp": "max_color_temp, maximale Farbtemperatur in Kelvin", + "min_brightness": "min_brightness, minimale Helligkeit in %", + "min_color_temp": "min_color_temp, minimale Farbtemperatur in Kelvin", + "only_once": "only_once, passe die Lichter nur beim Einschalten an", + "prefer_rgb_color": "prefer_rgb_color, nutze 'rgb_color' vor 'color_temp', wenn möglich", + "separate_turn_on_commands": "separate_turn_on_commands, für jedes Attribut (Farbe, Helligkeit usw.) in 'light.turn_on' werden separate Befehle gesendet. Wird für manche Leuchtmittel benötigt.", + "send_split_delay": "send_split_delay: Wartezeit zwischen dem Senden der Befehle (Millisekunden), wenn separate_turn_on_commands genutzt wird. Kann helfen, wenn die Leuchtmittel die separaten Befehle nicht korrekt umsetzen.", + "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, nutze 'rgb_color' oder 'color_temp'", + "sleep_rgb_color": "sleep_rgb_color, in RGB", + "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperatur in Kelvin", + "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- Sekunden", + "sunrise_time": "sunrise_time, Sonnenaufgangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenaufgangs an deiner Position verwendet)", + "max_sunrise_time": "max_sunrise_time: Manuelles Überschreiben der max. sunrise_time. Falls 'None', wird die tatsächliche sunrise_time an deiner Position verwendet (HH:MM:SS)", + "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- Sekunden", + "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)", + "min_sunset_time": "min_sunset_time: Manuelles Überschreiben der min. sunset_time. Falls 'None', wird die tatsächliche sunset_time an deiner Position verwendet (HH:MM:SS)", + "take_over_control": "take_over_control, wenn irgendetwas während ein Licht an ist außer Adaptive Lighting den Service 'light.turn_on' aufruft, stoppe die Anpassung des Lichtes (oder des Schalters) bis dieser wieder von off -> on geschaltet wird.", + "detect_non_ha_changes": "detect_non_ha_changes, entdeckt alle Änderungen über 10% am Licht (auch außerhalb von HA gemacht), 'take_over_control' muss aktiviert sein (ruft 'homeassistant.update_entity' jede 'interval' auf!)", + "transition": "transition, Wechselzeit in Sekunden", + "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.", + "skip_redundant_commands": "Keine Adaptierungsbefehle senden, deren erwünschter Status schon dem bekanntes Status von Lichtern entspricht. Minimiert die Netzwerkbelastung und verbessert die Adaptierung in manchen Situationen. Deaktiviert lassen falls der pysikalische Status der Lichter und der erkannte Status in HA nicht synchron bleiben." + } + } + }, + "error": { + "option_error": "Fehlerhafte Option", + "entity_missing": "Ein ausgewähltes Licht wurde nicht gefunden" } - } }, - "error": { - "option_error": "Fehlerhafte Option", - "entity_missing": "Ein ausgewähltes Licht wurde nicht gefunden" + "services": { + "apply": { + "fields": { + "lights": { + "description": "Ein Licht (oder eine Lichtliste), um die Einstellungen anzuwenden. RECHT" + } + } + } } - } } From 239aae8ed7501063b0fbec247534f3dc7af7b3a3 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 17 Aug 2023 23:43:44 +0200 Subject: [PATCH 0662/1077] Translated using Weblate (Dutch) (#760) * Translated using Weblate (Dutch) Currently translated at 57.5% (88 of 153 strings) Translation: Adaptive Lighting/Adaptive Lighting Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/ * Translated using Weblate (Dutch) Currently translated at 58.1% (89 of 153 strings) Translation: Adaptive Lighting/Adaptive Lighting Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/ --------- Co-authored-by: Bas Nijholt --- .../adaptive_lighting/translations/nl.json | 134 ++++++++++-------- 1 file changed, 72 insertions(+), 62 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json index cc269685..6faff432 100644 --- a/custom_components/adaptive_lighting/translations/nl.json +++ b/custom_components/adaptive_lighting/translations/nl.json @@ -1,67 +1,77 @@ { - "title": "Adaptieve verlichting", - "config": { - "step": { - "user": { - "title": "Kies een naam voor de adaptieve verlichting integratie", - "description": "Kies een naam voor deze integratie. U kunt verschillende integratie van Adaptieve verlichting uitvoeren, elk van deze kan meerdere lichten bevatten!", - "data": { - "name": "Naam" - } - } - }, - "abort": { - "already_configured": "Dit apparaat is al geconfigureerd" + "title": "Adaptieve verlichting", + "config": { + "step": { + "user": { + "title": "Kies een naam voor de adaptieve verlichting integratie", + "description": "Kies een naam voor deze integratie. U kunt verschillende integratie van Adaptieve verlichting uitvoeren, elk van deze kan meerdere lichten bevatten!", + "data": { + "name": "Naam" } + } }, - "options": { - "step": { - "init": { - "title": "Adaptieve verlichting instellingen", - "description": "Alle instellingen voor een Adaptieve verlichting component. De optienamen komen overeen met de YAML-instellingen. Er worden geen opties weergegeven als u het item adaptive_lighting hebt gedefinieerd in uw YAML-configuratie.", - "data": { - "lights": "Lichten", - "initial_transition": "initial_transition: Wanneer lichten van 'uit' naar 'aan' gaan. (seconden)", - "sleep_transition": "sleep_transition: Wanneer 'sleep_state' verandert. (seconden)", - "interval": "interval: Tijd tussen switch-updates. (seconden)", - "max_brightness": "max_brightness: Hoogste helderheid van lichten tijdens een cyclus. (%)", - "max_color_temp": "max_color_temp: Koudste tint van de kleurtemperatuurcyclus. (kelvin)", - "min_brightness": "min_brightness: Laagste helderheid van lichten tijdens een cyclus. (%)", - "min_color_temp": "min_color_temp, Warmste tint van de kleurtemperatuurcyclus. (kelvin)", - "only_once": "only_once: pas de verlichting alleen aan wanneer u ze aanzet.", - "prefer_rgb_color": "prefer_rgb_color: Gebruik waar mogelijk 'rgb_color' in plaats van 'color_temp'.", - "separate_turn_on_commands": "separate_turn_on_commands: Scheid de commando's voor elk attribuut (kleur, helderheid, enz.) in 'light.turn_on' (vereist voor sommige lampen).", - "send_split_delay": "send_split_delay: wacht tussen commando's (milliseconden), wanneer separate_turn_on_commands wordt gebruikt. Kan ervoor zorgen dat beide commando's correct door de lamp worden afgehandeld.", - "sleep_brightness": "sleep_brightness, helderheidsinstelling voor slaapstand. (%)", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, gebruik 'rgb_color' of 'color_temp'", - "sleep_rgb_color": "sleep_rgb_color, in RGB", - "sleep_color_temp": "sleep_color_temp: Kleurtemperatuurinstelling voor slaapstand. (kelvin)", - "sunrise_offset": "sunrise_offset: Hoe lang voor(-) of na(+) zonsopgang uitvoeren (+/- seconden)", - "sunrise_time": "sunrise_time: Handmatige wijziging van de zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", - "max_sunrise_time": "max_sunrise_time: handmatige aanpassing van de maximale zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", - "sunset_offset": "sunset_offset: Hoe lang voor(-) of na(+) zonsondergang uitvoeren (+/- seconden)", - "sunset_time": "sunset_time: handmatige onderdrukking van de zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", - "min_sunset_time": "min_sunset_time: handmatige onderdrukking van de minimale zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsondergangstijd op uw locatie gebruikt (UU:MM:SS)", - "take_over_control": "take_over_control: Als iets anders dan Adaptive Lighting 'light.turn_on' roept wanneer een lamp al aan is, stop dan met het aanpassen van het licht totdat het (of de schakelaar) uit -> aan gaat.", - "detect_non_ha_changes": "detect_non_ha_changes: detecteert alle >10% wijzigingen aan de lichten (ook buiten HA), vereist dat 'take_over_control' is ingeschakeld (heet 'homeassistant.update_entity' elke 'interval'!)", - "transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)", - "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Wanneer je de lichten in eerste instantie aanzet. Indien ingesteld op `true`, past AL zich alleen aan als `light.turn_on` wordt aangeroepen zonder kleur of helderheid op te geven. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Indien `false`, past AL zich aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. Moet 'take_over_control' ingeschakeld zijn. 🕵️ " - } - } - }, - "error": { - "option_error": "Ongeldige optie", - "entity_missing": "Een of meer geselecteerde lichtentiteiten ontbreken in Home Assistant" - } - }, - "services": { - "change_switch_settings": { - "fields": { - "only_once": { - "description": "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄" - } - } - } + "abort": { + "already_configured": "Dit apparaat is al geconfigureerd" } + }, + "options": { + "step": { + "init": { + "title": "Adaptieve verlichting instellingen", + "description": "Alle instellingen voor een Adaptieve verlichting component. De optienamen komen overeen met de YAML-instellingen. Er worden geen opties weergegeven als u het item adaptive_lighting hebt gedefinieerd in uw YAML-configuratie.", + "data": { + "lights": "Lichten", + "initial_transition": "initial_transition: Wanneer lichten van 'uit' naar 'aan' gaan. (seconden)", + "sleep_transition": "sleep_transition: Wanneer 'sleep_state' verandert. (seconden)", + "interval": "interval: Tijd tussen switch-updates. (seconden)", + "max_brightness": "max_brightness: Hoogste helderheid van lichten tijdens een cyclus. (%)", + "max_color_temp": "max_color_temp: Koudste tint van de kleurtemperatuurcyclus. (kelvin)", + "min_brightness": "min_brightness: Laagste helderheid van lichten tijdens een cyclus. (%)", + "min_color_temp": "min_color_temp, Warmste tint van de kleurtemperatuurcyclus. (kelvin)", + "only_once": "only_once: pas de verlichting alleen aan wanneer u ze aanzet.", + "prefer_rgb_color": "prefer_rgb_color: Gebruik waar mogelijk 'rgb_color' in plaats van 'color_temp'.", + "separate_turn_on_commands": "separate_turn_on_commands: Scheid de commando's voor elk attribuut (kleur, helderheid, enz.) in 'light.turn_on' (vereist voor sommige lampen).", + "send_split_delay": "send_split_delay: wacht tussen commando's (milliseconden), wanneer separate_turn_on_commands wordt gebruikt. Kan ervoor zorgen dat beide commando's correct door de lamp worden afgehandeld.", + "sleep_brightness": "sleep_brightness, helderheidsinstelling voor slaapstand. (%)", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, gebruik 'rgb_color' of 'color_temp'", + "sleep_rgb_color": "sleep_rgb_color, in RGB", + "sleep_color_temp": "sleep_color_temp: Kleurtemperatuurinstelling voor slaapstand. (kelvin)", + "sunrise_offset": "sunrise_offset: Hoe lang voor(-) of na(+) zonsopgang uitvoeren (+/- seconden)", + "sunrise_time": "sunrise_time: Handmatige wijziging van de zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", + "max_sunrise_time": "max_sunrise_time: handmatige aanpassing van de maximale zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", + "sunset_offset": "sunset_offset: Hoe lang voor(-) of na(+) zonsondergang uitvoeren (+/- seconden)", + "sunset_time": "sunset_time: handmatige onderdrukking van de zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", + "min_sunset_time": "min_sunset_time: handmatige onderdrukking van de minimale zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsondergangstijd op uw locatie gebruikt (UU:MM:SS)", + "take_over_control": "take_over_control: Als iets anders dan Adaptive Lighting 'light.turn_on' roept wanneer een lamp al aan is, stop dan met het aanpassen van het licht totdat het (of de schakelaar) uit -> aan gaat.", + "detect_non_ha_changes": "detect_non_ha_changes: detecteert alle >10% wijzigingen aan de lichten (ook buiten HA), vereist dat 'take_over_control' is ingeschakeld (heet 'homeassistant.update_entity' elke 'interval'!)", + "transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)", + "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Bij het initieel inschakelen van de lampen. Als dit op `true` is ingesteld, past AL alleen aan als `light.turn_on` wordt aangeroepen zonder een kleur of helderheid te specificeren. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Als het `false` is, past AL aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. `take_over_control` moet ingeschakeld zijn. 🕵️ " + }, + "data_description": { + "sunrise_offset": "Pas de zonsopkomsttijd aan met een positieve of negatieve offset in seconden. ⏰", + "sunset_offset": "Pas de tijd van zonsondergang aan met een positieve of negatieve verschuiving in seconden. ⏰" + } + } + }, + "error": { + "option_error": "Ongeldige optie", + "entity_missing": "Een of meer geselecteerde lichtentiteiten ontbreken in Home Assistant" + } + }, + "services": { + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Pas lampen alleen aan wanneer ze zijn ingeschakeld (`true`) of blijf ze aanpassen (`false`). 🔄" + }, + "sunrise_offset": { + "description": "Pas de tijd van zonsopkomst aan met een positieve of negatieve verschuiving in seconden. ⏰" + }, + "sunset_offset": { + "description": "Pas de tijd van zonsondergang aan met een positieve of negatieve offset in seconden. ⏰" + } + } + } + } } From 857ac10856106549f5ca200674074f728cd5f79c Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Fri, 18 Aug 2023 04:45:24 +0200 Subject: [PATCH 0663/1077] Translations update from Hosted Weblate (#761) * Translated using Weblate (Dutch) Currently translated at 57.5% (88 of 153 strings) Translation: Adaptive Lighting/Adaptive Lighting Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/ * Translated using Weblate (Dutch) Currently translated at 58.1% (89 of 153 strings) Translation: Adaptive Lighting/Adaptive Lighting Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/ * Translated using Weblate (Dutch) Currently translated at 62.0% (95 of 153 strings) Translation: Adaptive Lighting/Adaptive Lighting Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/ --------- Co-authored-by: Bas Nijholt --- .../adaptive_lighting/translations/nl.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json index 6faff432..2e2e64aa 100644 --- a/custom_components/adaptive_lighting/translations/nl.json +++ b/custom_components/adaptive_lighting/translations/nl.json @@ -46,11 +46,17 @@ "detect_non_ha_changes": "detect_non_ha_changes: detecteert alle >10% wijzigingen aan de lichten (ook buiten HA), vereist dat 'take_over_control' is ingeschakeld (heet 'homeassistant.update_entity' elke 'interval'!)", "transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)", "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Bij het initieel inschakelen van de lampen. Als dit op `true` is ingesteld, past AL alleen aan als `light.turn_on` wordt aangeroepen zonder een kleur of helderheid te specificeren. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Als het `false` is, past AL aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. `take_over_control` moet ingeschakeld zijn. 🕵️ " + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Bij het initieel inschakelen van de lampen. Als dit op `true` is ingesteld, past AL alleen aan als `light.turn_on` wordt aangeroepen zonder een kleur of helderheid te specificeren. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Als het `false` is, past AL aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. `take_over_control` moet ingeschakeld zijn. 🕵️ ", + "transition_until_sleep": "transition_until_sleep: Wanneer ingeschakeld, zal Adaptieve Verlichting de slaapinstellingen behandelen als het minimum, overgaand naar deze waarden na zonsondergang. 🌙", + "skip_redundant_commands": "skip_redundant_commands: Sla het verzenden van aanpassingscommando's over waarvan de doelstatus al gelijk is aan de bekende status van de lamp. Minimaliseert netwerkverkeer en verbetert de responsiviteit van de aanpassing in sommige situaties. 📉Schakel uit als de fysieke lichtstatus niet meer synchroon loopt met de door HA geregistreerde status.", + "intercept": "intercept: Onderschep en pas `light.turn_on` oproepen aan om directe kleur- en helderheidsaanpassing mogelijk te maken. 🏎️ Schakel uit voor lampen die `light.turn_on` niet ondersteunen met kleur en helderheid.", + "include_config_in_attributes": "include_config_in_attributes: Toon alle opties als attributen op de schakelaar in Home Assistant wanneer ingesteld op `true`. 📝", + "multi_light_intercept": "multi_light_intercept: Onderschep en pas `light.turn_on` oproepen aan die gericht zijn op meerdere lampen. ➗⚠️ Dit kan resulteren in het opsplitsen van een enkele `light.turn_on` call in meerdere calls, bijvoorbeeld wanneer lampen zich in verschillende schakelaars bevinden. Vereist dat `intercept` is ingeschakeld." }, "data_description": { "sunrise_offset": "Pas de zonsopkomsttijd aan met een positieve of negatieve offset in seconden. ⏰", - "sunset_offset": "Pas de tijd van zonsondergang aan met een positieve of negatieve verschuiving in seconden. ⏰" + "sunset_offset": "Pas de tijd van zonsondergang aan met een positieve of negatieve verschuiving in seconden. ⏰", + "interval": "Frequentie om de lampen aan te passen, in seconden. 🔄" } } }, From 65b82b605e8c86cafa05ec481077481beffe329b Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Fri, 18 Aug 2023 19:25:55 +0200 Subject: [PATCH 0664/1077] Translated using Weblate (Polish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 64.0% (98 of 153 strings) Co-authored-by: Łukasz Marek Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pl/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/pl.json | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json index 99a97e3c..038d9bee 100644 --- a/custom_components/adaptive_lighting/translations/pl.json +++ b/custom_components/adaptive_lighting/translations/pl.json @@ -3,8 +3,8 @@ "config": { "step": { "user": { - "title": "Wybierz nazwę grupy dla Adaptacyjnego oświetlenia", - "description": "Wybierz nazwę dla grupy. Możesz użyć wiele grup Adaptacyjnego oświetlenia, każda może mieć dowolną konfigurację świateł!", + "title": "Wybierz nazwę dla tej instancji Adaptacyjnego oświetlenia", + "description": "Każda instancja może zawierać wiele świateł.", "data": { "name": "Nazwa" } @@ -18,34 +18,51 @@ "step": { "init": { "title": "Adaptacyjne oświetlenie opcje", - "description": "Wszystkie ustawienia dla Adaptacyjnego oświetlenia. Nazwy opcji odpowiadają ustawieniom YAML. Żadne opcje nie są wyświetlane, jeśli masz wpis adaptive_lighting zdefiniowany w konfiguracji YAML.", + "description": "Konfiguracja komponentu Adaptacyjnego oświetlenia. Nazwy opcji odpowiadają ustawieniom YAML. Żadne opcje nie są wyświetlane, jeśli są zdefiniowany w konfiguracji YAML. Aby zobaczyć interaktywne wykresy demonstrujące działanie parametrów odwiedź [this web app](https://basnijholt.github.io/adaptive-lighting). Aby zobaczyć więcej szczegółów odwiedź [official documentation](https://github.com/basnijholt/adaptive-lighting#readme).", "data": { - "lights": "światła", + "lights": "lights: Lista entity_ids, które mają być kontrolowane (może być pusta). 🌟", "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (sekund)", "sleep_transition": "sleep_transition: When 'sleep_state' changes. (sekund)", "interval": "interval: Time between switch updates. (sekund)", - "max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)", - "max_color_temp": "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)", - "min_brightness": "min_brightness: Lowest brightness of lights during a cycle. (%)", - "min_color_temp": "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)", - "only_once": "only_once: Only adapt the lights when turning them on.", - "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.", - "separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).", + "max_brightness": "max_brightness: Maksymajna josność (w procentach). 💡", + "max_color_temp": "max_color_temp: Najzimniejsza temperatura barwowa (w Kelwinach). ❄️", + "min_brightness": "min_brightness: Minimalna jasność (w procentach). 💡", + "min_color_temp": "min_color_temp: Najcieplejsza temperatura barwowa (w Kelwinach). 🔥", + "only_once": "only_once: Adaptuj światło tylko podczas włączenia (`true`) lub adaptuj cały czas (`false`). 🔄", + "prefer_rgb_color": "prefer_rgb_color: Czy w miarę możliwości preferować regulację kolorów RGB zamiast temperatury barwowej światła.. 🌈", + "separate_turn_on_commands": "separate_turn_on_commands: Używaj oddzielnych wywołań `light.turn_on` dla koloru i jasności, wymagane dla niektórych typów świateł. 🔀", "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)", "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)", "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- sekund)", "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- sekund)", "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", - "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.", - "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)", - "transition": "Transition time when applying a change to the lights (sekund)" + "take_over_control": "take_over_control: Wyłącz adaptowanie oświetlenia gdy inna usługa wywoła `light.turn_on` gdy oświetlenie jest już włączone. Zauważ że to wywołuje `homeassistant.update_entity` co`interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan 'on', co może powodować nieoczekiwane włączenia światła. Wyłącz to ustawienie jeżeli doświadczasz takich objawów.", + "transition": "Transition time when applying a change to the lights (sekund)", + "transition_until_sleep": "transition_until_sleep: Gdy włączone, Adaptacyjne oświetlenie będzie traktowało ustawienia spania jako minimalne i przejdzie do nich po zachodzie słońca. 🌙", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Gdy włączone `true` to Adaptacyjne oświetlenie włączy adaptacje tylko jeżeli `light.turn_on` jest wywołane bez konkretnego koloru lub jasności. ❌🌈 To ustawienie zapobiega między innymi adaptacji gdy aktywowana jest scena. Gdy wyłączone `false`, Adaptacyjne oświetlenie włączy adaptacje niezależnie czy `service_data zawiera kolor lub jasność`. Potrzebuje włączonej opcji `take_over_control`. 🕵️ ", + "skip_redundant_commands": "skip_redundant_commands: Pomiń wysyłanie polecenia adaptacji jeżeli stan światła jest taki sam jak docelowy stan adaptacji. Minimalizuje to ruch sieciowy oraz w niektórych przysadkach poprawia szybkość działania. 📉Wyłącz jeżeli faktyczny stan światła się nie pokrywa z tym który widnieje w Home Assistant.", + "include_config_in_attributes": "include_config_in_attributes: Gdy włącone `true` pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant. 📝", + "intercept": "intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`aby błyskawicznie dostosować kolor i jasność . 🏎️ Wyłącz dla świateł które nie akceptują wywołania `light.turn_on` zawierającego kolor i jasność.", + "multi_light_intercept": "multi_light_intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`, które dotyczą wielu świateł. ➗⚠️ Może to powodować rozdzielenie pojedynczego wywołania `light.turn_on` na wiele wywołań, na przykład gdy światła są przypisane do rożnych instancji. Wymaga włączonej opcji `intercept`." + }, + "data_description": { + "interval": "Częstotliwość adaptacji świateł w sekundach. 🔄", + "transition": "Długość tranzycji do nowego stanu w sekundach. 🕑", + "initial_transition": "Długość pierwszej tranzycji gdy światło zostanie włączone z `off` na `on`w sekundach. ⏲️", + "sleep_rgb_or_color_temp": "Użyj `\"rgb_color\"` albo `\"color_temp\"`trybie spania. 🌙", + "sleep_color_temp": "Temperatura barwowa w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `color_temp`) (w Kelwinach). 😴", + "sleep_rgb_color": "Kolor RGB w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈", + "sleep_transition": "Długość tranzycji gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴", + "sunrise_time": "Ustaw stały czas wschodu słońca (HH:MM:SS). 🌅", + "sleep_brightness": "Jasność świateł w trybie spania (w procentach). 😴" } } }, "error": { "option_error": "Błędne opcje", - "entity_missing": "Nie znaleziono wybranego światła" + "entity_missing": "Jednego lub więcej wybranych świateł nie można znaleźć w Home Assistant" } } } From c53816224ca1cbbb001fed7f46a096251464730e Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Fri, 18 Aug 2023 19:25:55 +0200 Subject: [PATCH 0665/1077] Translated using Weblate (French) Currently translated at 54.9% (84 of 153 strings) Co-authored-by: Maxime Bailleul Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/fr/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/fr.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json index a41d84a0..c7835639 100644 --- a/custom_components/adaptive_lighting/translations/fr.json +++ b/custom_components/adaptive_lighting/translations/fr.json @@ -40,6 +40,10 @@ "take_over_control": "take_over_control : Si quelque chose d'autre que l'éclairage adaptatif appelle « light.turn_on » alors qu'une lampe est déjà allumée, cesser d'adapter cette lampe jusqu'à ce qu'elle (ou le commutateur) soit éteinte puis rallumée.", "detect_non_ha_changes": "detect_non_ha_changes : Détecter tout changement de plus de 10 % appliqué aux lampes (même en dehors de HA). Nécessite que « take_over_control » soit activé. (Appelle « homeassistant.update_entity » tous les « interval » !)", "transition": "transition : Durée de la transition (en secondes) des changements appliqués aux lampes." + }, + "data_description": { + "interval": "Fréquence d'adaptation des lumières, en secondes. 🔄", + "sleep_brightness": "Pourcentage de luminosité des lumières en mode sommeil. 😴" } } }, @@ -47,5 +51,14 @@ "option_error": "Option non valide", "entity_missing": "Une lumière sélectionnée n’a pas été trouvée" } + }, + "services": { + "change_switch_settings": { + "fields": { + "sleep_brightness": { + "description": "Pourcentage de luminosité des lumières en mode sommeil. 😴" + } + } + } } } From 3042ac85ef28f4840df8ae832ee532ffc24081c1 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 18 Aug 2023 11:31:53 -0700 Subject: [PATCH 0666/1077] docs: add lukerix as a contributor for translation (#763) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index c264a714..cf49d196 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -458,6 +458,15 @@ "contributions": [ "design" ] + }, + { + "login": "lukerix", + "name": "lukerix", + "avatar_url": "https://avatars.githubusercontent.com/u/93864731?v=4", + "profile": "https://github.com/lukerix", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 1997c351..5de65115 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-49-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-50-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -525,6 +525,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + + + From d6d7c4aee9b6d7bc069956e2c573672b45d6ca2b Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 18 Aug 2023 11:34:22 -0700 Subject: [PATCH 0667/1077] docs: add michelbalzer as a contributor for translation (#764) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index cf49d196..64eff534 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -467,6 +467,15 @@ "contributions": [ "translation" ] + }, + { + "login": "michelbalzer", + "name": "Michel Balzer", + "avatar_url": "https://avatars.githubusercontent.com/u/1337412?v=4", + "profile": "https://michelbalzer.de", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 5de65115..e221cdd3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-50-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-51-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -527,6 +527,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From a10f7e2550f03567de56a62cef0c57a4f5c5933e Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 18 Aug 2023 23:43:27 -0700 Subject: [PATCH 0668/1077] docs: add Mexx62 as a contributor for translation (#765) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 64eff534..ae9114e2 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -468,6 +468,15 @@ "translation" ] }, + { + "login": "Mexx62", + "name": "Maxime Bailleul", + "avatar_url": "https://avatars.githubusercontent.com/u/8066485?v=4", + "profile": "https://github.com/Mexx62", + "contributions": [ + "translation" + ] + }, { "login": "michelbalzer", "name": "Michel Balzer", diff --git a/README.md b/README.md index e221cdd3..f8386453 100644 --- a/README.md +++ b/README.md @@ -527,6 +527,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From be440b96411c5879f6ddf2f71d532e486d04ef82 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sat, 19 Aug 2023 08:43:34 +0200 Subject: [PATCH 0669/1077] Translated using Weblate (Polish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 76.4% (117 of 153 strings) Translated using Weblate (Polish) Currently translated at 69.2% (106 of 153 strings) Translated using Weblate (Polish) Currently translated at 65.3% (100 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Łukasz Marek Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pl/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/pl.json | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json index 038d9bee..be3b5763 100644 --- a/custom_components/adaptive_lighting/translations/pl.json +++ b/custom_components/adaptive_lighting/translations/pl.json @@ -56,7 +56,20 @@ "sleep_rgb_color": "Kolor RGB w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈", "sleep_transition": "Długość tranzycji gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴", "sunrise_time": "Ustaw stały czas wschodu słońca (HH:MM:SS). 🌅", - "sleep_brightness": "Jasność świateł w trybie spania (w procentach). 😴" + "sleep_brightness": "Jasność świateł w trybie spania (w procentach). 😴", + "min_sunrise_time": "Ustaw czas najwcześniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na opóźnienie wschodu słońca. 🌅", + "max_sunrise_time": "Ustaw czas najpóźniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na przyspieszenie wschodu słońca. 🌅", + "sunrise_offset": "Dostosuj czas wschodu słońca - przesunięcie o +/- sekund. ⏰", + "sunset_time": "Ustaw stały czas zachodu słońca (HH:MM:SS). 🌇", + "min_sunset_time": "Ustaw czas najwcześniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na opóźnienie zachodu słońca. 🌇", + "brightness_mode": "Tryb ustawianie jasności. Dostępne opcje `default`, `linear`, i `tanh` (używa `brightness_mode_time_dark` i `brightness_mode_time_light`). 📈", + "max_sunset_time": "Ustaw czas najpóźniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na przyspieszenie zachodu słońca. 🌇", + "sunset_offset": "Dostosuj czas zachodu słońca - przesunięcie o +/- sekund. ⏰", + "brightness_mode_time_dark": "(pomijany gdy`brightness_mode='default'`) Czas w sekundach kiedy jasność będzie: zwiększana przed wschodem słońca/zmniejszana po zachodzie słońca. 📈📉", + "brightness_mode_time_light": "(pomijany gdy`brightness_mode='default'`) Czas w sekundach kiedy jasność będzie: zwiększana po wschodzie słońca/zmniejszana przed zachodem słońca. 📈📉", + "autoreset_control_seconds": "Czas po którym manualna kontrola zostanie wyłączona (w sekundach). Ustaw 0 aby wyłączyć. ⏲️", + "send_split_delay": "Opóźnienie (w ms) pomiędzy `separate_turn_on_commands` dla świateł, które nie akceptują jednoczesnego ustawiania jasności i koloru. ⏲️", + "adapt_delay": "Czas (w sekundach) pomiędzy włączeniem światła a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Morze pomóc zredukować migotanie. ⏲️" } } }, @@ -64,5 +77,27 @@ "option_error": "Błędne opcje", "entity_missing": "Jednego lub więcej wybranych świateł nie można znaleźć w Home Assistant" } + }, + "services": { + "apply": { + "description": "Stosuje bieżące ustawienia Adaptacyjnego oświetlenia do świateł.", + "fields": { + "entity_id": { + "description": "`entity_id` przełącznika, którego ustawienia mają być zastosowane. 📝" + }, + "lights": { + "description": "Światło(albo lista świateł), do których mają być zastosowane ustawieniam 💡" + }, + "transition": { + "description": "Czas tranzycji przy zmianie ustawień (w sekundach). 🕑" + }, + "adapt_color": { + "description": "Czy adaptować kolor światła" + }, + "adapt_brightness": { + "description": "Czy adaptować jasność swiatła. 🌞" + } + } + } } } From e86744c6a3ad93b1e411c9b506b5b9e7cbca2db6 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sat, 19 Aug 2023 08:43:34 +0200 Subject: [PATCH 0670/1077] Translated using Weblate (Dutch) Currently translated at 68.6% (105 of 153 strings) Co-authored-by: Bas Rutjes Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/nl.json | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json index 2e2e64aa..2a64b971 100644 --- a/custom_components/adaptive_lighting/translations/nl.json +++ b/custom_components/adaptive_lighting/translations/nl.json @@ -4,7 +4,7 @@ "step": { "user": { "title": "Kies een naam voor de adaptieve verlichting integratie", - "description": "Kies een naam voor deze integratie. U kunt verschillende integratie van Adaptieve verlichting uitvoeren, elk van deze kan meerdere lichten bevatten!", + "description": "Elk exemplaar kan meerdere lichten bevatten!", "data": { "name": "Naam" } @@ -56,7 +56,9 @@ "data_description": { "sunrise_offset": "Pas de zonsopkomsttijd aan met een positieve of negatieve offset in seconden. ⏰", "sunset_offset": "Pas de tijd van zonsondergang aan met een positieve of negatieve verschuiving in seconden. ⏰", - "interval": "Frequentie om de lampen aan te passen, in seconden. 🔄" + "interval": "Frequentie om de lampen aan te passen, in seconden. 🔄", + "sleep_transition": "Duur van de overgang in seconden, als slaapstand wordt geactiveerd. 😴", + "autoreset_control_seconds": "Herstel de handmatige bediening automatisch na een aantal seconden. Stel in op 0 om uit te schakelen." } } }, @@ -76,6 +78,36 @@ }, "sunset_offset": { "description": "Pas de tijd van zonsondergang aan met een positieve of negatieve offset in seconden. ⏰" + }, + "sleep_transition": { + "description": "Duur van de overgang in seconden, als slaapstand wordt geactiveerd. 😴" + }, + "entity_id": { + "description": "entiteit_id van de schakelaar. 📝" + }, + "transition": { + "description": "Duur van de overgang in seconden, als lampen aanpassen. 🕑" + }, + "autoreset_control_seconds": { + "description": "Herstel de handmatige bediening na een aantal seconden. Stel in op 0 om uit te schakelen." + } + } + }, + "apply": { + "fields": { + "lights": { + "description": "Een lamp (of een lijst van lampen) waarop de instellingen worden toegepast." + }, + "transition": { + "description": "Duur van de overgang in seconden, als lampen aanpassen. 🕑" + } + }, + "description": "Past de huidige Adaptive Lights instellingen toe op de lampen." + }, + "set_manual_control": { + "fields": { + "lights": { + "description": "entiteit_id(s) van de lamp(en), indien niets wordt gespecificeerd, worden alle lampen in de schakelaar geselecteerd. 💡" } } } From 2ab247243dca70fd8966d7b13fa4650a06c81613 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sat, 19 Aug 2023 08:43:34 +0200 Subject: [PATCH 0671/1077] Translated using Weblate (French) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 67.9% (104 of 153 strings) Translated using Weblate (French) Currently translated at 67.9% (104 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Loïc R Co-authored-by: Maxime Bailleul Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/fr/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/fr.json | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json index c7835639..e9bb3710 100644 --- a/custom_components/adaptive_lighting/translations/fr.json +++ b/custom_components/adaptive_lighting/translations/fr.json @@ -39,16 +39,23 @@ "sunset_time": "sunset_time : Heure (HH:MM:SS) du coucher du soleil. Si « None », utilise l'heure correspondant à votre emplacement.", "take_over_control": "take_over_control : Si quelque chose d'autre que l'éclairage adaptatif appelle « light.turn_on » alors qu'une lampe est déjà allumée, cesser d'adapter cette lampe jusqu'à ce qu'elle (ou le commutateur) soit éteinte puis rallumée.", "detect_non_ha_changes": "detect_non_ha_changes : Détecter tout changement de plus de 10 % appliqué aux lampes (même en dehors de HA). Nécessite que « take_over_control » soit activé. (Appelle « homeassistant.update_entity » tous les « interval » !)", - "transition": "transition : Durée de la transition (en secondes) des changements appliqués aux lampes." + "transition": "transition : Durée de la transition (en secondes) des changements appliqués aux lampes.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quand on allume les lumières au départ. Si le paramètre est < < vrai > > , AL s ' adapte uniquement si l ' on invoque < < light.turn_on > > sans préciser la couleur ou la luminosité. ❌ Ceci, par exemple, empêche l'adaptation lors de l'activation d'une scène. Si `false`, AL s'adapte indépendamment de la présence de couleur ou de luminosité dans le `service_data' initial. Besoins `take_over_control` activé. 🕵∫ " }, "data_description": { "interval": "Fréquence d'adaptation des lumières, en secondes. 🔄", - "sleep_brightness": "Pourcentage de luminosité des lumières en mode sommeil. 😴" + "sleep_brightness": "Pourcentage de luminosité des lumières en mode sommeil. 😴", + "autoreset_control_seconds": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲∫", + "sunset_offset": "Réglez le temps de coucher avec un décalage positif ou négatif en quelques secondes. ⏰", + "brightness_mode": "Mode de luminosité à utiliser. Les valeurs possibles sont < < par défaut > > , < < linéaire > > et < < parois > > , < < par défaut > > , et < < par coup > > , < < par défaut > > et par > par > . 📈", + "send_split_delay": "Délai (ms) entre \"separate_turn_on_commands\" pour les lumières qui ne supportent pas la commande de luminosité et le réglage de couleur en même temps. ⏲", + "sleep_color_temp": "Température de couleur en mode sommeil (utilisée lorsque `sleep_rgb_or_color_temp` est `color_temp`) en Kelvin. 😴", + "sunrise_offset": "Ajuster l'heure du lever de soleil avec un décalage positif ou négatif en secondes. ⏰" } } }, "error": { - "option_error": "Option non valide", + "option_error": "Option invalide", "entity_missing": "Une lumière sélectionnée n’a pas été trouvée" } }, @@ -57,6 +64,39 @@ "fields": { "sleep_brightness": { "description": "Pourcentage de luminosité des lumières en mode sommeil. 😴" + }, + "only_once": { + "description": "Adapter les lumières seulement quand elles sont allumées (\"vrai\") ou quel que soit leur état (\"faux\")." + }, + "sunrise_offset": { + "description": "Ajustez l'heure de lever de soleil avec un décalage positif ou négatif en secondes. ⏰" + }, + "max_color_temp": { + "description": "Température de couleur la plus froide en Kelvin. assemblage" + }, + "send_split_delay": { + "description": "Délai (ms) entre \"separate_turn_on_commands\" pour les lumières qui ne supportent pas la commande de luminosité et le réglage de couleur en même temps. ⏲" + }, + "detect_non_ha_changes": { + "description": "Détecte et arrête les adaptations pour un changement d'état autre que \"light.turn_on\". Nécessite \"take_over_control\" activé. 🕵️ Attention: ⚠️Certaines lumières pourraient faussement indiquer un état \"on\", ce qui pourrait donner lieu à des allumages inattendus. Désactivez cette fonctionnalité si vous rencontrez ce problème." + }, + "autoreset_control_seconds": { + "description": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲∫" + }, + "sunset_offset": { + "description": "Ajustez l'heure de coucher de soleil avec un décalage positif ou négatif en secondes. ⏰" + }, + "sleep_color_temp": { + "description": "Température de couleur en mode sommeil (utilisé lorsque \"sleep_rgb_or_color_temp\" est défini sur \"color_temp\") en Kelvin. 😴" + } + }, + "description": "Changez les réglages que vous souhaitez dans le commutateur. Toutes les options ici sont les mêmes que dans le flux de configuration." + }, + "apply": { + "description": "Applique les réglages d'éclairage adaptatif actuels aux lumières.", + "fields": { + "lights": { + "description": "Une lumière (ou une liste de lumières) pour appliquer les réglages. personnalisation" } } } From a4f435f662da7d635968df69ba409679c3229d71 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sun, 20 Aug 2023 17:52:05 +0200 Subject: [PATCH 0672/1077] Translated using Weblate (Polish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 85.6% (131 of 153 strings) Co-authored-by: Łukasz Marek Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pl/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/pl.json | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json index be3b5763..6bbd2523 100644 --- a/custom_components/adaptive_lighting/translations/pl.json +++ b/custom_components/adaptive_lighting/translations/pl.json @@ -24,7 +24,7 @@ "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (sekund)", "sleep_transition": "sleep_transition: When 'sleep_state' changes. (sekund)", "interval": "interval: Time between switch updates. (sekund)", - "max_brightness": "max_brightness: Maksymajna josność (w procentach). 💡", + "max_brightness": "max_brightness: Maksymalna jasność (w procentach). 💡", "max_color_temp": "max_color_temp: Najzimniejsza temperatura barwowa (w Kelwinach). ❄️", "min_brightness": "min_brightness: Minimalna jasność (w procentach). 💡", "min_color_temp": "min_color_temp: Najcieplejsza temperatura barwowa (w Kelwinach). 🔥", @@ -43,7 +43,7 @@ "transition_until_sleep": "transition_until_sleep: Gdy włączone, Adaptacyjne oświetlenie będzie traktowało ustawienia spania jako minimalne i przejdzie do nich po zachodzie słońca. 🌙", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Gdy włączone `true` to Adaptacyjne oświetlenie włączy adaptacje tylko jeżeli `light.turn_on` jest wywołane bez konkretnego koloru lub jasności. ❌🌈 To ustawienie zapobiega między innymi adaptacji gdy aktywowana jest scena. Gdy wyłączone `false`, Adaptacyjne oświetlenie włączy adaptacje niezależnie czy `service_data zawiera kolor lub jasność`. Potrzebuje włączonej opcji `take_over_control`. 🕵️ ", "skip_redundant_commands": "skip_redundant_commands: Pomiń wysyłanie polecenia adaptacji jeżeli stan światła jest taki sam jak docelowy stan adaptacji. Minimalizuje to ruch sieciowy oraz w niektórych przysadkach poprawia szybkość działania. 📉Wyłącz jeżeli faktyczny stan światła się nie pokrywa z tym który widnieje w Home Assistant.", - "include_config_in_attributes": "include_config_in_attributes: Gdy włącone `true` pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant. 📝", + "include_config_in_attributes": "include_config_in_attributes: Gdy włączone `true` pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant. 📝", "intercept": "intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`aby błyskawicznie dostosować kolor i jasność . 🏎️ Wyłącz dla świateł które nie akceptują wywołania `light.turn_on` zawierającego kolor i jasność.", "multi_light_intercept": "multi_light_intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`, które dotyczą wielu świateł. ➗⚠️ Może to powodować rozdzielenie pojedynczego wywołania `light.turn_on` na wiele wywołań, na przykład gdy światła są przypisane do rożnych instancji. Wymaga włączonej opcji `intercept`." }, @@ -96,6 +96,52 @@ }, "adapt_brightness": { "description": "Czy adaptować jasność swiatła. 🌞" + }, + "prefer_rgb_color": { + "description": "Czy w miarę możliwości preferować regulację kolorów RGB zamiast temperatury barwowej światła. 🌈" + }, + "turn_on_lights": { + "description": "Czy włączyć światła, które są aktualnie wyłączone? 🔆" + } + } + }, + "set_manual_control": { + "description": "Zaznacza czy światło jest 'recznie sterowane'.", + "fields": { + "entity_id": { + "description": "`entity_id` encji przełącznika, w której należy odznaczyć flagę `ręczne sterowanie`. 📝" + }, + "lights": { + "description": "`entity_id` świateł, dla których należy odznaczyć flagę `ręczne sterowanie`.💡Gdy lista będzie pusta wszystkie światła będą brane pod uwagę." + }, + "manual_control": { + "description": "Dodaj (\"true\") albo usuń (\"false\") światło z listy \"ręczne sterowanie\". 🔒" + } + } + }, + "change_switch_settings": { + "description": "Zmienia dowolny parametr w przełączniku. Wszystkie opcje są takie same jak w konfiguracji.", + "fields": { + "entity_id": { + "description": "ID encji przełacznika. 📝" + }, + "use_defaults": { + "description": "Jak mają się zmienić ustawienia, które nie są wyszczególnione w tym wywołaniu. Opcje: \"current\" (domyślne, pozostawia obecne ustawienia), \"factory\" (przywraca ustawienia z dokumentacji), albo \"configuration\" (przywraca wartości z konfiguracji przelacznika). ⚙️" + }, + "include_config_in_attributes": { + "description": "Gdy włączone `true` pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant." + }, + "sleep_transition": { + "description": "Długość tranzycji gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴" + }, + "max_brightness": { + "description": "Maksymalna jasność (w procentach). 💡" + }, + "turn_on_lights": { + "description": "Czy włączyć światła, które są aktualnie wyłączone? 🔆" + }, + "initial_transition": { + "description": "Długość pierwszej tranzycji gdy światło zostanie włączone z `off` na `on`(w sekundach). ⏲️" } } } From badf53427c1786a44a5fc52208e76fe72f9f5d3b Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 29 Aug 2023 04:20:26 +0200 Subject: [PATCH 0673/1077] Translated using Weblate (Catalan) Currently translated at 60.1% (92 of 153 strings) Added translation using Weblate (Catalan) Co-authored-by: Gerard Rubio Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ca/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/ca.json | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/ca.json diff --git a/custom_components/adaptive_lighting/translations/ca.json b/custom_components/adaptive_lighting/translations/ca.json new file mode 100644 index 00000000..11e6d43d --- /dev/null +++ b/custom_components/adaptive_lighting/translations/ca.json @@ -0,0 +1,77 @@ +{ + "title": "Il·luminació Adaptativa", + "options": { + "step": { + "init": { + "data_description": { + "initial_transition": "Durada de la primera transició quan els llums s'encenen de `off` a `on` en segons. ⏲️", + "sunset_offset": "Ajusta l'hora de la posta del sol amb una compensació positiva o negativa en segons. ⏰", + "send_split_delay": "Retard (ms) entre `separate_turn_on_commands` per als llums que no admeten la configuració simultània de brillantor i color. ⏲️", + "sunrise_offset": "Ajusta l'hora de sortida del sol amb una compensació positiva o negativa en segons. ⏰", + "autoreset_control_seconds": "Restableix automàticament el control manual al cap d'uns segons. Posar a 0 per desactivar. ⏲️", + "brightness_mode": "Mode de brillantor a utilitzar. Els valors possibles són `default`, \"linear\" i \"tanh\" (utilitza `brightness_mode_time_dark` i `brightness_mode_time_light`). 📈", + "sleep_color_temp": "Temperatura de color en mode nit (s'utilitza quan `sleep_rgb_or_color_temp` és `color_temp`) en Kelvin. 😴", + "sleep_brightness": "Percentatge de brillantor de les llums en mode nit. 😴" + }, + "title": "Opcions Il·luminació Adaptativa", + "data": { + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quan s'encenen les llums inicialment. Si el valor és `true`, AL adapta només si s'ha cridat `light.turn_on` sense especificar color o brillantor. ❌🌈 Això impedeix l'adaptació quan s'activa una escena. Si el valor és `false`, AL adapta independentment de la presencia de color o brillantor en les dades inicials `service_data`. Necessita `take_over_control` habilitat. 🕵️ ", + "detect_non_ha_changes": "detect_non_ha_changes: Detecta i atura les adaptacions per als canvis d'estat diferents a `light.turn_on`. Necessita `take_over_control` habilitat. 🕵️ Precaució: ⚠️ Alguns llums poden indicar falsament un estat \"encès\", cosa que podria provocar que els llums s'encenguessin inesperadament. Desactiva aquesta funció si trobes aquests problemes." + }, + "description": "Configura un component d'Adaptive Lighting. Els noms de les opcions s'alineen amb la configuració de YAML. Si has definit aquesta entrada a YAML, aquí no apareixerà cap opció. Per veure gràfics interactius que demostren efectes de paràmetres, visita [aquesta aplicació web] (https://basnijholt.github.io/adaptive-lighting). Per a més detalls, pots veure la [documentació oficial] (https://github.com/basnijholt/adaptive-lighting#readme)." + } + } + }, + "services": { + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Ajustar les llums només quan s'encenguin (`true`) o ajustar contínuament(`false`). 🔄" + }, + "sleep_color_temp": { + "description": "Temperatura de color en mode nit (s'utilitza quan `sleep_rgb_or_color_temp` és `color_temp`) en Kelvin. 😴" + }, + "sunrise_offset": { + "description": "Ajusta l'hora de sortida del sol amb una compensació positiva o negativa en segons. ⏰" + }, + "sunset_offset": { + "description": "Ajusta l'hora de la posta del sol amb una compensació positiva o negativa en segons. ⏰" + }, + "autoreset_control_seconds": { + "description": "Restableix automàticament el control manual al cap d'uns segons. Posar a 0 per desactivar. ⏲️" + }, + "sleep_brightness": { + "description": "Percentatge de brillantor de les llums en mode nit. 😴" + }, + "max_color_temp": { + "description": "Temperatura de color més freda en Kelvin. ❄️" + }, + "send_split_delay": { + "description": "Retard (ms) entre `separate_turn_on_commands` per als llums que no admeten la configuració simultània de brillantor i color. ⏲️" + }, + "detect_non_ha_changes": { + "description": "Detecta i atura les adaptacions per als canvis d'estat diferents a `light.turn_on`. Necessita `take_over_control` habilitat. 🕵️ Precaució: ⚠️ Alguns llums poden indicar falsament un estat \"encès\", cosa que podria provocar que els llums s'encenguessin inesperadament. Desactiva aquesta funció si trobes aquests problemes." + }, + "take_over_control": { + "description": "Desactiva Adaptive Lighting si una altra font crida `light.turn_on` mentre els llums estan encesos i adaptats. Tingues en compte que això crida `homeassistant.update_entity` cada `interval`! 🔒" + } + }, + "description": "Canvia les opcions de configuració que vulguis al commutador. Totes les opcions d'aquí són les mateixes que en el flux de configuració." + }, + "apply": { + "fields": { + "lights": { + "description": "Una llum (o una llista de llums) a la qual aplicar la configuració. 💡" + } + }, + "description": "Aplica la configuració actual d'Adaptive Lighting a les llums." + } + }, + "config": { + "step": { + "user": { + "title": "Tria un nom per a la instància d'Adaptive Lighting" + } + } + } +} From 8b0616f184c2254c8e468e87b16cdf4ae298d614 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 29 Aug 2023 04:20:26 +0200 Subject: [PATCH 0674/1077] Translated using Weblate (Italian) Currently translated at 56.2% (86 of 153 strings) Co-authored-by: Enrico Gambini Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/it/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/it.json | 101 ++++++++++-------- 1 file changed, 57 insertions(+), 44 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/it.json b/custom_components/adaptive_lighting/translations/it.json index c1ae6163..c1dccbbe 100644 --- a/custom_components/adaptive_lighting/translations/it.json +++ b/custom_components/adaptive_lighting/translations/it.json @@ -1,52 +1,65 @@ { - "title": "Illuminazione Adattiva", - "config": { - "step": { - "user": { - "title": "Scegli un nome per l'istanza di Illuminazione Adattiva", - "description": "Scegli un nome per questa istanza. Puoi eseguire più istanze di Illuminazione adattiva, ognuna delle quali può contenere più luci!", - "data": { - "name": "Nome" - } + "title": "Illuminazione Adattiva", + "config": { + "step": { + "user": { + "title": "Scegli un nome per l'istanza di Illuminazione Adattiva", + "description": "Scegli un nome per questa istanza. Puoi eseguire più istanze di Illuminazione adattiva, ognuna delle quali può contenere più luci!", + "data": { + "name": "Nome" } - }, - "abort": { - "already_configured": "Questo dispositivo è già stato configurato" } }, - "options": { - "step": { - "init": { - "title": "Opzioni Illuminazione Adattiva", - "description": "Tutte le opzioni per il componente Illuminazione Adattiva. I nomi delle opzioni corrispondono con le impostazioni YAML. Non sono mostrate opzioni se hai la voce adaptive-lighting definita nella tua configurazione YAML.", - "data": { - "lights": "luci", - "initial_transition": "initial_transition: Quando le luci vengono accese (off -> on). (secondi)", - "sleep_transition": "sleep_transition: Quando 'sleep_state' cambia. (secondi)", - "interval": "interval: Tempo tra i cambiamenti dello switch. (secondi)", - "max_brightness": "max_brightness: Luminosità massima delle luci durante un ciclo. (%)", - "max_color_temp": "max_color_temp: Gradazione più fredda del ciclo di temperatura del colore. (Kelvin)", - "min_brightness": "min_brightness: Luminosità minima delle luci durante un ciclo. (%)", - "min_color_temp": "min_color_temp: Gradazione più calda del ciclo di temperatura del colore. (Kelvin)", - "only_once": "only_once: Adatta le luci solo quando vengono accese.", - "prefer_rgb_color": "prefer_rgb_color: Usa 'rgb_color' al posto di 'color_temp' quando possibile.", - "separate_turn_on_commands": "separate_turn_on_commands: Separa i comandi per ogni attributo (color, brightness, etc.) in 'light.turn_on' (richiesto per alcune luci).", - "sleep_brightness": "sleep_brightness: Impostazione della luminosità per la modalità notturna. (%)", - "sleep_color_temp": "sleep_color_temp: Impostazione della temperatura colore per la modalità notturna. (Kelvin)", - "sunrise_offset": "sunrise_offset: Imposta quanto anticipare(-) o ritardare(+) l'alba nel ciclo (+/- secondi)", - "sunrise_time": "sunrise_time: Imposta manualmente l'ora dell'alba, se 'None', usa l'ora effettiva dell'alba alla tua posizione (HH:MM:SS)", - "sunset_offset": "sunset_offset: Imposta quanto anticipare(-) o ritardare(+) il tramonto nel ciclo (+/- secondi)", - "sunset_time": "sunset_time: Imposta manualmente l'ora del tramonto, se 'None', usa l'ora effettiva del tramonto alla tua posizione (HH:MM:SS)", - "take_over_control": "take_over_control: Se viene chiamato il servizio 'lights.turn_on' (non da Illuminazione Adattiva) quando una luce è già accesa, interrompi l'adattamento della luce finquando essa o l'interruttore non vengono riaccesi (off -> on.)", - "detect_non_ha_changes": "detect_non_ha_changes: rileva tutti i cambiamenti >10% applicati alle luci (anche fuori da HA), richiede che 'take_over_control' sia abilitato (chiama 'homeassistant.update_entity' ad ogni 'intervallo'!)", - "transition": "Tempo di transizione quando viene applicata una modifica alle luci (secondi)", - "adapt_delay": "Tempo di attesa tra l'accensione della luce, e Illuminazione Adattiva che applica le modifiche allo stato della luce. Potrebbe evitare sfarfallii." - } + "abort": { + "already_configured": "Questo dispositivo è già stato configurato" + } + }, + "options": { + "step": { + "init": { + "title": "Opzioni Illuminazione Adattiva", + "description": "Tutte le opzioni per il componente Illuminazione Adattiva. I nomi delle opzioni corrispondono con le impostazioni YAML. Non sono mostrate opzioni se hai la voce adaptive-lighting definita nella tua configurazione YAML.", + "data": { + "lights": "luci", + "initial_transition": "initial_transition: Quando le luci vengono accese (off -> on). (secondi)", + "sleep_transition": "sleep_transition: Quando 'sleep_state' cambia. (secondi)", + "interval": "interval: Tempo tra i cambiamenti dello switch. (secondi)", + "max_brightness": "max_brightness: Luminosità massima delle luci durante un ciclo. (%)", + "max_color_temp": "max_color_temp: Gradazione più fredda del ciclo di temperatura del colore. (Kelvin)", + "min_brightness": "min_brightness: Luminosità minima delle luci durante un ciclo. (%)", + "min_color_temp": "min_color_temp: Gradazione più calda del ciclo di temperatura del colore. (Kelvin)", + "only_once": "only_once: Adatta le luci solo quando vengono accese.", + "prefer_rgb_color": "prefer_rgb_color: Usa 'rgb_color' al posto di 'color_temp' quando possibile.", + "separate_turn_on_commands": "separate_turn_on_commands: Separa i comandi per ogni attributo (color, brightness, etc.) in 'light.turn_on' (richiesto per alcune luci).", + "sleep_brightness": "sleep_brightness: Impostazione della luminosità per la modalità notturna. (%)", + "sleep_color_temp": "sleep_color_temp: Impostazione della temperatura colore per la modalità notturna. (Kelvin)", + "sunrise_offset": "sunrise_offset: Imposta quanto anticipare(-) o ritardare(+) l'alba nel ciclo (+/- secondi)", + "sunrise_time": "sunrise_time: Imposta manualmente l'ora dell'alba, se 'None', usa l'ora effettiva dell'alba alla tua posizione (HH:MM:SS)", + "sunset_offset": "sunset_offset: Imposta quanto anticipare(-) o ritardare(+) il tramonto nel ciclo (+/- secondi)", + "sunset_time": "sunset_time: Imposta manualmente l'ora del tramonto, se 'None', usa l'ora effettiva del tramonto alla tua posizione (HH:MM:SS)", + "take_over_control": "take_over_control: Se viene chiamato il servizio 'lights.turn_on' (non da Illuminazione Adattiva) quando una luce è già accesa, interrompi l'adattamento della luce finquando essa o l'interruttore non vengono riaccesi (off -> on.)", + "detect_non_ha_changes": "detect_non_ha_changes: rileva tutti i cambiamenti >10% applicati alle luci (anche fuori da HA), richiede che 'take_over_control' sia abilitato (chiama 'homeassistant.update_entity' ad ogni 'intervallo'!)", + "transition": "Tempo di transizione quando viene applicata una modifica alle luci (secondi)", + "adapt_delay": "Tempo di attesa tra l'accensione della luce, e Illuminazione Adattiva che applica le modifiche allo stato della luce. Potrebbe evitare sfarfallii.", + "transition_until_sleep": "transition_until_sleep: Quando abilitato, Adaptive Lighting tratterà le impostazioni di sleep come valori minimi, facendo la transizione a questi valori dopo il tramonto. 🌙" + }, + "data_description": { + "sunrise_offset": "Regola il momento dell'alba con un offset positivo o negativo. ⏰" + } + } + }, + "error": { + "option_error": "Opzione non valida", + "entity_missing": "Non è stata trovata una luce selezionata" + } + }, + "services": { + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Adatta le luci solo nel momento in cui vengono accese ('true') o continua ad adattarle ('false'). 🔄" } - }, - "error": { - "option_error": "Opzione non valida", - "entity_missing": "Non è stata trovata una luce selezionata" } } } +} From 08a61767ad66961eebfd971bf3393d3830380873 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 29 Aug 2023 04:20:26 +0200 Subject: [PATCH 0675/1077] Translated using Weblate (Polish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Łukasz Marek Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pl/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/pl.json | 70 ++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json index 6bbd2523..6ad9be29 100644 --- a/custom_components/adaptive_lighting/translations/pl.json +++ b/custom_components/adaptive_lighting/translations/pl.json @@ -37,7 +37,7 @@ "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- sekund)", "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", - "take_over_control": "take_over_control: Wyłącz adaptowanie oświetlenia gdy inna usługa wywoła `light.turn_on` gdy oświetlenie jest już włączone. Zauważ że to wywołuje `homeassistant.update_entity` co`interval`! 🔒", + "take_over_control": "take_over_control: Wyłącz adaptowanie oświetlenia gdy inna usługa wywoła `light.turn_on` gdy oświetlenie jest już włączone. Zauważ, że to wywołuje `homeassistant.update_entity` co`interval`! 🔒", "detect_non_ha_changes": "detect_non_ha_changes: Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan 'on', co może powodować nieoczekiwane włączenia światła. Wyłącz to ustawienie jeżeli doświadczasz takich objawów.", "transition": "Transition time when applying a change to the lights (sekund)", "transition_until_sleep": "transition_until_sleep: Gdy włączone, Adaptacyjne oświetlenie będzie traktowało ustawienia spania jako minimalne i przejdzie do nich po zachodzie słońca. 🌙", @@ -89,7 +89,7 @@ "description": "Światło(albo lista świateł), do których mają być zastosowane ustawieniam 💡" }, "transition": { - "description": "Czas tranzycji przy zmianie ustawień (w sekundach). 🕑" + "description": "Długość tranzycji do nowego stanu w sekundach. 🕑" }, "adapt_color": { "description": "Czy adaptować kolor światła" @@ -142,6 +142,72 @@ }, "initial_transition": { "description": "Długość pierwszej tranzycji gdy światło zostanie włączone z `off` na `on`(w sekundach). ⏲️" + }, + "min_sunset_time": { + "description": "Ustaw czas najwcześniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na opóźnienie zachodu słońca. 🌇" + }, + "take_over_control": { + "description": "Wyłącz adaptowanie oświetlenia gdy inna usługa wywoła `light.turn_on` gdy oświetlenie jest już włączone. Zauważ, że to wywołuje `homeassistant.update_entity` co`interval`! 🔒" + }, + "transition": { + "description": "Długość tranzycji do nowego stanu w sekundach. 🕑" + }, + "autoreset_control_seconds": { + "description": "Czas po którym manualna kontrola zostanie wyłączona (w sekundach). Ustaw 0 aby wyłączyć. ⏲️" + }, + "adapt_delay": { + "description": "Czas (w sekundach) pomiędzy włączeniem światła a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Morze pomóc zredukować migotanie. ⏲️" + }, + "max_color_temp": { + "description": "Najzimniejsza temperatura barwowa (w Kelwinach). ❄️" + }, + "min_brightness": { + "description": "Minimalna jasność (w procentach). 💡" + }, + "min_color_temp": { + "description": "Najcieplejsza temperatura barwowa (w Kelwinach). 🔥" + }, + "only_once": { + "description": "Adaptuj światło tylko podczas włączenia (`true`) lub adaptuj cały czas (`false`). 🔄" + }, + "prefer_rgb_color": { + "description": "Czy w miarę możliwości preferować regulację kolorów RGB zamiast temperatury barwowej światła.. 🌈" + }, + "separate_turn_on_commands": { + "description": "Używaj oddzielnych wywołań `light.turn_on` dla koloru i jasności, wymagane dla niektórych typów świateł. 🔀" + }, + "send_split_delay": { + "description": "Opóźnienie (w ms) pomiędzy `separate_turn_on_commands` dla świateł, które nie akceptują jednoczesnego ustawiania jasności i koloru. ⏲️" + }, + "sleep_brightness": { + "description": "Jasność świateł w trybie spania (w procentach). 😴" + }, + "sleep_rgb_or_color_temp": { + "description": "Użyj `\"rgb_color\"` albo `\"color_temp\"`trybie spania. 🌙" + }, + "sleep_rgb_color": { + "description": "Kolor RGB w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈" + }, + "sleep_color_temp": { + "description": "Temperatura barwowa w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `color_temp`) (w Kelwinach). 😴" + }, + "sunrise_offset": { + "description": "Dostosuj czas wschodu słońca - przesunięcie o +/- sekund. ⏰" + }, + "sunrise_time": { + "description": "Ustaw stały czas wschodu słońca (HH:MM:SS). 🌅" + }, + "sunset_offset": { + "description": "Dostosuj czas zachodu słońca - przesunięcie o +/- sekund. ⏰" + }, + "sunset_time": { + "description": "Ustaw stały czas zachodu słońca (HH:MM:SS). 🌇" + }, + "max_sunrise_time": { + "description": "Ustaw czas najpóźniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na przyspieszenie wschodu słońca. 🌅" + }, + "detect_non_ha_changes": { + "description": "Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan 'on', co może powodować nieoczekiwane włączenia światła. Wyłącz to ustawienie jeżeli doświadczasz takich objawów." } } } From 27d0c47d0a8bfd71cc3f02b3ad4aecfa1782f3ee Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 29 Aug 2023 04:20:26 +0200 Subject: [PATCH 0676/1077] Translated using Weblate (Dutch) Currently translated at 69.2% (106 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Kees Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/ Translation: Adaptive Lighting/Adaptive Lighting --- custom_components/adaptive_lighting/translations/nl.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json index 2a64b971..81030b6a 100644 --- a/custom_components/adaptive_lighting/translations/nl.json +++ b/custom_components/adaptive_lighting/translations/nl.json @@ -58,7 +58,8 @@ "sunset_offset": "Pas de tijd van zonsondergang aan met een positieve of negatieve verschuiving in seconden. ⏰", "interval": "Frequentie om de lampen aan te passen, in seconden. 🔄", "sleep_transition": "Duur van de overgang in seconden, als slaapstand wordt geactiveerd. 😴", - "autoreset_control_seconds": "Herstel de handmatige bediening automatisch na een aantal seconden. Stel in op 0 om uit te schakelen." + "autoreset_control_seconds": "Herstel de handmatige bediening automatisch na een aantal seconden. Stel in op 0 om uit te schakelen.", + "sleep_brightness": "Helderheidspercentage van lampen in slaapstand. 😴" } } }, From dd0b45f62330c07e77cf0b7a1b3914db9a406a6a Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 29 Aug 2023 04:20:26 +0200 Subject: [PATCH 0677/1077] Translated using Weblate (German) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (153 of 153 strings) Translated using Weblate (German) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Mirco Hülsemann Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/de/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/de.json | 278 ++++++++++++++---- 1 file changed, 216 insertions(+), 62 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json index 154d1fe0..acd2de22 100644 --- a/custom_components/adaptive_lighting/translations/de.json +++ b/custom_components/adaptive_lighting/translations/de.json @@ -1,67 +1,221 @@ { - "title": "Adaptive Lighting", - "config": { - "step": { - "user": { - "title": "Benenne das Adaptive Lighting", - "description": "Jede Instanz kann mehrere Licht Entitäten beinhalten", - "data": { - "name": "Name" - } - } - }, - "abort": { - "already_configured": "Gerät ist bereits konfiguriert!" + "title": "Adaptive Beleuchtung", + "config": { + "step": { + "user": { + "title": "Benenne die Adaptive Beleuchtung Instanz", + "description": "Jede Instanz kann mehrere Licht Entitäten beinhalten", + "data": { + "name": "Name" } + } }, - "options": { - "step": { - "init": { - "title": "Adaptive Lighting Optionen", - "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde.", - "data": { - "lights": "Lichter", - "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt", - "sleep_transition": "sleep_transition: Wenn 'sleep_state' sich ändert. (Sekunden)", - "interval": "interval, Zeit zwischen Updates des Switches", - "max_brightness": "max_brightness, maximale Helligkeit in %", - "max_color_temp": "max_color_temp, maximale Farbtemperatur in Kelvin", - "min_brightness": "min_brightness, minimale Helligkeit in %", - "min_color_temp": "min_color_temp, minimale Farbtemperatur in Kelvin", - "only_once": "only_once, passe die Lichter nur beim Einschalten an", - "prefer_rgb_color": "prefer_rgb_color, nutze 'rgb_color' vor 'color_temp', wenn möglich", - "separate_turn_on_commands": "separate_turn_on_commands, für jedes Attribut (Farbe, Helligkeit usw.) in 'light.turn_on' werden separate Befehle gesendet. Wird für manche Leuchtmittel benötigt.", - "send_split_delay": "send_split_delay: Wartezeit zwischen dem Senden der Befehle (Millisekunden), wenn separate_turn_on_commands genutzt wird. Kann helfen, wenn die Leuchtmittel die separaten Befehle nicht korrekt umsetzen.", - "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, nutze 'rgb_color' oder 'color_temp'", - "sleep_rgb_color": "sleep_rgb_color, in RGB", - "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperatur in Kelvin", - "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- Sekunden", - "sunrise_time": "sunrise_time, Sonnenaufgangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenaufgangs an deiner Position verwendet)", - "max_sunrise_time": "max_sunrise_time: Manuelles Überschreiben der max. sunrise_time. Falls 'None', wird die tatsächliche sunrise_time an deiner Position verwendet (HH:MM:SS)", - "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- Sekunden", - "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)", - "min_sunset_time": "min_sunset_time: Manuelles Überschreiben der min. sunset_time. Falls 'None', wird die tatsächliche sunset_time an deiner Position verwendet (HH:MM:SS)", - "take_over_control": "take_over_control, wenn irgendetwas während ein Licht an ist außer Adaptive Lighting den Service 'light.turn_on' aufruft, stoppe die Anpassung des Lichtes (oder des Schalters) bis dieser wieder von off -> on geschaltet wird.", - "detect_non_ha_changes": "detect_non_ha_changes, entdeckt alle Änderungen über 10% am Licht (auch außerhalb von HA gemacht), 'take_over_control' muss aktiviert sein (ruft 'homeassistant.update_entity' jede 'interval' auf!)", - "transition": "transition, Wechselzeit in Sekunden", - "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.", - "skip_redundant_commands": "Keine Adaptierungsbefehle senden, deren erwünschter Status schon dem bekanntes Status von Lichtern entspricht. Minimiert die Netzwerkbelastung und verbessert die Adaptierung in manchen Situationen. Deaktiviert lassen falls der pysikalische Status der Lichter und der erkannte Status in HA nicht synchron bleiben." - } - } - }, - "error": { - "option_error": "Fehlerhafte Option", - "entity_missing": "Ein ausgewähltes Licht wurde nicht gefunden" - } - }, - "services": { - "apply": { - "fields": { - "lights": { - "description": "Ein Licht (oder eine Lichtliste), um die Einstellungen anzuwenden. RECHT" - } - } - } + "abort": { + "already_configured": "Dieses Gerät ist bereits konfiguriert." } + }, + "options": { + "step": { + "init": { + "title": "Optionen für Adaptive Beleuchtung", + "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde. Interaktive Diagramme zur Veranschaulichung der Auswirkungen der Parameter finden Sie unter [dieser Webanwendung](https://basnijholt.github.io/adaptive-lighting). Weitere Details finden Sie in der [offiziellen Dokumentation](https://github.com/basnijholt/adaptive-lighting#readme).", + "data": { + "lights": "Lichter", + "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt", + "sleep_transition": "sleep_transition: Wenn 'sleep_state' sich ändert. (Sekunden)", + "interval": "interval, Zeit zwischen Updates des Switches", + "max_brightness": "max_brightness: Maximale Helligkeit in Prozent. 💡", + "max_color_temp": "max_color_temp: Kälteste Farbtemperatur in Kelvin. ❄️", + "min_brightness": "min_brightness: Minimale Helligkeit in Prozent. 💡", + "min_color_temp": "min_color_temp: Wärmste Farbtemperatur in Kelvin. 🔥", + "only_once": "only_once: Lichter nur einmalig anpassen, wenn sie eingeschaltet werden (`true`) oder sie immer wieder anpassen (`false`). 🔄", + "prefer_rgb_color": "prefer_rgb_color: Ob die RGB-Farbanpassung der Farbtemperaturanpassung vorgezogen werden soll, wenn möglich. 🌈", + "separate_turn_on_commands": "separate_turn_on_commands: Verwende getrennte `light.turn_on`-Aufrufe für Farbe und Helligkeit, erforderlich für einige Lichttypen. 🔀", + "send_split_delay": "send_split_delay: Wartezeit zwischen dem Senden der Befehle (Millisekunden), wenn separate_turn_on_commands genutzt wird. Kann helfen, wenn die Leuchtmittel die separaten Befehle nicht korrekt umsetzen.", + "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, nutze 'rgb_color' oder 'color_temp'", + "sleep_rgb_color": "sleep_rgb_color, in RGB", + "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperatur in Kelvin", + "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- Sekunden", + "sunrise_time": "sunrise_time, Sonnenaufgangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenaufgangs an deiner Position verwendet)", + "max_sunrise_time": "max_sunrise_time: Manuelles Überschreiben der max. sunrise_time. Falls 'None', wird die tatsächliche sunrise_time an deiner Position verwendet (HH:MM:SS)", + "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- Sekunden", + "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)", + "min_sunset_time": "min_sunset_time: Manuelles Überschreiben der min. sunset_time. Falls 'None', wird die tatsächliche sunset_time an deiner Position verwendet (HH:MM:SS)", + "take_over_control": "take_over_control: Deaktiviere die adaptive Beleuchtung, wenn eine andere Quelle `light.turn_on` aufruft, während die Beleuchtung eingeschaltet ist und angepasst wird. Beachte, dass dies `homeassistant.update_entity` jedes `Intervall` aufruft! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Erkennt und stoppt Anpassungen für nicht-`light.turn_on`-Zustandsänderungen. Benötigt, dass `take_over_control` aktiviert ist. 🕵️ Vorsicht: ⚠️ Einige Lichter können fälschlicherweise einen 'an'-Zustand anzeigen, was dazu führen kann, dass Lichter unerwartet eingeschaltet werden. Deaktiviere diese Funktion, wenn solche Probleme auftreten.", + "transition": "transition, Wechselzeit in Sekunden", + "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.", + "skip_redundant_commands": "skip_redundant_commands: Überspringt das Senden von Anpassungsbefehlen, deren Zielzustand bereits mit dem bekannten Zustand der Leuchte übereinstimmt. Minimiert den Netzwerkverkehr und verbessert die Anpassungsreaktion in einigen Situationen. 📉 Deaktivieren, falls der physikalische Zustand der Lichter nicht mehr mit dem Zustand in HA übereinstimmt.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Beim ersten Einschalten des Lichts. Wenn auf `true` gesetzt, passt AL das Licht nur an, wenn `light.turn_on` ohne eine Angabe von Farbe oder Helligkeit aufgerufen wird. ❌🌈 Dies verhindert z.B. die Anpassung durch AL beim Aktivieren einer Szene. Wenn auf \"false\" gesetzt, passt AL das licht unabhängig von der Angabe von Farbe oder Helligkeit in den ursprünglichen `service_data` an. Benötigt das `take_over_control` aktiviert ist. 🕵️ ", + "include_config_in_attributes": "include_config_in_attributes: Alle Optionen als Attribute auf dem Schalter im Home Assistant anzeigen, wenn auf `true` gesetzt. 📝", + "multi_light_intercept": "multi_light_intercept: Abfangen und Anpassen von `light.turn_on`-Aufrufen, die auf mehrere Lichter aufrufen. ➗⚠️ Dies kann dazu führen, dass ein einzelner `light.turn_on`-Aufruf in mehrere Aufrufe aufgeteilt wird, z.B. wenn Lichter in verschiedenen Schaltern sind. Erfordert, dass `intercept` aktiviert ist.", + "transition_until_sleep": "transition_until_sleep: Wenn diese Option aktiviert ist, behandelt die adaptive Beleuchtung die Schlafeinstellungen als Minimum und geht nach Sonnenuntergang zu diesen Werten über. 🌙", + "intercept": "intercept: Abfangen und Anpassen von `light.turn_on`-Aufrufen, um eine sofortige Anpassung von Farbe und Helligkeit zu ermöglichen. 🏎️ Deaktivieren für Leuchten, die `light.turn_on` mit Farbe und Helligkeit nicht unterstützen." + }, + "data_description": { + "sunrise_offset": "Anpassung der Sonnenaufgangszeit mit positivem oder negativem Versatz in Sekunden. ⏰", + "sunset_offset": "Anpassung der Sonnenuntergangszeit mit positivem oder negativem Versatz in Sekunden. ⏰", + "brightness_mode": "Helligkeitsmodus, der verwendet werden soll. Mögliche Werte sind `default`, `linear` und `tanh` (verwendet `brightness_mode_time_dark` und `brightness_mode_time_light`). 📈", + "send_split_delay": "Verzögerung (ms) zwischen `separate_turn_on_commands` für Leuchten, die keine gleichzeitige Einstellung von Helligkeit und Farbe unterstützen. ⏲️", + "transition": "Dauer des Übergangs beim Lichtwechsel in Sekunden. 🕑", + "sleep_rgb_color": "RGB-Farbe im Schlafmodus (wird verwendet, wenn `sleep_rgb_or_color_temp` `rgb_color` ist). 🌈", + "sunset_time": "Stelle eine feste Zeit (HH:MM:SS) für den Sonnenuntergang ein. 🌇", + "max_sunrise_time": "Lege die späteste virtuelle Sonnenaufgangszeit (HH:MM:SS) fest, um einen früheren Sonnenaufgang zu ermöglichen. 🌅", + "min_sunset_time": "Lege die früheste virtuelle Sonnenuntergangszeit (HH:MM:SS) fest, um spätere Sonnenuntergänge zu ermöglichen. 🌇", + "max_sunset_time": "Lege die späteste virtuelle Sonnenuntergangszeit (HH:MM:SS) fest, um frühere Sonnenuntergänge zu ermöglichen. 🌇", + "adapt_delay": "Wartezeit (Sekunden) zwischen dem Einschalten des Lichts und der Anwendung der adaptiven Beleuchtung. Könnte helfen, Flackern zu vermeiden. ⏲️", + "min_sunrise_time": "Lege die früheste virtuelle Sonnenaufgangszeit (HH:MM:SS) fest, um einen späteren Sonnenaufgang zu ermöglichen. 🌅", + "interval": "Häufigkeit der Lichtanpassung in Sekunden. 🔄", + "brightness_mode_time_light": "(Wird ignoriert, wenn `brightness_mode='default'`) Die Dauer in Sekunden, um die Helligkeit nach/vor Sonnenaufgang/Sonnenuntergang hoch/runter zu fahren. 📈📉.", + "brightness_mode_time_dark": "(Wird ignoriert, wenn `brightness_mode='default'`) Die Dauer in Sekunden, um die Helligkeit vor/nach Sonnenaufgang/Sonnenuntergang hoch/runter zu fahren. 📈📉.", + "autoreset_control_seconds": "Setzt die manuelle Steuerung nach einer bestimmten Anzahl von Sekunden automatisch zurück. Zum Deaktivieren auf 0 setzen. ⏲️", + "sleep_brightness": "Helligkeit der Lichter im Schlafmodus in Prozent. 😴", + "sleep_color_temp": "Farbtemperatur im Schlafmodus in Kelvin (wird verwendet, wenn `sleep_rgb_or_color_temp` `color_temp` ist) . 😴", + "initial_transition": "Dauer des ersten Übergangs, wenn das Licht von `off` auf `on` schaltet, in Sekunden. ⏲️", + "sleep_rgb_or_color_temp": "Verwende entweder `rgb_color` oder `color_temp` im Schlafmodus. 🌙", + "sleep_transition": "Dauer des Übergangs, wenn der \"Schlafmodus\" umgeschaltet wird, in Sekunden. 😴", + "sunrise_time": "Stelle eine feste Zeit (HH:MM:SS) für den Sonnenaufgang ein. 🌅" + } + } + }, + "error": { + "option_error": "Ungültige Option", + "entity_missing": "Ein oder mehrere ausgewählte Lichter fehlen in Home Assistant" + } + }, + "services": { + "apply": { + "fields": { + "lights": { + "description": "Eine Leuchte (oder eine Liste von Leuchten), auf die die Einstellungen angewendet werden sollen. 💡" + }, + "entity_id": { + "description": "Die `entity_id` des Schalters mit den zu übernehmenden Einstellungen. 📝" + }, + "prefer_rgb_color": { + "description": "Ob die RGB-Farbanpassung der Farbtemperaturanpassung vorgezogen werden soll, wenn möglich. 🌈" + }, + "transition": { + "description": "Dauer des Übergangs beim Lichtwechsel in Sekunden. 🕑" + }, + "adapt_brightness": { + "description": "Ob die Helligkeit des Lichts angepasst werden soll. 🌞" + }, + "adapt_color": { + "description": "Ob die Farbtemperatur des Lichts angepasst werden soll. 🌈" + }, + "turn_on_lights": { + "description": "Ob Lichter eingeschaltet werden sollen, die derzeit ausgeschaltet sind. 🔆" + } + }, + "description": "Wendet die aktuellen Einstellungen der adaptiven Beleuchtung auf die Lichter an." + }, + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Lichter nur einmalig anpassen, wenn sie eingeschaltet werden (`true`) oder sie immer wieder anpassen (`false`). 🔄" + }, + "detect_non_ha_changes": { + "description": "Erkennt und stoppt Anpassungen für nicht-`light.turn_on`-Zustandsänderungen. Benötigt, dass `take_over_control` aktiviert ist. 🕵️ Vorsicht: ⚠️ Einige Lichter können fälschlicherweise einen 'an'-Zustand anzeigen, was dazu führen kann, dass Lichter unerwartet eingeschaltet werden. Deaktiviere diese Funktion, wenn solche Probleme auftreten." + }, + "min_brightness": { + "description": "Minimale Helligkeit in Prozent. 💡" + }, + "sunset_time": { + "description": "Stelle eine feste Zeit (HH:MM:SS) für den Sonnenuntergang ein. 🌇" + }, + "use_defaults": { + "description": "Setzt die nicht in diesem Service-Aufruf angegebenen Standardwerte. Optionen: `current` (Standard, behält die aktuellen Werte bei), `factory` (setzt auf die in der Dokumentation angegebenen Standardwerte zurück) oder `configuration` (setzt auf die Standardwerte der Switch-Konfiguration zurück). ⚙️" + }, + "max_sunrise_time": { + "description": "Lege die späteste virtuelle Sonnenaufgangszeit (HH:MM:SS) fest, um einen früheren Sonnenaufgang zu ermöglichen. 🌅" + }, + "include_config_in_attributes": { + "description": "Zeige alle Optionen als Attribute auf dem Schalter im Home Assistant, wenn auf `true` gesetzt. 📝" + }, + "min_sunset_time": { + "description": "Lege die früheste virtuelle Sonnenuntergangszeit (HH:MM:SS) fest, um spätere Sonnenuntergänge zu ermöglichen. 🌇" + }, + "sunrise_offset": { + "description": "Anpassung der Sonnenaufgangszeit mit positivem oder negativem Versatz in Sekunden. ⏰" + }, + "sunset_offset": { + "description": "Anpassung der Sonnenuntergangszeit mit positivem oder negativem Versatz in Sekunden. ⏰" + }, + "take_over_control": { + "description": "Deaktiviere die adaptive Beleuchtung, wenn eine andere Quelle `light.turn_on` aufruft, während die Beleuchtung eingeschaltet ist und angepasst wird. Beachte, dass dies `homeassistant.update_entity` jedes `Intervall` aufruft! 🔒" + }, + "max_brightness": { + "description": "Maximale Helligkeit in Prozent. 💡" + }, + "separate_turn_on_commands": { + "description": "Verwende getrennte `light.turn_on`-Aufrufe für Farbe und Helligkeit, erforderlich für einige Lichttypen. 🔀" + }, + "sleep_brightness": { + "description": "Helligkeit der Lichter im Schlafmodus in Prozent. 😴" + }, + "sleep_rgb_color": { + "description": "RGB-Farbe im Schlafmodus (wird verwendet, wenn `sleep_rgb_or_color_temp` `rgb_color` ist). 🌈" + }, + "sleep_rgb_or_color_temp": { + "description": "Verwende entweder `rgb_color` oder `color_temp` im Schlafmodus. 🌙" + }, + "sleep_color_temp": { + "description": "Farbtemperatur im Schlafmodus in Kelvin (wird verwendet, wenn `sleep_rgb_or_color_temp` `color_temp` ist) . 😴" + }, + "sunrise_time": { + "description": "Stelle eine feste Zeit (HH:MM:SS) für den Sonnenaufgang ein. 🌅" + }, + "transition": { + "description": "Dauer des Übergangs beim Lichtwechsel in Sekunden. 🕑" + }, + "adapt_delay": { + "description": "Wartezeit (Sekunden) zwischen dem Einschalten des Lichts und der Anwendung der adaptiven Beleuchtung. Könnte helfen, Flackern zu vermeiden. ⏲️" + }, + "autoreset_control_seconds": { + "description": "Setzt die manuelle Steuerung nach einer bestimmten Anzahl von Sekunden automatisch zurück. Zum Deaktivieren auf 0 setzen. ⏲️" + }, + "entity_id": { + "description": "Entity ID des Schalters. 📝" + }, + "turn_on_lights": { + "description": "Ob Lichter eingeschaltet werden sollen, die derzeit ausgeschaltet sind. 🔆" + }, + "initial_transition": { + "description": "Dauer des ersten Übergangs, wenn das Licht von `off` auf `on` schaltet, in Sekunden. ⏲️" + }, + "sleep_transition": { + "description": "Dauer des Übergangs, wenn der \"Schlafmodus\" umgeschaltet wird, in Sekunden. 😴" + }, + "max_color_temp": { + "description": "Kälteste Farbtemperatur in Kelvin. ❄️" + }, + "min_color_temp": { + "description": "Wärmste Farbtemperatur in Kelvin. 🔥" + }, + "send_split_delay": { + "description": "Verzögerung (ms) zwischen `separate_turn_on_commands` für Leuchten, die keine gleichzeitige Einstellung von Helligkeit und Farbe unterstützen. ⏲️" + }, + "prefer_rgb_color": { + "description": "Ob die RGB-Farbanpassung der Farbtemperaturanpassung vorgezogen werden soll, wenn möglich. 🌈" + } + }, + "description": "Ändern Sie alle Einstellungen, die Sie im Schalter wünschen. Alle Optionen hier sind die gleichen wie im config Flow." + }, + "set_manual_control": { + "fields": { + "manual_control": { + "description": "Ob das Licht aus der Liste `manual_control` hinzugefügt (`true`) oder entfernt (`false`) werden soll. 🔒" + }, + "lights": { + "description": "entity_id(s) der Lichter, wenn nichts angegeben wird, werden alle Lichter des Schalters ausgewählt. 💡" + }, + "entity_id": { + "description": "Die `entity_id` des Schalters, in dem das Licht als `manuell gesteuert` (un)markiert werden soll. 📝" + } + }, + "description": "Markiere, ob ein Licht \"manuell gesteuert\" ist." + } + } } From e8cbc6a8d2b07df5ccf60067a20c5f416824b192 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 29 Aug 2023 04:20:26 +0200 Subject: [PATCH 0678/1077] Translated using Weblate (Spanish) Currently translated at 100.0% (153 of 153 strings) Translated using Weblate (Spanish) Currently translated at 47.0% (72 of 153 strings) Added translation using Weblate (Spanish) Co-authored-by: Fernando Belaza Co-authored-by: Gerard Rubio Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/es/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/es.json | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/es.json diff --git a/custom_components/adaptive_lighting/translations/es.json b/custom_components/adaptive_lighting/translations/es.json new file mode 100644 index 00000000..c90b842e --- /dev/null +++ b/custom_components/adaptive_lighting/translations/es.json @@ -0,0 +1,202 @@ +{ + "title": "Iluminación Adaptativa", + "options": { + "step": { + "init": { + "title": "Configuración de la Iluminación Adaptativa", + "data_description": { + "sunset_offset": "Define la hora de la puesta del sol con un desfase (positivo o negativo) en segundos. ⏰", + "sunrise_offset": "Define la hora de la salida del sol con un desfase (positivo o negativo) en segundos. ⏰", + "sleep_color_temp": "Temperatura de color en modo noche (usado cuando`sleep_rgb_or_color_temp` es `color_temp`) en grados Kelvin. 😴", + "send_split_delay": "Retraso (ms) entre `separate_turn_on_commands` para luces que no soportan ajustes simultáneos de brillo y color. ⏲️", + "transition": "Duración de la transición cuando las luces se adaptan, en segundos. ⏲️", + "initial_transition": "Duración de la primera transición cuando las luces pasan de `off` a `on` en segundos. ⏲️", + "sleep_transition": "Duración de la transición cuando el \"modo noche\" se activa o desactiva, en segundos. 😴", + "max_sunrise_time": "Define el amanecer virtual más tardío (HH:MM:SS), permitiendo amaneceres más tempranos. 🌅", + "max_sunset_time": "Define el atardecer virtual más tardío (HH:MM:SS), permitiendo atardeceres más tempranos. 🌇", + "sleep_brightness": "Porcentaje de brillo de las luces en el modo noche. 😴", + "interval": "Frecuencia de adaptación de las luces, en segundos. 🔄", + "sleep_rgb_color": "Color RGB en modo noche(usado cuando `sleep_rgb_or_color_temp` es \"rgb_color\"). 🌈", + "sunrise_time": "Fijar una hora (HH:MM:SS) para el amanecer. 🌅", + "min_sunrise_time": "Define el amanecer virtual más temprano (HH:MM:SS), permitiendo amaneceres más tardíos. 🌅", + "sleep_rgb_or_color_temp": "Usar el modo`\"rgb_color\"` o `\"color_temp\"` en el modo noche. 🌙", + "autoreset_control_seconds": "Resetear automáticamente el control manual tras `X` segundos. Poner a 0 para deshabilitar.", + "brightness_mode": "Modo de brillo a usar. Valores posibles son: `default`, `linear` y `tanh` (usa`brightness_mode_time_dark` y `brightness_mode_time_light`). 📈", + "brightness_mode_time_light": "(Ignorado si `brightness_mode='default'`) La duración, en segundos, de la transición del brillo después/antes del amanecer/atardecer. 📈📉.", + "brightness_mode_time_dark": "(Ignorado si `brightness_mode='default'`) La duración, en segundos, de la transición del brillo después/antes del amanecer/atardecer. 📈📉.", + "sunset_time": "Fijar una hora (HH:MM:SS) para el atardecer. 🌇", + "min_sunset_time": "Define el atardecer virtual más temprano (HH:MM:SS), permitiendo atardeceres más tardíos. 🌇", + "adapt_delay": "Tiempo de espera (segundos) entre el encendido de la luz y Adaptive Lighting aplicando cambios. Puede ayudar a evitar parpadeos. ⏲️" + }, + "data": { + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Al encender las luces. Si el valor es `true`, AL adapta sólo si se llama `light.turn_on` sin especificar el color o brillo. ❌🌈 Esto, por ejemplo, previene la adaptación al activar una escena. Si el valor es `false`, AL adapta independientemente de la presencia de color o brillo en `service_data` inicial. Necesita `take_over_control` habilitado. 🕵️ ", + "detect_non_ha_changes": "detect_non_ha_changes: Detecta e interrumpe adaptaciones para cambios de estado no `light.turn_on`. Necesita `take_over_control` habilitado. 🕵️ Precaución: ⚠️ Algunas luces pueden indicar de forma errónea un estado 'on', que puede resultar en luces que se enciendan de forma no esperada. Deshabilita esta función si encuentras dichos problemas.", + "intercept": "intercept: Intercepta y adapta llamadas a `light.turn_on` para habilitar adaptaciones instantáneas de color y brillo. 🏎️ Deshabilitar para luces que no soporten `light.turn_on` con color y brillo.", + "min_color_temp": "min_color_temp: Temperatura de color más cálida en grados Kelvin. 🔥", + "lights": "lights: Lista de entity_ids de luces a controlar (puede estar vacía). 🌟", + "max_brightness": "max_brightness: Porcentaje máximo de brillo. 💡", + "max_color_temp": "max_color_temp: Temperatura de color más fría en grados Kelvin. ❄️", + "min_brightness": "min_brightness: Porcentaje mínimo de brillo. 💡", + "prefer_rgb_color": "prefer_rgb_color: Preferir ajustar el color RGB a la temperatura de color cuando sea posible. 🌈", + "transition_until_sleep": "transition_until_sleep: Cuando habilitado, Adaptive Lighting tratará los ajustes del modo noche como los valores mínimos, transicionando a esos valores tras la puesta del sol. 🌙", + "include_config_in_attributes": "include_config_in_attributes: Muestra todas las opciones como atributes del interruptor en Home Assistant cuando sea `true`. 📝", + "multi_light_intercept": "multi_light_intercept: Intercepta y adapta llamadas a `light.turn_on` que apuntan a múltiples luces. ➗⚠️ Esto puede resultar en dividir una única llamada a `light.turn_on` en múltiples llamadas, por ejemplo, cuando las luces están vinculadas a distintos interruptores. Requiere que `intercept` esté habilitado.", + "only_once": "only_once: Adapta las luces sólo cuando se encienden (`true`) o mantener adaptadas (`false`). 🔄", + "separate_turn_on_commands": "separate_turn_on_commands: Usar llamadas independientes a `light.turn_on` para color y brillo, necesario para ciertos tipos de luces. 🔀", + "skip_redundant_commands": "skip_redundant_commands: Evitar mandar comandos de adaptación a luces cuyo estado ya sea el esperado. Reduce tráfico en la red y mejora la respuesta de la adaptación en ciertas situaciones. 📉Deshabilitar si el estado real de las luces se desincroniza con el estado registrado en Home Assistant.", + "take_over_control": "take_over_control: Deshabilita Adaptive Lighting si otra fuente llama`light.turn_on` mientras las luces están encendidas y adaptándose. Cuidado, esto llama`homeassistant.update_entity` cada `interval`! 🔒" + }, + "description": "Configura un componente Adaptive Lighting. Los nombres de las opciones se asemejan a las disponibles en la configuración YAML. Si has definido esta entrada en YAML, no aparecerá ninguna opción aquí. Para gráficos interactivos que demuestran los efectos de los parámetros, visita [esta web app](https://basnijholt.github.io/adaptive-lighting). Para más detalles, ver la [documentación oficial](https://github.com/basnijholt/adaptive-lighting#readme)." + } + }, + "error": { + "option_error": "Opción no válida", + "entity_missing": "Una o más entidades de luz seleccionadas no se encuentran en Home Assistant" + } + }, + "services": { + "apply": { + "fields": { + "lights": { + "description": "Luz (o listado de luces) sobre las que aplicar la configuración. 💡" + }, + "transition": { + "description": "Duración de la transición cuando las luces se adaptan, en segundos. ⏲️" + }, + "adapt_color": { + "description": "Adaptar (o no) el color en luces que lo soporten. 🌈" + }, + "prefer_rgb_color": { + "description": "Preferir ajustes de color RGB a temperatura de color cuando sea posible. 🌈" + }, + "turn_on_lights": { + "description": "Encender (o no) luces que estén apagadas. 🔆" + }, + "entity_id": { + "description": "El `entity_id` del interruptor con los ajustes a aplicar. 📝" + }, + "adapt_brightness": { + "description": "Adaptar (o no) el brillo de la luz. 🌞" + } + }, + "description": "Aplica la configuración actual de Adaptive Lighting a las luces." + }, + "change_switch_settings": { + "fields": { + "sunrise_offset": { + "description": "Define la hora de la salida del sol con un desfase (positivo o negativo) en segundos. ⏰" + }, + "sunset_offset": { + "description": "Define la hora de la puesta del sol con un desfase (positivo o negativo) en segundos. ⏰" + }, + "only_once": { + "description": "Adaptar las luces solo cuando se enciendan (`true`) o hacerlo siempre (`false`). 🔄" + }, + "sleep_color_temp": { + "description": "Temperatura de color en modo noche (usado cuando`sleep_rgb_or_color_temp` es `color_temp`) en grados Kelvin. 😴" + }, + "max_color_temp": { + "description": "Temperatura de color más fría en grados Kelvin. ❄️" + }, + "send_split_delay": { + "description": "Retraso (ms) entre `separate_turn_on_commands` para luces que no soportan ajustes simultáneos de brillo y color. ⏲️" + }, + "detect_non_ha_changes": { + "description": "detect_non_ha_changes: Detecta e interrumpe adaptaciones para cambios de estado no `light.turn_on`. Necesita `take_over_control` habilitado. 🕵️ Precaución: ⚠️ Algunas luces pueden indicar de forma errónea un estado 'on', que puede resultar en luces que se enciendan de forma no esperada. Deshabilita esta función si encuentras dichos problemas." + }, + "take_over_control": { + "description": "Deshabilita Adaptive Lighting si otra fuente llama `light.turn_on` mientras las luces se estan adaptando. Cuidado porque esto llama a `homeassistant.update_entity` cada`interval`! 🔒" + }, + "initial_transition": { + "description": "Duración de la primera transición cuando las luces pasan de `off` a `on` en segundos. ⏲️" + }, + "transition": { + "description": "Duración de la transición cuando las luces se adaptan, en segundos. ⏲️" + }, + "entity_id": { + "description": "ID de la entidad del interruptor. 📝" + }, + "sleep_transition": { + "description": "Duración de la transición cuando el \"modo noche\" se activa o desactiva, en segundos. 😴" + }, + "min_brightness": { + "description": "Porcentaje mínimo de brillo. 💡" + }, + "include_config_in_attributes": { + "description": "Muestra todas las opciones como atributos del interruptor en Home Assistant cuando sea `true`. 📝" + }, + "prefer_rgb_color": { + "description": "Preferir ajustes de color RGB a temperatura de color cuando sea posible. 🌈" + }, + "turn_on_lights": { + "description": "Encender (o no) luces que estén apagadas. 🔆" + }, + "max_brightness": { + "description": "Porcentaje máximo de brillo. 💡" + }, + "use_defaults": { + "description": "Define los valores por defecto no especificados en la llamada al servicio. Opciones: \"current\" (predeterminado, mantiene los valores actuales), \"factory\" (resetea a los valores documentados predeterminados), o \"configuration\" (revierte a los valores por defecto del interruptor). ⚙️" + }, + "separate_turn_on_commands": { + "description": "Usar llamadas independientes a`light.turn_on` para color y brillo, necesario para cierto tipo de luces. 🔀" + }, + "min_color_temp": { + "description": "Temperatura de color más cálida en grados Kelvin. 🔥" + }, + "autoreset_control_seconds": { + "description": "Resetear automáticamente el control manual tras `x` segundos. Poner a 0 para deshabilitar. ⏲️" + }, + "sleep_brightness": { + "description": "Porcentaje de brillo en el modo noche. 😴" + }, + "sleep_rgb_color": { + "description": "Color RGB en modo noche(usado cuando `sleep_rgb_or_color_temp` es \"rgb_color\"). 🌈" + }, + "sunrise_time": { + "description": "Fijar una hora (HH:MM:SS) para el amanecer. 🌅" + }, + "sunset_time": { + "description": "Fijar una hora (HH:MM:SS) para el atardecer. 🌇" + }, + "min_sunset_time": { + "description": "Define el atardecer virtual más temprano (HH:MM:SS), permitiendo atardeceres más tardíos. 🌇" + }, + "max_sunrise_time": { + "description": "Define el amanecer virtual más tardío (HH:MM:SS), permitiendo amaneceres más tempranos. 🌅" + }, + "sleep_rgb_or_color_temp": { + "description": "Usar el modo`\"rgb_color\"` o `\"color_temp\"` en el modo noche. 🌙" + }, + "adapt_delay": { + "description": "Tiempo de espera (segundos) entre el encendido de la luz y Adaptive Lighting aplicando cambios. Puede ayudar a evitar parpadeos. ⏲️" + } + }, + "description": "Modifica cualquier ajuste que quieras en el interruptor. Todas las opciones aquí presentes son idénticas a la configuración del flujo." + }, + "set_manual_control": { + "fields": { + "manual_control": { + "description": "Añadir (\"true\") o quitar (\"false\") la luz de la lista de `manual_control`. 🔒" + }, + "lights": { + "description": "entity_id(s) de las luces, si no se especifica, se seleccionaran todas las luces vinculadas al interruptor. 💡" + }, + "entity_id": { + "description": "El `entity_id` del interruptor en el cual (des)marcar la luz como estando `manually controlled`. 📝" + } + }, + "description": "Señala si una luz está 'controlada manualmente'." + } + }, + "config": { + "step": { + "user": { + "title": "Elige un nombre para la instancia de Adaptive Lighting", + "description": "Cada instancia puede contener múltiples luces!" + } + }, + "abort": { + "already_configured": "El dispositivo ya está configurado" + } + } +} From e332b225f8ed71ad4f9b4244b2e27d5105521ddf Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 29 Aug 2023 02:24:22 +0000 Subject: [PATCH 0679/1077] docs: update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f8386453..cb261607 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-51-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-53-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -529,6 +529,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 5144fab7a3c94d7ed566291a9809a72626aee7d2 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 29 Aug 2023 02:24:23 +0000 Subject: [PATCH 0680/1077] docs: update .all-contributorsrc --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index ae9114e2..da89bbe4 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -485,6 +485,15 @@ "contributions": [ "translation" ] + }, + { + "login": "enrico1036", + "name": "Enrico Gambini", + "avatar_url": "https://avatars.githubusercontent.com/u/9280405?v=4", + "profile": "https://github.com/enrico1036", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, From f7f3c8fd25bf52606a5495d8dace4a4734ca95d0 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 28 Aug 2023 19:25:06 -0700 Subject: [PATCH 0681/1077] docs: add MirCore as a contributor for translation (#777) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index da89bbe4..cc77e287 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -494,6 +494,15 @@ "contributions": [ "translation" ] + }, + { + "login": "MirCore", + "name": "MirCore", + "avatar_url": "https://avatars.githubusercontent.com/u/9919366?v=4", + "profile": "https://github.com/MirCore", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index cb261607..72b318ba 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-53-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-54-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -530,6 +530,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From f3044efca9fda5076a50035cb62ffc2536bb780d Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 28 Aug 2023 19:25:32 -0700 Subject: [PATCH 0682/1077] docs: add KilFer as a contributor for translation (#778) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index cc77e287..5bd4f818 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -503,6 +503,15 @@ "contributions": [ "translation" ] + }, + { + "login": "KilFer", + "name": "Fernando Belaza", + "avatar_url": "https://avatars.githubusercontent.com/u/290854?v=4", + "profile": "http://kilfer.es/", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 72b318ba..792540fe 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-54-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-55-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -531,6 +531,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 570c5cde2bf63dbdcfd778a55df8c6ac3c36a878 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 20:42:06 -0700 Subject: [PATCH 0683/1077] docs: add wilcomir as a contributor for translation (#781) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 5bd4f818..3ff26034 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -512,6 +512,15 @@ "contributions": [ "translation" ] + }, + { + "login": "wilcomir", + "name": "Vladimir Cravero", + "avatar_url": "https://avatars.githubusercontent.com/u/795981?v=4", + "profile": "https://github.com/wilcomir", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 792540fe..3144f254 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-55-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-56-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -532,6 +532,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 7a19ac7ace0c6b004532281542f89c7dd6a63ed8 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 20:47:07 -0700 Subject: [PATCH 0684/1077] docs: add letroll as a contributor for translation (#782) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 3ff26034..1d853b07 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -521,6 +521,15 @@ "contributions": [ "translation" ] + }, + { + "login": "letroll", + "name": "Julien Quiévreux", + "avatar_url": "https://avatars.githubusercontent.com/u/255774?v=4", + "profile": "http://www.latavernedutroll.fr", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 3144f254..909e36d2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-56-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-57-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -534,6 +534,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + + + From c0d28ab4c572a2283a79f79e42c239467ed81bc6 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 30 Aug 2023 20:54:18 +0200 Subject: [PATCH 0685/1077] Translated using Weblate (Italian) Currently translated at 58.1% (89 of 153 strings) Co-authored-by: Vladimir Cravero Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/it/ Translation: Adaptive Lighting/Adaptive Lighting --- custom_components/adaptive_lighting/translations/it.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/it.json b/custom_components/adaptive_lighting/translations/it.json index c1dccbbe..3f8c19e8 100644 --- a/custom_components/adaptive_lighting/translations/it.json +++ b/custom_components/adaptive_lighting/translations/it.json @@ -41,10 +41,12 @@ "detect_non_ha_changes": "detect_non_ha_changes: rileva tutti i cambiamenti >10% applicati alle luci (anche fuori da HA), richiede che 'take_over_control' sia abilitato (chiama 'homeassistant.update_entity' ad ogni 'intervallo'!)", "transition": "Tempo di transizione quando viene applicata una modifica alle luci (secondi)", "adapt_delay": "Tempo di attesa tra l'accensione della luce, e Illuminazione Adattiva che applica le modifiche allo stato della luce. Potrebbe evitare sfarfallii.", - "transition_until_sleep": "transition_until_sleep: Quando abilitato, Adaptive Lighting tratterà le impostazioni di sleep come valori minimi, facendo la transizione a questi valori dopo il tramonto. 🌙" + "transition_until_sleep": "transition_until_sleep: Quando abilitato, Adaptive Lighting tratterà le impostazioni di sleep come valori minimi, facendo la transizione a questi valori dopo il tramonto. 🌙", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quando accendi le luci la prima volta. Se impostato su `true`, AL adatta solo se `light.turn_on è invocato senza specificare il colore o la luminosità. ❌🌈 Questo, per esempio, previene l'adattamento quando si attiva una scena. Se `false`, AL adatta indipendentemente dalla presenza di colore o luminosità nei `service_data` iniziali. Necessita che `take_over_control` sia abilitato. 🕵️ " }, "data_description": { - "sunrise_offset": "Regola il momento dell'alba con un offset positivo o negativo. ⏰" + "sunrise_offset": "Regola il momento dell'alba con un offset positivo o negativo. ⏰", + "sunset_offset": "Modifica l'orario del tramonto con un offset in secondi positivo o negativo. ⏰" } } }, @@ -58,6 +60,9 @@ "fields": { "only_once": { "description": "Adatta le luci solo nel momento in cui vengono accese ('true') o continua ad adattarle ('false'). 🔄" + }, + "sunrise_offset": { + "description": "Modifica l'orario dell'alba con un offset in secondi positivo o negativo." } } } From f085c47b6376fbb454eac285301715ea116dff23 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 30 Aug 2023 20:54:18 +0200 Subject: [PATCH 0686/1077] Translated using Weblate (French) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 73.2% (112 of 153 strings) Co-authored-by: Julien Quiévreux Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/fr/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/fr.json | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json index e9bb3710..26728081 100644 --- a/custom_components/adaptive_lighting/translations/fr.json +++ b/custom_components/adaptive_lighting/translations/fr.json @@ -50,7 +50,10 @@ "brightness_mode": "Mode de luminosité à utiliser. Les valeurs possibles sont < < par défaut > > , < < linéaire > > et < < parois > > , < < par défaut > > , et < < par coup > > , < < par défaut > > et par > par > . 📈", "send_split_delay": "Délai (ms) entre \"separate_turn_on_commands\" pour les lumières qui ne supportent pas la commande de luminosité et le réglage de couleur en même temps. ⏲", "sleep_color_temp": "Température de couleur en mode sommeil (utilisée lorsque `sleep_rgb_or_color_temp` est `color_temp`) en Kelvin. 😴", - "sunrise_offset": "Ajuster l'heure du lever de soleil avec un décalage positif ou négatif en secondes. ⏰" + "sunrise_offset": "Ajuster l'heure du lever de soleil avec un décalage positif ou négatif en secondes. ⏰", + "transition": "Durée de la transition des changements lumineux, en secondes. 🕑", + "initial_transition": "Durée de la première transition des lampes passant de `off` à `on` en secondes. ⏲️", + "sleep_transition": "Durée de la transition quand le \"mode sommeil\" est déclenché en secondes. 😴" } } }, @@ -88,6 +91,18 @@ }, "sleep_color_temp": { "description": "Température de couleur en mode sommeil (utilisé lorsque \"sleep_rgb_or_color_temp\" est défini sur \"color_temp\") en Kelvin. 😴" + }, + "entity_id": { + "description": "ID de l'Entité de l'interrupteur. 📝" + }, + "initial_transition": { + "description": "Durée de la première transition des lampes passant de `off` à `on` en secondes. ⏲️" + }, + "transition": { + "description": "Durée de la transition des changements lumineux, en secondes. 🕑" + }, + "sleep_transition": { + "description": "Durée de la transition quand le \"mode sommeil\" est déclenché en secondes. 😴" } }, "description": "Changez les réglages que vous souhaitez dans le commutateur. Toutes les options ici sont les mêmes que dans le flux de configuration." @@ -97,6 +112,9 @@ "fields": { "lights": { "description": "Une lumière (ou une liste de lumières) pour appliquer les réglages. personnalisation" + }, + "transition": { + "description": "Durée de la transition des changements lumineux, en secondes. 🕑" } } } From c978c43f38dd18f6c98029fb8930833af999ff09 Mon Sep 17 00:00:00 2001 From: lightrabbit Date: Fri, 1 Sep 2023 05:57:42 +0800 Subject: [PATCH 0687/1077] Add Simplified Chinese translation (#775) * Add Simplified Chinese translation * Update zh-Hans.json Remove unexisted word and fix some titles. --- .../translations/zh-Hans.json | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/zh-Hans.json diff --git a/custom_components/adaptive_lighting/translations/zh-Hans.json b/custom_components/adaptive_lighting/translations/zh-Hans.json new file mode 100644 index 00000000..e9b206c6 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/zh-Hans.json @@ -0,0 +1,269 @@ +{ + "title": "自适应照明", + "config": { + "step": { + "user": { + "title": "为自适应照明实例选择一个名称", + "description": "每个实例可以包含多个灯光!", + "data": { + "name": "名称" + } + } + }, + "abort": { + "already_configured": "此设备已配置" + } + }, + "options": { + "step": { + "init": { + "title": "自适应照明选项", + "description": "配置自适应照明组件。选项名称与YAML设置对齐。如果在YAML中定义了此条目,则此处不会显示任何选项。有关演示参数影响的交互式图表,请访问[此Web应用程序](https://basnijholt.github.io/adaptive-lighting)。有关更多详细信息,请参阅[官方文档](https://github.com/basnijholt/adaptive-lighting#readme)。", + "data": { + "lights": "lights:要控制的灯光实体ID列表(可以为空)。🌟", + "interval": "频率(interval)", + "transition": "过渡(transition)", + "initial_transition": "初始过渡(initial_transition)", + "min_brightness": "min_brightness:最小亮度百分比。💡", + "max_brightness": "max_brightness:最大亮度百分比。💡", + "min_color_temp": "min_color_temp:最暖的色温,以开尔文为单位。🔥", + "max_color_temp": "max_color_temp:最冷的色温,以开尔文为单位。❄️", + "prefer_rgb_color": "prefer_rgb_color:在可能时是否优先使用RGB颜色调整而不是灯光色温。🌈", + "sleep_brightness": "睡眠模式亮度(sleep_brightness)", + "sleep_rgb_or_color_temp": "睡眠模式RGB或色温(sleep_rgb_or_color_temp)", + "sleep_color_temp": "睡眠模式中的色温(sleep_color_temp)", + "sleep_rgb_color": "睡眠模式中的RGB颜色(sleep_rgb_color)", + "sleep_transition": "睡眠模式过渡时间(sleep_transition)", + "transition_until_sleep": "transition_until_sleep:启用时,自适应照明将将睡眠设置视为最小值,在日落后过渡到这些值。🌙", + "sunrise_time": "日出时间(sunrise_time)", + "min_sunrise_time": "最早日出时间(min_sunrise_time)", + "max_sunrise_time": "最晚日出时间(max_sunrise_time)", + "sunrise_offset": "日出时间偏移(sunrise_offset)", + "sunset_time": "日落时间(sunset_time)", + "min_sunset_time": "最早日落时间(min_sunset_time)", + "max_sunset_time": "最晚日落时间(max_sunset_time)", + "sunset_offset": "日落时间偏移(sunset_offset)", + "brightness_mode": "亮度模式(brightness_mode)", + "brightness_mode_time_dark": "变暗时间(brightness_mode_time_dark)", + "brightness_mode_time_light": "变亮时间(brightness_mode_time_light)", + "take_over_control": "take_over_control: 如果在灯光处于开启并处于适应照明的状态时,另一个来源调用`light.turn_on`,则禁用自适应照明。请注意,这会在每个`interval`调用`homeassistant.update_entity`!🔒", + "detect_non_ha_changes": "detect_non_ha_changes: 检测非`light.turn_on`的状态更改,并停止自适应照明。需要启用`take_over_control`。🕵️ 注意:⚠️ 一些灯光可能错误地显示为“开启”状态,这可能会导致灯光意外打开。如果遇到此类问题,请禁用此功能。", + "autoreset_control_seconds": "自动重置时间(autoreset_control_seconds)", + "only_once": "only_once:仅在打开时调整灯光(`true`)或始终调整灯光(`false`)。🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on:当首次打开灯光时。如果设置为`true`,仅在没有指定颜色或亮度的情况下,AL才进行适应。❌🌈 例如,这可以防止在激活场景时进行适应。如果为`false`,则不考虑初始`service_data`中是否存在颜色或亮度,AL都会适应。需要启用`take_over_control`。🕵️ ", + "separate_turn_on_commands": "separate_turn_on_commands:为某些灯光类型需要使用单独的`light.turn_on`调用来设置颜色和亮度。🔀", + "send_split_delay": "指令发送间隔延迟(send_split_delay)", + "adapt_delay": "自适应照明延迟(adapt_delay)", + "skip_redundant_commands": "skip_redundant_commands:跳过目标状态已经等于灯光已知状态的自适应命令。在某些情况下,可以减少网络流量并提高适应响应性。📉如果物理灯光状态与HA的记录状态不同步,请禁用此功能。", + "intercept": "intercept:拦截并适应`light.turn_on`调用,以实现即时的颜色和亮度适应。🏎️ 对于不支持使用颜色和亮度进行`light.turn_on`的灯光,禁用此功能。", + "multi_light_intercept": "multi_light_intercept:拦截和适应针对多个灯光的`light.turn_on`调用。➗⚠️ 这可能会将单个`light.turn_on`调用拆分为多个调用,例如当灯光位于不同的开关中时。需要启用`intercept`。", + "include_config_in_attributes": "include_config_in_attributes:在Home Assistant中将所有选项显示为开关的属性时,设置为`true`。📝" + }, + "data_description": { + "interval": "调整灯光的频率,以秒为单位。🔄", + "transition": "灯光变化时的过渡持续时间,以秒为单位。🕑", + "initial_transition": "灯光从“关闭”到“开启”时的第一个过渡持续时间,以秒为单位。⏲️", + "sleep_brightness": "睡眠模式中的亮度百分比。😴", + "sleep_rgb_or_color_temp": "在睡眠模式中使用“rgb_color”或“color_temp”。🌙", + "sleep_color_temp": "睡眠模式中的色温(当`sleep_rgb_or_color_temp`为`color_temp`时使用),以开尔文为单位。😴", + "sleep_rgb_color": "睡眠模式中的RGB颜色(当`sleep_rgb_or_color_temp`为“rgb_color”时使用)。🌈", + "sleep_transition": "切换“睡眠模式”时的过渡持续时间,以秒为单位。😴", + "sunrise_time": "设置固定的日出时间(HH:MM:SS)。🌅", + "min_sunrise_time": "设置最早的虚拟日出时间(HH:MM:SS),允许更晚的日出。🌅", + "max_sunrise_time": "设置最晚的虚拟日出时间(HH:MM:SS),允许更早的日出。🌅", + "sunrise_offset": "以秒为单位的正负偏移调整日出时间。⏰", + "sunset_time": "设置固定的日落时间(HH:MM:SS)。🌇", + "min_sunset_time": "设置最早的虚拟日落时间(HH:MM:SS),允许更晚的日落。🌇", + "max_sunset_time": "设置最晚的虚拟日落时间(HH:MM:SS),允许更早的日落。🌇", + "sunset_offset": "以秒为单位的正负偏移调整日落时间。⏰", + "brightness_mode": "要使用的亮度模式。可能的值为`default`、`linear`和`tanh`(使用`brightness_mode_time_dark`和`brightness_mode_time_light`)。📈", + "brightness_mode_time_dark": "(如果`brightness_mode='default'`将被忽略)日出/日落之前/之后亮度逐渐增加/减少的持续时间,以秒为单位。📈📉", + "brightness_mode_time_light": "(如果`brightness_mode='default'`将被忽略)日出/日落之后/之前亮度逐渐增加/减少的持续时间,以秒为单位。📈📉。", + "autoreset_control_seconds": "在若干秒后自动重置手动控制。设置为0以禁用。⏲️", + "send_split_delay": "对于不支持同时设置亮度和颜色的灯光,`separate_turn_on_commands`之间的延迟时间(毫秒)。⏲️", + "adapt_delay": "灯光打开和自适应照明应用更改之间的等待时间(秒)。可能有助于避免闪烁。⏲️" + } + } + }, + "error": { + "option_error": "无效的选项", + "entity_missing": "一个或多个选择的灯光实体在Home Assistant中不存在" + } + }, + "services": { + "apply": { + "name": "应用", + "description": "将当前自适应照明设置应用于灯光。", + "fields": { + "entity_id": { + "description": "具有要应用设置的开关的`entity_id`。📝", + "name": "entity_id" + }, + "lights": { + "description": "要应用设置的灯光(或灯光列表)。💡", + "name": "lights" + }, + "transition": { + "description": "灯光变化时的过渡持续时间,以秒为单位。🕑", + "name": "transition" + }, + "adapt_brightness": { + "description": "是否调整灯光的亮度。🌞", + "name": "adapt_brightness" + }, + "adapt_color": { + "description": "是否在支持的灯光上调整颜色。🌈", + "name": "adapt_color" + }, + "prefer_rgb_color": { + "description": "在可能时是否优先使用RGB颜色调整而不是灯光色温。🌈", + "name": "prefer_rgb_color" + }, + "turn_on_lights": { + "description": "是否打开当前关闭的灯光。🔆", + "name": "turn_on_lights" + } + } + }, + "set_manual_control": { + "name": "设置手动控制", + "description": "标记灯光是否为'手动控制'。", + "fields": { + "entity_id": { + "description": "要在其中(取消)标记灯光为“手动控制”的开关的`entity_id`。📝", + "name": "entity_id" + }, + "lights": { + "description": "如果未指定,则为灯光的entity_id(s),如果未指定,则选择开关中的所有灯光。💡", + "name": "lights" + }, + "manual_control": { + "description": "是否将灯光从“手动控制”列表中添加(“true”)或删除(“false”)。🔒", + "name": "manual_control" + } + } + }, + "change_switch_settings": { + "name": "更改开关设置", + "description": "在开关中更改您想要的任何设置。此处的所有选项与配置流中的选项相同。", + "fields": { + "entity_id": { + "description": "开关的实体ID。📝", + "name": "entity_id" + }, + "use_defaults": { + "description": "设置未在此服务调用中指定的默认值。选项:“current”(默认值,保留当前值)、“factory”(重置为文档默认值)或“configuration”(恢复到开关配置默认值)。⚙️", + "name": "use_defaults" + }, + "include_config_in_attributes": { + "description": "在Home Assistant中将所有选项显示为开关的属性时,设置为`true`。📝", + "name": "include_config_in_attributes" + }, + "turn_on_lights": { + "description": "是否打开当前关闭的灯光。🔆", + "name": "turn_on_lights" + }, + "initial_transition": { + "description": "灯光从“关闭”到“开启”时的第一个过渡持续时间,以秒为单位。⏲️", + "name": "initial_transition" + }, + "sleep_transition": { + "description": "切换“睡眠模式”时的过渡持续时间,以秒为单位。😴", + "name": "sleep_transition" + }, + "max_brightness": { + "description": "最大亮度百分比。💡", + "name": "max_brightness" + }, + "max_color_temp": { + "description": "最低的色温,以开尔文为单位。❄️", + "name": "max_color_temp" + }, + "min_brightness": { + "description": "最小亮度百分比。💡", + "name": "min_brightness" + }, + "min_color_temp": { + "description": "最高的色温,以开尔文为单位。🔥", + "name": "min_color_temp" + }, + "only_once": { + "description": "仅在打开时调整灯光(`true`)或始终调整灯光(`false`)。🔄", + "name": "only_once" + }, + "prefer_rgb_color": { + "description": "在可能时是否优先使用RGB颜色调整而不是灯光色温。🌈", + "name": "prefer_rgb_color" + }, + "separate_turn_on_commands": { + "description": "为某些灯光类型需要使用单独的`light.turn_on`调用来设置颜色和亮度。🔀", + "name": "separate_turn_on_commands" + }, + "send_split_delay": { + "description": "对于不支持同时设置亮度和颜色的灯光,`separate_turn_on_commands`之间的延迟时间(毫秒)。⏲️", + "name": "send_split_delay" + }, + "sleep_brightness": { + "description": "睡眠模式中的亮度百分比。😴", + "name": "sleep_brightness" + }, + "sleep_rgb_or_color_temp": { + "description": "在睡眠模式中使用“rgb_color”或“color_temp”。🌙", + "name": "sleep_rgb_or_color_temp" + }, + "sleep_rgb_color": { + "description": "睡眠模式中的RGB颜色(当`sleep_rgb_or_color_temp`为“rgb_color”时使用)。🌈", + "name": "sleep_rgb_color" + }, + "sleep_color_temp": { + "description": "睡眠模式中的色温(当`sleep_rgb_or_color_temp`为`color_temp`时使用),以开尔文为单位。😴", + "name": "sleep_color_temp" + }, + "sunrise_offset": { + "description": "Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "name": "sunrise_offset" + }, + "sunrise_time": { + "description": "设置固定的日出时间(HH:MM:SS)。🌅", + "name": "sunrise_time" + }, + "sunset_offset": { + "description": "以正负偏移秒调整日落时间。⏰", + "name": "sunset_offset" + }, + "sunset_time": { + "description": "设置固定的日落时间(HH:MM:SS)。🌇", + "name": "sunset_time" + }, + "max_sunrise_time": { + "description": "设置最晚的虚拟日出时间(HH:MM:SS),允许更早的日出。🌅", + "name": "max_sunrise_time" + }, + "min_sunset_time": { + "description": "设置最早的虚拟日落时间(HH:MM:SS),允许更晚的日落。🌇", + "name": "min_sunset_time" + }, + "take_over_control": { + "description": "如果其他来源在灯光处于打开和正在适应状态时调用`light.turn_on`,则禁用自适应照明。请注意,这会每个`interval`调用`homeassistant.update_entity`!🔒", + "name": "take_over_control" + }, + "detect_non_ha_changes": { + "description": "检测并停止对非`light.turn_on`状态更改的适应。需要启用`take_over_control`。🕵️ 注意:⚠️ 一些灯光可能错误地显示为“开启”状态,这可能导致灯光意外打开。如果遇到此类问题,请禁用此功能。", + "name": "detect_non_ha_changes" + }, + "transition": { + "description": "灯光变化时的过渡持续时间,以秒为单位。🕑", + "name": "transition" + }, + "adapt_delay": { + "description": "灯光打开和自适应照明应用更改之间的等待时间(秒)。可能有助于避免闪烁。⏲️", + "name": "adapt_delay" + }, + "autoreset_control_seconds": { + "description": "在若干秒后自动重置手动控制。设置为0以禁用。⏲️", + "name": "autoreset_control_seconds" + } + } + } + } +} From c0e7eab402f1561e5ddf9cd89cf97a350b7703b7 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 31 Aug 2023 15:11:18 -0700 Subject: [PATCH 0688/1077] docs: add lightrabbit as a contributor for translation (#784) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 1d853b07..48e23abc 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -530,6 +530,15 @@ "contributions": [ "translation" ] + }, + { + "login": "lightrabbit", + "name": "lightrabbit", + "avatar_url": "https://avatars.githubusercontent.com/u/1521765?v=4", + "profile": "https://github.com/lightrabbit", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 909e36d2..6fbf2beb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-57-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-58-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -536,6 +536,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From a7791a747843c466457b894eaa53822348725fc4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 3 Sep 2023 17:28:35 -0700 Subject: [PATCH 0689/1077] Add note (#786) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6fbf2beb..38f3fc4e 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,8 @@ adaptive_lighting: `adaptive_lighting.change_switch_settings` (new in 1.7.0) Change any of the above configuration options of Adaptive Lighting (such as `sunrise_time` or `prefer_rgb_color`) with a service call directly from your script/automation. +> ⚠️ **_Note: These settings will **not** be written to your config and will be reset on restart of Home Assistant! You can see the current settings in the `switch.adaptive_lighting_XXX` attributes if `include_config_in_attributes` is enabled._** + | Service data attribute | Required | Description | | --------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `use_defaults` | ❌ | (default: `current` for current settings) Choose from `factory`, `configuration`, or `current` to reset variables not being set with this service call. `current` leaves them as they are, `configuration` resets to initial startup values, `factory` resets to default values listed in the documentation. | From 5cd060a37c2cdad3c7e266b937dc50e4b1c79711 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 12 Sep 2023 22:58:33 +0200 Subject: [PATCH 0690/1077] Translated using Weblate (Ukrainian) (#788) Currently translated at 55.5% (85 of 153 strings) Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/uk/ Translation: Adaptive Lighting/Adaptive Lighting Co-authored-by: Fujitsu Chrome --- custom_components/adaptive_lighting/translations/uk.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/uk.json b/custom_components/adaptive_lighting/translations/uk.json index 90265f2e..866cee2c 100644 --- a/custom_components/adaptive_lighting/translations/uk.json +++ b/custom_components/adaptive_lighting/translations/uk.json @@ -38,7 +38,9 @@ "sunset_time": "sunset_time: Ручний перезапис часу заходу сонця, якщо 'None', тоді використовується час заходу сонця у вашій локації (HH:MM:SS)", "take_over_control": "take_over_control: Якщо що-небудь, окрім Адаптивного освітлення, викликає 'light.turn_on', коли світло вже увімкнено, чи адаптувати освітлення допоки світло (або перемикач) перемкнеться (off -> on).", "detect_non_ha_changes": "detect_non_ha_changes: виявляти всі зміни >10% до освітлення (включаючи ті, що зроблені поза HA), вимагає, щоб 'take_over_control' був включений (виклик 'homeassistant.update_entity' кожного оновлення 'interval'!)", - "transition": "Час переходу, який застосовується до освітлення (секунди)" + "transition": "Час переходу, який застосовується до освітлення (секунди)", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Коли спочатку вмикається світло. Якщо `true`, Адаптивне Освітлення адаптується лише якщо `light.turn_on` було викликано без вказування кольору чи яскравості. ❌🌈 Це в тому числі запобігає адаптації, коли активується сцена. Якщо `false`, Адаптивне Освітлення адаптується не залежно від присутності кольору чи яскравості в першочерговому `service_data`. Потребує активації `take_over_control`. 🕵️ ", + "transition_until_sleep": "transition_until_sleep: Коли активовано, адаптивне освітлення буде ставитись до налаштування сну як мінімум, переходячи до цих значень після заходу сонця. 🌙" } } }, From 6502dae19fdf3b6710b0f62ab40240b9c253dff7 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sat, 23 Sep 2023 17:01:55 +0000 Subject: [PATCH 0691/1077] Translated using Weblate (Dutch) Currently translated at 73.8% (113 of 153 strings) Co-authored-by: Arie6414 Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/nl.json | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json index 81030b6a..d8f54bcf 100644 --- a/custom_components/adaptive_lighting/translations/nl.json +++ b/custom_components/adaptive_lighting/translations/nl.json @@ -59,7 +59,10 @@ "interval": "Frequentie om de lampen aan te passen, in seconden. 🔄", "sleep_transition": "Duur van de overgang in seconden, als slaapstand wordt geactiveerd. 😴", "autoreset_control_seconds": "Herstel de handmatige bediening automatisch na een aantal seconden. Stel in op 0 om uit te schakelen.", - "sleep_brightness": "Helderheidspercentage van lampen in slaapstand. 😴" + "sleep_brightness": "Helderheidspercentage van lampen in slaapstand. 😴", + "sleep_color_temp": "Kleurtemperatuur in slaapmodus (gebruikt wanneer `sleep_rgb_or_color_temp` `color_temp` is) in Kelvin. 😴", + "brightness_mode": "Helderheidsmodus om te gebruiken. Mogelijke waarden zijn `default`, `linear` en `tanh` (gebruikt `brightness_mode_time_dark` en `brightness_mode_time_light`). 📈", + "send_split_delay": "Vertraging (ms) tussen `separate_turn_on_commands` voor lampen die geen gelijktijdige helderheids- en kleurinstelling ondersteunen. ⏲️" } } }, @@ -91,8 +94,18 @@ }, "autoreset_control_seconds": { "description": "Herstel de handmatige bediening na een aantal seconden. Stel in op 0 om uit te schakelen." + }, + "sleep_brightness": { + "description": "Helderheidspercentage van lampen in slaapmodus. 😴" + }, + "sleep_color_temp": { + "description": "Kleurtemperatuur in slaapmodus (gebruikt wanneer `sleep_rgb_or_color_temp` `color_temp` is) in Kelvin. 😴" + }, + "max_color_temp": { + "description": "Koudste kleurtemperatuur in Kelvin. ❄️" } - } + }, + "description": "Wijzig alle gewenste instellingen in de schakelaar. Alle opties hier zijn hetzelfde als in de configuratie." }, "apply": { "fields": { From 9c72952ed0f32f19c2738bb8f57e401eb8bee9b7 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 23 Sep 2023 10:19:26 -0700 Subject: [PATCH 0692/1077] docs: add Arie6414 as a contributor for translation (#793) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 48e23abc..e1a6341c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -539,6 +539,15 @@ "contributions": [ "translation" ] + }, + { + "login": "Arie6414", + "name": "Arie6414", + "avatar_url": "https://avatars.githubusercontent.com/u/129661911?v=4", + "profile": "https://github.com/Arie6414", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 38f3fc4e..9239e0ab 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-58-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-59-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -539,6 +539,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From e08fc141cfdddca8eddbfacad65236815099020d Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 4 Oct 2023 20:10:12 +0200 Subject: [PATCH 0693/1077] Translated using Weblate (Czech) Currently translated at 54.9% (84 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Petr Vyleta Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/cs/ Translation: Adaptive Lighting/Adaptive Lighting --- custom_components/adaptive_lighting/translations/cs.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/cs.json b/custom_components/adaptive_lighting/translations/cs.json index 58f8fead..e909c0d2 100644 --- a/custom_components/adaptive_lighting/translations/cs.json +++ b/custom_components/adaptive_lighting/translations/cs.json @@ -45,7 +45,8 @@ "take_over_control": "take_over_control: Je-li volán 'light.turn_on' z jiného zdroje, než Adaptivním osvětlením, když je světlo již rozsvíceno, přestaňte toto světlo ovládat, dokud není vypnuto -> zapnuto (nebo i vypínačem).", "detect_non_ha_changes": "detect_non_ha_changes: detekuje všechny změny >10% provedených pro osvětlení (také mimo HA), vyžaduje povolení atributu 'take_over_control' (každý 'interval' spouští 'homeassistant.update_entity'!)", "transition": "transition: doba přechodu při změně osvětlení (sekundy)", - "adapt_delay": "adapt_delay: prodleva mezi zapnutím světla ( sekundy) a projevem změny v Adaptivní osvětlení. Může předcházet blikání." + "adapt_delay": "adapt_delay: prodleva mezi zapnutím světla ( sekundy) a projevem změny v Adaptivní osvětlení. Může předcházet blikání.", + "transition_until_sleep": "transition_until_sleep: Pokud je zapnuto, Adaptive Lighting bude zacházet s nastavením spánku jako s minimem, na tyto hodnoty přejde po západu slunce. 🌙" } } }, From 276dd9e5826f053a43d5bf6650de7be0f327d73e Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 4 Oct 2023 20:10:12 +0200 Subject: [PATCH 0694/1077] Translated using Weblate (Portuguese) Currently translated at 49.0% (75 of 153 strings) Added translation using Weblate (Portuguese) Co-authored-by: Hosted Weblate Co-authored-by: Luis Caetano Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pt/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/pt.json | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/pt.json diff --git a/custom_components/adaptive_lighting/translations/pt.json b/custom_components/adaptive_lighting/translations/pt.json new file mode 100644 index 00000000..f3230f6a --- /dev/null +++ b/custom_components/adaptive_lighting/translations/pt.json @@ -0,0 +1,42 @@ +{ + "title": "Iluminação Adaptativa", + "services": { + "change_switch_settings": { + "fields": { + "sunrise_offset": { + "description": "Ajustar a hora do nascer do sol com um offset positivo ou negativo em segundos. ⏰" + }, + "only_once": { + "description": "Adaptar as luzes apenas quando estão ligadas (`true`) ou continuar a adaptá-las (`false`)." + }, + "sunset_offset": { + "description": "Ajustar a hora do pôr do sol com um offset positivo ou negativo em segundos. ⏰" + } + } + }, + "apply": { + "description": "Aplica as definições atuais da Iluminação Adaptativa às luzes.", + "fields": { + "lights": { + "description": "Uma luz (ou lista de luzes) para a qual serão aplicadas as definições.💡" + } + } + } + }, + "config": { + "abort": { + "already_configured": "Este dispositivo já está configurado" + } + }, + "options": { + "step": { + "init": { + "data_description": { + "sunrise_offset": "Ajustar a hora do nascer do sol com um offset positivo ou negativo em segundos. ⏰", + "sunset_offset": "Ajustar a hora do pôr do sol com um offset positivo ou negativo em segundos. ⏰" + }, + "title": "Opções da Iluminação Adaptativa" + } + } + } +} From b6d891019fb7cb8b0b3525e61dc7970bfa357dbd Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 4 Oct 2023 20:10:12 +0200 Subject: [PATCH 0695/1077] Translated using Weblate (Swedish) Currently translated at 70.5% (108 of 153 strings) Co-authored-by: fmarcu Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/sv/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/sv.json | 159 +++++++++++++----- 1 file changed, 114 insertions(+), 45 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/sv.json b/custom_components/adaptive_lighting/translations/sv.json index fe5fb653..ee085f13 100644 --- a/custom_components/adaptive_lighting/translations/sv.json +++ b/custom_components/adaptive_lighting/translations/sv.json @@ -1,50 +1,119 @@ { - "title": "Adaptiv Ljussättning", - "config": { - "step": { - "user": { - "title": "Välj ett namn för Adaptiv Ljussättning", - "description": "Varje konfiguration kan innehålla flera ljuskällor!", - "data": { - "name": "Namn" - } - } - }, - "abort": { - "already_configured": "Enheten är redan konfiguerad" + "title": "Adaptiv Ljussättning", + "config": { + "step": { + "user": { + "title": "Välj ett namn för Adaptiv Ljussättning", + "description": "Varje konfiguration kan innehålla flera ljuskällor!", + "data": { + "name": "Namn" } + } }, - "options": { - "step": { - "init": { - "title": "Adaptiv Ljussättning Inställningar", - "description": "Alla inställningar för en Adaptiv Ljussättning komponent. Titeln på inställningarna är desamma som i YAML konfigurationen. Inga inställningar visas om enheten redan är konfigurerad i YAML.", - "data": { - "lights": "lights, ljuskällor", - "initial_transition": "initial_transition, när ljuskällorna går från 'av' till 'på' eller när 'sleep_state' ändras", - "interval": "interval, Tid mellan uppdateringar i sekunder", - "max_brightness": "max_brightness, i procent %", - "max_color_temp": "max_color_temp, i Kelvin", - "min_brightness": "min_brightness, i %", - "min_color_temp": "min_color_temp, i Kelvin", - "only_once": "only_once, Adaptivt justera endast ljuskällorna när de sätts från 'av' till 'på'", - "prefer_rgb_color": "prefer_rgb_color, Använd 'rgb_color' över 'color_temp' om möjligt", - "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.", - "sleep_brightness": "sleep_brightness, i %", - "sleep_color_temp": "sleep_color_temp, i Kelvin", - "sunrise_offset": "sunrise_offset, i +/- sekunder", - "sunrise_time": "sunrise_time, i 'HH:MM:SS' format (om 'None', används den faktiskta soluppgången för din position)", - "sunset_offset": "sunset_offset, i +/- sekunder", - "sunset_time": "sunset_time, i 'HH:MM:SS' format (om 'None', används den faktiskta solnedgången för din position)", - "take_over_control": "take_over_control, om något utöver 'Adaptiv Ljussättning' komponenten kallar på 'light.turn_on' när en ljuskälla redan är på, stängs den adaptiva justeringen av tills ljuskällan stängs av -> på igen, alternativt switchen för konfigurationen", - "detect_non_ha_changes": "detect_non_ha_changes, Upptäcker alla ändringar större än 5% gjorda på ljuskällorna som inte kommer från HA. Kräver att 'take_over_control' är påslaget.(Kallar på 'homeassistant.update_entity' vid varje 'interval'!)", - "transition": "transition, i sekunder" - } - } - }, - "error": { - "option_error": "Ogiltlig inställning", - "entity_missing": "Ett valt ljus hittades inte" - } + "abort": { + "already_configured": "Enheten är redan konfiguerad" } + }, + "options": { + "step": { + "init": { + "title": "Adaptiv Ljussättning Inställningar", + "description": "Alla inställningar för en Adaptiv Ljussättning komponent. Titeln på inställningarna är desamma som i YAML konfigurationen. Inga inställningar visas om enheten redan är konfigurerad i YAML.", + "data": { + "lights": "lights, ljuskällor", + "initial_transition": "initial_transition, när ljuskällorna går från 'av' till 'på' eller när 'sleep_state' ändras", + "interval": "interval, Tid mellan uppdateringar i sekunder", + "max_brightness": "max_brightness, i procent %", + "max_color_temp": "max_color_temp, i Kelvin", + "min_brightness": "min_brightness, i %", + "min_color_temp": "min_color_temp, i Kelvin", + "only_once": "only_once, Adaptivt justera endast ljuskällorna när de sätts från 'av' till 'på'", + "prefer_rgb_color": "prefer_rgb_color, Använd 'rgb_color' över 'color_temp' om möjligt", + "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.", + "sleep_brightness": "sleep_brightness, i %", + "sleep_color_temp": "sleep_color_temp, i Kelvin", + "sunrise_offset": "sunrise_offset, i +/- sekunder", + "sunrise_time": "sunrise_time, i 'HH:MM:SS' format (om 'None', används den faktiskta soluppgången för din position)", + "sunset_offset": "sunset_offset, i +/- sekunder", + "sunset_time": "sunset_time, i 'HH:MM:SS' format (om 'None', används den faktiskta solnedgången för din position)", + "take_over_control": "take_over_control, om något utöver 'Adaptiv Ljussättning' komponenten kallar på 'light.turn_on' när en ljuskälla redan är på, stängs den adaptiva justeringen av tills ljuskällan stängs av -> på igen, alternativt switchen för konfigurationen", + "detect_non_ha_changes": "detect_non_ha_changes, Upptäcker alla ändringar större än 5% gjorda på ljuskällorna som inte kommer från HA. Kräver att 'take_over_control' är påslaget.(Kallar på 'homeassistant.update_entity' vid varje 'interval'!)", + "transition": "transition, i sekunder" + }, + "data_description": { + "sleep_color_temp": "Färgtemperatur i sovläge (används när `sleep_rgb_or_color_temp` är `color_temp`) i Kelvin. 😴", + "sleep_transition": "Dröjsmål för övergång när \"sov läge\" slås på/av, i sekunder. 🕑", + "autoreset_control_seconds": "Nollställ automatiskt manuell kontroll efter ett antal sekunder. Sätt till 0 för at avaktivera. ⏲️", + "sleep_brightness": "Procent ljusstyrka för lampor i sovläge. 😴", + "interval": "Frekvens för att anpassa lamporna, i sekunder. 🔄", + "sunrise_offset": "Justera soluppgångstiden med positiv och negativ förskutning is sekunder. ⏰", + "transition": "Dröjsmål för övergång när lampor ändras, i sekunder. 🕑", + "sunset_offset": "Justera solnedgångstiden med positiv och negativ förskutning is sekunder. ⏰", + "send_split_delay": "Dröjsmål (ms) mellan `separate_turn_on_commands` för lampor som inte stödjer samtidiga ljussyrke och färg inställningar. ⏲️" + } + } + }, + "error": { + "option_error": "Ogiltlig inställning", + "entity_missing": "Ett valt ljus hittades inte" + } + }, + "services": { + "change_switch_settings": { + "fields": { + "sleep_brightness": { + "description": "Procent ljusstyrka för lampor i sovläge. 😴" + }, + "sunrise_offset": { + "description": "Justera soluppgångstiden med positiv och negativ förskutning is sekunder. ⏰Justera soluppgångstiden med positiv och negativ förskutning is sekunder. ⏰" + }, + "sleep_color_temp": { + "description": "Färgtemperatur i sovläge (används när `sleep_rgb_or_color_temp` är `color_temp`) i Kelvin. 😴" + }, + "entity_id": { + "description": "Enhets-ID för strömbrytaren. 📝" + }, + "sleep_transition": { + "description": "Dröjsmål för övergång när \"sov läge\" slås på/av, i sekunder. 🕑" + }, + "autoreset_control_seconds": { + "description": "Nollställ automatiskt manuell kontroll efter ett antal sekunder. Sätt till 0 för at avaktivera. ⏲️" + }, + "only_once": { + "description": "Anpassa lampor endast när de slås på ('true') eller fortsätt anpassa dem ('false'). 🔄" + }, + "max_color_temp": { + "description": "Kallaste färgtemperatur i Kelvin. ❄️" + }, + "sunset_offset": { + "description": "Justera solnedgångstiden med positiv och negativ förskutning is sekunder. ⏰" + }, + "send_split_delay": { + "description": "Dröjsmål (ms) mellan `separate_turn_on_commands` för lampor som inte stödjer samtidiga ljussyrke och färg inställningar. ⏲️" + }, + "transition": { + "description": "Dröjsmål för övergång när lampor ändras, i sekunder. 🕑" + } + }, + "description": "Ändra vilka inställningar du vill ha i strömbrytaren. All dessa inställningar är likadana som i config flow." + }, + "set_manual_control": { + "fields": { + "lights": { + "description": "Enhets-ID för lampor. Om inget anges väljs alla lampor i strömbrytaren. 💡" + } + } + }, + "apply": { + "description": "Tillämpar nuvarande Adaptiv Ljussätting inställningar till lampor.", + "fields": { + "lights": { + "description": "En lampa (eller en lamplista) till vilka inställningarna tillämpas." + }, + "transition": { + "description": "Dröjsmål för övergång när lampor ändras, i sekunder. 🕑" + } + } + } + } } From fed52d2cc14488377aedfa3b2301e09d36641d0d Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 13:53:32 -0700 Subject: [PATCH 0696/1077] docs: add luixcaetano as a contributor for translation (#803) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index e1a6341c..2f8bc65d 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -548,6 +548,15 @@ "contributions": [ "translation" ] + }, + { + "login": "luixcaetano", + "name": "luixcaetano", + "avatar_url": "https://avatars.githubusercontent.com/u/4554163?v=4", + "profile": "https://github.com/luixcaetano", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 9239e0ab..ed35bbf7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-59-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-60-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -540,6 +540,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 304ec8d7224e696dc6a42968e477f72cfa7cd3a2 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 13:53:52 -0700 Subject: [PATCH 0697/1077] docs: add fmarcu as a contributor for translation (#804) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 2f8bc65d..290e895f 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -557,6 +557,15 @@ "contributions": [ "translation" ] + }, + { + "login": "fmarcu", + "name": "fmarcu", + "avatar_url": "https://avatars.githubusercontent.com/u/81946691?v=4", + "profile": "https://github.com/fmarcu", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index ed35bbf7..cc8831a0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-60-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-61-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -541,6 +541,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 2f19dc22f8e5de82d5126c5a34fad5cc53fda157 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 17:45:53 +0000 Subject: [PATCH 0698/1077] [pre-commit.ci] pre-commit autoupdate (#770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0) - [github.com/astral-sh/ruff-pre-commit: v0.0.284 → v0.0.292](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.284...v0.0.292) - [github.com/psf/black: 23.7.0 → 23.9.1](https://github.com/psf/black/compare/23.7.0...23.9.1) * Fix suggestion --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .pre-commit-config.yaml | 6 +++--- custom_components/adaptive_lighting/color_and_brightness.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e7b8b253..e032e91e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-added-large-files - id: trailing-whitespace @@ -8,11 +8,11 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.284 + rev: v0.0.292 hooks: - id: ruff args: ["--fix"] - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 23.9.1 hooks: - id: black diff --git a/custom_components/adaptive_lighting/color_and_brightness.py b/custom_components/adaptive_lighting/color_and_brightness.py index 2968fcc0..9441e9e6 100644 --- a/custom_components/adaptive_lighting/color_and_brightness.py +++ b/custom_components/adaptive_lighting/color_and_brightness.py @@ -186,10 +186,10 @@ class SunEvents: def closest_event(self, dt: datetime.datetime) -> tuple[str, float]: """Get the closest sunset or sunrise event.""" (prev_event, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt) - if prev_event == SUN_EVENT_SUNRISE or next_event == SUN_EVENT_SUNRISE: + if SUN_EVENT_SUNRISE in (prev_event, next_event): ts_event = prev_ts if prev_event == SUN_EVENT_SUNRISE else next_ts return SUN_EVENT_SUNRISE, ts_event - if prev_event == SUN_EVENT_SUNSET or next_event == SUN_EVENT_SUNSET: + if SUN_EVENT_SUNSET in (prev_event, next_event): ts_event = prev_ts if prev_event == SUN_EVENT_SUNSET else next_ts return SUN_EVENT_SUNSET, ts_event msg = "No sunrise or sunset event found." From 29b2cc76cae0e8316013550829ba37884da47090 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 10 Oct 2023 20:07:06 +0200 Subject: [PATCH 0699/1077] Translated using Weblate (Portuguese) (#806) Currently translated at 56.8% (87 of 153 strings) Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pt/ Translation: Adaptive Lighting/Adaptive Lighting Co-authored-by: Luis Caetano Co-authored-by: Bas Nijholt --- .../adaptive_lighting/translations/pt.json | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/pt.json b/custom_components/adaptive_lighting/translations/pt.json index f3230f6a..11184a20 100644 --- a/custom_components/adaptive_lighting/translations/pt.json +++ b/custom_components/adaptive_lighting/translations/pt.json @@ -11,6 +11,21 @@ }, "sunset_offset": { "description": "Ajustar a hora do pôr do sol com um offset positivo ou negativo em segundos. ⏰" + }, + "turn_on_lights": { + "description": "Para ligar luzes que estão neste momento desligadas. 🔆" + }, + "entity_id": { + "description": "ID Entidade do interruptor. 📝" + }, + "sleep_transition": { + "description": "Duração da transição quando o \"modo dormir\" é alternado em segundos. 😴" + }, + "autoreset_control_seconds": { + "description": "Reiniciar o controlo manual automaticamente após um número de segundos. Definir 0 para desativar. ⏲️" + }, + "transition": { + "description": "Duração da transição quando as luzes mudam, em segundos. 🕑" } } }, @@ -19,6 +34,9 @@ "fields": { "lights": { "description": "Uma luz (ou lista de luzes) para a qual serão aplicadas as definições.💡" + }, + "transition": { + "description": "Duração da transição quando as luzes mudam, em segundos. 🕑" } } } @@ -26,6 +44,12 @@ "config": { "abort": { "already_configured": "Este dispositivo já está configurado" + }, + "step": { + "user": { + "description": "Cada instância pode conter múltiplas luzes!", + "title": "Escolha um nome para a instância de Iluminação Adaptativa" + } } }, "options": { @@ -33,10 +57,16 @@ "init": { "data_description": { "sunrise_offset": "Ajustar a hora do nascer do sol com um offset positivo ou negativo em segundos. ⏰", - "sunset_offset": "Ajustar a hora do pôr do sol com um offset positivo ou negativo em segundos. ⏰" + "sunset_offset": "Ajustar a hora do pôr do sol com um offset positivo ou negativo em segundos. ⏰", + "sleep_transition": "Duração da transição quando o \"modo dormir\" é alternado em segundos. 😴", + "autoreset_control_seconds": "Reiniciar o controlo manual automaticamente após um número de segundos. Definir 0 para desativar. ⏲️", + "transition": "Duração da transição quando as luzes mudam, em segundos. 🕑" }, "title": "Opções da Iluminação Adaptativa" } + }, + "error": { + "option_error": "Opção inválida" } } } From 1a74f4f4bdbe6f1f4964c816565d03838d19daf5 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 19 Oct 2023 06:07:02 +0200 Subject: [PATCH 0700/1077] Translated using Weblate (Czech) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Michael Kmoch Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/cs/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/cs.json | 165 +++++++++++++++++- 1 file changed, 164 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/cs.json b/custom_components/adaptive_lighting/translations/cs.json index e909c0d2..1f533935 100644 --- a/custom_components/adaptive_lighting/translations/cs.json +++ b/custom_components/adaptive_lighting/translations/cs.json @@ -46,7 +46,36 @@ "detect_non_ha_changes": "detect_non_ha_changes: detekuje všechny změny >10% provedených pro osvětlení (také mimo HA), vyžaduje povolení atributu 'take_over_control' (každý 'interval' spouští 'homeassistant.update_entity'!)", "transition": "transition: doba přechodu při změně osvětlení (sekundy)", "adapt_delay": "adapt_delay: prodleva mezi zapnutím světla ( sekundy) a projevem změny v Adaptivní osvětlení. Může předcházet blikání.", - "transition_until_sleep": "transition_until_sleep: Pokud je zapnuto, Adaptive Lighting bude zacházet s nastavením spánku jako s minimem, na tyto hodnoty přejde po západu slunce. 🌙" + "transition_until_sleep": "transition_until_sleep: Pokud je zapnuto, Adaptive Lighting bude zacházet s nastavením spánku jako s minimem, na tyto hodnoty přejde po západu slunce. 🌙", + "multi_light_intercept": "multi_light_intercept: Zachytí a přizpůsobí volání `light.turn_on`, která se zaměřují na více světel. ➗⚠️ To může vést k rozdělení jednoho volání `light.turn_on` na více volání, např. když jsou světla v různých vypínačích. Vyžaduje, aby bylo povoleno `intercept`.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Při prvním zapnutí světel. Je-li nastaveno na `true`, AL se přizpůsobí pouze tehdy, je-li vyvoláno `light.turn_on` bez zadání barvy nebo jasu. ❌🌈 Tím se např. zabrání přizpůsobení při aktivaci scény. Pokud je `false`, AL se přizpůsobí bez ohledu na přítomnost barvy nebo jasu v počátečních `service_data`. Vyžaduje zapnutí funkce `take_over_control`. 🕵️ ", + "skip_redundant_commands": "skip_redundant_commands: Přeskočí odesílání adaptačních příkazů, jejichž cílový stav se již rovná známému stavu světla. Minimalizuje síťový provoz a v některých situacích zlepšuje odezvu adaptace. 📉Zakažte, pokud se fyzické stavy světel dostanou mimo synchronizaci se zaznamenaným stavem HA.", + "intercept": "intercept: Zachytit a přizpůsobit volání `light.turn_on` a umožnit tak okamžité přizpůsobení barev a jasu. 🏎️ Zakažte pro světla, která nepodporují `light.turn_on` s barvou a jasem najednou.", + "include_config_in_attributes": "include_config_in_attributes: Zobrazit všechny možnosti jako atributy přepínače v Home Assistant, pokud je nastaveno na `true`. 📝" + }, + "data_description": { + "sleep_rgb_or_color_temp": "V režimu spánku se použije buď `\"rgb_color\"`, nebo `\"color_temp\"`. 🌙", + "sleep_color_temp": "Teplota barev v režimu spánku (používá se, když `sleep_rgb_or_color_temp` je `color_temp`) v Kelvinech. 😴", + "sleep_transition": "Doba trvání přechodu do režimu spánku v sekundách. 😴", + "autoreset_control_seconds": "Automatické resetování ručního ovládání po určitém počtu sekund. Nastavením na 0 se vypne. ⏲️", + "min_sunset_time": "Nastavte nejbližší virtuální čas západu slunce (HH:MM:SS), abyste mohli nastavit pozdější západ slunce. 🌅", + "sleep_brightness": "Jas světel v procentech během režimu spánku", + "min_sunrise_time": "Nastavte nejbližší virtuální čas východu slunce (HH:MM:SS), abyste mohli nastavit pozdější východ slunce. 🌅", + "interval": "Frekvence přizpůsobení světel v sekundách. 🔄", + "adapt_delay": "Doba čekání (v sekundách) mezi zapnutím světla a změnou adaptivního osvětlení. Mohlo by to pomoci zabránit blikání. ⏲️", + "sleep_rgb_color": "RGB barva v režimu spánku (používané když `sleep_rgb_or_color_temp` je \"rgb_color\"). 🌈", + "sunrise_offset": "Upravte čas východu slunce s pozitivním nebo negativním posunem v sekundách. ⏰", + "transition": "Doba trvání přechodu změny světel v sekundách. 🕑", + "brightness_mode": "Výběr režimu jasu. Možné hodnoty jsou `default`, `linear` a `tanh` (používá `brightness_mode_time_dark` a `brightness_mode_time_light`). 📈", + "brightness_mode_time_light": "(Ignorováno, pokud `brightness_mode='default'`) Doba trvání v sekundách pro zvýšení/snížení jasu po/před východem/západem slunce. 📈📉.", + "sunset_offset": "Nastavte čas západu slunce s kladným nebo záporným posunem v sekundách. ⏰", + "sunset_time": "Nastavit pevný čas (HH:MM:SS) pro západ slunce. 🌅", + "max_sunset_time": "Nastavte nejpozdější virtuální čas západu slunce (HH:MM:SS), což umožňuje dřívější západ slunce. 🌅", + "sunrise_time": "Nastavit pevný čas (HH:MM:SS) pro východ slunce. 🌅", + "initial_transition": "Doba trvání prvního přechodu, kdy se světla změní z `vypnuto` na `zapnuto`, v sekundách. ⏲️", + "brightness_mode_time_dark": "(Ignorováno, pokud `brightness_mode='default'`) Doba trvání v sekundách pro zvýšení/snížení jasu po/před východem/západem slunce. 📈📉.", + "max_sunrise_time": "Nastavte nejpozdější virtuální čas východu slunce (HH:MM:SS), což umožňuje dřívější východ slunce. 🌅", + "send_split_delay": "Zpoždění (ms) mezi příkazy `separate_turn_on_commands` pro světla, která nepodporují současné nastavení jasu a barvy. ⏲️" } } }, @@ -54,5 +83,139 @@ "option_error": "Neplatná možnost", "entity_missing": "V aplikaci Home Assistant chybí jedna nebo více vybraných entit osvětlení" } + }, + "services": { + "change_switch_settings": { + "fields": { + "sleep_brightness": { + "description": "Jas světel v procentech během režimu spánku" + }, + "detect_non_ha_changes": { + "description": "Zjistí a zastaví adaptace při změnách stavu, které nejsou ve stavu `light.turn_on`. Nutno mít zapnutou funkci `take_over_control`. 🕵️ Upozornění: ⚠️ Některá světla mohou falešně indikovat stav 'zapnuto', což může vést k neočekávanému zapnutí světel. Pokud se s takovými problémy setkáte, zakažte tuto funkci." + }, + "sunrise_offset": { + "description": "Upravte čas východu slunce s pozitivním nebo negativním posunem v sekundách. ⏰" + }, + "max_sunrise_time": { + "description": "Nastavte nejpozdější virtuální čas východu slunce (HH:MM:SS), což umožňuje dřívější východ slunce. 🌅" + }, + "sleep_color_temp": { + "description": "Teplota barev v režimu spánku (používá se, když `sleep_rgb_or_color_temp` je `color_temp`) v Kelvinech. 😴" + }, + "min_brightness": { + "description": "Minimální hodnota jasu. 💡" + }, + "min_color_temp": { + "description": "Nejvyšší teplota barvy v Kelvinech. 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "V režimu spánku se použije buď `\"rgb_color\"`, nebo `\"color_temp\"`. 🌙" + }, + "turn_on_lights": { + "description": "Zda se mají zapnout světla, která jsou aktuálně vypnutá. 🔆" + }, + "initial_transition": { + "description": "Doba trvání prvního přechodu, kdy se světla změní z `vypnuto` na `zapnuto`, v sekundách. ⏲️" + }, + "entity_id": { + "description": "Entita ID přepínače. 📝" + }, + "sunrise_time": { + "description": "Nastavit pevný čas (HH:MM:SS) pro východ slunce. 🌅" + }, + "include_config_in_attributes": { + "description": "Zobrazení všech možností jako atributů přepínače v aplikaci Home Assistant, pokud je zaškrtnuto. 📝" + }, + "max_brightness": { + "description": "Maximální hodnota jasu. 💡" + }, + "sleep_rgb_color": { + "description": "RGB barva v režimu spánku (používané když `sleep_rgb_or_color_temp` je \"rgb_color\"). 🌈" + }, + "take_over_control": { + "description": "Zakáže adaptivní osvětlení, pokud jiný zdroj volá `light.turn_on`, zatímco jsou světla zapnutá a přizpůsobují se. Vemte na vědomí, že `homeassistant.update_entity` volá každý `interval`! 🔒" + }, + "sleep_transition": { + "description": "Doba trvání přechodu do režimu spánku v sekundách. 😴" + }, + "autoreset_control_seconds": { + "description": "Automatické resetování ručního ovládání po určitém počtu sekund. Nastavením na 0 se vypne. ⏲️" + }, + "adapt_delay": { + "description": "Doba čekání (v sekundách) mezi zapnutím světla a změnou adaptivního osvětlení. Mohlo by to pomoci zabránit blikání." + }, + "only_once": { + "description": "Přizpůsobit světla pouze při zapnutí(`true`) nebo je nechat pokaždé přizpůsobit (`false`). 🔄" + }, + "use_defaults": { + "description": "Nastaví výchozí hodnoty, které nebyly zadány v tomto volání služby. Možnosti: \"stávající\" (výchozí, zachovává aktuální hodnoty), \"výchozí\" (obnovuje do výchozích hodnot) nebo \"konfigurace\" (vrací výchozí hodnoty konfigurace přepínače). ⚙️" + }, + "separate_turn_on_commands": { + "description": "Použití samostatných volání `light.turn_on` pro barvu a jas, které jsou potřebné pro některé typy světel. 🔀" + }, + "prefer_rgb_color": { + "description": "Zda upřednostnit nastavení barev RGB před teplotou barev světla, pokud je to možné. 🌈" + }, + "max_color_temp": { + "description": "Nejchladnější teplota barvy v Kelvinech. ❄️" + }, + "sunset_offset": { + "description": "Nastavte čas západu slunce s kladným nebo záporným posunem v sekundách. ⏰" + }, + "send_split_delay": { + "description": "Zpoždění (ms) mezi příkazy `separate_turn_on_commands` pro světla, která nepodporují současné nastavení jasu a barvy. ⏲️" + }, + "sunset_time": { + "description": "Nastavit pevný čas (HH:MM:SS) pro západ slunce. 🌅" + }, + "transition": { + "description": "Doba trvání přechodu změny světel v sekundách. 🕑" + }, + "min_sunset_time": { + "description": "Nastavte nejbližší virtuální čas západu slunce (HH:MM:SS), abyste mohli nastavit pozdější západ slunce. 🌅" + } + }, + "description": "V přepínači změňte libovolné nastavení. Všechny možnosti jsou zde stejné jako v konfiguračním souboru." + }, + "apply": { + "fields": { + "entity_id": { + "description": "`entity_id` přepínače s nastavením, které se má použít. 📝" + }, + "adapt_brightness": { + "description": "Přizpůsobení jasu světla. 🌞" + }, + "turn_on_lights": { + "description": "Zda se mají zapnout světla, která jsou aktuálně vypnutá. 🔆" + }, + "adapt_color": { + "description": "Zda se má přizpůsobit barva na podporovaných světlech. 🌈" + }, + "prefer_rgb_color": { + "description": "Zda upřednostnit nastavení barev RGB před teplotou barev světla, pokud je to možné. 🌈" + }, + "lights": { + "description": "Světlo (nebo seznam světel), na které se má nastavení použít. 💡" + }, + "transition": { + "description": "Doba trvání přechodu změny světel v sekundách. 🕑" + } + }, + "description": "Aplikuje současné nastavení Adaptivního osvětlení na světla." + }, + "set_manual_control": { + "fields": { + "manual_control": { + "description": "Zda přidat (\"true\") nebo odebrat (\"false\") světlo ze seznamu \"manual_control\". 🔒" + }, + "entity_id": { + "description": "`entity_id` spínače, ve kterém se světlo (ne)označí jako `ručně ovládané`. 📝" + }, + "lights": { + "description": "entity_id(s) světel, pokud není zadáno jinak, jsou vybrána všechna světla ve spínači. 💡" + } + }, + "description": "Označte, zda je světlo \"ručně ovládané\"." + } } } From 8bb2ed1de2f8519c643e5675e27276e4b8f234a3 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 19 Oct 2023 06:07:02 +0200 Subject: [PATCH 0701/1077] Translated using Weblate (Dutch) Currently translated at 76.4% (117 of 153 strings) Co-authored-by: Fred Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/nl.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json index d8f54bcf..7a9b7896 100644 --- a/custom_components/adaptive_lighting/translations/nl.json +++ b/custom_components/adaptive_lighting/translations/nl.json @@ -62,7 +62,9 @@ "sleep_brightness": "Helderheidspercentage van lampen in slaapstand. 😴", "sleep_color_temp": "Kleurtemperatuur in slaapmodus (gebruikt wanneer `sleep_rgb_or_color_temp` `color_temp` is) in Kelvin. 😴", "brightness_mode": "Helderheidsmodus om te gebruiken. Mogelijke waarden zijn `default`, `linear` en `tanh` (gebruikt `brightness_mode_time_dark` en `brightness_mode_time_light`). 📈", - "send_split_delay": "Vertraging (ms) tussen `separate_turn_on_commands` voor lampen die geen gelijktijdige helderheids- en kleurinstelling ondersteunen. ⏲️" + "send_split_delay": "Vertraging (ms) tussen `separate_turn_on_commands` voor lampen die geen gelijktijdige helderheids- en kleurinstelling ondersteunen. ⏲️", + "transition": "Duur van de overgang, in seconden. 🕑", + "initial_transition": "Duur van de eerste overgang wanneer de lampen van `uit` naar `aan`gaan, in seconden. ⏲️" } } }, @@ -103,6 +105,12 @@ }, "max_color_temp": { "description": "Koudste kleurtemperatuur in Kelvin. ❄️" + }, + "initial_transition": { + "description": "Duur van de eerste overgang wanneer de lampen van `uit` naar `aan`gaan, in seconden. ⏲️" + }, + "take_over_control": { + "description": "Schakel Adaptive Lighting uit wanneer een andere bron `light.turn_on` aanroept terwijl de lampen aan staan en worden aangepast. N.b. dit zal `homeassistant.update_entity` elke `interval` uitvoeren." } }, "description": "Wijzig alle gewenste instellingen in de schakelaar. Alle opties hier zijn hetzelfde als in de configuratie." From 3162c0ab32e9f502683ae85148a1ded472a83ee7 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 19 Oct 2023 06:07:02 +0200 Subject: [PATCH 0702/1077] Translated using Weblate (Swedish) Currently translated at 100.0% (153 of 153 strings) Translated using Weblate (Swedish) Currently translated at 92.1% (141 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: fmarcu Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/sv/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/sv.json | 103 +++++++++++++++++- 1 file changed, 99 insertions(+), 4 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/sv.json b/custom_components/adaptive_lighting/translations/sv.json index ee085f13..df81271e 100644 --- a/custom_components/adaptive_lighting/translations/sv.json +++ b/custom_components/adaptive_lighting/translations/sv.json @@ -11,7 +11,7 @@ } }, "abort": { - "already_configured": "Enheten är redan konfiguerad" + "already_configured": "Den här enheten är redan konfiguerad" } }, "options": { @@ -38,7 +38,13 @@ "sunset_time": "sunset_time, i 'HH:MM:SS' format (om 'None', används den faktiskta solnedgången för din position)", "take_over_control": "take_over_control, om något utöver 'Adaptiv Ljussättning' komponenten kallar på 'light.turn_on' när en ljuskälla redan är på, stängs den adaptiva justeringen av tills ljuskällan stängs av -> på igen, alternativt switchen för konfigurationen", "detect_non_ha_changes": "detect_non_ha_changes, Upptäcker alla ändringar större än 5% gjorda på ljuskällorna som inte kommer från HA. Kräver att 'take_over_control' är påslaget.(Kallar på 'homeassistant.update_entity' vid varje 'interval'!)", - "transition": "transition, i sekunder" + "transition": "transition, i sekunder", + "multi_light_intercept": "multi_light_intercept: Fånga upp och anpassa \"light.turn_on\"-anrop som riktar sig mot flera lampor. ➗⚠️ Detta kan resultera i att ett enda `light.turn_on`-anrop delas upp i flera anrop, t.ex. när lamporna är kopplade till olika strömbrytare. Kräver att \"intercept\" är aktiverat.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: När lampor först tänds. Om satt till \"true\", anpassar AL endast om \"light.turn_on\" anropas utan att ange färg eller ljusstyrka. ❌🌈 Detta förhindrar t.ex. anpassning när en scen aktiveras. Om \"false\" anpassas AL oavsett förekomsten av färg eller ljusstyrka i den initiala \"service_data\". \"takeover_control\" måste vara aktiverat. 🕵️ ", + "skip_redundant_commands": "skip_redundant_commands: Hoppa över att skicka anpassningskommandon vars måltillstånd redan är lika med lampans kända tillstånd. Minimerar nätverkstrafik och förbättrar anpassningsförmågan i vissa situationer. 📉 Inaktivera om lampans tillstånd blir osynkroniserade med HA:s registrerade tillstånd.", + "intercept": "intercept: Fånga upp och anpassa `light.turn_on`-anrop för att möjliggöra omedelbar anpassning av färg och ljusstyrka. 🏎️ Inaktivera för lampor som inte stöder `light.turn_on` med färg och ljusstyrka.", + "transition_until_sleep": "transition_until_sleep: När aktiverat kommer Adaptive Lighting att behandla sömninställningarna som ett minimum och övergå till dessa värden efter solnedgången. 🌙", + "include_config_in_attributes": "include_config_in_attributes: Visa alla alternativ som attribut på strömbrytaren i Home Assistant när den är inställd på \"true\". 📝" }, "data_description": { "sleep_color_temp": "Färgtemperatur i sovläge (används när `sleep_rgb_or_color_temp` är `color_temp`) i Kelvin. 😴", @@ -49,7 +55,20 @@ "sunrise_offset": "Justera soluppgångstiden med positiv och negativ förskutning is sekunder. ⏰", "transition": "Dröjsmål för övergång när lampor ändras, i sekunder. 🕑", "sunset_offset": "Justera solnedgångstiden med positiv och negativ förskutning is sekunder. ⏰", - "send_split_delay": "Dröjsmål (ms) mellan `separate_turn_on_commands` för lampor som inte stödjer samtidiga ljussyrke och färg inställningar. ⏲️" + "send_split_delay": "Dröjsmål (ms) mellan `separate_turn_on_commands` för lampor som inte stödjer samtidiga ljussyrke och färg inställningar. ⏲️", + "sleep_rgb_or_color_temp": "Använd antingen`\"rgb_color\"` eller `\"color_temp\"` i sovläge. 🌙", + "min_sunset_time": "Ställ in den tidigaste virtuella solnedgångstiden (TT: MM: SS), vilket möjliggör senare solnedgångar. 🌇", + "min_sunrise_time": "Ställ in den tidigaste virtuella soluppgångstiden (TT: MM: SS), vilket möjliggör senare soluppgångar. 🌅", + "adapt_delay": "Väntetid (sekunder) mellan lamptändning och Adaptiv Ljussättning tillämpar ändringar. Kan hjälpa till att undvika flimmer. ⏲️", + "sleep_rgb_color": "RGB-färg i sovläge (används när \"sleep_rgb_or_color_temp\" är \"rgb_color\"). 🌈", + "sunset_time": "Ställ in en fast tid (TT:MM:SS) för solnedgången. 🌇", + "max_sunset_time": "Ställ in den senaste virtuella solnedgångstiden (TT: MM: SS), vilket möjliggör tidigare solnedgångar. 🌇", + "sunrise_time": "Ställ in en fast tid (TT:MM:SS) för soluppgången. 🌅", + "initial_transition": "Den första övergångens varaktighet när lampan slås från ”av” till ”på” i sekunder. ⏲️", + "max_sunrise_time": "Ställ in den senaste virtuella soluppgångstiden (TT: MM: SS), vilket möjliggör tidigare soluppgångar. 🌅", + "brightness_mode": "Ljusstyrkeinställing att använda. Möjliga värden är \"default\", \"linear\" och \"tanh\" (använder \"brightness_mode_time_dark\" och \"brightness_mode_time_light\"). 📈", + "brightness_mode_time_light": "(Ignoreras om `brightness_mode='default'`) Varaktigheten i sekunder för att öka/minska ljusstyrkan efter/före soluppgång/solnedgång. 📈📉.", + "brightness_mode_time_dark": "(Ignoreras om `brightness_mode='default'`) Varaktigheten i sekunder för att öka/minska ljusstyrkan efter/före soluppgång/solnedgång. 📈📉." } } }, @@ -93,6 +112,60 @@ }, "transition": { "description": "Dröjsmål för övergång när lampor ändras, i sekunder. 🕑" + }, + "max_sunrise_time": { + "description": "Ställ in den senaste virtuella soluppgångstiden (TT: MM: SS), vilket möjliggör tidigare soluppgångar. 🌅" + }, + "min_brightness": { + "description": "Minimal ljusstyrka i procent. 💡" + }, + "min_color_temp": { + "description": "Varmaste färgtemperaturen i Kelvin. 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "Använd antingen`\"rgb_color\"` eller `\"color_temp\"` i sovläge. 🌙" + }, + "turn_on_lights": { + "description": "Om att tända lampor som är för närvarande släckta. 🔆" + }, + "initial_transition": { + "description": "Den första övergångens varaktighet när lampan slås från ”av” till ”på” i sekunder. ⏲️" + }, + "sunrise_time": { + "description": "Ställ in en fast tid (TT:MM:SS) för soluppgången. 🌅" + }, + "include_config_in_attributes": { + "description": "Visa alla alternativ som attribut på strömbrytaren i Home Assistant när ”true”. 📝" + }, + "max_brightness": { + "description": "Maximal ljusstyrka i procent. 💡" + }, + "sleep_rgb_color": { + "description": "RGB-färg i sovläge (används när \"sleep_rgb_or_color_temp\" är \"rgb_color\"). 🌈" + }, + "adapt_delay": { + "description": "Väntetid (sekunder) mellan lamptändning och Adaptiv Ljussättning tillämpar ändringar. Kan hjälpa till att undvika flimmer. ⏲️" + }, + "separate_turn_on_commands": { + "description": "Använd separata `light.turn_on`anrop för färg och ljusstyrka, behövs för vissa lamptyper. 🔀" + }, + "prefer_rgb_color": { + "description": "Om att föredra RGB-färgjustering framför ljusfärgtemperatur när det är möjligt. 🌈" + }, + "sunset_time": { + "description": "Ställ in en fast tid (TT:MM:SS) för solnedgången. 🌇" + }, + "min_sunset_time": { + "description": "Ställ in den tidigaste virtuella solnedgångstiden (TT: MM: SS), vilket möjliggör senare solnedgångar. 🌇" + }, + "detect_non_ha_changes": { + "description": "Upptäcker och stoppar anpassningar för tillståndsändringar som inte är \"light.turn_on\". Behöver \"takeover_control\" aktiverat. 🕵️ Varning: ⚠️ Vissa lampor kan felaktigt indikera ett \"på\"-läge, vilket kan resultera i att lamporna tänds oväntat. Inaktivera den här funktionen om du stöter på sådana problem." + }, + "take_over_control": { + "description": "Inaktivera Adaptive Ligting om en annan källa anropar 'light.turn_on' medan lamporna är tända och anpassas. Observera att detta anropar `homeassistant.update_entity` varje `intervall`! 🔒" + }, + "use_defaults": { + "description": "Ställer in standardvärden som inte anges i detta serviceanrop. Alternativ: \"current\" (standard, behåller nuvarande värden), \"factory\" (återställer till dokumenterade standardinställningar) eller \"configuration\" (återgår till strömbrytarens standardinställningar). ⚙️" } }, "description": "Ändra vilka inställningar du vill ha i strömbrytaren. All dessa inställningar är likadana som i config flow." @@ -101,8 +174,15 @@ "fields": { "lights": { "description": "Enhets-ID för lampor. Om inget anges väljs alla lampor i strömbrytaren. 💡" + }, + "manual_control": { + "description": "Lägg till (\"true\") eller ta bort (\"false\") ljuset från listan \"manual_control\". 🔒" + }, + "entity_id": { + "description": "Strömbrytarens ”entity_id\" i vilken lampan ska (av)markeras som \"manuellt styrd\". 📝" } - } + }, + "description": "Swedish: Markera om en lampa är \"styrd manuellt\"." }, "apply": { "description": "Tillämpar nuvarande Adaptiv Ljussätting inställningar till lampor.", @@ -112,6 +192,21 @@ }, "transition": { "description": "Dröjsmål för övergång när lampor ändras, i sekunder. 🕑" + }, + "entity_id": { + "description": "\"entity_id\" för strömbrytaren med inställningarna som ska tillämpas. 📝" + }, + "adapt_brightness": { + "description": "Om lampans ljusstyrka ska anpassas. 🌞" + }, + "turn_on_lights": { + "description": "Om att tända lampor som är för närvarande släckta. 🔆" + }, + "adapt_color": { + "description": "Om färgen på lampor som stödjer ska anpassas. 🌈" + }, + "prefer_rgb_color": { + "description": "Om att föredra RGB-färgjustering framför ljusfärgtemperatur när det är möjligt. 🌈" } } } From dc98e4b9335ce6b0548b3f4cce7f7876658369a7 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 19 Oct 2023 06:07:02 +0200 Subject: [PATCH 0703/1077] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Z-weapon Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/zh_Hans/ Translation: Adaptive Lighting/Adaptive Lighting --- custom_components/adaptive_lighting/translations/zh-Hans.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/zh-Hans.json b/custom_components/adaptive_lighting/translations/zh-Hans.json index e9b206c6..87d236d1 100644 --- a/custom_components/adaptive_lighting/translations/zh-Hans.json +++ b/custom_components/adaptive_lighting/translations/zh-Hans.json @@ -220,7 +220,7 @@ "name": "sleep_color_temp" }, "sunrise_offset": { - "description": "Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "description": "以秒为单位的正负偏移调整日出时间。⏰", "name": "sunrise_offset" }, "sunrise_time": { From 3f54647ba0bfa13354a1b1590c5b08465d9471bb Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 21:57:20 -0700 Subject: [PATCH 0704/1077] docs: add michaelkmoch as a contributor for translation (#814) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 290e895f..92715cd8 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -566,6 +566,15 @@ "contributions": [ "translation" ] + }, + { + "login": "michaelkmoch", + "name": "michaelkmoch", + "avatar_url": "https://avatars.githubusercontent.com/u/107689026?v=4", + "profile": "https://github.com/michaelkmoch", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index cc8831a0..1ccf2433 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-61-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-62-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -542,6 +542,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 47f7a6e68eda74ca583bdb472a2b6d5aa80f75c5 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 21:57:47 -0700 Subject: [PATCH 0705/1077] docs: add fbloemhof as a contributor for translation (#815) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 92715cd8..169d2338 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -575,6 +575,15 @@ "contributions": [ "translation" ] + }, + { + "login": "fbloemhof", + "name": "Fred", + "avatar_url": "https://avatars.githubusercontent.com/u/8753211?v=4", + "profile": "https://github.com/fbloemhof", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 1ccf2433..7a2137ef 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-62-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-63-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -543,6 +543,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 4d1108a3cfa4ed014185c2c6634bfe906fed13f8 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 21:58:02 -0700 Subject: [PATCH 0706/1077] docs: add Z-weapon as a contributor for translation (#816) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 169d2338..dd503bc1 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -584,6 +584,15 @@ "contributions": [ "translation" ] + }, + { + "login": "Z-weapon", + "name": "Z-weapon", + "avatar_url": "https://avatars.githubusercontent.com/u/13939632?v=4", + "profile": "https://github.com/Z-weapon", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 7a2137ef..221ca899 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-63-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-64-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -545,6 +545,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + + + From 98a48ec0711784defb3f4953b86612babe563389 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 14:02:43 -0700 Subject: [PATCH 0707/1077] [pre-commit.ci] pre-commit autoupdate (#821) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.0.292 → v0.1.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.292...v0.1.1) - [github.com/psf/black: 23.9.1 → 23.10.0](https://github.com/psf/black/compare/23.9.1...23.10.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e032e91e..835445c3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,11 +8,11 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.292 + rev: v0.1.1 hooks: - id: ruff args: ["--fix"] - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.10.0 hooks: - id: black From dce2a35147e3f9c0cba7f47ca4d5c1bfd85f20c2 Mon Sep 17 00:00:00 2001 From: Kyle Bjordahl <3489222+kylebjordahl@users.noreply.github.com> Date: Sun, 19 Nov 2023 17:21:05 -0800 Subject: [PATCH 0708/1077] Protect for None attributes after HA core change (#846) --- custom_components/adaptive_lighting/switch.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 7e910525..9aec8ba1 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -710,6 +710,10 @@ def _attributes_have_changed( adapt_color: bool, context: Context, ) -> bool: + # 2023-11-19: HA core no longer removes light domain attributes when off + # so we must protect for `None` here + # see https://github.com/home-assistant/core/pull/101946 + if adapt_color: old_attributes, new_attributes = _add_missing_attributes( old_attributes, @@ -718,8 +722,8 @@ def _attributes_have_changed( if ( adapt_brightness - and ATTR_BRIGHTNESS in old_attributes - and ATTR_BRIGHTNESS in new_attributes + and old_attributes.get(ATTR_BRIGHTNESS) + and new_attributes.get(ATTR_BRIGHTNESS) ): last_brightness = old_attributes[ATTR_BRIGHTNESS] current_brightness = new_attributes[ATTR_BRIGHTNESS] @@ -736,8 +740,8 @@ def _attributes_have_changed( if ( adapt_color - and ATTR_COLOR_TEMP_KELVIN in old_attributes - and ATTR_COLOR_TEMP_KELVIN in new_attributes + and old_attributes.get(ATTR_COLOR_TEMP_KELVIN) + and new_attributes.get(ATTR_COLOR_TEMP_KELVIN) ): last_color_temp = old_attributes[ATTR_COLOR_TEMP_KELVIN] current_color_temp = new_attributes[ATTR_COLOR_TEMP_KELVIN] @@ -754,8 +758,8 @@ def _attributes_have_changed( if ( adapt_color - and ATTR_RGB_COLOR in old_attributes - and ATTR_RGB_COLOR in new_attributes + and old_attributes.get(ATTR_RGB_COLOR) + and new_attributes.get(ATTR_RGB_COLOR) ): last_rgb_color = old_attributes[ATTR_RGB_COLOR] current_rgb_color = new_attributes[ATTR_RGB_COLOR] From e8bd39f6d4995500624359e709aad14ef93fbfe5 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 19 Nov 2023 17:22:37 -0800 Subject: [PATCH 0709/1077] docs: add kylebjordahl as a contributor for code (#848) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index dd503bc1..7ab14c4d 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -593,6 +593,15 @@ "contributions": [ "translation" ] + }, + { + "login": "kylebjordahl", + "name": "Kyle Bjordahl", + "avatar_url": "https://avatars.githubusercontent.com/u/3489222?v=4", + "profile": "https://github.com/kylebjordahl", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 221ca899..93d2703c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-64-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-65-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -547,6 +547,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From f3c1bdb0bde34d85446c7af46ddaf9383d99d279 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sun, 3 Dec 2023 13:02:19 +0100 Subject: [PATCH 0710/1077] Translated using Weblate (Polish) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Olek Bruks Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pl/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/pl.json | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json index 6ad9be29..0ec6f6c4 100644 --- a/custom_components/adaptive_lighting/translations/pl.json +++ b/custom_components/adaptive_lighting/translations/pl.json @@ -17,10 +17,10 @@ "options": { "step": { "init": { - "title": "Adaptacyjne oświetlenie opcje", - "description": "Konfiguracja komponentu Adaptacyjnego oświetlenia. Nazwy opcji odpowiadają ustawieniom YAML. Żadne opcje nie są wyświetlane, jeśli są zdefiniowany w konfiguracji YAML. Aby zobaczyć interaktywne wykresy demonstrujące działanie parametrów odwiedź [this web app](https://basnijholt.github.io/adaptive-lighting). Aby zobaczyć więcej szczegółów odwiedź [official documentation](https://github.com/basnijholt/adaptive-lighting#readme).", + "title": "Opcje adaptacyjnego oświetlenia", + "description": "Konfiguracja komponentu Adaptacyjnego oświetlenia. Nazwy opcji odpowiadają ustawieniom YAML. Żadne opcje nie są wyświetlane, jeśli są zdefiniowane w konfiguracji YAML. Aby zobaczyć interaktywne wykresy demonstrujące działanie parametrów odwiedź [tą aplikację webową](https://basnijholt.github.io/adaptive-lighting). Aby zobaczyć więcej szczegółów odwiedź [oficjalną dokumentację](https://github.com/basnijholt/adaptive-lighting#readme).", "data": { - "lights": "lights: Lista entity_ids, które mają być kontrolowane (może być pusta). 🌟", + "lights": "lights: Lista `entity_id`, które mają być kontrolowane (może być pusta). 🌟", "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (sekund)", "sleep_transition": "sleep_transition: When 'sleep_state' changes. (sekund)", "interval": "interval: Time between switch updates. (sekund)", @@ -29,7 +29,7 @@ "min_brightness": "min_brightness: Minimalna jasność (w procentach). 💡", "min_color_temp": "min_color_temp: Najcieplejsza temperatura barwowa (w Kelwinach). 🔥", "only_once": "only_once: Adaptuj światło tylko podczas włączenia (`true`) lub adaptuj cały czas (`false`). 🔄", - "prefer_rgb_color": "prefer_rgb_color: Czy w miarę możliwości preferować regulację kolorów RGB zamiast temperatury barwowej światła.. 🌈", + "prefer_rgb_color": "prefer_rgb_color: Czy w miarę możliwości preferować regulację kolorów RGB zamiast regulacji temperatury barwowej światła. 🌈", "separate_turn_on_commands": "separate_turn_on_commands: Używaj oddzielnych wywołań `light.turn_on` dla koloru i jasności, wymagane dla niektórych typów świateł. 🔀", "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)", "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)", @@ -37,24 +37,24 @@ "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- sekund)", "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", - "take_over_control": "take_over_control: Wyłącz adaptowanie oświetlenia gdy inna usługa wywoła `light.turn_on` gdy oświetlenie jest już włączone. Zauważ, że to wywołuje `homeassistant.update_entity` co`interval`! 🔒", - "detect_non_ha_changes": "detect_non_ha_changes: Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan 'on', co może powodować nieoczekiwane włączenia światła. Wyłącz to ustawienie jeżeli doświadczasz takich objawów.", + "take_over_control": "take_over_control: Wyłącz adaptowanie oświetlenia, kiedy inna usługa wywoła `light.turn_on`, gdy oświetlenie jest już włączone. Zauważ, że to wywołuje `homeassistant.update_entity` co`interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan \"on\", co może powodować nieoczekiwane włączanie się świateł. Wyłącz to ustawienie, jeżeli doświadczasz takich objawów.", "transition": "Transition time when applying a change to the lights (sekund)", "transition_until_sleep": "transition_until_sleep: Gdy włączone, Adaptacyjne oświetlenie będzie traktowało ustawienia spania jako minimalne i przejdzie do nich po zachodzie słońca. 🌙", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Gdy włączone `true` to Adaptacyjne oświetlenie włączy adaptacje tylko jeżeli `light.turn_on` jest wywołane bez konkretnego koloru lub jasności. ❌🌈 To ustawienie zapobiega między innymi adaptacji gdy aktywowana jest scena. Gdy wyłączone `false`, Adaptacyjne oświetlenie włączy adaptacje niezależnie czy `service_data zawiera kolor lub jasność`. Potrzebuje włączonej opcji `take_over_control`. 🕵️ ", - "skip_redundant_commands": "skip_redundant_commands: Pomiń wysyłanie polecenia adaptacji jeżeli stan światła jest taki sam jak docelowy stan adaptacji. Minimalizuje to ruch sieciowy oraz w niektórych przysadkach poprawia szybkość działania. 📉Wyłącz jeżeli faktyczny stan światła się nie pokrywa z tym który widnieje w Home Assistant.", - "include_config_in_attributes": "include_config_in_attributes: Gdy włączone `true` pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant. 📝", - "intercept": "intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`aby błyskawicznie dostosować kolor i jasność . 🏎️ Wyłącz dla świateł które nie akceptują wywołania `light.turn_on` zawierającego kolor i jasność.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Gdy włączone (`true`) to adaptacyjne oświetlenie zastosuje adaptacje tylko jeżeli `light.turn_on` jest wywołane bez konkretnego koloru lub jasności. ❌🌈 To ustawienie zapobiega między innymi adaptacji, gdy aktywowana jest scena. Gdy wyłączone (`false`), adaptacyjne oświetlenie zastosuje adaptacje niezależnie czy `service_data` zawiera kolor lub jasność. Potrzebuje włączonej opcji `take_over_control`. 🕵️ ", + "skip_redundant_commands": "skip_redundant_commands: Pomiń wysyłanie polecenia adaptacji, jeżeli stan światła jest taki sam jak docelowy stan adaptacji. Minimalizuje to ruch sieciowy oraz w niektórych przypadkach poprawia szybkość działania. 📉 Wyłącz, jeżeli faktyczny stan światła się nie pokrywa z tym który widnieje w Home Assistant.", + "include_config_in_attributes": "include_config_in_attributes: Gdy włączone (`true`) pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant. 📝", + "intercept": "intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`, aby błyskawicznie dostosować kolor i jasność. 🏎️ Wyłącz dla świateł, które nie akceptują wywołania `light.turn_on` zawierającego kolor i jasność.", "multi_light_intercept": "multi_light_intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`, które dotyczą wielu świateł. ➗⚠️ Może to powodować rozdzielenie pojedynczego wywołania `light.turn_on` na wiele wywołań, na przykład gdy światła są przypisane do rożnych instancji. Wymaga włączonej opcji `intercept`." }, "data_description": { "interval": "Częstotliwość adaptacji świateł w sekundach. 🔄", - "transition": "Długość tranzycji do nowego stanu w sekundach. 🕑", - "initial_transition": "Długość pierwszej tranzycji gdy światło zostanie włączone z `off` na `on`w sekundach. ⏲️", - "sleep_rgb_or_color_temp": "Użyj `\"rgb_color\"` albo `\"color_temp\"`trybie spania. 🌙", + "transition": "Długość przejścia do nowego stanu (w sekundach). 🕑", + "initial_transition": "Długość pierwszego przejścia, gdy światło zostanie przełączone z `off` na `on` (w sekundach). ⏲️", + "sleep_rgb_or_color_temp": "Użyj `\"rgb_color\"` albo `\"color_temp\"` w trybie spania. 🌙", "sleep_color_temp": "Temperatura barwowa w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `color_temp`) (w Kelwinach). 😴", - "sleep_rgb_color": "Kolor RGB w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈", - "sleep_transition": "Długość tranzycji gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴", + "sleep_rgb_color": "Kolor RGB w trybie spania (używane, gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈", + "sleep_transition": "Długość przejścia, gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴", "sunrise_time": "Ustaw stały czas wschodu słońca (HH:MM:SS). 🌅", "sleep_brightness": "Jasność świateł w trybie spania (w procentach). 😴", "min_sunrise_time": "Ustaw czas najwcześniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na opóźnienie wschodu słońca. 🌅", @@ -62,19 +62,19 @@ "sunrise_offset": "Dostosuj czas wschodu słońca - przesunięcie o +/- sekund. ⏰", "sunset_time": "Ustaw stały czas zachodu słońca (HH:MM:SS). 🌇", "min_sunset_time": "Ustaw czas najwcześniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na opóźnienie zachodu słońca. 🌇", - "brightness_mode": "Tryb ustawianie jasności. Dostępne opcje `default`, `linear`, i `tanh` (używa `brightness_mode_time_dark` i `brightness_mode_time_light`). 📈", + "brightness_mode": "Tryb ustawiania jasności. Dostępne opcje to `default`, `linear` i `tanh` (używa `brightness_mode_time_dark` i `brightness_mode_time_light`). 📈", "max_sunset_time": "Ustaw czas najpóźniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na przyspieszenie zachodu słońca. 🌇", "sunset_offset": "Dostosuj czas zachodu słońca - przesunięcie o +/- sekund. ⏰", - "brightness_mode_time_dark": "(pomijany gdy`brightness_mode='default'`) Czas w sekundach kiedy jasność będzie: zwiększana przed wschodem słońca/zmniejszana po zachodzie słońca. 📈📉", - "brightness_mode_time_light": "(pomijany gdy`brightness_mode='default'`) Czas w sekundach kiedy jasność będzie: zwiększana po wschodzie słońca/zmniejszana przed zachodem słońca. 📈📉", - "autoreset_control_seconds": "Czas po którym manualna kontrola zostanie wyłączona (w sekundach). Ustaw 0 aby wyłączyć. ⏲️", + "brightness_mode_time_dark": "(Pomijany, gdy `brightness_mode='default'`). Czas w sekundach, kiedy jasność będzie zwiększana przed wschodem słońca/zmniejszana po zachodzie słońca. 📈📉", + "brightness_mode_time_light": "(Pomijany, gdy `brightness_mode='default'`). Czas w sekundach, kiedy jasność będzie zwiększana po wschodzie słońca/zmniejszana przed zachodem słońca. 📈📉", + "autoreset_control_seconds": "Czas, po którym manualna kontrola zostanie wyłączona (w sekundach). Ustaw 0, aby wyłączyć. ⏲️", "send_split_delay": "Opóźnienie (w ms) pomiędzy `separate_turn_on_commands` dla świateł, które nie akceptują jednoczesnego ustawiania jasności i koloru. ⏲️", - "adapt_delay": "Czas (w sekundach) pomiędzy włączeniem światła a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Morze pomóc zredukować migotanie. ⏲️" + "adapt_delay": "Czas (w sekundach) pomiędzy włączeniem światła, a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Może pomóc zredukować migotanie. ⏲️" } } }, "error": { - "option_error": "Błędne opcje", + "option_error": "Błędna opcja", "entity_missing": "Jednego lub więcej wybranych świateł nie można znaleźć w Home Assistant" } }, @@ -86,16 +86,16 @@ "description": "`entity_id` przełącznika, którego ustawienia mają być zastosowane. 📝" }, "lights": { - "description": "Światło(albo lista świateł), do których mają być zastosowane ustawieniam 💡" + "description": "Światło (albo lista świateł), do których mają być zastosowane ustawienia. 💡" }, "transition": { - "description": "Długość tranzycji do nowego stanu w sekundach. 🕑" + "description": "Długość przejścia do nowego stanu (w sekundach). 🕑" }, "adapt_color": { - "description": "Czy adaptować kolor światła" + "description": "Czy adaptować kolor światła. 🌈" }, "adapt_brightness": { - "description": "Czy adaptować jasność swiatła. 🌞" + "description": "Czy adaptować jasność światła. 🌞" }, "prefer_rgb_color": { "description": "Czy w miarę możliwości preferować regulację kolorów RGB zamiast temperatury barwowej światła. 🌈" @@ -106,16 +106,16 @@ } }, "set_manual_control": { - "description": "Zaznacza czy światło jest 'recznie sterowane'.", + "description": "Zaznacza czy światło jest \"ręcznie sterowane\".", "fields": { "entity_id": { - "description": "`entity_id` encji przełącznika, w której należy odznaczyć flagę `ręczne sterowanie`. 📝" + "description": "`entity_id` encji przełącznika, w której należy zaznaczyć/odznaczyć flagę `ręczne sterowanie`. 📝" }, "lights": { - "description": "`entity_id` świateł, dla których należy odznaczyć flagę `ręczne sterowanie`.💡Gdy lista będzie pusta wszystkie światła będą brane pod uwagę." + "description": "`entity_id` świateł, dla których należy odznaczyć flagę `ręczne sterowanie`. 💡Gdy lista będzie pusta wszystkie światła będą brane pod uwagę." }, "manual_control": { - "description": "Dodaj (\"true\") albo usuń (\"false\") światło z listy \"ręczne sterowanie\". 🔒" + "description": "Czy dodać (\"true\"), czy usunąć (\"false\") światło z listy \"ręczne sterowanie\". 🔒" } } }, @@ -123,16 +123,16 @@ "description": "Zmienia dowolny parametr w przełączniku. Wszystkie opcje są takie same jak w konfiguracji.", "fields": { "entity_id": { - "description": "ID encji przełacznika. 📝" + "description": "ID encji przełącznika. 📝" }, "use_defaults": { - "description": "Jak mają się zmienić ustawienia, które nie są wyszczególnione w tym wywołaniu. Opcje: \"current\" (domyślne, pozostawia obecne ustawienia), \"factory\" (przywraca ustawienia z dokumentacji), albo \"configuration\" (przywraca wartości z konfiguracji przelacznika). ⚙️" + "description": "Jak mają się zmienić ustawienia, które nie są wyszczególnione w tym wywołaniu. Opcje: \"current\" (domyślne, pozostawia obecne ustawienia), \"factory\" (przywraca ustawienia z dokumentacji), albo \"configuration\" (przywraca wartości z konfiguracji przełącznika). ⚙️" }, "include_config_in_attributes": { - "description": "Gdy włączone `true` pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant." + "description": "Gdy włączone (`true`) pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant." }, "sleep_transition": { - "description": "Długość tranzycji gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴" + "description": "Długość przejścia, gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴" }, "max_brightness": { "description": "Maksymalna jasność (w procentach). 💡" @@ -141,37 +141,37 @@ "description": "Czy włączyć światła, które są aktualnie wyłączone? 🔆" }, "initial_transition": { - "description": "Długość pierwszej tranzycji gdy światło zostanie włączone z `off` na `on`(w sekundach). ⏲️" + "description": "Długość pierwszego przejścia, gdy światło zostanie przełączone z `off` na `on` (w sekundach). ⏲️" }, "min_sunset_time": { "description": "Ustaw czas najwcześniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na opóźnienie zachodu słońca. 🌇" }, "take_over_control": { - "description": "Wyłącz adaptowanie oświetlenia gdy inna usługa wywoła `light.turn_on` gdy oświetlenie jest już włączone. Zauważ, że to wywołuje `homeassistant.update_entity` co`interval`! 🔒" + "description": "Wyłącz adaptowanie oświetlenia, kiedy inna usługa wywoła `light.turn_on`, gdy oświetlenie jest już włączone. Zauważ, że to wywołuje `homeassistant.update_entity` co `interval`! 🔒" }, "transition": { - "description": "Długość tranzycji do nowego stanu w sekundach. 🕑" + "description": "Długość przejścia do nowego stanu (w sekundach). 🕑" }, "autoreset_control_seconds": { - "description": "Czas po którym manualna kontrola zostanie wyłączona (w sekundach). Ustaw 0 aby wyłączyć. ⏲️" + "description": "Czas, po którym manualna kontrola zostanie wyłączona (w sekundach). Ustaw 0 aby wyłączyć. ⏲️" }, "adapt_delay": { - "description": "Czas (w sekundach) pomiędzy włączeniem światła a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Morze pomóc zredukować migotanie. ⏲️" + "description": "Czas (w sekundach) pomiędzy włączeniem światła, a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Może pomóc zredukować migotanie. ⏲️" }, "max_color_temp": { "description": "Najzimniejsza temperatura barwowa (w Kelwinach). ❄️" }, "min_brightness": { - "description": "Minimalna jasność (w procentach). 💡" + "description": "Minimalna jasność (w procentach). 💡" }, "min_color_temp": { "description": "Najcieplejsza temperatura barwowa (w Kelwinach). 🔥" }, "only_once": { - "description": "Adaptuj światło tylko podczas włączenia (`true`) lub adaptuj cały czas (`false`). 🔄" + "description": "Adaptuj światło tylko podczas włączania (`true`) lub adaptuj cały czas (`false`). 🔄" }, "prefer_rgb_color": { - "description": "Czy w miarę możliwości preferować regulację kolorów RGB zamiast temperatury barwowej światła.. 🌈" + "description": "Czy w miarę możliwości preferować regulację kolorów RGB zamiast temperatury barwowej światła. 🌈" }, "separate_turn_on_commands": { "description": "Używaj oddzielnych wywołań `light.turn_on` dla koloru i jasności, wymagane dla niektórych typów świateł. 🔀" @@ -183,10 +183,10 @@ "description": "Jasność świateł w trybie spania (w procentach). 😴" }, "sleep_rgb_or_color_temp": { - "description": "Użyj `\"rgb_color\"` albo `\"color_temp\"`trybie spania. 🌙" + "description": "Użyj `\"rgb_color\"` albo `\"color_temp\"` w trybie spania. 🌙" }, "sleep_rgb_color": { - "description": "Kolor RGB w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈" + "description": "Kolor RGB w trybie spania (używane, gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈" }, "sleep_color_temp": { "description": "Temperatura barwowa w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `color_temp`) (w Kelwinach). 😴" @@ -207,7 +207,7 @@ "description": "Ustaw czas najpóźniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na przyspieszenie wschodu słońca. 🌅" }, "detect_non_ha_changes": { - "description": "Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan 'on', co może powodować nieoczekiwane włączenia światła. Wyłącz to ustawienie jeżeli doświadczasz takich objawów." + "description": "Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan \"on\", co może powodować nieoczekiwane włączanie się świateł. Wyłącz to ustawienie, jeżeli doświadczasz takich objawów." } } } From 6271dda2cfe6eaadcd1bf9ceafec17bed2ff63f1 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sun, 3 Dec 2023 13:02:19 +0100 Subject: [PATCH 0711/1077] Translated using Weblate (Italian) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Gabriele Baldassarre Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/it/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/it.json | 152 +++++++++++++++++- 1 file changed, 149 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/it.json b/custom_components/adaptive_lighting/translations/it.json index 3f8c19e8..015f4f6c 100644 --- a/custom_components/adaptive_lighting/translations/it.json +++ b/custom_components/adaptive_lighting/translations/it.json @@ -42,11 +42,35 @@ "transition": "Tempo di transizione quando viene applicata una modifica alle luci (secondi)", "adapt_delay": "Tempo di attesa tra l'accensione della luce, e Illuminazione Adattiva che applica le modifiche allo stato della luce. Potrebbe evitare sfarfallii.", "transition_until_sleep": "transition_until_sleep: Quando abilitato, Adaptive Lighting tratterà le impostazioni di sleep come valori minimi, facendo la transizione a questi valori dopo il tramonto. 🌙", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quando accendi le luci la prima volta. Se impostato su `true`, AL adatta solo se `light.turn_on è invocato senza specificare il colore o la luminosità. ❌🌈 Questo, per esempio, previene l'adattamento quando si attiva una scena. Se `false`, AL adatta indipendentemente dalla presenza di colore o luminosità nei `service_data` iniziali. Necessita che `take_over_control` sia abilitato. 🕵️ " + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quando accendi le luci la prima volta. Se impostato su `true`, AL adatta solo se `light.turn_on è invocato senza specificare il colore o la luminosità. ❌🌈 Questo, per esempio, previene l'adattamento quando si attiva una scena. Se `false`, AL adatta indipendentemente dalla presenza di colore o luminosità nei `service_data` iniziali. Necessita che `take_over_control` sia abilitato. 🕵️ ", + "multi_light_intercept": "multi_light_intercept: Intercetta e adatta le chiamate a `light.turn_on` destinate a più luci. ➗⚠️ Questo potrebbe causare la divisione della singola chiamata `light.turn_on`in più chiamate, ad esempio quando le luci sono su switch diversi. Richiede che l'opzione `intercept` sia abilitata.", + "skip_redundant_commands": "skip_redundant_commands: Salta l'invio di comandi di adattamento rivolti ad entità in cui lo stato desiderato è identico allo stato attuale. Minimizza il traffico sulla rete e migliora la responsività dell'adattamento in alcune situazioni. 📉 Disabilitalo se lo stato reale delle luci va fuori sincrono con quello registrato da HA.", + "intercept": "intercept: Intercetta e adatta alle chiamate a`light.turn_on` per abilitare adattamenti istantanei di colore e luminosità. 🏎️ Disabilita per quelle luci che non supportano l'impostazione di luci e colori a seguito dell'evento `light.turn_on`.", + "include_config_in_attributes": "include_config_in_attributes: Quando impostato come `true`, mostra tutte le opzioni come attributi dello switch in Home Assistant. 📝" }, "data_description": { "sunrise_offset": "Regola il momento dell'alba con un offset positivo o negativo. ⏰", - "sunset_offset": "Modifica l'orario del tramonto con un offset in secondi positivo o negativo. ⏰" + "sunset_offset": "Modifica l'orario del tramonto con un offset in secondi positivo o negativo. ⏰", + "sleep_rgb_or_color_temp": "Usa uno tra `\"rgb_color\"` or`\"color_temp\"` in modalità notturna. 🌙", + "sleep_color_temp": "Temperatura colore per la modalità notturna (utilizzata quando `sleep_rgb_or_color_temp` vale `color_temp`), espressa in Kelvin. 😴", + "sleep_transition": "Durata della transizione al passaggio da/verso la modalità luce notturna, espressa in secondi. 😴", + "autoreset_control_seconds": "Rimuovi automaticamente il colore impostato manualmente dopo un certo numero di secondi. Imposta 0 per disabilitare. ⏲️", + "min_sunset_time": "Imposta il minimo orario per il tramonto (HH:MM:SS), per eventualmente ritardarlo. 🌅", + "sleep_brightness": "Luminosità percentuale delle luci in modalità notturna. 😴", + "min_sunrise_time": "Imposta il minimo orario per l'alba (HH:MM:SS), per eventualmente ritardarla. 🌅", + "interval": "Frequenza di adattamento delle luci, espressa in secondi. 🔄", + "adapt_delay": "Tempo di attesa (in secondi) tra l'accensione della luce e i cambiamenti indotti da Illuminazione Adattativa. Può contribuire a ridurre lo sfarfallio. ⏲️", + "sleep_rgb_color": "Colore RGB in modalità notturna (usato quando `sleep_rgb_or_color_temp` è impostato su \"rgb_color\"). 🌈", + "transition": "Durata della transizione quando le luci cambiano, espressa in secondi. 🕑", + "brightness_mode": "Modalità per la luminosità da utilizzare. I valori possibili sono `default`, `linear`, and `tanh` (usa`brightness_mode_time_dark` e `brightness_mode_time_light`). 📈", + "brightness_mode_time_light": "La durata, espressa in secondi, della variazione di luminosità durante le albe/tramonti (ignorato se `brightness_mode='default'`). 📈📉", + "sunset_time": "Imposta un orario fisso (HH:MM:SS) per il tramonto. 🌇", + "max_sunset_time": "Imposta il massimo orario per il tramonto (HH:MM:SS), in modo da eventualmente anticiparlo. 🌇", + "sunrise_time": "Imposta un orario fisso (HH:MM:SS) per l'alba. 🌅", + "initial_transition": "Durata della prima transizione quando le luci passano dallo stato `off` a `on`, espressa in secondi. ⏲️", + "brightness_mode_time_dark": "La durata, espressa in secondi, della variazione di luminosità durante le albe/tramonti (ignorato se `brightness_mode='default'`). 📈📉", + "max_sunrise_time": "Imposta l'orario massimo per l'alba (HH:MM:SS), in modo da eventualmente anticiparla. 🌅", + "send_split_delay": "Ritardo (ms) tra i comandi, per le luci che hanno `separate_turn_on_commands` e che non supportano l'impostazione simultanea di luminosità e colore. ⏲️" } } }, @@ -63,8 +87,130 @@ }, "sunrise_offset": { "description": "Modifica l'orario dell'alba con un offset in secondi positivo o negativo." + }, + "sleep_brightness": { + "description": "Luminosità percentuale delle luci in modalità notturna. 😴" + }, + "detect_non_ha_changes": { + "description": "Individua e arresta l'adattamento per i cambiamenti di stato diversi da `light.turn_on`. Richiede che `take_over_control` sia abilitato. 🕵️ Avvertenza: ⚠️ Alcune luci potrebbero riportare erroneamente lo stato di 'on', il che potrebbe causarne accensione inaspettata. Disabilita questa opzione se riscontri questa casistica." + }, + "max_sunrise_time": { + "description": "Imposta l'orario massimo per l'alba (HH:MM:SS), in modo da eventualmente anticiparla. 🌅" + }, + "sleep_color_temp": { + "description": "Temperatura colore per la modalità notturna (utilizzata quando `sleep_rgb_or_color_temp` vale `color_temp`), espressa in Kelvin. 😴" + }, + "min_brightness": { + "description": "Minima luminosità, in percentuale.💡" + }, + "min_color_temp": { + "description": "Temperatura colore più calda, espressa in Kelvin.🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "Usa uno tra `\"rgb_color\"` or`\"color_temp\"` in modalità notturna. 🌙" + }, + "turn_on_lights": { + "description": "Seleziona per accedere le luci, qualora fossero spente. 🔆" + }, + "initial_transition": { + "description": "Durata della prima transizione quando le luci passano dallo stato `off` a `on`, espressa in secondi. ⏲️" + }, + "entity_id": { + "description": "ID entità dello switch. 📝" + }, + "sunrise_time": { + "description": "Imposta un orario fisso (HH:MM:SS) per l'alba. 🌅" + }, + "include_config_in_attributes": { + "description": "Quando impostato su `true`, tutte le opzioni saranno visibili come attributi dello switch in Home Assistant. 📝" + }, + "max_brightness": { + "description": "Massima luminosità, in percentuale.💡" + }, + "sleep_rgb_color": { + "description": "Colore RGB in modalità notturna (usato quando `sleep_rgb_or_color_temp` è impostato su \"rgb_color\"). 🌈" + }, + "take_over_control": { + "description": "Disattiva Illuminazione Adattativa se un'altra sorcente chiama `light.turn_on` mentre le luci sono accese e soggette all'adattamento. Tieni conto che questo comporterà una chiamata a `homeassistant.update_entity` ad ogni `interval`! 🔒" + }, + "sleep_transition": { + "description": "Durata della transizione al passaggio da/verso la modalità luce notturna, espressa in secondi. 😴" + }, + "autoreset_control_seconds": { + "description": "Rimuovi automaticamente il colore impostato manualmente dopo un certo numero di secondi. Imposta 0 per disabilitare. ⏲️" + }, + "adapt_delay": { + "description": "Tempo di attesa (in secondi) tra l'accensione della luce e i cambiamenti indotti da Illuminazione Adattativa. Può contribuire a ridurre lo sfarfallio. ⏲️" + }, + "use_defaults": { + "description": "Imposta ai valori predefiniti non specificati nella chiamata al servizio. Opzioni possibili: \"current\" (predefinito, mantiene i valori correnti), \"factory\" (reimposta su un valore predefinito documentato) p \"configuration\" (reimposta sui valori predefiniti dello switch). ⚙️" + }, + "separate_turn_on_commands": { + "description": "Usa chiamate distinte a `light.turn_on` per il colore e per la luminosità, necessario per alcuni tipi di luci. 🔀" + }, + "prefer_rgb_color": { + "description": "Seleziona per preferire gli aggiustamenti di colore mediante RGB piuttosto che tramite temperatura colore, dove possibile. 🌈" + }, + "max_color_temp": { + "description": "Temperatura colore più fredda, espressa in Kelvin. ❄️" + }, + "sunset_offset": { + "description": "Modifica l'orario del tramonto con un offset positivo o negativo in secondi. ⏰" + }, + "send_split_delay": { + "description": "Ritardo (ms) tra i comandi, per le luci che hanno `separate_turn_on_commands` e che non supportano l'impostazione simultanea di luminosità e colore. ⏲️" + }, + "sunset_time": { + "description": "Imposta un orario fisso (HH:MM:SS) per il tramonto. 🌇" + }, + "transition": { + "description": "Durata della transizione quando le luci cambiano, espressa in secondi. 🕑" + }, + "min_sunset_time": { + "description": "Imposta il minimo orario per il tramonto (HH:MM:SS), per eventualmente ritardarlo. 🌅" } - } + }, + "description": "Cambia tutte le impostazioni che desideri nello switch. Le opzioni sono le stesse presenti nella procedura di configurazione." + }, + "apply": { + "fields": { + "entity_id": { + "description": "L'`entity_id` dello switch a cui si applicano le impostazioni.📝" + }, + "adapt_brightness": { + "description": "Seleziona per adattare la luminosità della luce. 🌞" + }, + "turn_on_lights": { + "description": "Seleziona per accedere le luci, qualora fossero spente. 🔆" + }, + "adapt_color": { + "description": "Seleziona per adattare il colore, per le luci che lo supportano. 🌈" + }, + "prefer_rgb_color": { + "description": "Seleziona per preferire gli aggiustamenti di colore mediante RGB piuttosto che tramite temperatura colore, dove possibile. 🌈" + }, + "lights": { + "description": "Una luce (o un insieme di luci) a cui applicare le impostazioni. 💡" + }, + "transition": { + "description": "Durata della transizione quando le luci cambiano, espressa in secondi. 🕑" + } + }, + "description": "Applica le impostazioni correnti di Illuminazione Adattativa alle luci." + }, + "set_manual_control": { + "fields": { + "manual_control": { + "description": "Seleziona per aggiungere (\"true\") o rimuovere (\"false\") la luce dalla lista di quelle controllate manualmente . 🔒" + }, + "entity_id": { + "description": "L'`entity_id` dello switch che controlla se la luce deve operare in modalità manualmente controllata.📝" + }, + "lights": { + "description": "entity_id delle luci, se non specificato, tutte le luci nello switch sono selezionate. 💡" + } + }, + "description": "Evidenzia quando una luce è controllata manualmente." } } } From 8c6689cdf2a2e896ea1b62a8567d3bbe2743eb02 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sun, 3 Dec 2023 13:02:19 +0100 Subject: [PATCH 0712/1077] Translated using Weblate (Dutch) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Pepijn Baart Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/ Translation: Adaptive Lighting/Adaptive Lighting --- .../adaptive_lighting/translations/nl.json | 108 ++++++++++++++++-- 1 file changed, 96 insertions(+), 12 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json index 7a9b7896..16fe3059 100644 --- a/custom_components/adaptive_lighting/translations/nl.json +++ b/custom_components/adaptive_lighting/translations/nl.json @@ -18,16 +18,16 @@ "step": { "init": { "title": "Adaptieve verlichting instellingen", - "description": "Alle instellingen voor een Adaptieve verlichting component. De optienamen komen overeen met de YAML-instellingen. Er worden geen opties weergegeven als u het item adaptive_lighting hebt gedefinieerd in uw YAML-configuratie.", + "description": "Alle instellingen voor een Adaptieve verlichting component. De optienamen komen overeen met de YAML-instellingen. Er worden geen opties weergegeven als u het item `adaptive_lighting` hebt gedefinieerd in uw YAML-configuratie.\nVoor een demonstratie met interactieve grafieken, parameters en effecten, bezoek [deze web applicatie](https://basnijholt.github.io/adaptive-lighting). Voor verdere details, bekijk de [officiële documentatie](https://github.com/basnijholt/adaptive-lighting#readme).", "data": { - "lights": "Lichten", + "lights": "Lampen: lijst van `light` entiteiten om te bedienen (kan leeg zijn). 🌟", "initial_transition": "initial_transition: Wanneer lichten van 'uit' naar 'aan' gaan. (seconden)", "sleep_transition": "sleep_transition: Wanneer 'sleep_state' verandert. (seconden)", "interval": "interval: Tijd tussen switch-updates. (seconden)", "max_brightness": "max_brightness: Hoogste helderheid van lichten tijdens een cyclus. (%)", "max_color_temp": "max_color_temp: Koudste tint van de kleurtemperatuurcyclus. (kelvin)", "min_brightness": "min_brightness: Laagste helderheid van lichten tijdens een cyclus. (%)", - "min_color_temp": "min_color_temp, Warmste tint van de kleurtemperatuurcyclus. (kelvin)", + "min_color_temp": "min_color_temp, Warmste tint van de kleurtemperatuurcyclus. (Kelvin)", "only_once": "only_once: pas de verlichting alleen aan wanneer u ze aanzet.", "prefer_rgb_color": "prefer_rgb_color: Gebruik waar mogelijk 'rgb_color' in plaats van 'color_temp'.", "separate_turn_on_commands": "separate_turn_on_commands: Scheid de commando's voor elk attribuut (kleur, helderheid, enz.) in 'light.turn_on' (vereist voor sommige lampen).", @@ -42,12 +42,12 @@ "sunset_offset": "sunset_offset: Hoe lang voor(-) of na(+) zonsondergang uitvoeren (+/- seconden)", "sunset_time": "sunset_time: handmatige onderdrukking van de zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", "min_sunset_time": "min_sunset_time: handmatige onderdrukking van de minimale zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsondergangstijd op uw locatie gebruikt (UU:MM:SS)", - "take_over_control": "take_over_control: Als iets anders dan Adaptive Lighting 'light.turn_on' roept wanneer een lamp al aan is, stop dan met het aanpassen van het licht totdat het (of de schakelaar) uit -> aan gaat.", + "take_over_control": "take_over_control: Als iets anders dan Adaptieve verlichting 'light.turn_on' roept wanneer een lamp al aan is, stop dan met het aanpassen van het licht totdat het (of de schakelaar) uit -> aan gaat.", "detect_non_ha_changes": "detect_non_ha_changes: detecteert alle >10% wijzigingen aan de lichten (ook buiten HA), vereist dat 'take_over_control' is ingeschakeld (heet 'homeassistant.update_entity' elke 'interval'!)", "transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)", "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Bij het initieel inschakelen van de lampen. Als dit op `true` is ingesteld, past AL alleen aan als `light.turn_on` wordt aangeroepen zonder een kleur of helderheid te specificeren. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Als het `false` is, past AL aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. `take_over_control` moet ingeschakeld zijn. 🕵️ ", - "transition_until_sleep": "transition_until_sleep: Wanneer ingeschakeld, zal Adaptieve Verlichting de slaapinstellingen behandelen als het minimum, overgaand naar deze waarden na zonsondergang. 🌙", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Bij het initieel inschakelen van de lampen. Als dit op `true` is ingesteld, past Av alleen aan als `light.turn_on` wordt aangeroepen zonder een kleur of helderheid te specificeren. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Als het `false` is, past Av aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. `take_over_control` moet ingeschakeld zijn. 🕵️ ", + "transition_until_sleep": "transition_until_sleep: Wanneer ingeschakeld, zal Adaptieve verlichting de slaapinstellingen behandelen als het minimum, overgaand naar deze waarden na zonsondergang. 🌙", "skip_redundant_commands": "skip_redundant_commands: Sla het verzenden van aanpassingscommando's over waarvan de doelstatus al gelijk is aan de bekende status van de lamp. Minimaliseert netwerkverkeer en verbetert de responsiviteit van de aanpassing in sommige situaties. 📉Schakel uit als de fysieke lichtstatus niet meer synchroon loopt met de door HA geregistreerde status.", "intercept": "intercept: Onderschep en pas `light.turn_on` oproepen aan om directe kleur- en helderheidsaanpassing mogelijk te maken. 🏎️ Schakel uit voor lampen die `light.turn_on` niet ondersteunen met kleur en helderheid.", "include_config_in_attributes": "include_config_in_attributes: Toon alle opties als attributen op de schakelaar in Home Assistant wanneer ingesteld op `true`. 📝", @@ -60,11 +60,22 @@ "sleep_transition": "Duur van de overgang in seconden, als slaapstand wordt geactiveerd. 😴", "autoreset_control_seconds": "Herstel de handmatige bediening automatisch na een aantal seconden. Stel in op 0 om uit te schakelen.", "sleep_brightness": "Helderheidspercentage van lampen in slaapstand. 😴", - "sleep_color_temp": "Kleurtemperatuur in slaapmodus (gebruikt wanneer `sleep_rgb_or_color_temp` `color_temp` is) in Kelvin. 😴", + "sleep_color_temp": "Kleurtemperatuur in slaapmodus (gebruikt wanneer `sleep_rgb_or_color_temp` gelijk is aan `color_temp`) in Kelvin. 😴", "brightness_mode": "Helderheidsmodus om te gebruiken. Mogelijke waarden zijn `default`, `linear` en `tanh` (gebruikt `brightness_mode_time_dark` en `brightness_mode_time_light`). 📈", "send_split_delay": "Vertraging (ms) tussen `separate_turn_on_commands` voor lampen die geen gelijktijdige helderheids- en kleurinstelling ondersteunen. ⏲️", - "transition": "Duur van de overgang, in seconden. 🕑", - "initial_transition": "Duur van de eerste overgang wanneer de lampen van `uit` naar `aan`gaan, in seconden. ⏲️" + "transition": "Duur van de overgang, in seconden, als lampen aanpassen. 🕑", + "initial_transition": "Duur van de eerste overgang wanneer de lampen van `uit` naar `aan`gaan, in seconden. ⏲️", + "sleep_rgb_or_color_temp": "Gebruik één van beide `\"rgb_color\"` of `\"color_temp\"` in slaapstand. 🌙", + "min_sunset_time": "Stel de tijd (HH:MM:SS) in voor de meest vroege virtuele zonsondergang, maakt latere zonsondergangen mogelijk. 🌇", + "min_sunrise_time": "Stel de tijd (HH:MM:SS) in voor de meest vroege virtuele zonsopkomst, maakt latere zonsopkomsten mogelijk. 🌅", + "adapt_delay": "Wachttijd in (seconden) tussen het aanzetten van de lamp en het toepassen van Adaptieve verlichting veranderingen. Het kan helpen om knipperen tegen te gaan. ⏲️", + "sleep_rgb_color": "RGB kleur in slaapstand (wordt gebruikt wanneer `sleep_rgb_or_color_temp` gelijk is aan \"rgb_color\"). 🌈", + "brightness_mode_time_light": "(Negeer wanneer `brightness_mode='default'`) De duur in seconden van oplopende/aflopende helderheid na/voor zonsopkomst/zonsondergang. 📈📉.", + "sunset_time": "Stel een vaste tijd (HH:MM:SS) in voor zonsondergang. 🌇", + "max_sunset_time": "Stel de tijd (HH:MM:SS) in voor de laatste virtuele zonsondergang, maakt eerdere zonsondergangen mogelijk. 🌇", + "sunrise_time": "Stel een vaste tijd (HH:MM:SS) in voor zonsopkomst. 🌅", + "brightness_mode_time_dark": "(Negeer wanneer `brightness_mode='default'`) De duur in seconden van oplopende/aflopende helderheid na/voor zonsopkomst/zonsondergang. 📈📉.", + "max_sunrise_time": "Stel de tijd (HH:MM:SS) in voor de laatste virtuele zonsopkomst, maakt eerdere zonsopkomsten mogelijk. 🌅" } } }, @@ -110,7 +121,58 @@ "description": "Duur van de eerste overgang wanneer de lampen van `uit` naar `aan`gaan, in seconden. ⏲️" }, "take_over_control": { - "description": "Schakel Adaptive Lighting uit wanneer een andere bron `light.turn_on` aanroept terwijl de lampen aan staan en worden aangepast. N.b. dit zal `homeassistant.update_entity` elke `interval` uitvoeren." + "description": "Schakel Adaptieve verlichting uit wanneer een andere bron `light.turn_on` aanroept terwijl de lampen aan staan en worden aangepast. N.b. dit zal `homeassistant.update_entity` elke `interval` uitvoeren." + }, + "detect_non_ha_changes": { + "description": "Detecteert en stopt aanpassingen voor niet-`light.turn_on` state veranderingen. `take_over_control` moet actief zijn. 🕵️ Let op:⚠️Sommige lampen kunnen incorrect een 'on' state weergeven, wat resulteert in lampen die onverwacht aan gaan. Schakel deze feature uit wanneer deze fout zich voordoet." + }, + "max_sunrise_time": { + "description": "Stel de tijd (HH:MM:SS) in voor de laatste virtuele zonsopkomst, maakt eerdere zonsopkomsten mogelijk. 🌅" + }, + "min_brightness": { + "description": "Minimale helderheid in procenten. 💡" + }, + "min_color_temp": { + "description": "Meest warme kleurtemperatuur ins Kelvin. 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "Gebruik één van beide `\"rgb_color\"` of `\"color_temp\"` in slaapstand. 🌙" + }, + "turn_on_lights": { + "description": "Of de lampen moeten worden aangezet die momenteel uit zijn.🔆" + }, + "sunrise_time": { + "description": "Stel een vaste tijd (HH:MM:SS) in voor zonsopkomst. 🌅" + }, + "include_config_in_attributes": { + "description": "Toon alle opties als attributen bij de schakelaar in Home Assistant wanneer ingesteld op `true`. 📝" + }, + "max_brightness": { + "description": "Maximale helderheid in procenten. 💡" + }, + "sleep_rgb_color": { + "description": "RGB kleur in slaapstand (wordt gebruikt wanneer `sleep_rgb_or_color_temp` gelijk is aan \"rgb_color\"). 🌈" + }, + "adapt_delay": { + "description": "Wachttijd in (seconden) tussen het aanzetten van de lamp en het toepassen van Adaptieve verlichting veranderingen. Het kan helpen om knipperen tegen te gaan. ⏲️" + }, + "use_defaults": { + "description": "Stelt niet gespecificeerde waarden in voor deze service call. Opties: \"current\" (standaard, behoudt huidige waarden), \"factory\" (herstelt de gedocumenteerde standaardwaarden), of \"configuration\" (zet instellingen terug naar de standaardwaarden in de configuratie ). ⚙️" + }, + "separate_turn_on_commands": { + "description": "Gebruik aparte `light.turn_on` calls voor kleur en helderheid, dit is nodig voor bepaalde lampen. 🔀" + }, + "prefer_rgb_color": { + "description": "Geef de voorkeur aan RGB kleuren boven de kleurtemperatuur van de lamp wanneer mogelijk. 🌈" + }, + "send_split_delay": { + "description": "Vertraging (ms) tussen `separate_turn_on_commands` voor lampen die geen gelijktijdige helderheids- en kleurinstelling ondersteunen. ⏲️" + }, + "sunset_time": { + "description": "Stel een vaste tijd (HH:MM:SS) in voor zonsondergang. 🌇" + }, + "min_sunset_time": { + "description": "Stel de tijd (HH:MM:SS) in voor de meest vroege virtuele zonsondergang, maakt latere zonsondergangen mogelijk. 🌇" } }, "description": "Wijzig alle gewenste instellingen in de schakelaar. Alle opties hier zijn hetzelfde als in de configuratie." @@ -122,16 +184,38 @@ }, "transition": { "description": "Duur van de overgang in seconden, als lampen aanpassen. 🕑" + }, + "entity_id": { + "description": "De `entity_id` van de schakelaar met de toe te passen instellingen. 📝" + }, + "adapt_brightness": { + "description": "Of de helderheid van het licht moet worden aangepast. 🌞" + }, + "turn_on_lights": { + "description": "Of de lampen moeten worden aangezet die momenteel uit zijn.🔆" + }, + "adapt_color": { + "description": "Aanpassen aan de kleur van de omringende verlichting. 🌈" + }, + "prefer_rgb_color": { + "description": "Geef de voorkeur aan RGB kleuren boven de kleurtemperatuur van de lamp wanneer mogelijk. 🌈" } }, - "description": "Past de huidige Adaptive Lights instellingen toe op de lampen." + "description": "Past de huidige Adaptieve verlichting instellingen toe op de lampen." }, "set_manual_control": { "fields": { "lights": { "description": "entiteit_id(s) van de lamp(en), indien niets wordt gespecificeerd, worden alle lampen in de schakelaar geselecteerd. 💡" + }, + "manual_control": { + "description": "Of de lamp moet worden toegevoegd (`\"true\"`) of verwijderd (`\"false\"`) van de `manual_control` lijst. 🔒" + }, + "entity_id": { + "description": "De `entity_id` van de schakelaar waarvan het licht moet worden (on)gemarkeerd als `manually controlled`. 📝" } - } + }, + "description": "Geef aan of een lamp 'manually controlled' is." } } } From 8dfef6d65bfe68c79dc395f5a1d616a76dbf4155 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 23:48:51 -0800 Subject: [PATCH 0713/1077] docs: add olekbruks as a contributor for translation (#863) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 7ab14c4d..64752f00 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -602,6 +602,15 @@ "contributions": [ "code" ] + }, + { + "login": "olekbruks", + "name": "Olek Bruks", + "avatar_url": "https://avatars.githubusercontent.com/u/8738016?v=4", + "profile": "https://github.com/olekbruks", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 93d2703c..4ce97ad8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-65-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-66-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -548,6 +548,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 5f2a67edd8f4462f969bb3f71873e8c27528f549 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 23:51:11 -0800 Subject: [PATCH 0714/1077] docs: add theclue as a contributor for translation (#864) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 64752f00..6df562be 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -611,6 +611,15 @@ "contributions": [ "translation" ] + }, + { + "login": "theclue", + "name": "Gabriele Baldassarre", + "avatar_url": "https://avatars.githubusercontent.com/u/1724406?v=4", + "profile": "https://gabrielebaldassarre.com", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 4ce97ad8..d4e081c7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-66-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-67-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -549,6 +549,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 4d4da04eb7a1fe35b636599e245cf89575d98e84 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 23:51:46 -0800 Subject: [PATCH 0715/1077] docs: add pbaart as a contributor for translation (#865) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 6df562be..9d410308 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -620,6 +620,15 @@ "contributions": [ "translation" ] + }, + { + "login": "pbaart", + "name": "Pepijn Baart", + "avatar_url": "https://avatars.githubusercontent.com/u/2856849?v=4", + "profile": "https://github.com/pbaart", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index d4e081c7..54adc03e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-67-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-68-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -550,6 +550,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From bb29bcc6c5e2ae19030bda0fff77f1fad1d1e6ff Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 7 Dec 2023 23:52:02 -0800 Subject: [PATCH 0716/1077] Update manifest.json (#862) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index e93d3897..7461e73d 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.19.1" + "version": "1.20.0" } From a247a02b3745ceb742f92b597eb7dffa1766e1f2 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 23:58:14 -0800 Subject: [PATCH 0717/1077] docs: add kylebjordahl as a contributor for bug, and code (#866) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 9d410308..b38dac23 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -600,7 +600,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/3489222?v=4", "profile": "https://github.com/kylebjordahl", "contributions": [ - "code" + "code", + "bug" ] }, { diff --git a/README.md b/README.md index 54adc03e..544b03b5 100644 --- a/README.md +++ b/README.md @@ -547,7 +547,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark - + From c3e4b77c7f11ff18ecff9db75fa968df3f8d7582 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sat, 9 Dec 2023 07:46:12 +0100 Subject: [PATCH 0718/1077] Translations update from Hosted Weblate (#867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Russian) Currently translated at 80.3% (123 of 153 strings) Co-authored-by: Artem Pastukhov Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ru/ Translation: Adaptive Lighting/Adaptive Lighting * Added translation using Weblate (Japanese) Co-authored-by: Hosted Weblate Co-authored-by: pantan-cymk * Translated using Weblate (Hungarian) Currently translated at 48.3% (74 of 153 strings) Added translation using Weblate (Hungarian) Co-authored-by: Hosted Weblate Co-authored-by: LUKÁCS Miklós Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/hu/ Translation: Adaptive Lighting/Adaptive Lighting * Added translation using Weblate (Finnish) Co-authored-by: Eero Konttaniemi Co-authored-by: Hosted Weblate * Translated using Weblate (Slovak) Currently translated at 56.8% (87 of 153 strings) Added translation using Weblate (Slovak) Co-authored-by: Hosted Weblate Co-authored-by: Martin Štefany Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/sk/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Urdu) Currently translated at 100.0% (153 of 153 strings) Added translation using Weblate (Urdu) Co-authored-by: Hosted Weblate Co-authored-by: yousaf465 Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ur/ Translation: Adaptive Lighting/Adaptive Lighting --------- Co-authored-by: Artem Pastukhov Co-authored-by: pantan-cymk Co-authored-by: LUKÁCS Miklós Co-authored-by: Eero Konttaniemi Co-authored-by: Martin Štefany Co-authored-by: yousaf465 --- .../adaptive_lighting/translations/fi.json | 1 + .../adaptive_lighting/translations/hu.json | 41 ++++ .../adaptive_lighting/translations/ja.json | 1 + .../adaptive_lighting/translations/ru.json | 80 ++++++- .../adaptive_lighting/translations/sk.json | 52 +++++ .../adaptive_lighting/translations/ur.json | 202 ++++++++++++++++++ 6 files changed, 375 insertions(+), 2 deletions(-) create mode 100644 custom_components/adaptive_lighting/translations/fi.json create mode 100644 custom_components/adaptive_lighting/translations/hu.json create mode 100644 custom_components/adaptive_lighting/translations/ja.json create mode 100644 custom_components/adaptive_lighting/translations/sk.json create mode 100644 custom_components/adaptive_lighting/translations/ur.json diff --git a/custom_components/adaptive_lighting/translations/fi.json b/custom_components/adaptive_lighting/translations/fi.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/fi.json @@ -0,0 +1 @@ +{} diff --git a/custom_components/adaptive_lighting/translations/hu.json b/custom_components/adaptive_lighting/translations/hu.json new file mode 100644 index 00000000..1844163d --- /dev/null +++ b/custom_components/adaptive_lighting/translations/hu.json @@ -0,0 +1,41 @@ +{ + "options": { + "step": { + "init": { + "data_description": { + "sleep_color_temp": "Színhőmérséklet alvó üzemmódban (amikor a `sleep_rgb_or_color_temp` értéke `color_temp`) Kelvinben megadva. 😴" + }, + "data": { + "max_brightness": "max_brightness: Maximális fényerő százalékban megadva. 💡" + }, + "title": "Adaptív világítás beállításai" + } + } + }, + "title": "Adaptív világítás", + "services": { + "change_switch_settings": { + "fields": { + "entity_id": { + "description": "A kapcsoló entitásazonosítója. 📝" + }, + "max_brightness": { + "description": "Maximális fényerő százalékban megadva. 💡" + }, + "max_color_temp": { + "description": "Leghidegebb színhőmérséklet Kelvinben megadva. ❄️" + } + } + }, + "set_manual_control": { + "description": "Jelölje meg, hogy egy lámpa „kézi vezérlésű”-e." + } + }, + "config": { + "step": { + "user": { + "title": "Válasszon nevet az Adaptív világítás példánynak" + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/ja.json b/custom_components/adaptive_lighting/translations/ja.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/ja.json @@ -0,0 +1 @@ +{} diff --git a/custom_components/adaptive_lighting/translations/ru.json b/custom_components/adaptive_lighting/translations/ru.json index 2e160f9e..57a554e1 100644 --- a/custom_components/adaptive_lighting/translations/ru.json +++ b/custom_components/adaptive_lighting/translations/ru.json @@ -20,7 +20,7 @@ "title": "Настройки Adaptive Lighting", "description": "Все настройки компонента Adaptive Lighting. Названия опций соответствуют настройкам в YAML. Параметры не отображаются, если в конфигурации YAML определена запись adaptive_lighting.", "data": { - "lights": "Осветительные приборы", + "lights": "Осветительные приборы: список источников света, которыми нужно управлять (может быть пустым). 🌟", "initial_transition": "initial_transition: Начальный переход, когда свет переключается с 'off' на 'on'. (секунды)", "sleep_transition": "sleep_transition: Когда прибор переходит в Режима Сна (Sleep Mode) и 'sleep_state' изменяется. (секунды)", "interval": "interval: Интервал между обновлениями переключателя. (секунды)", @@ -40,7 +40,36 @@ "take_over_control": "take_over_control: Если что-либо, кроме Adaptive Lighting, вызывает службу 'light.turn_on', когда свет уже включен, прекратить адаптацию этого осветительного прибора, пока он (или переключатель) не переключится off -> on.", "detect_non_ha_changes": "detect_non_ha_changes: Обнаруживает все изменения на >10% примененные к освещению (также и из-за пределов Home Assistant), требует включения 'take_over_control' (вызывает 'homeassistant.update_entity' каждый 'interval'!)", "transition": "Время перехода при применении изменения к источникам света. (секунды)", - "adapt_delay": "Время ожидания между включением света и применением адаптации. Может помочь избежать мерцания. (секунды)" + "adapt_delay": "Время ожидания между включением света и применением адаптации. Может помочь избежать мерцания. (секунды)", + "multi_light_intercept": "multi_light_intercept: перехватывает и адаптирует вызовы `light.turn_on`, нацеленные на несколько источников света. ➗⚠️ Это может привести к разделению одного вызова `light.turn_on` на несколько вызовов, например, когда освещение включено в разные выключатели. Требуется, чтобы `перехват` был включен.", + "adapt_only_on_bare_turn_on": "Adapt_only_on_bare_turn_on: При первоначальном включении света. Если установлено значение «true», AL адаптируется только в том случае, если «light.turn_on» вызывается без указания цвета или яркости. ❌🌈 Это, например, предотвращает адаптацию при активации сцены. Если false, AL адаптируется независимо от наличия цвета или яркости в исходных service_data. Требуется включить take_over_control. 🕵️ ", + "skip_redundant_commands": "Skip_redundant_commands: Пропустить отправку команд адаптации, целевое состояние которых уже равно известному состоянию источника света. Минимизирует сетевой трафик и улучшает скорость адаптации в некоторых ситуациях. 📉Отключите, если физические состояния освещения не синхронизируются с записанным состоянием HA.", + "intercept": "intercept: перехватывать и адаптировать вызовы `light.turn_on` для обеспечения мгновенной адаптации цвета и яркости. 🏎️ Отключите источники света, которые не поддерживают `light.turn_on` с цветом и яркостью.", + "include_config_in_attributes": "include_config_in_attributes: отображать все параметры в качестве атрибутов на переключателе в Home Assistant, если установлено значение `true`. 📝" + }, + "data_description": { + "sleep_rgb_or_color_temp": "Используйте либо `\"rgb_color\"`, либо `\"color_temp\"` в спящем режиме. 🌙", + "sleep_color_temp": "Цветовая температура в спящем режиме (используется, когда параметр `sleep_rgb_or_color_temp` имеет значение `color_temp`) в Кельвинах. 😴", + "sleep_transition": "Длительность перехода при переключении \"спящего режима\" в секундах. 😴", + "autoreset_control_seconds": "Автоматический сброс ручного управления через несколько секунд. Установите значение 0, чтобы отключить. ⏲️", + "min_sunset_time": "Устанавливает самое раннее время виртуального заката (ЧЧ:ММ:СС), чтобы обеспечить более поздние закаты. 🌇", + "sleep_brightness": "Процент яркости света в спящем режиме. 😴", + "min_sunrise_time": "Устанавливает самое раннее время виртуального восхода солнца (ЧЧ:ММ:СС), чтобы обеспечить возможность более позднего восхода солнца. 🌅", + "interval": "Частота адаптации освещения в секундах. 🔄", + "adapt_delay": "Время ожидания (в секундах) между включением света и применением изменений адаптивного освещения. Возможно поможет избежать мерцания. ⏲️", + "sleep_rgb_color": "Цвет RGB в спящем режиме (используется, когда параметр `sleep_rgb_or_color_temp` имеет значение \"rgb_color\"). 🌈", + "sunrise_offset": "Регулирует время восхода солнца с положительным или отрицательным смещением в секундах. ⏰", + "transition": "Продолжительность перехода при смене освещения, в секундах. 🕑", + "brightness_mode": "Режим яркости для использования. Возможные значения: `default`, `linear` и `tanh` (используются `brightness_mode_time_dark` и `brightness_mode_time_light`). 📈", + "brightness_mode_time_light": "(Игнорируется, если `brightness_mode='default'`) Продолжительность в секундах увеличения/уменьшения яркости после/до восхода/заката. 📈📉.", + "sunset_offset": "Регулирует время заката с помощью положительного или отрицательного смещения в секундах. ⏰", + "sunset_time": "Устанавливает фиксированное время (ЧЧ:ММ:СС) для заката. 🌇", + "max_sunset_time": "Устанавливает последнее время виртуального заката (ЧЧ:ММ:СС), чтобы обеспечить более ранние закаты. 🌇", + "sunrise_time": "Устанавливает фиксированное время (ЧЧ:ММ:СС) восхода солнца. 🌅", + "initial_transition": "Продолжительность первого перехода, когда освещение переключается с `выключено` на `включено` в секундах. ⏲️", + "brightness_mode_time_dark": "(Игнорируется, если `brightness_mode='default'`) Продолжительность в секундах увеличения/уменьшения яркости до/после восхода/заката. 📈📉", + "max_sunrise_time": "Устанавливает последнее время виртуального восхода солнца (ЧЧ:ММ:СС), что позволит восходить раньше. 🌅", + "send_split_delay": "Задержка (миллисекунды) между отдельными командами поворота для источников света, которые не поддерживают одновременную настройку яркости и цвета. ⏲️" } } }, @@ -48,5 +77,52 @@ "option_error": "Ошибка в настройках!", "entity_missing": "Выбранный индикатор не найден" } + }, + "services": { + "apply": { + "fields": { + "entity_id": { + "description": "`entity_id` переключателя с применяемыми настройками. 📝" + }, + "lights": { + "description": "Источник света (или список источников света), к которому нужно применить настройки. 💡" + } + }, + "description": "Применяет текущие настройки адаптивного освещения к источникам света." + }, + "change_switch_settings": { + "fields": { + "max_sunrise_time": { + "description": "Устанавливает самое раннее время виртуального заката (ЧЧ:ММ:СС), чтобы обеспечить более поздние закаты. 🌇" + }, + "min_brightness": { + "description": "Минимальный процент яркости. 💡" + }, + "sunrise_time": { + "description": "Устанавливает фиксированное время (ЧЧ:ММ:СС) восхода солнца. 🌅" + }, + "include_config_in_attributes": { + "description": "Показывать все параметры в качестве атрибутов переключателя в Home Assistant, если установлено значение `true`. 📝" + }, + "max_brightness": { + "description": "Максимальный процент яркости. 💡" + }, + "sleep_rgb_color": { + "description": "Цвет RGB в спящем режиме (используется, когда параметр `sleep_rgb_or_color_temp` имеет значение \"rgb_color\"). 🌈" + }, + "use_defaults": { + "description": "Устанавливает значения по умолчанию, не указанные в этом вызове службы. Варианты: \"current\" (по умолчанию, сохраняются текущие значения), \"factory\" (сброс к документированным настройкам по умолчанию) или \"configuration\" (возврат к настройкам конфигурации по умолчанию). ⚙️" + }, + "sunset_time": { + "description": "Устанавливает фиксированное время (ЧЧ:ММ:СС) для заката. 🌇" + }, + "min_sunset_time": { + "description": "Устанавливает самое раннее время виртуального заката (ЧЧ:ММ:СС), чтобы обеспечить более поздние закаты. 🌇" + } + } + }, + "set_manual_control": { + "description": "Отметьте, контролируется ли свет вручную." + } } } diff --git a/custom_components/adaptive_lighting/translations/sk.json b/custom_components/adaptive_lighting/translations/sk.json new file mode 100644 index 00000000..802e30fd --- /dev/null +++ b/custom_components/adaptive_lighting/translations/sk.json @@ -0,0 +1,52 @@ +{ + "options": { + "step": { + "init": { + "data": { + "detect_non_ha_changes": "detect_non_ha_changes: Deteguje a zastaví prispôbovanie pre zmeny mimo `light.turn_on`. Vyžaduje zapnutie `take_over_control`. 🕵️ Upozornenie: ⚠️ Niektoré svetlá môžu falošne indikovať zapnutý stav, čo spôsobí, že sa svetlo neočakávane zapne. Ak narazíte na tento problém, funkciu vypnite.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Len pri čistom zapnutí svetiel. Pri nastavení `true` prispôsobí Adaptívne osvetlenie svetlá len pri zavolaní služby `light.turn_on` bez parametrov jasu alebo teploty svetla. ❌🌈 Napríklad: zamedzí to prispôsobovaniu ak je aktivovaná scéna. Pri nastavení `false` dôjde k prispôsobeniu nezávisle na tom či sú parametre jasu alebo teploty svetla prítomné v`service_data`. Vyžaduje zapnutie `take_over_control`. 🕵️ ", + "separate_turn_on_commands": "separate_turn_on_commands: Použiť samostatné volania služby `light.turn_on` pre nastavenie teploty svetla a jasu (môže byť potrebné pre niektoré typy svetiel). 🔀", + "max_color_temp": "max_color_temp: Najstudenejšia teplota svetla v Kelvinoch. ❄️", + "prefer_rgb_color": "prefer_rgb_color: Ak je to možné, preferovať nastavenie cez RGB než nastavením teploty svetla. 🌈", + "max_brightness": "max_brightness: Najvyšší jas (v %). 💡", + "only_once": "only_once: Prispôsobiť svetlá iba pri zapnutí (`true`) alebo prispôsobovať ich priebežne (`false`). 🔄", + "take_over_control": "take_over_control: Ak sú svetlá zapnuté a prispôsobované a niečo zavolá službu `light.turn_on`, dôjde k vypnutiu Adaptívneho osvetlenia. Poznámka: Zapnutie tejto voľby spôsobí volanie služby `homeassistant.update_entity` každý `interval`! 🔒", + "lights": "svetlá: Zoznam svetiel (entity_id), ktoré majú byť ovládané (môže byť prázdny). 🌟", + "min_brightness": "min_brightness: Najnižší jas (v %). 💡", + "min_color_temp": "min_color_temp: Najteplejšia teplota svetla v Kelvinoch. 🔥", + "transition_until_sleep": "transition_until_sleep: Keď je funkcia povolená, Adaptívne osvetlenie bude považovať nastavenia režimu spánku ako minimum, a na tieto hodnoty prejde po západe slnka. 🌙" + }, + "data_description": { + "sunset_time": "Nastaviť pevný čas (HH:MM:SS) pre západ slnka. 🌇", + "sunrise_time": "Nastaviť pevný čas (HH:MM:SS) pre východ slnka. 🌅" + }, + "title": "Nastavenia Adaptívneho osvetlenia", + "description": "Nastavte komponentu Adaptívneho osvetlenia. Názvy nastavení sú zhodné s názvami v súbore YAML. Ak ste túto položku nastavili už v YAML, tak tu sa nezobrazia žiadne možnosti nastavenia. Interaktívne grafy, ktoré zobrazujú vplyv nastavení, navštívte [túto webovú aplikáciu](https://basnijholt.github.io/adaptive-lighting). Ďalšie informácie nájdete v [oficiálnej dokumentácii](https://github.com/basnijholt/adaptive-lighting#readme)." + } + } + }, + "title": "Adaptívne osvetlenie", + "config": { + "step": { + "user": { + "description": "Každá inštancia môže obsahovať viacero svetiel!", + "title": "Vyberte názov inštancie Adaptívneho osvetlenia" + } + }, + "abort": { + "already_configured": "Toto zariadenie už je nastavené" + } + }, + "services": { + "change_switch_settings": { + "fields": { + "sunrise_time": { + "description": "Nastaviť pevný čas (HH:MM:SS) pre východ slnka. 🌅" + }, + "sunset_time": { + "description": "Nastaviť pevný čas (HH:MM:SS) pre západ slnka. 🌇" + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/ur.json b/custom_components/adaptive_lighting/translations/ur.json new file mode 100644 index 00000000..67f3d3e0 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/ur.json @@ -0,0 +1,202 @@ +{ + "services": { + "change_switch_settings": { + "fields": { + "sleep_brightness": { + "description": "نیند کے موڈ میں روشنی کی چمک کا فیصد. 😴" + }, + "detect_non_ha_changes": { + "description": "غیر light.turn_on ریاست کی تبدیلیوں کے لئے موافقت کا پتہ لگاتا ہے اور روکتا ہے۔ 'take_over_control' کو فعال کرنے کی ضرورت ہے۔ 🕵️ احتیاط: ⚠️ کچھ لائٹس غلط طور پر 'آن' حالت کی نشاندہی کر سکتی ہیں ، جس کے نتیجے میں لائٹس غیر متوقع طور پر آن ہوسکتی ہیں۔ اگر آپ کو اس طرح کے مسائل کا سامنا کرنا پڑتا ہے تو اس خصوصیت کو غیر فعال کریں۔" + }, + "sunrise_offset": { + "description": "طلوع آفتاب کے وقت کو سیکنڈوں میں مثبت یا منفی آفسیٹ کے ساتھ ایڈجسٹ کریں۔ ⏰" + }, + "max_sunrise_time": { + "description": "تازہ ترین مجازی طلوع آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) سیٹ کریں ، جس سے قبل طلوع آفتاب کی اجازت ملتی ہے۔ 🌅" + }, + "sleep_color_temp": { + "description": "کیلون میں نیند کے موڈ میں رنگ کا درجہ حرارت (جب 'sleep_rgb_or_color_temp' 'color_temp' ہوتا ہے) میں استعمال ہوتا ہے۔ 😴" + }, + "min_brightness": { + "description": "کم سے کم چمک کا فیصد. 💡" + }, + "min_color_temp": { + "description": "کیلون میں گرم ترین رنگ کا درجہ حرارت. 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "نیند کے موڈ میں \"rgb_color\" یا \"color_temp\" کا استعمال کریں۔ 🌙" + }, + "turn_on_lights": { + "description": "کیا ان لائٹس کو آن کرنا ہے جو فی الحال بند ہیں۔ 🔆" + }, + "initial_transition": { + "description": "پہلی منتقلی کا دورانیہ جب لائٹس سیکنڈوں میں 'بند' سے 'آن' میں تبدیل ہوجاتی ہیں۔ ⏲️" + }, + "entity_id": { + "description": "سوئچ کی اینٹیٹی آئی ڈی۔ 📝" + }, + "sunrise_time": { + "description": "طلوع آفتاب کے لئے ایک مقررہ وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں۔ 🌅" + }, + "include_config_in_attributes": { + "description": "'سچ' پر سیٹ ہونے پر ہوم اسسٹنٹ میں سوئچ پر تمام اختیارات بطور خصوصیات دکھائیں۔ 📝" + }, + "max_brightness": { + "description": "زیادہ سے زیادہ چمک کا فیصد. 💡" + }, + "sleep_rgb_color": { + "description": "نیند کے موڈ میں آر جی بی رنگ (جب 'sleep_rgb_or_color_temp' \"rgb_color\" ہوتا ہے تو استعمال کیا جاتا ہے). 🌈" + }, + "take_over_control": { + "description": "اگر کوئی دوسرا ذریعہ 'light.turn_on' کا نام دیتا ہے تو ایڈاپٹو لائٹنگ کو غیر فعال کریں جب لائٹس آن ہیں اور اسے اپنایا جارہا ہے۔ نوٹ کریں کہ یہ ہر 'وقفے' کو 'homeassistant.update_entity' کہتا ہے! 🔒" + }, + "sleep_transition": { + "description": "منتقلی کا دورانیہ جب \"نیند کا موڈ\" سیکنڈوں میں طے کیا جاتا ہے۔ 😴" + }, + "autoreset_control_seconds": { + "description": "کئی سیکنڈ کے بعد دستی کنٹرول کو خود بخود ری سیٹ کریں۔ غیر فعال کرنے کے لئے 0 پر سیٹ کریں۔ ⏲️" + }, + "adapt_delay": { + "description": "لائٹ آن ہونے اور ایڈاپٹو لائٹنگ کے درمیان انتظار کا وقت (سیکنڈ) تبدیلیاں لاگو کرتا ہے۔ جھلکنے سے بچنے میں مدد مل سکتی ہے۔ ⏲️" + }, + "only_once": { + "description": "روشنیوں کو صرف اس وقت ڈھالیں جب وہ آن ہوں (`سچ`) یا انہیں ڈھالتے رہیں (`غلط`)۔ 🔄" + }, + "use_defaults": { + "description": "اس سروس کال میں متعین نہ ہونے والی ڈیفالٹ اقدار سیٹ کرتا ہے۔ اختیارات: \"موجودہ\" (ڈیفالٹ، موجودہ اقدار کو برقرار رکھتا ہے)، \"فیکٹری\" (دستاویزی ڈیفالٹ میں ری سیٹ) ، یا \"کنفیگریشن\" (سوئچ کنفگ ڈیفالٹس پر واپس آجاتا ہے)۔ ⚙️" + }, + "separate_turn_on_commands": { + "description": "رنگ اور چمک کے لئے علیحدہ 'light.turn_on' کا استعمال کریں ، جو کچھ روشنی کی اقسام کے لئے ضروری ہے۔ 🔀" + }, + "prefer_rgb_color": { + "description": "جب ممکن ہو تو روشنی کے رنگ کے درجہ حرارت پر آر جی بی رنگ ایڈجسٹمنٹ کو ترجیح دیں یا نہیں۔ 🌈" + }, + "max_color_temp": { + "description": "کیلون میں سرد ترین رنگ کا درجہ حرارت. ❄️" + }, + "sunset_offset": { + "description": "غروب آفتاب کے وقت کو سیکنڈوں میں مثبت یا منفی آفسیٹ کے ساتھ ایڈجسٹ کریں۔ ⏰" + }, + "send_split_delay": { + "description": "ان روشنیوں کے لئے 'separate_turn_on_commands' کے درمیان تاخیر (ایم ایس) جو بیک وقت چمک اور رنگ کی ترتیب کی حمایت نہیں کرتی ہیں۔ ⏲️" + }, + "sunset_time": { + "description": "غروب آفتاب کے لئے ایک مقررہ وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں۔ 🌇" + }, + "transition": { + "description": "جب روشنیاں تبدیل ہوتی ہیں تو منتقلی کا دورانیہ ، سیکنڈوں میں۔ 🕑" + }, + "min_sunset_time": { + "description": "سب سے پہلے مجازی غروب آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں ، جس سے بعد میں غروب آفتاب کی اجازت ملتی ہے۔ 🌇" + } + }, + "description": "سوئچ میں آپ جو بھی ترتیبات چاہتے ہیں اسے تبدیل کریں۔ یہاں تمام اختیارات وہی ہیں جو کنفگ بہاؤ میں ہیں۔" + }, + "apply": { + "fields": { + "entity_id": { + "description": "لاگو کرنے کے لئے ترتیبات کے ساتھ سوئچ کا 'entity_id'۔ 📝" + }, + "adapt_brightness": { + "description": "کیا روشنی کی چمک کو ڈھالنا ہے۔ 🌞" + }, + "turn_on_lights": { + "description": "کیا ان لائٹس کو آن کرنا ہے جو فی الحال بند ہیں۔ 🔆" + }, + "adapt_color": { + "description": "کیا معاون لائٹس پر رنگ کو ڈھالنا ہے۔ 🌈" + }, + "prefer_rgb_color": { + "description": "جب ممکن ہو تو روشنی کے رنگ کے درجہ حرارت پر آر جی بی رنگ ایڈجسٹمنٹ کو ترجیح دیں یا نہیں۔ 🌈" + }, + "lights": { + "description": "ترتیبات کو لاگو کرنے کے لیے روشنی (یا لائٹس کی فہرست)۔ 💡" + }, + "transition": { + "description": "جب روشنیاں تبدیل ہوتی ہیں تو منتقلی کا دورانیہ ، سیکنڈوں میں۔ 🕑" + } + }, + "description": "روشنیوں پر موجودہ مطابقت پذیر روشنی کی ترتیبات کا اطلاق ہوتا ہے۔" + }, + "set_manual_control": { + "fields": { + "manual_control": { + "description": "کیا روشنی کو \"manual_control\" کی فہرست سے شامل کرنا ہے (\"سچ\") یا (\"غلط\") ہٹانا ہے۔ 🔒" + }, + "entity_id": { + "description": "سوئچ کا 'entity_id' جس میں روشنی کو 'دستی طور پر کنٹرول' کے طور پر نشان زد کرنا ہے۔ 📝" + }, + "lights": { + "description": "لائٹس کے entity_id ، اگر واضح نہیں ہیں تو ، سوئچ میں موجود تمام لائٹس منتخب کی جاتی ہیں۔ 💡" + } + }, + "description": "نشان لگائیں کہ آیا روشنی کو 'دستی طور پر کنٹرول' کیا جاتا ہے۔" + } + }, + "options": { + "step": { + "init": { + "data_description": { + "sleep_rgb_or_color_temp": "نیند کے موڈ میں \"rgb_color\" یا \"color_temp\" کا استعمال کریں۔ 🌙", + "sleep_color_temp": "کیلون میں نیند کے موڈ میں رنگ کا درجہ حرارت (جب 'sleep_rgb_or_color_temp' 'color_temp' ہوتا ہے) میں استعمال ہوتا ہے۔ 😴", + "sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "autoreset_control_seconds": "کئی سیکنڈ کے بعد دستی کنٹرول کو خود بخود ری سیٹ کریں۔ غیر فعال کرنے کے لئے 0 پر سیٹ کریں۔ ⏲️", + "min_sunset_time": "سب سے پہلے مجازی غروب آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں ، جس سے بعد میں غروب آفتاب کی اجازت ملتی ہے۔ 🌇", + "sleep_brightness": "نیند کے موڈ میں روشنی کی چمک کا فیصد. 😴", + "min_sunrise_time": "ابتدائی مجازی طلوع آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں ، جس سے بعد میں طلوع آفتاب کی اجازت ملتی ہے۔ 🌅", + "interval": "روشنیوں کو سیکنڈوں میں ڈھالنے کی فریکوئنسی۔ 🔄", + "adapt_delay": "لائٹ آن ہونے اور ایڈاپٹو لائٹنگ کے درمیان انتظار کا وقت (سیکنڈ) تبدیلیاں لاگو کرتا ہے۔ جھلکنے سے بچنے میں مدد مل سکتی ہے۔ ⏲️", + "sleep_rgb_color": "نیند کے موڈ میں آر جی بی رنگ (جب 'sleep_rgb_or_color_temp' \"rgb_color\" ہوتا ہے تو استعمال کیا جاتا ہے). 🌈", + "sunrise_offset": "طلوع آفتاب کے وقت کو سیکنڈوں میں مثبت یا منفی آفسیٹ کے ساتھ ایڈجسٹ کریں۔ ⏰", + "transition": "جب روشنیاں تبدیل ہوتی ہیں تو منتقلی کا دورانیہ ، سیکنڈوں میں۔ 🕑", + "brightness_mode": "استعمال کرنے کے لئے چمک کا موڈ۔ ممکنہ قدریں 'ڈیفالٹ'، 'لکیری' اور 'تن' ہیں ('brightness_mode_time_dark' اور 'brightness_mode_time_light' کا استعمال کرتی ہیں)۔ 📈", + "brightness_mode_time_light": "(اگر 'brightness_mode='ڈیفالٹ') سورج طلوع ہونے کے بعد / اس سے پہلے / غروب آفتاب سے پہلے چمک کو بڑھانے کے لئے سیکنڈوں میں دورانیہ۔ 📈📉.", + "sunset_offset": "غروب آفتاب کے وقت کو سیکنڈوں میں مثبت یا منفی آفسیٹ کے ساتھ ایڈجسٹ کریں۔ ⏰", + "sunset_time": "غروب آفتاب کے لئے ایک مقررہ وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں۔ 🌇", + "max_sunset_time": "تازہ ترین مجازی غروب آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) سیٹ کریں ، جس سے قبل غروب آفتاب کی اجازت ملتی ہے۔ 🌇", + "sunrise_time": "طلوع آفتاب کے لئے ایک مقررہ وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں۔ 🌅", + "initial_transition": "پہلی منتقلی کا دورانیہ جب لائٹس سیکنڈوں میں 'بند' سے 'آن' میں تبدیل ہوجاتی ہیں۔ ⏲️", + "brightness_mode_time_dark": "(اگر 'brightness_mode='ڈیفالٹ') سورج طلوع ہونے سے پہلے / غروب آفتاب سے پہلے / بعد میں چمک کو بڑھانے کے لئے سیکنڈ میں دورانیہ۔ 📈📉", + "max_sunrise_time": "تازہ ترین مجازی طلوع آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) سیٹ کریں ، جس سے قبل طلوع آفتاب کی اجازت ملتی ہے۔ 🌅", + "send_split_delay": "ان روشنیوں کے لئے 'separate_turn_on_commands' کے درمیان تاخیر (ایم ایس) جو بیک وقت چمک اور رنگ کی ترتیب کی حمایت نہیں کرتی ہیں۔ ⏲️" + }, + "data": { + "detect_non_ha_changes": "detect_non_ha_changes: غیر light.turn_on ریاست کی تبدیلیوں کے لئے موافقت کا پتہ لگاتا ہے اور روکتا ہے۔ 'take_over_control' کو فعال کرنے کی ضرورت ہے۔ 🕵️ احتیاط: ⚠️ کچھ لائٹس غلط طور پر 'آن' حالت کی نشاندہی کر سکتی ہیں ، جس کے نتیجے میں لائٹس غیر متوقع طور پر آن ہوسکتی ہیں۔ اگر آپ کو اس طرح کے مسائل کا سامنا کرنا پڑتا ہے تو اس خصوصیت کو غیر فعال کریں۔", + "multi_light_intercept": "multi_light_intercept: 'light.turn_on' کالز کو روکیں اور ان کے مطابق ڈھالیں جو متعدد روشنیوں کو نشانہ بناتی ہیں۔ ➗⚠️ اس کے نتیجے میں ایک ہی 'light.turn_on' کال کو متعدد کالز میں تقسیم کیا جاسکتا ہے ، مثال کے طور پر ، جب لائٹس مختلف سوئچوں میں ہوتی ہیں۔ 'انٹرسیپٹ' کو فعال کرنے کی ضرورت ہے۔", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: شروع میں لائٹس آن کرتے وقت۔ اگر 'true' پر سیٹ کیا جاتا ہے، AL صرف اس صورت میں موافق ہوتا ہے جب رنگ یا چمک کی وضاحت کیے بغیر 'light.turn_on' کو مدعو کیا جاتا ہے۔ ❌🌈 یہ مثال کے طور پر، کسی منظر کو چالو کرتے وقت موافقت کو روکتا ہے۔ اگر 'غلط'، AL ابتدائی `سروس_ڈیٹا` میں رنگ یا چمک کی موجودگی سے قطع نظر موافقت کرتا ہے۔ 'ٹیک_اوور_کنٹرول' کو فعال کرنے کی ضرورت ہے۔ 🕵️ ", + "skip_redundant_commands": "skip_redundant_commands: موافقت کے احکامات بھیجنے سے گریز کریں جن کی ہدف کی حالت پہلے سے ہی روشنی کی معلوم حالت کے برابر ہے۔ نیٹ ورک ٹریفک کو کم سے کم کرتا ہے اور کچھ حالات میں موافقت کی ذمہ داری کو بہتر بناتا ہے۔ 📉اگر جسمانی روشنی کی حالت یں ایچ اے کی ریکارڈ شدہ حالت کے ساتھ مطابقت سے باہر ہوجاتی ہیں تو غیر فعال کریں۔", + "separate_turn_on_commands": "separate_turn_on_commands: رنگ اور چمک کے لئے الگ الگ 'light.turn_on' کا استعمال کریں، جو کچھ روشنی کی اقسام کے لئے ضروری ہے. 🔀", + "max_color_temp": "max_color_temp: کیلون میں سرد ترین رنگ کا درجہ حرارت۔ ❄️", + "prefer_rgb_color": "prefer_rgb_color: جب ممکن ہو تو روشنی کے رنگ کے درجہ حرارت پر آر جی بی رنگ ایڈجسٹمنٹ کو ترجیح دیں یا نہیں۔ 🌈", + "max_brightness": "max_brightness: زیادہ سے زیادہ چمک کا فیصد. 💡", + "intercept": "انٹرسیپٹ: 'light.turn_on' کالز کو فوری طور پر رنگ اور چمک کے مطابقت پذیری کو قابل بنانے کے لئے روکیں اور اپنائیں۔ 🏎️ ایسی روشنیوں کو غیر فعال کریں جو رنگ اور چمک کے ساتھ 'light.turn_on' کی حمایت نہیں کرتی ہیں۔", + "only_once": "only_once: لائٹس کو صرف اس وقت ڈھالیں جب وہ آن ہوں ('سچ') یا انہیں اپناتے رہیں ('جھوٹ')۔ 🔄", + "take_over_control": "take_over_control: اگر کوئی دوسرا ذریعہ 'light.turn_on' کا نام دیتا ہے تو ایڈاپٹو لائٹنگ کو غیر فعال کریں جب لائٹس آن ہیں اور اسے اپنایا جارہا ہے۔ نوٹ کریں کہ یہ ہر 'وقفے' کو 'homeassistant.update_entity' کہتا ہے! 🔒", + "lights": "لائٹس: کنٹرول کی جانے والی روشنی کے entity_ids کی فہرست (خالی ہوسکتی ہے). 🌟", + "min_brightness": "min_brightness: کم سے کم چمک کا فیصد. 💡", + "min_color_temp": "min_color_temp: کیلون میں گرم ترین رنگ کا درجہ حرارت. 🔥", + "transition_until_sleep": "transition_until_sleep: جب فعال کیا جاتا ہے تو ، ایڈاپٹو لائٹنگ نیند کی ترتیبات کو کم سے کم تصور کرے گی ، غروب آفتاب کے بعد ان اقدار میں منتقل ہوگی۔ 🌙", + "include_config_in_attributes": "include_config_in_attributes: 'سچ' پر سیٹ ہونے پر ہوم اسسٹنٹ میں سوئچ پر خصوصیات کے طور پر تمام اختیارات دکھائیں۔ 📝" + }, + "title": "مطابقت پذیر روشنی کے اختیارات", + "description": "ایک مطابقت پذیر لائٹنگ جزو تشکیل دیں۔ آپشن کے نام YAML کی ترتیبات کے ساتھ مطابقت رکھتے ہیں۔ اگر آپ نے YAML میں اس اندراج کی وضاحت کی ہے تو ، یہاں کوئی آپشن ظاہر نہیں ہوگا۔ انٹرایکٹو گراف کے لئے جو پیرامیٹر کے اثرات کو ظاہر کرتے ہیں ، ملاحظہ کریں [اس ویب ایپ] (https://basnijholt.github.io/adaptive-lighting)۔ مزید تفصیلات کے لئے ، [سرکاری دستاویزات] (https://github.com/basnijholt/adaptive-lighting#readme) ملاحظہ کریں۔" + } + }, + "error": { + "option_error": "غیر قانونی آپشن", + "entity_missing": "ہوم اسسٹنٹ سے ایک یا ایک سے زیادہ منتخب لائٹ ادارے غائب ہیں" + } + }, + "title": "مطابقت پذیر روشنی", + "config": { + "step": { + "user": { + "description": "ہر مثال میں متعدد روشنیاں ہوسکتی ہیں!", + "title": "ایڈاپٹو لائٹنگ مثال کے لئے ایک نام منتخب کریں" + } + }, + "abort": { + "already_configured": "یہ آلہ پہلے ہی تشکیل دیا گیا ہے" + } + } +} From 96558c2c69a4d77b4099b8852def5f14e47d7230 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 22:46:34 -0800 Subject: [PATCH 0719/1077] docs: add pastukhov as a contributor for translation (#870) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index b38dac23..11cdac33 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -630,6 +630,15 @@ "contributions": [ "translation" ] + }, + { + "login": "pastukhov", + "name": "Artem Pastukhov", + "avatar_url": "https://avatars.githubusercontent.com/u/3490616?v=4", + "profile": "http://ovoi.io", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 544b03b5..46584c36 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-68-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-69-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -551,6 +551,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 556cde6c42d51748885f9def9c79f8a78262c6a3 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 22:47:08 -0800 Subject: [PATCH 0720/1077] docs: add mstefany as a contributor for translation (#872) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 11cdac33..2b993e65 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -639,6 +639,15 @@ "contributions": [ "translation" ] + }, + { + "login": "mstefany", + "name": "Martin Štefany", + "avatar_url": "https://avatars.githubusercontent.com/u/57348587?v=4", + "profile": "https://stefany.eu", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 46584c36..14da28ce 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-69-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-70-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -552,6 +552,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 4fbf9ead90889564cabcc327817af5761ec93267 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 22:47:37 -0800 Subject: [PATCH 0721/1077] docs: add quenthal as a contributor for translation (#873) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 2b993e65..9d1ab295 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -648,6 +648,15 @@ "contributions": [ "translation" ] + }, + { + "login": "quenthal", + "name": "quenthal", + "avatar_url": "https://avatars.githubusercontent.com/u/17827203?v=4", + "profile": "https://github.com/quenthal", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 14da28ce..5fd48dd5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-70-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-71-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -554,6 +554,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + + + From 7c85895f31bcdee45a00b173512b6c50a23f628d Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 22:47:56 -0800 Subject: [PATCH 0722/1077] docs: add Luki72 as a contributor for translation (#874) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 9d1ab295..b8d80fbc 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -657,6 +657,15 @@ "contributions": [ "translation" ] + }, + { + "login": "Luki72", + "name": "Luki72", + "avatar_url": "https://avatars.githubusercontent.com/u/22493116?v=4", + "profile": "https://github.com/Luki72", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 5fd48dd5..06aa561d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-71-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-72-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -556,6 +556,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From e3ca3aa0810f6f6a6b380ac9a75a4cdf061df7ec Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 22:48:16 -0800 Subject: [PATCH 0723/1077] docs: add pantan-cymk as a contributor for translation (#875) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index b8d80fbc..8f6393cb 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -666,6 +666,15 @@ "contributions": [ "translation" ] + }, + { + "login": "pantan-cymk", + "name": "pantan-cymk", + "avatar_url": "https://avatars.githubusercontent.com/u/87476229?v=4", + "profile": "https://github.com/pantan-cymk", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 06aa561d..f969ba46 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-72-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-73-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -557,6 +557,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 6ff114b855c4491eddedf8e7234a10538bddfdc0 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 22:49:21 -0800 Subject: [PATCH 0724/1077] docs: add yousaf465 as a contributor for translation (#876) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 8f6393cb..b8777397 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -675,6 +675,15 @@ "contributions": [ "translation" ] + }, + { + "login": "yousaf465", + "name": "yousaf465", + "avatar_url": "https://avatars.githubusercontent.com/u/83491212?v=4", + "profile": "https://github.com/yousaf465", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index f969ba46..a4a120bd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-73-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-74-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -558,6 +558,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 1bb52379e6b51405cbf31b05b36e9607224b786f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 9 Dec 2023 01:13:02 -0800 Subject: [PATCH 0725/1077] [pre-commit.ci] pre-commit autoupdate (#826) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.1 → v0.1.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.1...v0.1.6) - [github.com/psf/black: 23.10.0 → 23.11.0](https://github.com/psf/black/compare/23.10.0...23.11.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 835445c3..098bd9b2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,11 +8,11 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.1 + rev: v0.1.6 hooks: - id: ruff args: ["--fix"] - repo: https://github.com/psf/black - rev: 23.10.0 + rev: 23.11.0 hooks: - id: black From 1601a22dc238a21f46ac6c04c8e7b6f9645474b1 Mon Sep 17 00:00:00 2001 From: Pierre Belanger Date: Mon, 18 Dec 2023 23:08:29 -0500 Subject: [PATCH 0726/1077] README.md - Add note to access UI (#884) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a4a120bd..b1ea55d9 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ The `adaptive_lighting.manual_control` event is fired when a light is marked as ## :gear: Configuration -Adaptive Lighting supports configuration through both YAML and the frontend (**Configuration** -> **Integrations** -> **Adaptive Lighting**, **Adaptive Lighting** -> **Options**), with identical option names in both methods. +Adaptive Lighting supports configuration through both YAML and the frontend (**Settings** -> **Devices and Services** -> **Adaptive Lighting**, **Adaptive Lighting** -> **Options**), with identical option names in both methods. ```yaml # Example configuration.yaml entry @@ -87,6 +87,7 @@ adaptive_lighting: lights: - light.living_room_lights ``` +Note: If you plan to strictly use the UI, the `adaptive_lighting:` entry must still be added to the YAML. Transform your home's atmosphere with Adaptive Lighting 🏠, and experience the benefits of intelligent, sun-synchronized lighting today! From 4de96afcd0227adfcc838e843ae2d8dd40d6dc43 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 20:11:21 -0800 Subject: [PATCH 0727/1077] docs: add baylanger as a contributor for doc (#885) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index b8777397..d2f81350 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -684,6 +684,15 @@ "contributions": [ "translation" ] + }, + { + "login": "baylanger", + "name": "Pierre Belanger", + "avatar_url": "https://avatars.githubusercontent.com/u/5240348?v=4", + "profile": "https://github.com/baylanger", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index b1ea55d9..59ae6524 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-74-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-75-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -560,6 +560,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 88a8a6d00e4aab2e4019fbb7e47dbd5049d81a59 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 20 Dec 2023 07:12:52 -0800 Subject: [PATCH 0728/1077] pin shiny in requirements.txt (#887) --- webapp/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/requirements.txt b/webapp/requirements.txt index 4a287a69..29f55dec 100644 --- a/webapp/requirements.txt +++ b/webapp/requirements.txt @@ -1,3 +1,4 @@ shinylive astral==2.2 shinyswatch +shiny==0.6.0 From 3118e6f308b13927049c764f8131bf7d62ea56f6 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 20 Dec 2023 07:15:20 -0800 Subject: [PATCH 0729/1077] pin click in requirements.txt --- webapp/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/requirements.txt b/webapp/requirements.txt index 29f55dec..25248916 100644 --- a/webapp/requirements.txt +++ b/webapp/requirements.txt @@ -2,3 +2,4 @@ shinylive astral==2.2 shinyswatch shiny==0.6.0 +click==8.1.3 From 9f8eb97cfc8e1b6d99a47cdc11874f4aff2e009d Mon Sep 17 00:00:00 2001 From: Nilesh Date: Fri, 5 Jan 2024 17:20:19 -0800 Subject: [PATCH 0730/1077] Fix webapp not loading (#895) --- webapp/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements.txt b/webapp/requirements.txt index 25248916..16afaf2e 100644 --- a/webapp/requirements.txt +++ b/webapp/requirements.txt @@ -2,4 +2,4 @@ shinylive astral==2.2 shinyswatch shiny==0.6.0 -click==8.1.3 +click==8.1.7 From 99cbe75f30fc26c02e3eebc17c6095462b6f634e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 5 Jan 2024 17:38:29 -0800 Subject: [PATCH 0731/1077] Pin compatible requirements (#896) --- webapp/requirements.txt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/webapp/requirements.txt b/webapp/requirements.txt index 16afaf2e..54732c09 100644 --- a/webapp/requirements.txt +++ b/webapp/requirements.txt @@ -1,5 +1,4 @@ -shinylive +shinylive==0.1.1 astral==2.2 -shinyswatch -shiny==0.6.0 -click==8.1.7 +shinyswatch==0.3.1 +shiny==0.5.0 From a47f7ce49f91bd624f1a3186beeb70ac4b7c0f09 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 5 Jan 2024 17:51:10 -0800 Subject: [PATCH 0732/1077] Add requirements-locked.txt for WebApp (#897) --- .github/workflows/deploy-webapp.yml | 2 +- webapp/requirements-locked.txt | 77 +++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 webapp/requirements-locked.txt diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml index 629902b5..51cbab81 100644 --- a/.github/workflows/deploy-webapp.yml +++ b/.github/workflows/deploy-webapp.yml @@ -39,7 +39,7 @@ jobs: - name: Install Dependencies run: | - pip install -r webapp/requirements.txt + pip install -r webapp/requirements-locked.txt - name: Build the WebAssembly app run: | diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt new file mode 100644 index 00000000..e965a1b5 --- /dev/null +++ b/webapp/requirements-locked.txt @@ -0,0 +1,77 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --output-file=requirements-locked.txt requirements.txt +# +anyio==4.2.0 + # via + # starlette + # watchfiles +appdirs==1.4.4 + # via + # shiny + # shinylive +asgiref==3.7.2 + # via shiny +astral==2.2 + # via -r requirements.txt +click==8.1.7 + # via + # shiny + # shinylive + # uvicorn +h11==0.14.0 + # via uvicorn +htmltools==0.5.1 + # via + # shiny + # shinyswatch +idna==3.6 + # via anyio +linkify-it-py==2.0.2 + # via shiny +markdown-it-py==3.0.0 + # via + # mdit-py-plugins + # shiny +mdit-py-plugins==0.4.0 + # via shiny +mdurl==0.1.2 + # via markdown-it-py +packaging==23.2 + # via + # htmltools + # shinyswatch +python-multipart==0.0.6 + # via shiny +pytz==2023.3.post1 + # via astral +shiny==0.5.0 + # via + # -r requirements.txt + # shinylive + # shinyswatch +shinylive==0.1.1 + # via -r requirements.txt +shinyswatch==0.3.1 + # via -r requirements.txt +sniffio==1.3.0 + # via anyio +starlette==0.34.0 + # via shiny +typing-extensions==4.9.0 + # via + # htmltools + # shiny + # shinyswatch +uc-micro-py==1.0.2 + # via linkify-it-py +uvicorn==0.25.0 + # via shiny +watchfiles==0.21.0 + # via shiny +websockets==12.0 + # via shiny +xstatic-bootswatch==3.3.7.0 + # via shinyswatch From 95a59438b65d4644b3a51e4b0dd5b4b9253a8521 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:13:13 -0800 Subject: [PATCH 0733/1077] [pre-commit.ci] pre-commit autoupdate (#882) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.6 → v0.2.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.6...v0.2.0) - [github.com/psf/black: 23.11.0 → 24.1.1](https://github.com/psf/black/compare/23.11.0...24.1.1) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/update-services.py | 3 +- .github/update-strings.py | 3 +- .pre-commit-config.yaml | 4 +- .../adaptive_lighting/__init__.py | 1 + .../adaptive_lighting/adaptation_utils.py | 1 + .../adaptive_lighting/color_and_brightness.py | 3 ++ .../adaptive_lighting/config_flow.py | 1 + custom_components/adaptive_lighting/const.py | 48 +++++++++---------- .../adaptive_lighting/hass_utils.py | 1 + .../adaptive_lighting/helpers.py | 1 + custom_components/adaptive_lighting/switch.py | 1 + test_dependencies.py | 1 + tests/conftest.py | 1 + tests/test_config_flow.py | 1 + tests/test_init.py | 1 + tests/test_switch.py | 1 + webapp/homeassistant_util_color.py | 1 + 17 files changed, 45 insertions(+), 28 deletions(-) diff --git a/.github/update-services.py b/.github/update-services.py index ee001beb..9ea1860f 100644 --- a/.github/update-services.py +++ b/.github/update-services.py @@ -1,4 +1,5 @@ """Creates a services.yaml file with the latest docs.""" + import sys from pathlib import Path @@ -6,7 +7,7 @@ import yaml sys.path.append(str(Path(__file__).parent.parent)) -from custom_components.adaptive_lighting import const # noqa: E402 +from custom_components.adaptive_lighting import const services_filename = Path("custom_components") / "adaptive_lighting" / "services.yaml" with open(services_filename) as f: # noqa: PTH123 diff --git a/.github/update-strings.py b/.github/update-strings.py index f90ac437..d25a7af2 100644 --- a/.github/update-strings.py +++ b/.github/update-strings.py @@ -1,4 +1,5 @@ """Update strings.json and en.json from const.py.""" + import json import sys from pathlib import Path @@ -8,7 +9,7 @@ import yaml sys.path.append(str(Path(__file__).parent.parent)) -from custom_components.adaptive_lighting import const # noqa: E402 +from custom_components.adaptive_lighting import const folder = Path("custom_components") / "adaptive_lighting" strings_fname = folder / "strings.json" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 098bd9b2..9a1c5f2f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,11 +8,11 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.6 + rev: v0.2.0 hooks: - id: ruff args: ["--fix"] - repo: https://github.com/psf/black - rev: 23.11.0 + rev: 24.1.1 hooks: - id: black diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 98c2e94f..a235d70f 100644 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -1,4 +1,5 @@ """Adaptive Lighting integration in Home-Assistant.""" + import logging from typing import Any diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py index 593a746b..595911f0 100644 --- a/custom_components/adaptive_lighting/adaptation_utils.py +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -1,4 +1,5 @@ """Utility functions for adaptation commands.""" + import logging from collections.abc import AsyncGenerator from dataclasses import dataclass diff --git a/custom_components/adaptive_lighting/color_and_brightness.py b/custom_components/adaptive_lighting/color_and_brightness.py index 9441e9e6..52386bf7 100644 --- a/custom_components/adaptive_lighting/color_and_brightness.py +++ b/custom_components/adaptive_lighting/color_and_brightness.py @@ -1,4 +1,5 @@ """Switch for the Adaptive Lighting integration.""" + from __future__ import annotations import bisect @@ -432,6 +433,7 @@ def find_a_b(x1: float, x2: float, y1: float, y2: float) -> tuple[float, float]: Notes ----- The values of y1 and y2 should lie between 0 and 1, inclusive. + """ a = (math.atanh(2 * y2 - 1) - math.atanh(2 * y1 - 1)) / (x2 - x1) b = x1 - (math.atanh(2 * y1 - 1) / a) @@ -477,6 +479,7 @@ def scaled_tanh( Returns ------- float: The output of the function, which lies in the range [y_min, y_max]. + """ a, b = find_a_b(x1, x2, y1, y2) return y_min + (y_max - y_min) * 0.5 * (math.tanh(a * (x - b)) + 1) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 8f82582a..f0098937 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Adaptive Lighting integration.""" + import logging import homeassistant.helpers.config_validation as cv diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index cf93cee6..79d571e2 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -50,9 +50,9 @@ DOCS[CONF_INITIAL_TRANSITION] = ( ) CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION = "sleep_transition", 1 -DOCS[ - CONF_SLEEP_TRANSITION -] = 'Duration of transition when "sleep mode" is toggled in seconds. 😴' +DOCS[CONF_SLEEP_TRANSITION] = ( + 'Duration of transition when "sleep mode" is toggled in seconds. 😴' +) CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 DOCS[CONF_INTERVAL] = "Frequency to adapt the lights, in seconds. 🔄" @@ -112,30 +112,30 @@ DOCS[CONF_SLEEP_COLOR_TEMP] = ( ) CONF_SLEEP_RGB_COLOR, DEFAULT_SLEEP_RGB_COLOR = "sleep_rgb_color", [255, 56, 0] -DOCS[ - CONF_SLEEP_RGB_COLOR -] = 'RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈' +DOCS[CONF_SLEEP_RGB_COLOR] = ( + 'RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈' +) CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = ( "sleep_rgb_or_color_temp", "color_temp", ) -DOCS[ - CONF_SLEEP_RGB_OR_COLOR_TEMP -] = 'Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙' +DOCS[CONF_SLEEP_RGB_OR_COLOR_TEMP] = ( + 'Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙' +) CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 -DOCS[ - CONF_SUNRISE_OFFSET -] = "Adjust sunrise time with a positive or negative offset in seconds. ⏰" +DOCS[CONF_SUNRISE_OFFSET] = ( + "Adjust sunrise time with a positive or negative offset in seconds. ⏰" +) CONF_SUNRISE_TIME = "sunrise_time" DOCS[CONF_SUNRISE_TIME] = "Set a fixed time (HH:MM:SS) for sunrise. 🌅" CONF_MIN_SUNRISE_TIME = "min_sunrise_time" -DOCS[ - CONF_MIN_SUNRISE_TIME -] = "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅" +DOCS[CONF_MIN_SUNRISE_TIME] = ( + "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅" +) CONF_MAX_SUNRISE_TIME = "max_sunrise_time" DOCS[CONF_MAX_SUNRISE_TIME] = ( @@ -144,22 +144,22 @@ DOCS[CONF_MAX_SUNRISE_TIME] = ( ) CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 -DOCS[ - CONF_SUNSET_OFFSET -] = "Adjust sunset time with a positive or negative offset in seconds. ⏰" +DOCS[CONF_SUNSET_OFFSET] = ( + "Adjust sunset time with a positive or negative offset in seconds. ⏰" +) CONF_SUNSET_TIME = "sunset_time" DOCS[CONF_SUNSET_TIME] = "Set a fixed time (HH:MM:SS) for sunset. 🌇" CONF_MIN_SUNSET_TIME = "min_sunset_time" -DOCS[ - CONF_MIN_SUNSET_TIME -] = "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇" +DOCS[CONF_MIN_SUNSET_TIME] = ( + "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇" +) CONF_MAX_SUNSET_TIME = "max_sunset_time" -DOCS[ - CONF_MAX_SUNSET_TIME -] = "Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇" +DOCS[CONF_MAX_SUNSET_TIME] = ( + "Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇" +) CONF_BRIGHTNESS_MODE, DEFAULT_BRIGHTNESS_MODE = "brightness_mode", "default" DOCS[CONF_BRIGHTNESS_MODE] = ( diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py index cc3257ce..c87d481f 100644 --- a/custom_components/adaptive_lighting/hass_utils.py +++ b/custom_components/adaptive_lighting/hass_utils.py @@ -1,4 +1,5 @@ """Utility functions for HA core.""" + import logging from collections.abc import Awaitable, Callable diff --git a/custom_components/adaptive_lighting/helpers.py b/custom_components/adaptive_lighting/helpers.py index 2e7ba23e..fa3af6ef 100644 --- a/custom_components/adaptive_lighting/helpers.py +++ b/custom_components/adaptive_lighting/helpers.py @@ -34,6 +34,7 @@ def int_to_base36(num: int) -> str: >>> base36_num = int_to_base36(num) >>> print(base36_num) '2N9' + """ alphanumeric_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 9aec8ba1..06cd2f87 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1,4 +1,5 @@ """Switch for the Adaptive Lighting integration.""" + from __future__ import annotations import asyncio diff --git a/test_dependencies.py b/test_dependencies.py index f8fbf5b9..a07f9ee6 100644 --- a/test_dependencies.py +++ b/test_dependencies.py @@ -1,4 +1,5 @@ """Extracts the dependencies of the components required for testing.""" + from collections import defaultdict from pathlib import Path diff --git a/tests/conftest.py b/tests/conftest.py index 14f7e644..4b61832d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ """Fixtures for testing.""" + import os import sys diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 22384143..4242417a 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,4 +1,5 @@ """Test Adaptive Lighting config flow.""" + from homeassistant import data_entry_flow from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_NAME diff --git a/tests/test_init.py b/tests/test_init.py index bb0cf977..b6c82673 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,4 +1,5 @@ """Tests for Adaptive Lighting integration.""" + from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_NAME from homeassistant.setup import async_setup_component diff --git a/tests/test_switch.py b/tests/test_switch.py index 018c8965..8664433c 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1,4 +1,5 @@ """Tests for Adaptive Lighting switches.""" + # pylint: disable=protected-access import asyncio import itertools diff --git a/webapp/homeassistant_util_color.py b/webapp/homeassistant_util_color.py index 33df5cf3..c1541f4c 100644 --- a/webapp/homeassistant_util_color.py +++ b/webapp/homeassistant_util_color.py @@ -1,4 +1,5 @@ """Color util methods.""" + # Slightly modified from homeassistant.util.color at # https://github.com/home-assistant/core/blob/798fb3e31a6ba87358adc93a4c5b772b64451712/homeassistant/util/color.py#L14 # to remove the dependency on homeassistant.util.color in sun.py From 087b445c5a4e08f96633ffbbe068cde38328cc92 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 14:59:44 -0800 Subject: [PATCH 0734/1077] [pre-commit.ci] pre-commit autoupdate (#922) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.2.0 → v0.2.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.0...v0.2.1) - [github.com/psf/black: 24.1.1 → 24.2.0](https://github.com/psf/black/compare/24.1.1...24.2.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9a1c5f2f..acb6dd76 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,11 +8,11 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.0 + rev: v0.2.1 hooks: - id: ruff args: ["--fix"] - repo: https://github.com/psf/black - rev: 24.1.1 + rev: 24.2.0 hooks: - id: black From 44b517155fd1c5558149742b06997aea55a0c0f6 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 17 Feb 2024 10:47:14 -0800 Subject: [PATCH 0735/1077] docs: add jansigu as a contributor for translation (#924) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index d2f81350..66897461 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -693,6 +693,15 @@ "contributions": [ "doc" ] + }, + { + "login": "jansigu", + "name": "Jan-Sigurd Sørensen", + "avatar_url": "https://avatars.githubusercontent.com/u/8410766?v=4", + "profile": "http://www.jan-sigurd.com", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 59ae6524..c80377a6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-75-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-76-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -561,6 +561,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 9d72f0334394f27cb807f36e158f27569ae53c71 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 17 Feb 2024 10:47:56 -0800 Subject: [PATCH 0736/1077] docs: add EF01 as a contributor for translation (#925) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 66897461..a11a461a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -702,6 +702,15 @@ "contributions": [ "translation" ] + }, + { + "login": "EF01", + "name": "EF01", + "avatar_url": "https://avatars.githubusercontent.com/u/20759250?v=4", + "profile": "https://github.com/EF01", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index c80377a6..0fbef7dc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-76-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-77-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -562,6 +562,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From cf672a2c51405007df10aeafea11403c3aa5ecef Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 17 Feb 2024 10:48:47 -0800 Subject: [PATCH 0737/1077] docs: add MrSnakeSPb as a contributor for translation (#926) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index a11a461a..94424f86 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -711,6 +711,15 @@ "contributions": [ "translation" ] + }, + { + "login": "MrSnakeSPb", + "name": "Mr Snake", + "avatar_url": "https://avatars.githubusercontent.com/u/68160409?v=4", + "profile": "https://github.com/MrSnakeSPb", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 0fbef7dc..748f49a8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-77-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-78-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -564,6 +564,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + + + From bd20939a48958be6f31d9d6206ed934ba31232b3 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 17 Feb 2024 10:49:26 -0800 Subject: [PATCH 0738/1077] docs: add hungrymachine1 as a contributor for translation (#927) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 94424f86..1fde1f55 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -720,6 +720,15 @@ "contributions": [ "translation" ] + }, + { + "login": "hungrymachine1", + "name": "hungrymachine1", + "avatar_url": "https://avatars.githubusercontent.com/u/73683742?v=4", + "profile": "https://github.com/hungrymachine1", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 748f49a8..bf9a65ae 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-78-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-79-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -566,6 +566,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From d991a99b837b080d017a18bb072157482cd790d3 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 17 Feb 2024 10:50:40 -0800 Subject: [PATCH 0739/1077] docs: add 4D4M-Github as a contributor for translation (#928) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 1fde1f55..de2dbc2a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -729,6 +729,15 @@ "contributions": [ "translation" ] + }, + { + "login": "4D4M-Github", + "name": "4D4M-Github", + "avatar_url": "https://avatars.githubusercontent.com/u/123521171?v=4", + "profile": "https://github.com/4D4M-Github", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index bf9a65ae..4cbf2ba7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-79-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-80-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -567,6 +567,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 8e1a89efdfedb2c8d58fc4ad0ade04b73dff1d06 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 17 Feb 2024 10:51:06 -0800 Subject: [PATCH 0740/1077] docs: add sayaivan as a contributor for translation (#929) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index de2dbc2a..2e0733f7 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -738,6 +738,15 @@ "contributions": [ "translation" ] + }, + { + "login": "sayaivan", + "name": "Ivan", + "avatar_url": "https://avatars.githubusercontent.com/u/49090860?v=4", + "profile": "https://github.com/sayaivan", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 4cbf2ba7..91dd9ac6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-80-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-81-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -568,6 +568,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From ef54669e9a8418880d8d814ecea63d12b0a51fb9 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 17 Feb 2024 10:51:33 -0800 Subject: [PATCH 0741/1077] docs: add Fllorent0D as a contributor for translation (#930) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 2e0733f7..54bf4372 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -747,6 +747,15 @@ "contributions": [ "translation" ] + }, + { + "login": "Fllorent0D", + "name": "Florent Cardoen", + "avatar_url": "https://avatars.githubusercontent.com/u/13313104?v=4", + "profile": "https://www.floca.be", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 91dd9ac6..4791e437 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-81-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-82-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -569,6 +569,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From e6ea8eee7d7ab4a0ff13d31d54683daa805ea662 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 17 Feb 2024 10:52:10 -0800 Subject: [PATCH 0742/1077] docs: add moemeli as a contributor for translation (#931) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 54bf4372..1e0eb60c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -756,6 +756,15 @@ "contributions": [ "translation" ] + }, + { + "login": "moemeli", + "name": "moemeli", + "avatar_url": "https://avatars.githubusercontent.com/u/73445184?v=4", + "profile": "https://github.com/moemeli", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 4791e437..12dc6b8e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-82-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-83-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -570,6 +570,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From a1ce600db188b623f15eb586db5968f371898ee7 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 17 Feb 2024 10:52:33 -0800 Subject: [PATCH 0743/1077] docs: add saya6k as a contributor for translation (#932) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 1e0eb60c..e4aa63a4 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -765,6 +765,15 @@ "contributions": [ "translation" ] + }, + { + "login": "saya6k", + "name": "saya6k", + "avatar_url": "https://avatars.githubusercontent.com/u/63517312?v=4", + "profile": "https://github.com/saya6k", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 12dc6b8e..b5fa6293 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-83-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-84-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -571,6 +571,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 9fbc5ec0ad85793495563e450995175360a0e6d9 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sat, 17 Feb 2024 19:56:12 +0100 Subject: [PATCH 0744/1077] Translations update from Hosted Weblate (#879) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Danish) Currently translated at 85.6% (131 of 153 strings) Co-authored-by: Emil Friis Osmann Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/da/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Russian) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Mr Snake Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ru/ Translation: Adaptive Lighting/Adaptive Lighting * Added translation using Weblate (Bengali) Co-authored-by: Hosted Weblate Co-authored-by: Jarif Ansath Aorko * Translated using Weblate (Japanese) Currently translated at 48.3% (74 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: pantan-cymk Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ja/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Czech) Currently translated at 100.0% (153 of 153 strings) Translated using Weblate (Czech) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Martin Štefany Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/cs/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Hungarian) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Szalay Ádám Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/hu/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Indonesian) Currently translated at 100.0% (153 of 153 strings) Added translation using Weblate (Indonesian) Co-authored-by: Hosted Weblate Co-authored-by: Ivan D. Firmansyah Co-authored-by: sayaivan Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/id/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (French) Currently translated at 100.0% (153 of 153 strings) Translated using Weblate (French) Currently translated at 84.9% (130 of 153 strings) Co-authored-by: Florent Cardoen Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/fr/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Finnish) Currently translated at 56.8% (87 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Mikko Eloranta Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/fi/ Translation: Adaptive Lighting/Adaptive Lighting * Added translation using Weblate (Korean) Co-authored-by: saya6k * Translated using Weblate (Slovak) Currently translated at 98.0% (150 of 153 strings) Translated using Weblate (Slovak) Currently translated at 98.0% (150 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Martin Štefany Co-authored-by: Unambiguous Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/sk/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (153 of 153 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 58.1% (89 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Jan-Sigurd Sørensen Co-authored-by: Stian Lindvik Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nb_NO/ Translation: Adaptive Lighting/Adaptive Lighting --------- Co-authored-by: Emil Friis Osmann Co-authored-by: Mr Snake Co-authored-by: Jarif Ansath Aorko Co-authored-by: pantan-cymk Co-authored-by: Martin Štefany Co-authored-by: Szalay Ádám Co-authored-by: Ivan D. Firmansyah Co-authored-by: Florent Cardoen Co-authored-by: Mikko Eloranta Co-authored-by: saya6k Co-authored-by: Unambiguous Co-authored-by: Jan-Sigurd Sørensen Co-authored-by: Stian Lindvik --- .../adaptive_lighting/translations/bn.json | 1 + .../adaptive_lighting/translations/cs.json | 54 ++-- .../adaptive_lighting/translations/da.json | 116 +++++++- .../adaptive_lighting/translations/fi.json | 71 ++++- .../adaptive_lighting/translations/fr.json | 97 ++++++- .../adaptive_lighting/translations/hu.json | 173 +++++++++++- .../adaptive_lighting/translations/id.json | 202 ++++++++++++++ .../adaptive_lighting/translations/ja.json | 40 ++- .../adaptive_lighting/translations/ko.json | 1 + .../adaptive_lighting/translations/nb.json | 256 ++++++++++++++---- .../adaptive_lighting/translations/ru.json | 94 ++++++- .../adaptive_lighting/translations/sk.json | 169 +++++++++++- 12 files changed, 1179 insertions(+), 95 deletions(-) create mode 100644 custom_components/adaptive_lighting/translations/bn.json create mode 100644 custom_components/adaptive_lighting/translations/id.json create mode 100644 custom_components/adaptive_lighting/translations/ko.json diff --git a/custom_components/adaptive_lighting/translations/bn.json b/custom_components/adaptive_lighting/translations/bn.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/bn.json @@ -0,0 +1 @@ +{} diff --git a/custom_components/adaptive_lighting/translations/cs.json b/custom_components/adaptive_lighting/translations/cs.json index 1f533935..3a7d9788 100644 --- a/custom_components/adaptive_lighting/translations/cs.json +++ b/custom_components/adaptive_lighting/translations/cs.json @@ -17,13 +17,13 @@ "options": { "step": { "init": { - "title": "Nastavení adaptivního osvětlení", + "title": "Nastavení Adaptivního osvětlení", "description": "Všechna nastavení komponenty Adaptivního osvětlení. Názvy možností odpovídají nastavení YAML. Pokud máte v konfiguraci YAML definovánu položku 'adaptive_lighting', nezobrazí se žádné možnosti.", "data": { - "lights": "osvětlení", - "initial_transition": "initial_transition: Prodlení pro změnu z 'vypnuto' do 'zapnuto' (sekundy)", - "sleep_transition": "sleep_transition: Prodleva pro přepnutí do „režimu spánku“ (sekundy)", - "interval": "interval: Prodleva pro změny osvětlení (v sekundách)", + "lights": "lights: Seznam světel (entity_id), které mají být ovládané (může být prázdný). 🌟", + "initial_transition": "", + "sleep_transition": "", + "interval": "", "max_brightness": "max_brightness: Nejvyšší jas osvětlení během cyklu. (%)", "max_color_temp": "max_color_temp: Nejchladnější odstín cyklu teploty barev. (Kelvin)", "min_brightness": "min_brightness: Nejnižší jas osvětlení během cyklu. (%)", @@ -31,24 +31,24 @@ "only_once": "only_once: Přizpůsobení osvětlení pouze při rozsvícení.", "prefer_rgb_color": "prefer_rgb_color: Upřednostněte použití 'rgb_color' před 'color_temp'.", "separate_turn_on_commands": "separate_turn_on_commands: Oddělení příkazů pro každý atribut (barva, jas, atd.) v atributu 'light.turn_on' (vyžadováno pro některá světla).", - "send_split_delay": "send_split_delay: prodleva mezi příkazy (milisekundy), když je použit atribut 'separate_turn_on_commands'. Může zajistit správné zpracování obou příkazů.", - "sleep_brightness": "sleep_brightness, Nastavení jasu pro režim spánku. (%)", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, použijte 'rgb_color' nebo 'color_temp'", - "sleep_rgb_color": "sleep_rgb_color, v RGB", - "sleep_color_temp": "sleep_color_temp: Nastavení teploty barev pro režim spánku. (v Kelvinech)", - "sunrise_offset": "sunrise_offset: Jak dlouho před (-) nebo po (+) definovat bod cyklu východu slunce (+/- v sekundách)", - "sunrise_time": "sunrise_time: Manuální přepsání času východu slunce, pokud je „None“, použije se skutečný čas východu slunce ve vaší lokalitě (HH:MM:SS)", - "max_sunrise_time": "max_sunrise_time: Ruční přepsání nejpozdějšího času východu slunce, pokud je „None“, použije se skutečný čas východu slunce vaší lokality (HH:MM:SS)", - "sunset_offset": "sunset_offset: Jak dlouho před (-) nebo po (+) definovat bod cyklu západu slunce (+/- v sekundách)", - "sunset_time": "sunset_time: Ruční přepsání času západu slunce, pokud je „None“, použije se skutečný čas západu slunce vaší lokality (HH:MM:SS)", - "min_sunset_time": "min_sunset_time: Ruční přepsání nejdřívějšího času západu slunce, pokud je „None“, použije se skutečný čas západu slunce vaší lokality (HH:MM:SS)", + "send_split_delay": "", + "sleep_brightness": "", + "sleep_rgb_or_color_temp": "", + "sleep_rgb_color": "", + "sleep_color_temp": "", + "sunrise_offset": "", + "sunrise_time": "", + "max_sunrise_time": "", + "sunset_offset": "", + "sunset_time": "", + "min_sunset_time": "", "take_over_control": "take_over_control: Je-li volán 'light.turn_on' z jiného zdroje, než Adaptivním osvětlením, když je světlo již rozsvíceno, přestaňte toto světlo ovládat, dokud není vypnuto -> zapnuto (nebo i vypínačem).", "detect_non_ha_changes": "detect_non_ha_changes: detekuje všechny změny >10% provedených pro osvětlení (také mimo HA), vyžaduje povolení atributu 'take_over_control' (každý 'interval' spouští 'homeassistant.update_entity'!)", - "transition": "transition: doba přechodu při změně osvětlení (sekundy)", - "adapt_delay": "adapt_delay: prodleva mezi zapnutím světla ( sekundy) a projevem změny v Adaptivní osvětlení. Může předcházet blikání.", + "transition": "", + "adapt_delay": "", "transition_until_sleep": "transition_until_sleep: Pokud je zapnuto, Adaptive Lighting bude zacházet s nastavením spánku jako s minimem, na tyto hodnoty přejde po západu slunce. 🌙", "multi_light_intercept": "multi_light_intercept: Zachytí a přizpůsobí volání `light.turn_on`, která se zaměřují na více světel. ➗⚠️ To může vést k rozdělení jednoho volání `light.turn_on` na více volání, např. když jsou světla v různých vypínačích. Vyžaduje, aby bylo povoleno `intercept`.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Při prvním zapnutí světel. Je-li nastaveno na `true`, AL se přizpůsobí pouze tehdy, je-li vyvoláno `light.turn_on` bez zadání barvy nebo jasu. ❌🌈 Tím se např. zabrání přizpůsobení při aktivaci scény. Pokud je `false`, AL se přizpůsobí bez ohledu na přítomnost barvy nebo jasu v počátečních `service_data`. Vyžaduje zapnutí funkce `take_over_control`. 🕵️ ", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Jenom při prvním zapnutí světel. Je-li nastaveno na `true`, AL udělá přizpůsobení pouze tehdy, je-li vyvoláno `light.turn_on` bez zadání barvy nebo jasu. ❌🌈 Tím se zabrání přizpůsobení např. při aktivaci scény. Pokud je `false`, AL udělá přizpůsobení bez ohledu na přítomnost barvy nebo jasu `service_data` volání. Vyžaduje zapnutí `take_over_control`. 🕵️ ", "skip_redundant_commands": "skip_redundant_commands: Přeskočí odesílání adaptačních příkazů, jejichž cílový stav se již rovná známému stavu světla. Minimalizuje síťový provoz a v některých situacích zlepšuje odezvu adaptace. 📉Zakažte, pokud se fyzické stavy světel dostanou mimo synchronizaci se zaznamenaným stavem HA.", "intercept": "intercept: Zachytit a přizpůsobit volání `light.turn_on` a umožnit tak okamžité přizpůsobení barev a jasu. 🏎️ Zakažte pro světla, která nepodporují `light.turn_on` s barvou a jasem najednou.", "include_config_in_attributes": "include_config_in_attributes: Zobrazit všechny možnosti jako atributy přepínače v Home Assistant, pokud je nastaveno na `true`. 📝" @@ -59,16 +59,16 @@ "sleep_transition": "Doba trvání přechodu do režimu spánku v sekundách. 😴", "autoreset_control_seconds": "Automatické resetování ručního ovládání po určitém počtu sekund. Nastavením na 0 se vypne. ⏲️", "min_sunset_time": "Nastavte nejbližší virtuální čas západu slunce (HH:MM:SS), abyste mohli nastavit pozdější západ slunce. 🌅", - "sleep_brightness": "Jas světel v procentech během režimu spánku", + "sleep_brightness": "Jas světel během režimu spánku (v %). 😴", "min_sunrise_time": "Nastavte nejbližší virtuální čas východu slunce (HH:MM:SS), abyste mohli nastavit pozdější východ slunce. 🌅", "interval": "Frekvence přizpůsobení světel v sekundách. 🔄", "adapt_delay": "Doba čekání (v sekundách) mezi zapnutím světla a změnou adaptivního osvětlení. Mohlo by to pomoci zabránit blikání. ⏲️", "sleep_rgb_color": "RGB barva v režimu spánku (používané když `sleep_rgb_or_color_temp` je \"rgb_color\"). 🌈", - "sunrise_offset": "Upravte čas východu slunce s pozitivním nebo negativním posunem v sekundách. ⏰", + "sunrise_offset": "Upravte čas východu slunce o sekundy dopředu nebo dozadu. ⏰", "transition": "Doba trvání přechodu změny světel v sekundách. 🕑", "brightness_mode": "Výběr režimu jasu. Možné hodnoty jsou `default`, `linear` a `tanh` (používá `brightness_mode_time_dark` a `brightness_mode_time_light`). 📈", "brightness_mode_time_light": "(Ignorováno, pokud `brightness_mode='default'`) Doba trvání v sekundách pro zvýšení/snížení jasu po/před východem/západem slunce. 📈📉.", - "sunset_offset": "Nastavte čas západu slunce s kladným nebo záporným posunem v sekundách. ⏰", + "sunset_offset": "Upravte čas západu slunce o sekundy dopředu nebo dozadu. ⏰", "sunset_time": "Nastavit pevný čas (HH:MM:SS) pro západ slunce. 🌅", "max_sunset_time": "Nastavte nejpozdější virtuální čas západu slunce (HH:MM:SS), což umožňuje dřívější západ slunce. 🌅", "sunrise_time": "Nastavit pevný čas (HH:MM:SS) pro východ slunce. 🌅", @@ -88,13 +88,13 @@ "change_switch_settings": { "fields": { "sleep_brightness": { - "description": "Jas světel v procentech během režimu spánku" + "description": "Jas světel během režimu spánku (v %). 😴" }, "detect_non_ha_changes": { "description": "Zjistí a zastaví adaptace při změnách stavu, které nejsou ve stavu `light.turn_on`. Nutno mít zapnutou funkci `take_over_control`. 🕵️ Upozornění: ⚠️ Některá světla mohou falešně indikovat stav 'zapnuto', což může vést k neočekávanému zapnutí světel. Pokud se s takovými problémy setkáte, zakažte tuto funkci." }, "sunrise_offset": { - "description": "Upravte čas východu slunce s pozitivním nebo negativním posunem v sekundách. ⏰" + "description": "Upravte čas východu slunce o sekundy dopředu nebo dozadu. ⏰" }, "max_sunrise_time": { "description": "Nastavte nejpozdější virtuální čas východu slunce (HH:MM:SS), což umožňuje dřívější východ slunce. 🌅" @@ -145,7 +145,7 @@ "description": "Doba čekání (v sekundách) mezi zapnutím světla a změnou adaptivního osvětlení. Mohlo by to pomoci zabránit blikání." }, "only_once": { - "description": "Přizpůsobit světla pouze při zapnutí(`true`) nebo je nechat pokaždé přizpůsobit (`false`). 🔄" + "description": "Přizpůsobit světla pouze při zapnutí (`true`) nebo je průběžně přizpůsobovat (`false`). 🔄" }, "use_defaults": { "description": "Nastaví výchozí hodnoty, které nebyly zadány v tomto volání služby. Možnosti: \"stávající\" (výchozí, zachovává aktuální hodnoty), \"výchozí\" (obnovuje do výchozích hodnot) nebo \"konfigurace\" (vrací výchozí hodnoty konfigurace přepínače). ⚙️" @@ -160,7 +160,7 @@ "description": "Nejchladnější teplota barvy v Kelvinech. ❄️" }, "sunset_offset": { - "description": "Nastavte čas západu slunce s kladným nebo záporným posunem v sekundách. ⏰" + "description": "Upravte čas západu slunce o sekundy dopředu nebo dozadu. ⏰" }, "send_split_delay": { "description": "Zpoždění (ms) mezi příkazy `separate_turn_on_commands` pro světla, která nepodporují současné nastavení jasu a barvy. ⏲️" @@ -195,7 +195,7 @@ "description": "Zda upřednostnit nastavení barev RGB před teplotou barev světla, pokud je to možné. 🌈" }, "lights": { - "description": "Světlo (nebo seznam světel), na které se má nastavení použít. 💡" + "description": "Světlo (nebo seznam světel), na které se má nastavení aplikovat. 💡" }, "transition": { "description": "Doba trvání přechodu změny světel v sekundách. 🕑" diff --git a/custom_components/adaptive_lighting/translations/da.json b/custom_components/adaptive_lighting/translations/da.json index 5b63c115..f0ff8742 100644 --- a/custom_components/adaptive_lighting/translations/da.json +++ b/custom_components/adaptive_lighting/translations/da.json @@ -38,7 +38,28 @@ "sunset_time": "sunset_time: Manuel overstyring af solnedgangstidspunktet, hvis 'None', bruges det egentlige tidspunkt for din lokation. (HH:MM:SS)", "take_over_control": "take_over_control: Hvis andet end Adaptiv Belysning kalder 'light.turn_on' på et lys der allerede er tændt, afbryd adaptering af lyset indtil at det tændes igen.", "detect_non_ha_changes": "detect_non_ha_changes: Registrer alle ændringer på >10% på et lys (også udenfor HA), kræver at 'take_over_control' er slået til (kalder 'homeassistant.update_entity' hvert 'interval'!)", - "transition": "Overgangsperiode når en ændring i lyset udføres (i sekunder)" + "transition": "Overgangsperiode når en ændring i lyset udføres (i sekunder)", + "transition_until_sleep": "overgang_til_sove: Når aktiveret, vil adaptiv belysning behandle søvnindstillinger som minimum, og overgår til disse værdier efter solnedgang. 🌙", + "adapt_only_on_bare_turn_on": "tilpas_kun_ved_enkelt_tænd: Når du tænder lys for første gang. Hvis indstillet til 'true', tilpasser AL kun, hvis 'lys.tænd' er kaldt uden at angive farve eller lysstyrke. ❌🌈 Dette forhindrer f.eks. tilpasning, når du aktiverer en scene. Hvis indstillet til 'false' tilpasser AL sig uanset tilstanden af farve eller lysstyrke i den oprindelige 'service_data'. Har brug for at 'take_over_control' er aktiveret. 🕵️ " + }, + "data_description": { + "interval": "Frekvens til at tilpasse lysene, i sekunder. 🔄", + "sleep_brightness": "Lysstyrkeprocent af lys i søvntilstand. 😴", + "transition": "Varighed af overgang, når lys ændres, i sekunder. 🕑", + "sleep_rgb_or_color_temp": "Brug enten `\"rgb_farve\"` eller `\"farve_temp\"` i søvntilstand. 🌙", + "sleep_transition": "Varigheden af overgangen, når \"sovetilstand\" skiftes, i sekunder. 😴", + "sunrise_time": "Sæt en fast tid (HH:MM:SS) for solopgang. 🌅", + "sunset_time": "Sæt en fast tid (HH:MM:SS) for solnedgang. 🌇", + "min_sunrise_time": "Indstil den tidligste virtuelle solopgangstid (HH:MM:SS), hvilket giver mulighed for senere solopgange. 🌅", + "max_sunrise_time": "Indstil den seneste virtuelle solopgangstid (HH:MM:SS), hvilket giver mulighed for tidligere solopgange. 🌅", + "autoreset_control_seconds": "Nulstil automatisk den manuelle styring efter et antal sekunder. Indstil til 0 for at deaktivere. ⏲️", + "min_sunset_time": "Indstil den tidligste virtuelle solnedgangstid (HH:MM:SS), hvilket giver mulighed for senere solnedgange. 🌇", + "adapt_delay": "Ventetid (sekunder) mellem lyset tændes og Adaptive Lighting anvender ændringer. Kan hjælpe med at undgå flimren. ⏲️", + "sunset_offset": "Juster solnedgang tid med et positivt eller negativt offset, i sekunder. ⏰", + "sunrise_offset": "Juster solopgangstiden med en positiv eller negativ offset på få sekunder. ⏰", + "max_sunset_time": "Indstil den seneste virtuelle solnedgangstid (HH:MM:SS), hvilket giver mulighed for tidligere solnedgange. 🌇", + "sleep_color_temp": "Farvetemperatur i søvntilstand (bruges når `sleep_rgb_or_color_temp` er `color_temp`) i Kelvin. 😴", + "brightness_mode": "Lysstyrketilstand til brug. Mulige værdier er \"default\", \"linear\" og \"tanh\" (bruger \"brightness_mode_time_dark\" og \"brightness_mode_time_light\"). 📈" } } }, @@ -46,5 +67,98 @@ "option_error": "Ugyldig indstilling", "entity_missing": "Et udvalgt lys blev ikke fundet " } + }, + "services": { + "apply": { + "description": "Anvender de aktuelle Adaptive Lighting indstillinger på lys.", + "fields": { + "prefer_rgb_color": { + "description": "Om man vil foretrække RGB-farvejustering frem for lysfarvetemperatur, når det er muligt. 🌈" + }, + "transition": { + "description": "Varighed af overgang, når lys ændres, i sekunder. 🕑" + }, + "turn_on_lights": { + "description": "Om lys der i øjeblikket er slukket, skal tændes. 🔆" + }, + "adapt_brightness": { + "description": "Om lysstyrken skal tilpasses. 🌞" + }, + "lights": { + "description": "Et lys (eller liste over lys) som indstillingerne skal anvendes til. 💡" + }, + "adapt_color": { + "description": "Om farven på støttelys skal tilpasses. 🌈" + } + } + }, + "change_switch_settings": { + "fields": { + "entity_id": { + "description": "Entity ID af kontakten. 📝" + }, + "turn_on_lights": { + "description": "Om lys der i øjeblikket er slukket, skal tændes. 🔆" + }, + "sleep_transition": { + "description": "Varigheden af overgangen, når \"sovetilstand\" skiftes, i sekunder. 😴" + }, + "only_once": { + "description": "Tilpas kun lys, når de er tændt ('sand'), eller fortsæt med at tilpasse dem ('falsk'). 🔄" + }, + "prefer_rgb_color": { + "description": "Om man vil foretrække RGB-farvejustering frem for lysfarvetemperatur, når det er muligt. 🌈" + }, + "sleep_brightness": { + "description": "Lysstyrkeprocent af lys i søvntilstand. 😴" + }, + "sunrise_time": { + "description": "Sæt en fast tid (HH:MM:SS) til solopgang. 🌅" + }, + "sunrise_offset": { + "description": "Juster solopgangstiden med et positivt eller negativt offset, i sekunder. ⏰" + }, + "sunset_offset": { + "description": "Juster solnedgang tid med et positivt eller negativt offset, i sekunder. ⏰" + }, + "sunset_time": { + "description": "Sæt en fast tid (HH:MM:SS) for solnedgang. 🌇" + }, + "max_sunrise_time": { + "description": "Indstil den seneste virtuelle solopgangstid (HH:MM:SS), hvilket giver mulighed for tidligere solopgange. 🌅" + }, + "min_sunset_time": { + "description": "Indstil den tidligste virtuelle solnedgangstid (HH:MM:SS), hvilket giver mulighed for senere solnedgange. 🌇" + }, + "transition": { + "description": "Varighed af overgang, når lys ændres, i sekunder. 🕑" + }, + "autoreset_control_seconds": { + "description": "Nulstil automatisk den manuelle styring efter et antal sekunder. Indstil til 0 for at deaktivere. ⏲️" + }, + "adapt_delay": { + "description": "Ventetid (sekunder) mellem lyset tændes og Adaptive Lighting anvender ændringer. Kan hjælpe med at undgå flimren. ⏲️" + }, + "max_brightness": { + "description": "Maksimal lysstyrkeprocent. 💡" + }, + "max_color_temp": { + "description": "Koldeste farvetemperatur i Kelvin. ❄️" + }, + "min_brightness": { + "description": "Mindste lysstyrkeprocent. 💡" + }, + "min_color_temp": { + "description": "Varmste farvetemperatur i Kelvin. 🔥" + }, + "sleep_color_temp": { + "description": "Farvetemperatur i søvntilstand (bruges når `sleep_rgb_or_color_temp` er `color_temp`) i Kelvin. 😴" + } + }, + "description": "Skift de indstillinger du ønsker i kontakten. Alle muligheder her er de samme som i konfigurationsflowet." + }, + "set_manual_control": { + "description": "Markér om et lys er 'manuelt kontrolleret'." + } } } diff --git a/custom_components/adaptive_lighting/translations/fi.json b/custom_components/adaptive_lighting/translations/fi.json index 0967ef42..7acd9578 100644 --- a/custom_components/adaptive_lighting/translations/fi.json +++ b/custom_components/adaptive_lighting/translations/fi.json @@ -1 +1,70 @@ -{} +{ + "services": { + "change_switch_settings": { + "fields": { + "sleep_brightness": { + "description": "Valojen kirkkausmäärä prosenteissa unitilassa (sleep mode)." + }, + "sunrise_offset": { + "description": "Muuta auringonnousun aikaa positiivisella tai negatiivisella korjauksella määritettynä sekunneissa." + }, + "initial_transition": { + "description": "Ensimmäisen siirtymän kesto sekunneissa, kun valot kytketään 'off'-tilasta 'on'-tilaan." + }, + "autoreset_control_seconds": { + "description": "Resetoi manuaalisen ohjauksen automaattisesti määritetyn sekuntimäärän jälkeen. Aseta arvoon 0 jos et halua käyttää asetusta." + }, + "only_once": { + "description": "Adaptoi valoja vain kun ne kytketään päälle ('true') tai adaptoi niitä jatkuvasti ('false')" + }, + "max_color_temp": { + "description": "Kylmin värilämpötila Kelvin-asteikolla." + }, + "sunset_offset": { + "description": "Muuta auringonlaskun aikaa positiivisella tai negatiivisella korjauksella määritettynä sekunneissa." + }, + "send_split_delay": { + "description": "Viive (ms) `separate_turn_on_commands` välillä valoille, jotka eivät tue yhtäaikaista kirkkauden ja värilämpötilan säätöä." + }, + "transition": { + "description": "Valojen siirtymän kesto sekunneissa, kun valaistusta muutetaan." + } + } + }, + "apply": { + "description": "Asettaa nykyiset Adaptiivisen Valaistuksen asetukset valoihin.", + "fields": { + "lights": { + "description": "Valo (tai lista valoista) joihin näitä asetuksia sovelletaan." + }, + "transition": { + "description": "Valojen siirtymän kesto sekunneissa, kun valaistusta muutetaan." + } + } + } + }, + "title": "Adaptiivinen valaistus", + "options": { + "step": { + "init": { + "data_description": { + "autoreset_control_seconds": "Resetoi manuaalisen ohjauksen automaattisesti määritetyn sekuntimäärän jälkeen. Aseta arvoon 0 jos et halua käyttää asetusta.", + "sleep_brightness": "Valojen kirkkausmäärä prosenteissa unitilassa (sleep mode).", + "sunrise_offset": "Muuta auringonnousun aikaa positiivisella tai negatiivisella korjauksella määritettynä sekunneissa.", + "transition": "Valojen siirtymän kesto sekunneissa, kun valaistusta muutetaan.", + "brightness_mode": "Kirkkaus-moodi jota käytetään. Mahdolliset arvot ovat `default`, `linear`, and `tanh` (käyttää arvoja `brightness_mode_time_dark` ja `brightness_mode_time_light`).", + "sunset_offset": "Muuta auringonlaskun aikaa positiivisella tai negatiivisella korjauksella määritettynä sekunneissa.", + "initial_transition": "Ensimmäisen siirtymän kesto sekunneissa, kun valot kytketään 'off'-tilasta 'on'-tilaan.", + "send_split_delay": "Viive (ms) `separate_turn_on_commands` välillä valoille, jotka eivät tue yhtäaikaista kirkkauden ja värilämpötilan säätöä." + } + } + } + }, + "config": { + "step": { + "user": { + "title": "Valitse nimi tälle Adaptiivisen Valaistuksen esiintymälle" + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json index 26728081..a3bc6bc5 100644 --- a/custom_components/adaptive_lighting/translations/fr.json +++ b/custom_components/adaptive_lighting/translations/fr.json @@ -40,7 +40,12 @@ "take_over_control": "take_over_control : Si quelque chose d'autre que l'éclairage adaptatif appelle « light.turn_on » alors qu'une lampe est déjà allumée, cesser d'adapter cette lampe jusqu'à ce qu'elle (ou le commutateur) soit éteinte puis rallumée.", "detect_non_ha_changes": "detect_non_ha_changes : Détecter tout changement de plus de 10 % appliqué aux lampes (même en dehors de HA). Nécessite que « take_over_control » soit activé. (Appelle « homeassistant.update_entity » tous les « interval » !)", "transition": "transition : Durée de la transition (en secondes) des changements appliqués aux lampes.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quand on allume les lumières au départ. Si le paramètre est < < vrai > > , AL s ' adapte uniquement si l ' on invoque < < light.turn_on > > sans préciser la couleur ou la luminosité. ❌ Ceci, par exemple, empêche l'adaptation lors de l'activation d'une scène. Si `false`, AL s'adapte indépendamment de la présence de couleur ou de luminosité dans le `service_data' initial. Besoins `take_over_control` activé. 🕵∫ " + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quand on allume les lumières au départ. Si le paramètre est < < vrai > > , AL s ' adapte uniquement si l ' on invoque < < light.turn_on > > sans préciser la couleur ou la luminosité. ❌ Ceci, par exemple, empêche l'adaptation lors de l'activation d'une scène. Si `false`, AL s'adapte indépendamment de la présence de couleur ou de luminosité dans le `service_data' initial. Besoins `take_over_control` activé. 🕵∫ ", + "multi_light_intercept": "multi_light_intercept : Intercepte et adapte les appels à `light.turn_on` qui ciblent plusieurs lumières. ➗⚠️ Cela peut entraîner la division d'un seul appel `light.turn_on` en plusieurs appels, par exemple, lorsque les lumières sont dans différents interrupteurs. Nécessite que `intercept` soit activé.", + "intercept": "intercept : Intercepter et adapter les appels à `light.turn_on` pour permettre une adaptation instantanée de la couleur et de la luminosité. 🏎️ Désactivez cette option pour les lumières qui ne prennent pas en charge `light.turn_on` avec couleur et luminosité.", + "include_config_in_attributes": "include_config_in_attributes : Afficher toutes les options en tant qu'attributs sur l'interrupteur dans Home Assistant lorsqu'il est défini sur `true`. 📝", + "skip_redundant_commands": "skip_redundant_commands : Évite d'envoyer des commandes d'adaptation lorsque l'état cible est déjà égal à l'état connu de la lumière. Minimise le trafic réseau et améliore la réactivité de l'adaptation dans certaines situations. 📉 Désactivez si les états physiques des lumières ne correspondent pas à l'état enregistré de Home Assistant.", + "transition_until_sleep": "transition_until_sleep : Lorsqu'activée, l'Éclairage Adaptatif considérera les paramètres de sommeil comme le minimum, effectuant la transition vers ces valeurs après le coucher du soleil. 🌙" }, "data_description": { "interval": "Fréquence d'adaptation des lumières, en secondes. 🔄", @@ -53,7 +58,18 @@ "sunrise_offset": "Ajuster l'heure du lever de soleil avec un décalage positif ou négatif en secondes. ⏰", "transition": "Durée de la transition des changements lumineux, en secondes. 🕑", "initial_transition": "Durée de la première transition des lampes passant de `off` à `on` en secondes. ⏲️", - "sleep_transition": "Durée de la transition quand le \"mode sommeil\" est déclenché en secondes. 😴" + "sleep_transition": "Durée de la transition quand le \"mode sommeil\" est déclenché en secondes. 😴", + "min_sunset_time": "Définir l'heure virtuelle de coucher du soleil la plus précoce (HH:MM:SS), permettant des couchers de soleil ultérieurs. 🌇", + "sleep_rgb_color": "Couleur RGB en mode sommeil (utilisée lorsque `sleep_rgb_or_color_temp` est `rgb_color`). 🌈", + "brightness_mode_time_light": "(Ignoré si `brightness_mode='default'`) La durée en secondes pour augmenter/diminuer progressivement la luminosité après/avant le lever/coucher du soleil. 📈📉.", + "sunset_time": "Définir une heure fixe (HH:MM:SS) pour le coucher du soleil. 🌅", + "sunrise_time": "Définir une heure fixe (HH:MM:SS) pour le lever du soleil. 🌅", + "brightness_mode_time_dark": "(Ignoré si brightness_mode='default') La durée en secondes pour augmenter/diminuer progressivement la luminosité après/avant le lever/coucher du soleil. 📈📉.", + "sleep_rgb_or_color_temp": "Utilisez soit `\"rgb_color\"` soit `\"color_temp\"` en mode sommeil. 🌙", + "min_sunrise_time": "Définir l'heure virtuelle de lever du soleil la plus précoce (HH:MM:SS), permettant des levers de soleil ultérieurs. 🌅", + "adapt_delay": "Temps d'attente (en secondes) entre l'allumage de la lumière et l'application des changements par l'Éclairage Adaptatif. Peut aider à éviter les scintillements. ⏲️", + "max_sunset_time": "Définir l'heure virtuelle de coucher du soleil la plus tardive (HH:MM:SS), permettant des couchers de soleil plus précoces. 🌇", + "max_sunrise_time": "Définir l'heure virtuelle de lever du soleil la plus tardive (HH:MM:SS), permettant des levers de soleil plus précoces. 🌅" } } }, @@ -103,6 +119,54 @@ }, "sleep_transition": { "description": "Durée de la transition quand le \"mode sommeil\" est déclenché en secondes. 😴" + }, + "min_brightness": { + "description": "Pourcentage de luminosité minimum. 💡" + }, + "sunrise_time": { + "description": "Définir une heure fixe (HH:MM:SS) pour le lever du soleil. 🌅" + }, + "max_brightness": { + "description": "Pourcentage de luminosité maximale. 💡" + }, + "take_over_control": { + "description": "Désactiver l'Éclairage Adaptatif si une autre source appelle `light.turn_on` lorsque les lumières sont allumées et en cours d'adaptation. Notez que cela appelle `homeassistant.update_entity` à chaque `intervalles` ! 🔒" + }, + "use_defaults": { + "description": "Définit les valeurs par défaut non spécifiées dans cet appel de service. Options : \"current\" (par défaut, conserve les valeurs actuelles), \"factory\" (réinitialise aux valeurs par défaut documentées) ou \"configuration\" (revient aux valeurs par défaut de la configuration de l'interrupteur). ⚙️" + }, + "sunset_time": { + "description": "Définir une heure fixe (HH:MM:SS) pour le coucher du soleil. 🌅" + }, + "min_sunset_time": { + "description": "Définir l'heure virtuelle de coucher du soleil la plus précoce (HH:MM:SS), permettant des couchers de soleil ultérieurs. 🌇" + }, + "max_sunrise_time": { + "description": "Définir l'heure virtuelle du lever du soleil la plus tardive (HH:MM:SS), permettant des levers de soleil plus précoces. 🌅" + }, + "min_color_temp": { + "description": "Température de couleur la plus chaude en Kelvin. 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "Utilisez soit `\"rgb_color\"` soit `\"color_temp\"` en mode sommeil. 🌙" + }, + "turn_on_lights": { + "description": "Indique s'il faut allumer les lumières qui sont actuellement éteintes. 🔆" + }, + "include_config_in_attributes": { + "description": "Afficher toutes les options en tant qu'attributs sur l'interrupteur dans Home Assistant lorsqu'il est défini sur `true`. 📝" + }, + "sleep_rgb_color": { + "description": "Couleur RGB en mode sommeil (utilisée lorsque `sleep_rgb_or_color_temp` est `rgb_color`). 🌈" + }, + "adapt_delay": { + "description": "Temps d'attente (en secondes) entre l'allumage de la lumière et l'application des changements par l'Éclairage Adaptatif. Peut aider à éviter les scintillements. ⏲️" + }, + "separate_turn_on_commands": { + "description": "Utilisez des appels distincts à `light.turn_on` pour la couleur et la luminosité, nécessaire pour certains types de lumières. 🔀" + }, + "prefer_rgb_color": { + "description": "Indique s'il faut privilégier l'ajustement de la couleur RGB plutôt que la température de couleur de la lumière lorsque c'est possible. 🌈" } }, "description": "Changez les réglages que vous souhaitez dans le commutateur. Toutes les options ici sont les mêmes que dans le flux de configuration." @@ -115,8 +179,37 @@ }, "transition": { "description": "Durée de la transition des changements lumineux, en secondes. 🕑" + }, + "entity_id": { + "description": "L'`entity_id` de l'interrupteur avec les paramètres à appliquer. 📝" + }, + "adapt_brightness": { + "description": "Indique s'il faut adapter la luminosité de la lumière. 🌞" + }, + "turn_on_lights": { + "description": "Indique s'il faut allumer les lumières qui sont actuellement éteintes. 🔆" + }, + "adapt_color": { + "description": "Indique s'il faut adapter la couleur sur les lumières compatibles. 🌈" + }, + "prefer_rgb_color": { + "description": "Indique s'il faut privilégier l'ajustement de la couleur RGB plutôt que la température de couleur de la lumière lorsque c'est possible. 🌈" } } + }, + "set_manual_control": { + "fields": { + "lights": { + "description": "entity_id(s) des lumières, si non spécifié, toutes les lumières dans le commutateur sont sélectionnées. 💡" + }, + "manual_control": { + "description": "Indique s'il faut ajouter (\"true\") ou retirer (\"false\") la lumière de la liste \"manual_control\". 🔒" + }, + "entity_id": { + "description": "L'`entity_id` de l'interrupteur dans lequel (dé)marquer la lumière comme étant `manuellement contrôlée`. 📝" + } + }, + "description": "Indiquer si une lumière est 'manuellement contrôlée'." } } } diff --git a/custom_components/adaptive_lighting/translations/hu.json b/custom_components/adaptive_lighting/translations/hu.json index 1844163d..3af17eff 100644 --- a/custom_components/adaptive_lighting/translations/hu.json +++ b/custom_components/adaptive_lighting/translations/hu.json @@ -3,13 +3,54 @@ "step": { "init": { "data_description": { - "sleep_color_temp": "Színhőmérséklet alvó üzemmódban (amikor a `sleep_rgb_or_color_temp` értéke `color_temp`) Kelvinben megadva. 😴" + "sleep_color_temp": "Színhőmérséklet alvó üzemmódban (amikor a `sleep_rgb_or_color_temp` értéke `color_temp`) Kelvinben megadva. 😴", + "sleep_rgb_or_color_temp": "Az `\"rgb_color\" vagy a `\"color_temp\" használata alvó üzemmódban. 🌙", + "sleep_transition": "Az transition időtartama az \"alvó üzemmód\" kapcsolásakor másodpercben. 😴", + "autoreset_control_seconds": "Automatikusan visszaállítja a kézi vezérlést néhány másodperc után. A letiltáshoz állítsa 0-ra. ⏲️", + "min_sunset_time": "Állítsa be a legkorábbi virtuális naplemente időpontját (HH:MM:SS), lehetővé téve a későbbi naplementéket. 🌇", + "sleep_brightness": "Az alvó üzemmódban lévő lights fényerejének százalékos értéke. 😴", + "min_sunrise_time": "Állítsa be a legkorábbi virtuális napfelkelte időpontját (HH:MM:SS), lehetővé téve a későbbi napfelkeltét. 🌅", + "interval": "Gyakoriság a lights illesztéséhez, másodpercekben. 🔄", + "adapt_delay": "Várakozási idő (másodpercben) a világítás bekapcsolása és az Adaptív világítás alkalmazása között. Segíthet elkerülni a villódzást. ⏲️", + "sleep_rgb_color": "RGB szín alvó üzemmódban (akkor érvényes, ha a `sleep_rgb_or_color_temp` értéke \"rgb_color\"). 🌈", + "sunrise_offset": "A napfelkelte idejének beállítása pozitív vagy negatív eltolással másodpercekben. ⏰", + "transition": "Az transition időtartama, amikor a lights változnak, másodpercben. 🕑", + "brightness_mode": "Használandó fényerő üzemmód. A lehetséges értékek: `default`, `linear` és `tanh` (a `brightness_mode_time_dark` és `brightness_mode_time_light` értékeket használja). 📈", + "brightness_mode_time_light": "(Figyelmen kívül hagyva, ha `brightness_mode='default'`) A fényerő növelésének/csökkentésének időtartama másodpercben napfelkelte/napnyugta után/előtt. 📈📉.", + "sunset_offset": "A naplemente idejének beállítása pozitív vagy negatív eltolással másodpercekben. ⏰", + "sunset_time": "Állítson be egy fix időpontot (HH:MM:SS) a naplementéhez. 🌇", + "max_sunset_time": "A legkésőbbi virtuális napnyugta időpontjának beállítása (HH:MM:SS), amely lehetővé teszi a korábbi naplementéket. 🌇", + "sunrise_time": "Állítson be egy fix időpontot (HH:MM:SS) a napfelkeltéhez. 🌅", + "initial_transition": "Az első transition időtartama, amikor a lights \"kikapcsolt\" állapotból \"bekapcsolt\" állapotba váltanak, másodpercben. ⏲️", + "brightness_mode_time_dark": "(Figyelmen kívül hagyva, ha `brightness_mode='default'`) A fényerő növelésének/csökkentésének időtartama másodpercben napfelkelte/napnyugta után/előtt. 📈📉.", + "max_sunrise_time": "A legkésőbbi virtuális napfelkelte időpontjának beállítása (HH:MM:SS), amely lehetővé teszi a korábbi napfelkeltét. 🌅", + "send_split_delay": "Késleltetés (ms-ban) a `separate_turn_on_commands` (különálló_bekapcsolási_parancsok) között olyan lights esetében, amelyek nem támogatják a fényerő és a szín egyidejű beállítását. ⏲️" }, "data": { - "max_brightness": "max_brightness: Maximális fényerő százalékban megadva. 💡" + "max_brightness": "max_brightness: Maximális fényerő százalékban megadva. 💡", + "detect_non_ha_changes": "detect_non_ha_changes: `Világítás: Bekapcsolás`- szolgáltatás meghívástól eltérő állapotváltozások esetén észleli és leállítja az illesztéseket. A `take_over_control` beállítás engedélyezése szükséges. 🕵️ Vigyázat: ⚠️ Egyes lights tévesen jelezhetik a \"bekapcsolt\" állapotot, ami váratlanul bekapcsolódó lámpákhoz vezethet. Ha ilyen problémákat tapasztal, tiltsa le ezt a funkciót.", + "multi_light_intercept": "multi_light_intercept: `Világítás: Bekapcsolás` szolgáltatás hívások elfogása és adaptálása, amelyek több fényt céloznak meg. ➗⚠️ Ez azt eredményezheti, hogy egyetlen `Világítás: Bekapcsolás` szolgáltatás hívás több hívásra oszlik fel, pl. ha a lights különböző kapcsolókban vannak. Az `elfogás` engedélyezése szükséges.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Kizárólag a bekapcsoláskor érvényes. A beállítást \"igaz\"-ra állítva, az AL csak akkor végzi az illesztést, amennyiben a \"Világítás: Bekapcsolás\" szolgáltatás meghívása a szín és fényerő paraméterek megadása nélkül történik.❌🌈 Ez pl. alkalmas az illesztés felfüggesztésére egy jelenet aktiválásakor. \"Hamis\" beállítás esetén az AL elvégzi a kezdeti illesztést a szín és fényerő paraméterek meghívásától függetlenül. A használatához engedélyezve kell lennie a \"take_over_control\" beállításnak. 🕵️ ", + "skip_redundant_commands": "skip_redundant_commands: Az olyan adaptációs parancsok küldésének kihagyása, amelyek célállapota már megegyezik a fény ismert állapotával. Minimalizálja a hálózati forgalmat, és bizonyos helyzetekben javítja az adaptációs reakciókészséget. 📉Kapcsolja ki, ha a fizikai fényállapotok nem szinkronizálódnak a HA rögzített állapotával.", + "separate_turn_on_commands": "separate_turn_on_commands: Elkülönített `Világítás: Bekapcsolás` hívásokat használ a szín és a fényerő beállításához, ami néhány világítás típusnál szükséges. 🔀", + "max_color_temp": "max_color_temp: A leghidegebb színhőmérséklet kelvinben. ❄️", + "prefer_rgb_color": "prefer_rgb_color: Lehetőség szerint az RGB színbeállítás előnyben részesítése a fény színhőmérsékletével szemben. 🌈", + "intercept": "elfogás: `Világítás: Bekapcsolás` szolgáltatás hívások elfogása és adaptálása a színek és a fényerő azonnali illesztésének lehetővé tétele érdekében. 🏎️ Letiltja az olyan lights esetében, amelyek nem támogatják a `Világítás: Bekapcsolás` szolgáltatás színnel és fényerővel történő szín- és fényerőszabályozását.", + "only_once": "only_once: lights illesztése kizárólag, amikor azok be vannak kapcsolva (`igaz`) vagy tartsa folyamatosan illesztve őket (`false`). 🔄", + "take_over_control": "take_over_control: Adaptív világítás kikapcsolása, amennyiben más forrásból érkező `Világítás: Bekapcsolás` szolgáltatás hívás történik, miközben a fények be vannak kapcsolva és illesztve vannak. Vegye figyelembe, hogy ez minden `interval`-ban meghívja a `homeassistant.update_entity`-t! 🔒", + "lights": "lights: Az entity_id-k listája, amelyeket az AL vezéreljen (üresen is maradhat).🌟", + "min_brightness": "min_brightness: Minimális fényerő százalékban. 💡", + "min_color_temp": "min_color_temp: A legmelegebb színhőmérséklet Kelvinben. 🔥", + "transition_until_sleep": "transition_until_sleep: Ha engedélyezve van, az Adaptív világítás az alvó mód beállításokat minimálisnak tekinti, és napnyugta után ezekre az értékekre vált át. 🌙", + "include_config_in_attributes": "include_config_in_attributes: A kapcsoló összes opciójának attribútumként való megjelenítése a Home Assistantben, ha a beállítás értéke `igaz`. 📝" }, - "title": "Adaptív világítás beállításai" + "title": "Adaptív világítás beállításai", + "description": "Egy Adaptív világítás komponens konfigurálása. Az opciók nevei a YAML-beállításokhoz igazodnak. Ha ezt a bejegyzést YAML-ben definiálta, itt nem jelennek meg beállítások. A paraméterek hatásait bemutató interaktív grafikonokért látogasson el [erre a webes alkalmazásra](https://basnijholt.github.io/adaptive-lighting). További részletekért olvasd el a [hivatalos dokumentációt](https://github.com/basnijholt/adaptive-lighting#readme)." } + }, + "error": { + "option_error": "Érvénytelen beállítás", + "entity_missing": "Egy vagy több kiválasztott világítás entitás hiányzik a Home Assistantból" } }, "title": "Adaptív világítás", @@ -24,18 +65,138 @@ }, "max_color_temp": { "description": "Leghidegebb színhőmérséklet Kelvinben megadva. ❄️" + }, + "sleep_brightness": { + "description": "Az alvó üzemmódban lévő lights fényerejének százalékos értéke. 😴" + }, + "detect_non_ha_changes": { + "description": "`Világítás: Bekapcsolás`- szolgáltatás meghívástól eltérő állapotváltozások esetén észleli és leállítja az illesztéseket. A `take_over_control` beállítás engedélyezése szükséges. 🕵️ Vigyázat: ⚠️ Egyes lights tévesen jelezhetik a \"bekapcsolt\" állapotot, ami váratlanul bekapcsolódó lámpákhoz vezethet. Ha ilyen problémákat tapasztal, tiltsa le ezt a funkciót." + }, + "sunrise_offset": { + "description": "A napfelkelte idejének beállítása pozitív vagy negatív eltolással másodpercekben. ⏰" + }, + "max_sunrise_time": { + "description": "A legkésőbbi virtuális napfelkelte időpontjának beállítása (HH:MM:SS), amely lehetővé teszi a korábbi napfelkeltét. 🌅" + }, + "sleep_color_temp": { + "description": "Színhőmérséklet alvó üzemmódban (akkor használatos, ha a `sleep_rgb_or_color_temp` a `color_temp`-re van állítva) kelvinben. 😴" + }, + "min_brightness": { + "description": "Minimális fényerő százalékban. 💡" + }, + "min_color_temp": { + "description": "A legmelegebb színhőmérséklet Kelvinben. 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "Az `\"rgb_color\" vagy a `\"color_temp\" használata alvó üzemmódban. 🌙" + }, + "turn_on_lights": { + "description": "A jelenleg kikapcsolt fények bekapcsolása. 🔆" + }, + "initial_transition": { + "description": "Az első transition időtartama, amikor a lights \"kikapcsolt\" állapotból \"bekapcsolt\" állapotba váltanak, másodpercben. ⏲️" + }, + "sunrise_time": { + "description": "Állítson be egy fix időpontot (HH:MM:SS) a napfelkeltéhez. 🌅" + }, + "include_config_in_attributes": { + "description": "Az összes beállítás megjelenítése a kapcsoló attribútumaként a Home Assistantban, ha a beállítás `igaz`. 📝" + }, + "sleep_rgb_color": { + "description": "RGB szín alvó üzemmódban (akkor érvényes, ha a `sleep_rgb_or_color_temp` értéke \"rgb_color\"). 🌈" + }, + "take_over_control": { + "description": "Az Adaptív világítás letiltása, ha egy másik forrásból érkező `Világítás: Bekapcsolás` hívja, miközben a világítás be van kapcsolva és AL által illesztve van. Vegye figyelembe, hogy ez minden \"interval\"-ban meghívja a `homeassistant.update_entity`-t! 🔒" + }, + "sleep_transition": { + "description": "Az transition időtartama az \"alvó üzemmód\" kapcsolásakor másodpercben. 😴" + }, + "autoreset_control_seconds": { + "description": "Automatikusan visszaállítja a kézi vezérlést néhány másodperc után. A letiltáshoz állítsa 0-ra. ⏲️" + }, + "adapt_delay": { + "description": "Várakozási idő (másodpercben) a világítás bekapcsolása és az Adaptív világítás alkalmazása között. Segíthet elkerülni a villódzást. ⏲️" + }, + "only_once": { + "description": "A lights illesztése csak a bekapcsoláskor egy alkalommal (\"igaz\") vagy folyamatosan történjen a bekapcsolás után is (\"hamis\")." + }, + "use_defaults": { + "description": "Beállítja az ebben a szolgáltatáshívásban meg nem adott alapértelmezett értékeket. Opciók: \"(alapértelmezett, megtartja az aktuális értékeket), \"gyári\" (visszaállítja a dokumentált alapértelmezett értékeket) vagy \"konfiguráció\" (visszaállítja a kapcsoló konfigurációjának alapértelmezett értékeit). ⚙️" + }, + "separate_turn_on_commands": { + "description": "Elkülönített `Világítás: Bekapcsolás` szolgáltatás hívások használata a szín és a fényerő számára, ami néhány világítás típusnál szükséges. 🔀" + }, + "prefer_rgb_color": { + "description": "Lehetőség szerint az RGB színbeállítás előnyben részesítése a fény színhőmérsékletével szemben. 🌈" + }, + "sunset_offset": { + "description": "A naplemente idejének beállítása pozitív vagy negatív eltolással másodpercekben. ⏰" + }, + "send_split_delay": { + "description": "Késleltetés (ms-ban) a `separate_turn_on_commands` (különálló_bekapcsolási_parancsok) között olyan lights esetében, amelyek nem támogatják a fényerő és a szín egyidejű beállítását. ⏲️" + }, + "sunset_time": { + "description": "Állítson be egy fix időpontot (HH:MM:SS) a naplementéhez. 🌇" + }, + "transition": { + "description": "Az transition időtartama, amikor a lights változnak, másodpercben. 🕑" + }, + "min_sunset_time": { + "description": "Állítsa be a legkorábbi virtuális naplemente időpontját (HH:MM:SS), lehetővé téve a későbbi naplementéket. 🌇" + } + }, + "description": "Módosítsa a kapcsolóban a kívánt beállításokat. Itt minden beállítás ugyanaz, mint a konfigurációs folyamban." + }, + "set_manual_control": { + "description": "Jelölje meg, hogy egy lámpa „kézi vezérlésű”-e.", + "fields": { + "manual_control": { + "description": "A világítás hozzáadása (\"true\") vagy eltávolítása (\"false\") a \"manual_control\" listából. 🔒" + }, + "entity_id": { + "description": "A kapcsoló `entity_id`-je, amelyben a lámpát \"kézi vezérlésűnek\" kell jelölni. 📝" + }, + "lights": { + "description": "a lights entity_id-je(i), ha nincs megadva, a kapcsoló összes lights ki lesz választva. 💡" } } }, - "set_manual_control": { - "description": "Jelölje meg, hogy egy lámpa „kézi vezérlésű”-e." + "apply": { + "fields": { + "entity_id": { + "description": "Az alkalmazandó beállításokat tartalmazó kapcsoló `entity_id`-je. 📝" + }, + "adapt_brightness": { + "description": "A világítás fényerejének beállítása. 🌞" + }, + "turn_on_lights": { + "description": "A jelenleg kikapcsolt lights bekapcsolása. 🔆" + }, + "adapt_color": { + "description": "A lights által támogatott színek beállítása. 🌈" + }, + "prefer_rgb_color": { + "description": "Lehetőség szerint az RGB színbeállítás előnyben részesítése a fény színhőmérsékletével szemben. 🌈" + }, + "lights": { + "description": "A világítás (vagy a világítások listája), amelyre a beállításokat alkalmazni kell.💡" + }, + "transition": { + "description": "Az transition időtartama, amikor a lights változnak, másodpercben. 🕑" + } + }, + "description": "Az aktuális Adaptív világítás beállításokat alkalmazza a lights-ra." } }, "config": { "step": { "user": { - "title": "Válasszon nevet az Adaptív világítás példánynak" + "title": "Válasszon nevet az Adaptív világítás példánynak", + "description": "Minden integrációs tétel több lights-t is tartalmazhat!" } + }, + "abort": { + "already_configured": "Ez az eszköz már be van állítva" } } } diff --git a/custom_components/adaptive_lighting/translations/id.json b/custom_components/adaptive_lighting/translations/id.json new file mode 100644 index 00000000..2474796b --- /dev/null +++ b/custom_components/adaptive_lighting/translations/id.json @@ -0,0 +1,202 @@ +{ + "services": { + "change_switch_settings": { + "fields": { + "sleep_brightness": { + "description": "Persentase kecerahan lampu dalam mode tidur. 😴" + }, + "detect_non_ha_changes": { + "description": "Mendeteksi dan menghentikan adaptasi untuk perubahan status non-`light.turn_on`. Perlu mengaktifkan `take_over_control`. 🕵️ Perhatian: ⚠️ Beberapa lampu mungkin salah menunjukkan status 'hidup' yang dapat mengakibatkan lampu menyala secara tidak terduga. Nonaktifkan fitur ini jika Anda mengalami masalah seperti itu." + }, + "sunrise_offset": { + "description": "Sesuaikan waktu matahari terbit dengan offset positif atau negatif dalam hitungan detik. ⏰" + }, + "max_sunrise_time": { + "description": "Atur waktu matahari terbit virtual terkini (HH:MM:SS), memungkinkan matahari terbit lebih cepat. 🌅" + }, + "sleep_color_temp": { + "description": "Suhu warna dalam mode tidur (digunakan ketika `sleep_rgb_or_color_temp` adalah `color_temp`) dalam Kelvin. 😴" + }, + "min_brightness": { + "description": "Persentase kecerahan minimum. 💡" + }, + "min_color_temp": { + "description": "Suhu warna terhangat dalam Kelvin. 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "Gunakan `\"rgb_color\"` atau `\"color_temp\"` dalam mode tidur. 🌙" + }, + "turn_on_lights": { + "description": "Kalau ingin menyalakan lampu yang sedang mati. 🔆" + }, + "initial_transition": { + "description": "Durasi transisi pertama saat lampu berubah dari `mati` ke `hidup` dalam hitungan detik. ⏲️" + }, + "entity_id": { + "description": "ID Entitas sakelar. 📝" + }, + "sunrise_time": { + "description": "Tetapkan waktu tetap (HH:MM:SS) untuk matahari terbit. 🌅" + }, + "include_config_in_attributes": { + "description": "Tampilkan semua opsi sebagai atribut pada sakelar di Home Assistant ketika diatur ke `true`. 📝" + }, + "max_brightness": { + "description": "Persentase kecerahan maksimum. 💡" + }, + "sleep_rgb_color": { + "description": "Warna RGB dalam mode tidur (digunakan ketika `sleep_rgb_or_color_temp` adalah \"rgb_color\"). 🌈" + }, + "take_over_control": { + "description": "Nonaktifkan Pencahayaan Adaptif jika sumber lain memanggil `light.turn_on` saat lampu menyala dan sedang diadaptasi. Perhatikan bahwa ini memanggil `homeassistant.update_entity` setiap `interval`! 🔒" + }, + "sleep_transition": { + "description": "Durasi transisi ketika \"mode tidur\" diubah, dalam hitungan detik. 😴" + }, + "autoreset_control_seconds": { + "description": "Secara otomatis mengatur ulang kontrol manual setelah beberapa detik. Setel ke 0 untuk menonaktifkan. ⏲️" + }, + "adapt_delay": { + "description": "Waktu tunggu (detik) antara lampu menyala dan penerapan ubahan Pencahayaan Adaptif. Mungkin membantu untuk menghindari kedipan. ⏲️" + }, + "only_once": { + "description": "Sesuaikan lampu hanya saat menyala (`true`) atau terus sesuaikan (`false`). 🔄" + }, + "use_defaults": { + "description": "Menetapkan nilai bawaan yang tidak ditentukan dalam panggilan layanan ini. Opsi: \"current\" (bawaan, mempertahankan nilai saat ini), \"factory\" (direset ke nilai bawaan yang terdokumentasi), atau \"configuration\" (kembali ke nilai bawaan konfigurasi sakelar). ⚙️" + }, + "separate_turn_on_commands": { + "description": "Gunakan panggilan `light.turn_on` terpisah untuk warna dan kecerahan, diperlukan untuk beberapa jenis lampu. 🔀" + }, + "prefer_rgb_color": { + "description": "Kalau lebih memilih penyesuaian warna RGB dibandingkan suhu warna terang jika memungkinkan. 🌈" + }, + "max_color_temp": { + "description": "Suhu warna terdingin dalam Kelvin. ❄️" + }, + "sunset_offset": { + "description": "Sesuaikan waktu matahari terbenam dengan offset positif atau negatif dalam hitungan detik. ⏰" + }, + "send_split_delay": { + "description": "Waktu tunda (ms) antara `separate_turn_on_commands` untuk lampu yang tidak mendukung pengaturan kecerahan dan warna secara bersamaan. ⏲️" + }, + "sunset_time": { + "description": "Tetapkan waktu tetap (HH:MM:SS) untuk matahari terbenam. 🌇" + }, + "transition": { + "description": "Durasi transisi saat lampu berganti, dalam hitungan detik. 🕑" + }, + "min_sunset_time": { + "description": "Tetapkan waktu matahari terbenam virtual paling awal (HH:MM:SS), memungkinkan matahari terbenam di kemudian waktu. 🌇" + } + }, + "description": "Ubah pengaturan apa pun yang Anda inginkan di sakelar. Semua opsi di sini sama seperti pada alur konfigurasi." + }, + "apply": { + "fields": { + "entity_id": { + "description": "`eEntity_id` sakelar dengan pengaturan yang akan diterapkan. 📝" + }, + "adapt_brightness": { + "description": "Kalau ingin menyesuaikan kecerahan lampu. 🌞" + }, + "turn_on_lights": { + "description": "Kalau ingin menyalakan lampu yang sedang mati. 🔆" + }, + "adapt_color": { + "description": "Kalau ingin menyesuaikan warna pada lampu pendukung. 🌈" + }, + "prefer_rgb_color": { + "description": "Kalau lebih memilih penyesuaian warna RGB dibandingkan suhu warna terang jika memungkinkan. 🌈" + }, + "lights": { + "description": "Lampu (atau daftar lampu) untuk menerapkan pengaturan. 💡" + }, + "transition": { + "description": "Durasi transisi saat lampu berganti, dalam hitungan detik. 🕑" + } + }, + "description": "Menerapkan pengaturan Pencahayaan Adaptif saat ini ke lampu." + }, + "set_manual_control": { + "fields": { + "manual_control": { + "description": "Kalau ingin menambahkan (\"true\") atau menghapus (\"false\") lampu dari daftar \"manual_control\". 🔒" + }, + "entity_id": { + "description": "`entity_id` dari sakelar yang digunakan untuk membatalkan penandaan lampu sebagai `manually controlled`. 📝" + }, + "lights": { + "description": "entity_id(s) lampu, jika tidak ditentukan, semua lampu di sakelar dipilih. 💡" + } + }, + "description": "Tandai kalau lampu 'dikontrol secara manual'." + } + }, + "options": { + "step": { + "init": { + "data_description": { + "sleep_rgb_or_color_temp": "Gunakan `\"rgb_color\"` atau `\"color_temp\"` dalam mode tidur. 🌙", + "sleep_color_temp": "Suhu warna dalam mode tidur (digunakan ketika `sleep_rgb_or_color_temp` adalah `color_temp`) dalam Kelvin. 😴", + "sleep_transition": "Durasi transisi ketika \"mode tidur\" diubah, dalam hitungan detik. 😴", + "autoreset_control_seconds": "Secara otomatis mengatur ulang kontrol manual setelah beberapa detik. Setel ke 0 untuk menonaktifkan. ⏲️", + "min_sunset_time": "Tetapkan waktu matahari terbenam virtual paling awal (HH:MM:SS), memungkinkan matahari terbenam di kemudian waktu. 🌇", + "sleep_brightness": "Persentase kecerahan lampu dalam mode tidur. 😴", + "min_sunrise_time": "Tetapkan waktu matahari terbit virtual paling awal (HH:MM:SS), memungkinkan matahari terbit di kemudian waktu. 🌅", + "interval": "Frekuensi untuk menyesuaikan lampu, dalam hitungan detik. 🔄", + "adapt_delay": "Waktu tunggu (detik) antara lampu menyala dan penerapan ubahan Pencahayaan Adaptif. Mungkin membantu untuk menghindari kedipan. ⏲️", + "sleep_rgb_color": "Warna RGB dalam mode tidur (digunakan ketika `sleep_rgb_or_color_temp` adalah \"rgb_color\"). 🌈", + "sunrise_offset": "Sesuaikan waktu matahari terbit dengan offset positif atau negatif dalam hitungan detik. ⏰", + "transition": "Durasi transisi saat lampu berganti, dalam hitungan detik. 🕑", + "brightness_mode": "Mode kecerahan untuk digunakan. Nilai yang memungkinkan adalah `default`, `linear`, dan `tanh` (menggunakan `brightness_mode_time_dark` dan `brightness_mode_time_light`). 📈", + "brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) Durasi dalam hitungan detik untuk meningkatkan/menurunkan kecerahan setelah/sebelum matahari terbit/terbenam. 📈📉.", + "sunset_offset": "Sesuaikan waktu matahari terbenam dengan offset positif atau negatif dalam hitungan detik. ⏰", + "sunset_time": "Tetapkan waktu tetap (HH:MM:SS) untuk matahari terbenam. 🌇", + "max_sunset_time": "Atur waktu matahari terbenam virtual terkini (HH:MM:SS), memungkinkan matahari terbenam lebih cepat. 🌇", + "sunrise_time": "Tetapkan waktu tetap (HH:MM:SS) untuk matahari terbit. 🌅", + "initial_transition": "Durasi transisi pertama saat lampu berubah dari `mati` ke `hidup` dalam hitungan detik. ⏲️", + "brightness_mode_time_dark": "(Diabaikan jika `brightness_mode='default'`) Durasi dalam hitungan detik untuk meningkatkan/menurunkan kecerahan sebelum/sesudah matahari terbit/terbenam. 📈📉", + "max_sunrise_time": "Atur waktu matahari terbit virtual terkini (HH:MM:SS), memungkinkan matahari terbit lebih cepat. 🌅", + "send_split_delay": "Waktu tunda (ms) antara `separate_turn_on_commands` untuk lampu yang tidak mendukung pengaturan kecerahan dan warna secara bersamaan. ⏲️" + }, + "data": { + "detect_non_ha_changes": "detect_non_ha_changes: Mendeteksi dan menghentikan adaptasi untuk perubahan status non-`light.turn_on`. Perlu mengaktifkan `take_over_control`. 🕵️ Perhatian: ⚠️ Beberapa lampu mungkin salah menunjukkan status 'hidup' yang dapat mengakibatkan lampu menyala secara tidak terduga. Nonaktifkan fitur ini jika Anda mengalami masalah seperti itu.", + "multi_light_intercept": "multi_light_intercept: Cegat dan sesuaikan panggilan `light.turn_on` yang menargetkan banyak lampu. ➗⚠️ Hal ini dapat mengakibatkan satu panggilan `light.turn_on` terpecah menjadi beberapa panggilan, misalnya saat lampu berada di sakelar yang berbeda. Membutuhkan `intercept` untuk diaktifkan.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Saat menyalakan lampu pada awalnya. Jika disetel ke `true`, Pencahayaan Adaptif hanya beradaptasi jika `light.turn_on` dipanggil tanpa menentukan warna atau kecerahan. ❌🌈 Misalnya mencegah adaptasi saat mengaktifkan scene. Jika `false`, Pencahayaan Adaptif beradaptasi terlepas dari keberadaan warna atau kecerahan di `service_data` awal. Perlu mengaktifkan `take_over_control`. 🕵️ ", + "skip_redundant_commands": "skip_redundant_commands: Lewati pengiriman perintah adaptasi yang status targetnya sudah sama dengan status cahaya yang diketahui. Meminimalkan lalu lintas jaringan dan meningkatkan respons adaptasi dalam beberapa situasi. 📉Nonaktifkan jika status cahaya fisik tidak sinkron dengan status rekaman HA.", + "separate_turn_on_commands": "separate_turn_on_commands: Gunakan panggilan `light.turn_on` terpisah untuk warna dan kecerahan, diperlukan untuk beberapa jenis lampu. 🔀", + "max_color_temp": "max_color_temp: Suhu warna terdingin dalam Kelvin. ❄️", + "prefer_rgb_color": "prefer_rgb_color: Kalau lebih memilih penyesuaian warna RGB dibandingkan suhu warna terang jika memungkinkan. 🌈", + "max_brightness": "max_brightness: Persentase kecerahan maksimum. 💡", + "intercept": "intercept: Cegat dan sesuaikan panggilan `light.turn_on` untuk mengaktifkan adaptasi warna dan kecerahan seketika. 🏎️ Nonaktifkan untuk lampu yang tidak mendukung `light.turn_on` dengan warna dan kecerahan.", + "only_once": "only_once: Sesuaikan lampu hanya saat menyala (`true`) atau terus sesuaikan (`false`). 🔄", + "take_over_control": "take_over_control: Nonaktifkan Pencahayaan Adaptif jika sumber lain memanggil `light.turn_on` saat lampu menyala dan sedang diadaptasi. Perhatikan bahwa ini memanggil `homeassistant.update_entity` setiap `interval`! 🔒", + "lights": "lights: Daftar entity_ids lampu yang akan dikontrol (boleh kosong). 🌟", + "min_brightness": "min_brightness: Persentase kecerahan minimum. 💡", + "min_color_temp": "min_color_temp: Suhu warna terhangat dalam Kelvin. 🔥", + "transition_until_sleep": "transition_until_sleep: Jika diaktifkan, Pencahayaan Adaptif akan menganggap pengaturan tidur sebagai minimum, dan beralih ke nilai ini setelah matahari terbenam. 🌙", + "include_config_in_attributes": "include_config_in_attributes: Tampilkan semua opsi sebagai atribut pada sakelar di Home Assistant ketika diatur ke `true`. 📝" + }, + "title": "Opsi Pencahayaan Adaptif", + "description": "Konfigurasikan komponen Pencahayaan Adaptif. Nama opsi selaras dengan pengaturan YAML. Jika Anda telah menentukan entri ini di YAML, tidak ada opsi yang akan muncul di sini. Untuk grafik interaktif yang menunjukkan efek parameter, kunjungi [aplikasi web ini](https://basnijholt.github.io/adaptive-lighting). Untuk detail lebih lanjut, lihat [dokumentasi resmi](https://github.com/basnijholt/adaptive-lighting#readme)." + } + }, + "error": { + "option_error": "Opsi tidak valid", + "entity_missing": "Satu atau lebih entitas cahaya yang dipilih hilang dari Home Assistant" + } + }, + "title": "Pencahayaan Adaptif", + "config": { + "step": { + "user": { + "description": "Setiap instance dapat berisi banyak lampu!", + "title": "Pilih nama untuk instance Pencahayaan Adaptif" + } + }, + "abort": { + "already_configured": "Perangkat ini sudah dikonfigurasi" + } + } +} diff --git a/custom_components/adaptive_lighting/translations/ja.json b/custom_components/adaptive_lighting/translations/ja.json index 0967ef42..20cc082c 100644 --- a/custom_components/adaptive_lighting/translations/ja.json +++ b/custom_components/adaptive_lighting/translations/ja.json @@ -1 +1,39 @@ -{} +{ + "title": "適応型照明", + "services": { + "change_switch_settings": { + "fields": { + "sunrise_offset": { + "description": "日の出時間を基準に秒単位で正値もしくは負値で調整する。⏰" + }, + "only_once": { + "description": "適応型照明を照明がオンになっているときのみ(`true`)それとも適応し続ける場合は(`false`)。" + }, + "sunset_offset": { + "description": "日の入時間を基準に秒単位で正値もしくは負値で調整する。⏰" + } + } + }, + "apply": { + "fields": { + "lights": { + "description": "適応する照明(か照明のリスト)の設定。 💡" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: 最初に照明オンにするとき。`true`を設定すると、AL(適応型照明)は調色や明るさを指定せずや`light.turn_on`をしたときのみ適応します。❌🌈 例えば、適応型照明をシーンを有効にするときにしないようにする。`false`であれば、`service_data`に最初から、ALはシーンの状態に関係なく調色や明るさを適応する。`take_over_control`を有効にすることが必要。🕵️ " + }, + "data_description": { + "sunrise_offset": "日の出時間を基準に秒単位で正値もしくは負値で調整する。⏰", + "sunset_offset": "日の入時間を基準に秒単位で正値もしくは負値で調整する。⏰" + }, + "title": "適応型照明オプション" + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/ko.json b/custom_components/adaptive_lighting/translations/ko.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/ko.json @@ -0,0 +1 @@ +{} diff --git a/custom_components/adaptive_lighting/translations/nb.json b/custom_components/adaptive_lighting/translations/nb.json index 7cfba678..8a8f6e1b 100644 --- a/custom_components/adaptive_lighting/translations/nb.json +++ b/custom_components/adaptive_lighting/translations/nb.json @@ -1,50 +1,214 @@ { - "title":"Adaptiv Belysning", - "config":{ - "step":{ - "user":{ - "title":"Velg et navn", - "description":"Velg et navn for denne konfigurasjonen for adaptiv belysning - hver konfigurasjon kan inneholde flere lyskilder!", - "data":{ - "name":"Navn" - } - } - }, - "abort":{ - "already_configured":"Denne enheten er allerede konfigurert!" + "title": "Adaptiv Belysning", + "config": { + "step": { + "user": { + "title": "Velg et navn", + "description": "Velg et navn for denne konfigurasjonen for adaptiv belysning - hver konfigurasjon kan inneholde flere lyskilder!", + "data": { + "name": "Navn" + } } - }, - "options":{ - "step":{ - "init":{ - "title":"Adaptiv Belysning Innstillinger", - "description":"Alle innstillinger for en adaptiv belysning konfigurasjon. Innstillingene er identiske med innstillingene for YAML konfigurasjon. Ingen innstillinger vises dersom du har definert adaptive_lighting i din YAML konfigurasjon.", - "data":{ - "lights":"Lys / Lyskilder", - "initial_transition":"'initial_transition': overgangen (i sekunder) når lysene skrus av eller på - eller når 'sleep_state' endres", - "interval":"'interval': tiden mellom oppdateringer (i sekunder)", - "max_brightness":"'max_brightness': den høyeste lysstyrken (i prosent) på lysene i løpet av en syklus", - "max_color_temp":"'max_color_temp': den høyeste fargetemperaturen (i kelvin) på lysene i løpet av en syklus", - "min_brightness":"'min_brightness': den laveste lysstyrken (i prosent) på lysene i løpet av en syklus", - "min_color_temp":"'min_color_temp': den laveste fargetemperaturen (i kelvin) på lysene i løpet av en syklus", - "only_once":"'only_once': anvend innstillingene for adaptiv belysning kun når lysene skrus av eller på", - "prefer_rgb_color":"'prefer_rgb_color': benytt rgb i stedet for fargetemperatur dersom det er mulig", - "separate_turn_on_commands":"'separate_turn_on_commands': separer kommandone i 'light.turn_on' for hver attributt (farge, lysstyrke, osv.). Dette kan være nødvendig for enkelte typer lys / lyskilder", - "sleep_brightness":"'sleep_brightness': lysstyrken på lysene (i prosent) når 'sleep_mode' (søvnmodus) er aktiv", - "sleep_color_temp":"'sleep_color_temp': fargetemperaturen på lysene (i kelvin) når 'sleep_mode' (søvnmodus) er aktiv", - "sunrise_offset":"'sunrise_offset': hvor lenge før (-) eller etter (+) tidspunktet solen står opp (lokalt) skal defineres som soloppgang (i sekunder)", - "sunrise_time":"'sunrise_time': definer tidspunktet for soloppgang manuelt (i følgende format: TT:MM:SS)", - "sunset_offset":"'sunset_offset': hvor lenge før (-) eller etter (+) tidspunktet solen går ned (lokalt) skal defineres som solnedgang (i sekunder)", - "sunset_time":"'sunset_time': definer tidspunktet for solnedgang manuelt (i følgende format: TT:MM:SS - f. eks: '20:30:00' vil definere tidspunktet for solnegang som halv-ni på kvelden)", - "take_over_control":"'take_over_control': dersom en annen tjeneste enn adaptiv belysning skrur lysene av eller på, vil automatisk adaptering av lyset stoppes inntil lyset (eller den tilhørende bryteren for adaptiv belysning) blir slått av - og på igjen", - "detect_non_ha_changes":"'detect_non_ha_changes': registrerer alle endringer i lysstyrke over 10% med opprinnelse utenfor Home Assistant - krever at 'take_over_control' er aktivert (OBS: tilkaller 'homeassistant.update_entity' ved hvert 'interval'!)", - "transition":"'transition': varigheten (i sekunder) på overgangen når lysene oppdateres " - } - } - }, - "error":{ - "option_error":"En eller flere valgte innstillinger er ugyldige", - "entity_missing": "Et utvalgt lys ble ikke funnet" + }, + "abort": { + "already_configured": "Denne enheten er allerede konfigurert!" + } + }, + "options": { + "step": { + "init": { + "title": "Adaptiv Belysning Innstillinger", + "description": "Alle innstillinger for en adaptiv belysning konfigurasjon. Innstillingene er identiske med innstillingene for YAML konfigurasjon. Ingen innstillinger vises dersom du har definert adaptive_lighting i din YAML konfigurasjon.", + "data": { + "lights": "Lys / Lyskilder", + "initial_transition": "'initial_transition': overgangen (i sekunder) når lysene skrus av eller på - eller når 'sleep_state' endres", + "interval": "'interval': tiden mellom oppdateringer (i sekunder)", + "max_brightness": "'max_brightness': den høyeste lysstyrken (i prosent) på lysene i løpet av en syklus", + "max_color_temp": "'max_color_temp': den høyeste fargetemperaturen (i kelvin) på lysene i løpet av en syklus", + "min_brightness": "'min_brightness': den laveste lysstyrken (i prosent) på lysene i løpet av en syklus", + "min_color_temp": "'min_color_temp': den laveste fargetemperaturen (i kelvin) på lysene i løpet av en syklus", + "only_once": "'only_once': anvend innstillingene for adaptiv belysning kun når lysene skrus av eller på", + "prefer_rgb_color": "'prefer_rgb_color': benytt rgb i stedet for fargetemperatur dersom det er mulig", + "separate_turn_on_commands": "'separate_turn_on_commands': separer kommandone i 'light.turn_on' for hver attributt (farge, lysstyrke, osv.). Dette kan være nødvendig for enkelte typer lys / lyskilder", + "sleep_brightness": "'sleep_brightness': lysstyrken på lysene (i prosent) når 'sleep_mode' (søvnmodus) er aktiv", + "sleep_color_temp": "'sleep_color_temp': fargetemperaturen på lysene (i kelvin) når 'sleep_mode' (søvnmodus) er aktiv", + "sunrise_offset": "'sunrise_offset': hvor lenge før (-) eller etter (+) tidspunktet solen står opp (lokalt) skal defineres som soloppgang (i sekunder)", + "sunrise_time": "'sunrise_time': definer tidspunktet for soloppgang manuelt (i følgende format: TT:MM:SS)", + "sunset_offset": "'sunset_offset': hvor lenge før (-) eller etter (+) tidspunktet solen går ned (lokalt) skal defineres som solnedgang (i sekunder)", + "sunset_time": "'sunset_time': definer tidspunktet for solnedgang manuelt (i følgende format: TT:MM:SS - f. eks: '20:30:00' vil definere tidspunktet for solnegang som halv-ni på kvelden)", + "take_over_control": "'take_over_control': dersom en annen tjeneste enn adaptiv belysning skrur lysene av eller på, vil automatisk adaptering av lyset stoppes inntil lyset (eller den tilhørende bryteren for adaptiv belysning) blir slått av - og på igjen", + "detect_non_ha_changes": "'detect_non_ha_changes': registrerer alle endringer i lysstyrke over 10% med opprinnelse utenfor Home Assistant - krever at 'take_over_control' er aktivert (OBS: tilkaller 'homeassistant.update_entity' ved hvert 'interval'!)", + "transition": "'transition': varigheten (i sekunder) på overgangen når lysene oppdateres ", + "transition_until_sleep": "transition_until_sleep: Når aktivert, Adaptive lightning vil behandle sove innstillingene som minimum, bevege seg til disse verdiene etter solnedgang.", + "skip_redundant_commands": "skip_redundant_commands: Dropp sending av tilpassnings kommandoer hvor målets tilstand allerede er lik den kjente tilstanden til lyset. Minimerer nettverk trafikk og forbedrer tilpasningens responsitivitet i noen situasjoner. Skru av hvis fysisk tilstand til lyset er ute av synkronisering med HA´s registrere tilstand.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_on: Når lysene skrues på. Hvis satt til \"sann\", AL vil bare hvis \"light.turn_on\" er aktivert uten spesifisert farge og styrke. Dette f.eks. forhindrer aktivering når en scene aktiveres. Hvis \"false\", AL vil aktivere uansett om farge og stryke er satt av i opprinnelig \"service_data\". Trenger \"take_over_control\" er aktivert. ", + "intercept": "Bryt: Bryt og tilpass `light.turn_on` kall for å aktivere umiddelbar farge og styrke tilpassning. Deaktiver for lys som ikke støtter `light.turn_on` med farge og styrke.", + "multi_light_intercept": "multi_light_incept: Avskjære og tilpasse \"light.turn_on\" kall til flere lyskilder. Dette kan medføre oppsplitting av et enkelt \"light.turn_on\" kall til flere kall, f.eks når lys tilhører flere brytere. Dette krever at \"intercept\" er aktivert.", + "include_config_in_attributes": "include_config_in_attributes: Vis alle valg som attributes på bryteren i Home Assistant når satt til `true`." + }, + "data_description": { + "sunrise_offset": "Juster soloppgang tidspunkt med en positiv eller negativ forskyvning i sekunder. ⏰", + "sunset_offset": "Juster soloppgang tidspunkt med en positiv eller negativ forskyvning i sekunder. ⏰", + "sleep_rgb_or_color_temp": "Bruk enten `\"rgb_color\"` eller `\"color_temp\"` i sove modus.", + "sleep_rgb_color": "RGB farger i sove modus (brukes når \"sleep_rgb_or_color_temp\" er \"rgb_color\")", + "sleep_brightness": "Lysstyrkeprosent på lysene i sove modus.", + "sleep_color_temp": "Fargetemperatur i sove modus (brukes når `sleep_rgb_or_color_temp` er `color_temp`) i Kelvin.", + "initial_transition": "Varighet på første overgang når lysene endres fra `off` til `on` i sekunder.", + "transition": "Varighet på overgang når lysene endres, i sekunder.", + "interval": "Frekvens til å tilpasse lys, i sekunder.", + "sunset_time": "Sett et fast tidspunkt (TT:MM:SS) for solnedgang.", + "sleep_transition": "Varighet på overgang når \"sleep mode\" er aktivert i sekunder.", + "sunrise_time": "Sett et fast tidspunkt (TT:MM:SS) for soloppgang.", + "min_sunrise_time": "Sett tidligste virituelle tidspunkt for soloppgang (TT:MM:SS), muliggjør for senere soloppganger", + "max_sunrise_time": "Sett det seneste virituelle tidspunktet for soloppgang (TT:MM:SS), muliggjør for tidligere soloppganger.", + "min_sunset_time": "Sett det tidligste virituelle tidspunktet for solnedgang (TT:MM:SS), muliggjør for senere solnedgang.", + "max_sunset_time": "Sett det seneste virituelle tidspunktet for solnedgang (TT:MM:SS), muliggjør for tidligere solnedgang.", + "brightness_mode": "Hvilken lysstyrke moduse skal brukes. Mulige verdier er `default`, `linear`, and `tanh` (bruker `brightness_mode_time_dark` og `brightness_mode_time_light`).", + "send_split_delay": "Forsinkelse (ms) mellom `separate_turn_on_commands` for lys som ikke støtter simultane styrke og farge innstillinger.", + "adapt_delay": "Ventetid (sekunder) mellom at lyset skrues på og Adaptive Lightning sender endringer. Kan hjelpe til for å unngå blinking.", + "autoreset_control_seconds": "Automatisk reset manuell kontroll etter et gitt antall sekunder. Sett til 0 for å skru av.", + "brightness_mode_time_light": "(Ignorere hvis `brightness_mode='default'`) Varigheten i sekunder for å justere opp/ned lysstyrken før/etter soloppgang/solnedgang.", + "brightness_mode_time_dark": "(Ignorere hvis `brightness_mode='default'`) Varigheten i sekunder for å justere opp/ned lysstyrken før/etter soloppgang/solnedgang." + } } - } + }, + "error": { + "option_error": "En eller flere valgte innstillinger er ugyldige", + "entity_missing": "Et utvalgt lys ble ikke funnet" + } + }, + "services": { + "change_switch_settings": { + "fields": { + "sunrise_offset": { + "description": "Juster soloppgang tidspunkt med en positiv eller negativ forskyvning i sekunder. ⏰" + }, + "only_once": { + "description": "Tilpass lys kun når dei er skrudd på (`true`) eller fortsett å tilpasse dei (`false`)" + }, + "sunset_offset": { + "description": "Juster soloppgang tidspunkt med en positiv eller negativ forskyvning i sekunder. ⏰" + }, + "use_defaults": { + "description": "Sett default verdier ikke spesifisert i dette service kallet. Muligheter: \"current\" (default, fortsetter med nåværende verdier), \"factory\" (nullstiller til dokumenterte defaults), eller \"configuration\" (går tilbake til bryter defaults)." + }, + "include_config_in_attributes": { + "description": "Vis alle muligheter som valg på bryteren i Home Assistant når satt til \"true\"." + }, + "initial_transition": { + "description": "Varighet på første overgang når lysene endres fra `off` til `on` i sekunder." + }, + "entity_id": { + "description": "Bryterens Entity ID." + }, + "sleep_transition": { + "description": "Varighet på overgang når \"sleep mode\" er aktivert i sekunder." + }, + "max_brightness": { + "description": "Maksimal lysstyrke prosent." + }, + "separate_turn_on_commands": { + "description": "Bruk separat `light.turn_on` kall for farge og styrke, nødvendig for noen typer lys." + }, + "min_color_temp": { + "description": "Varmeste farge temperatur i Kelvin." + }, + "prefer_rgb_color": { + "description": "Foretrekke RGB farge inntsillinger over lysets fargetemperatur innstilling når mulig." + }, + "max_color_temp": { + "description": "Kaldeste farge temperatur i Kelvin." + }, + "min_brightness": { + "description": "Minste lysstyrke prosent." + }, + "sleep_rgb_or_color_temp": { + "description": "Bruk enten `\"rgb_color\"` eller `\"color_temp\"` i sove modus." + }, + "sleep_brightness": { + "description": "Lysstyrkeprosent på lysene i sove modus." + }, + "send_split_delay": { + "description": "Forsinkelse (ms) mellom `separate_turn_on_commands` for lys som ikke støtter simultane styrke og farge innstillinger." + }, + "sleep_rgb_color": { + "description": "RGB farger i sove modus (brukes når \"sleep_rgb_or_color_temp\" er \"rgb_color\")" + }, + "sleep_color_temp": { + "description": "Fargetemperatur i sove modus (brukes når `sleep_rgb_or_color_temp` er `color_temp`) i Kelvin." + }, + "sunrise_time": { + "description": "Sett et fast tidspunkt (TT:MM:SS) for soloppgang." + }, + "sunset_time": { + "description": "Set et fast tidspunkt (TT:MM:SS) for solnedgang" + }, + "max_sunrise_time": { + "description": "Sett det seneste virituelle tidspunktet for soloppgang (TT:MM:SS), muliggjør for tidligere soloppganger." + }, + "min_sunset_time": { + "description": "Sett det tidligeste virituelle tidspunktet for solnedgang (TT:MM:SS), muliggjør for senere solnedgang." + }, + "detect_non_ha_changes": { + "description": "Detekterer og stopper tilpasningen for ikke-`light.turn_on` tilstander . Trenger `take_over_control` aktivert. Advarsel: Noen lys kan gi falske signal om 'on' tilstand, som kan medføre at lysene skrur seg på av seg selv. Skru av denne funksjonen om dette inntreffer." + }, + "autoreset_control_seconds": { + "description": "Automatisk reset manuell kontroll etter et gitt antall sekunder. Sett til 0 for å skru av." + }, + "transition": { + "description": "Varighet på overgang når lysene endres, i sekunder." + }, + "adapt_delay": { + "description": "Ventetid (sekunder) mellom at lyset skrues på og Adaptive Lightning sender endringer. Kan hjelpe til for å unngå blinking." + }, + "turn_on_lights": { + "description": "Skru på lys som fortiden er skrudd av." + }, + "take_over_control": { + "description": "Skrur av Adaptive Lightning hvis en annen kilde kaller `light.turn_on` mens lysene er på og blir styrt. Merk at dette kaller `homeassistant.update_entity` hvert eneste`interval`!" + } + }, + "description": "Endre hvilken som helst innstilling i bryteren. Alle valg er det samme som i konfigurasjons prosessen." + }, + "apply": { + "fields": { + "lights": { + "description": "Et lys (eller ei liste av lys) instillingene skal påvirke. 💡" + }, + "entity_id": { + "description": "\"entity_id\" på bryteren hvor innstillingene skal legges til." + }, + "transition": { + "description": "Varighet på overgang når lysene endres, i sekunder." + }, + "adapt_brightness": { + "description": "Om å tilpasse styrken til lyset." + }, + "adapt_color": { + "description": "Om å tilpasse fargen på støttelysene." + }, + "prefer_rgb_color": { + "description": "Foretrekke RGB farge inntsillinger over lysets fargetemperatur innstilling når mulig." + }, + "turn_on_lights": { + "description": "Skru på lys som fortiden er skrudd av." + } + }, + "description": "Aktiver nåværende Adaptive Lighting innstillinger til lysene." + }, + "set_manual_control": { + "fields": { + "entity_id": { + "description": "\"entity_id\" på bryteren som skal (u)markeres med at lyset er \"manuelt kontrollert\"" + }, + "manual_control": { + "description": "Enten å legge til (\"true\") eller fjerne (\"false\") lys fra \"manual_control\" listen." + }, + "lights": { + "description": "entity_id(s) til lysene, hvis ikke spesifisert, alle lys i bryteren som er valgt." + } + }, + "description": "Marker om et lys er 'manually controlled'" + } + } } diff --git a/custom_components/adaptive_lighting/translations/ru.json b/custom_components/adaptive_lighting/translations/ru.json index 57a554e1..fd4a4b55 100644 --- a/custom_components/adaptive_lighting/translations/ru.json +++ b/custom_components/adaptive_lighting/translations/ru.json @@ -45,7 +45,8 @@ "adapt_only_on_bare_turn_on": "Adapt_only_on_bare_turn_on: При первоначальном включении света. Если установлено значение «true», AL адаптируется только в том случае, если «light.turn_on» вызывается без указания цвета или яркости. ❌🌈 Это, например, предотвращает адаптацию при активации сцены. Если false, AL адаптируется независимо от наличия цвета или яркости в исходных service_data. Требуется включить take_over_control. 🕵️ ", "skip_redundant_commands": "Skip_redundant_commands: Пропустить отправку команд адаптации, целевое состояние которых уже равно известному состоянию источника света. Минимизирует сетевой трафик и улучшает скорость адаптации в некоторых ситуациях. 📉Отключите, если физические состояния освещения не синхронизируются с записанным состоянием HA.", "intercept": "intercept: перехватывать и адаптировать вызовы `light.turn_on` для обеспечения мгновенной адаптации цвета и яркости. 🏎️ Отключите источники света, которые не поддерживают `light.turn_on` с цветом и яркостью.", - "include_config_in_attributes": "include_config_in_attributes: отображать все параметры в качестве атрибутов на переключателе в Home Assistant, если установлено значение `true`. 📝" + "include_config_in_attributes": "include_config_in_attributes: отображать все параметры в качестве атрибутов на переключателе в Home Assistant, если установлено значение `true`. 📝", + "transition_until_sleep": "transition_until_sleep: когда включено, адаптивное освещение будет рассматривать настройки сна как минимальные, переходя к этим значениям после захода солнца. 🌙" }, "data_description": { "sleep_rgb_or_color_temp": "Используйте либо `\"rgb_color\"`, либо `\"color_temp\"` в спящем режиме. 🌙", @@ -86,6 +87,21 @@ }, "lights": { "description": "Источник света (или список источников света), к которому нужно применить настройки. 💡" + }, + "adapt_brightness": { + "description": "Нужно ли адаптировать яркость света. 🌞" + }, + "turn_on_lights": { + "description": "Включать ли свет, который в данный момент выключен. 🔆" + }, + "adapt_color": { + "description": "Нужно ли адаптировать цвет, если поддерживается источником света. 🌈" + }, + "prefer_rgb_color": { + "description": "Предпочитать ли настройку цвета RGB цветовой температуре света, когда это возможно. 🌈" + }, + "transition": { + "description": "Длительность плавного перехода при смене освещения, в секундах. 🕑" } }, "description": "Применяет текущие настройки адаптивного освещения к источникам света." @@ -118,11 +134,83 @@ }, "min_sunset_time": { "description": "Устанавливает самое раннее время виртуального заката (ЧЧ:ММ:СС), чтобы обеспечить более поздние закаты. 🌇" + }, + "sleep_brightness": { + "description": "Процент яркости света в режиме сна. 😴" + }, + "detect_non_ha_changes": { + "description": "Обнаруживает и останавливает адаптацию для изменений состояния, отличных от `light.turn_on`. Требуется включить `take_over_control`. 🕵️ Внимание: ⚠️ Некоторые индикаторы могут ошибочно указывать включенное состояние, что может привести к неожиданному включению света. Отключите эту функцию, если у вас возникнут такие проблемы." + }, + "sunrise_offset": { + "description": "Отрегулируйте время восхода солнца с положительным или отрицательным смещением в секундах. ⏰" + }, + "sleep_color_temp": { + "description": "Цветовая температура в режиме сна (используется, когда параметр «sleep_rgb_or_color_temp» имеет значение «color_temp») в Кельвинах. 😴" + }, + "min_color_temp": { + "description": "Самая теплая цветовая температура в Кельвинах. 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "Используйте либо «rgb_color», либо «color_temp» в режиме сна. 🌙" + }, + "turn_on_lights": { + "description": "Включать ли свет, который в данный момент выключен. 🔆" + }, + "initial_transition": { + "description": "Длительность первого плавного перехода, когда освещение переключается с «выключено» на «включено» в секундах. ⏲️" + }, + "entity_id": { + "description": "Сущность `Entity ID` переключателя `switch`. 📝" + }, + "take_over_control": { + "description": "Отключите адаптивное освещение, если другой источник вызывает `light.turn_on`, когда освещение включено и адаптируется. Обратите внимание, что это вызывает `homeassistant.update_entity` каждый `interval` интервал! 🔒" + }, + "sleep_transition": { + "description": "Длительность плавного перехода при переключении «спящего режима» в секундах. 😴" + }, + "autoreset_control_seconds": { + "description": "Автоматический сброс ручного управления через Х секунд. Установите значение 0, чтобы отключить. ⏲️" + }, + "adapt_delay": { + "description": "Время ожидания (в секундах) между включением света и применением адаптивного освещения. Может помочь избежать мерцаний. ⏲️" + }, + "only_once": { + "description": "Адаптировать освещение только тогда, когда оно включено («true») или продолжать его адаптировать («false»). 🔄" + }, + "separate_turn_on_commands": { + "description": "Используйте отдельные вызовы `light.turn_on` для цвета и яркости, требуется для некоторых типов освещения. 🔀" + }, + "prefer_rgb_color": { + "description": "Предпочитать ли настройку цвета RGB цветовой температуре света, когда это возможно. 🌈" + }, + "max_color_temp": { + "description": "Самая холодная цветовая температура в Кельвинах. ❄️" + }, + "sunset_offset": { + "description": "Отрегулируйте время заката с помощью положительного или отрицательного смещения в секундах. ⏰" + }, + "send_split_delay": { + "description": "Задержка (мс) между отдельными командами поворота `separate_turn_on_commands` для источников света, которые не поддерживают одновременную настройку яркости и цвета. ⏲️" + }, + "transition": { + "description": "Длительность плавного перехода при смене освещения, в секундах. 🕑" } - } + }, + "description": "Измените переключателями настройки, которые вам подходят. Все параметры здесь такие же, как и в процессе настройки." }, "set_manual_control": { - "description": "Отметьте, контролируется ли свет вручную." + "description": "Отметьте, контролируется ли свет вручную.", + "fields": { + "manual_control": { + "description": "Добавлять («true») или удалять («false») свет из списка «ручного упрваления». 🔒" + }, + "entity_id": { + "description": "Сущность `entity_id` переключателя для выбора ручного управления `manually controlled`. 📝" + }, + "lights": { + "description": "Сущность(и) `entity_id` источников света, если не указано, выбираются все источники света в переключателе. 💡" + } + } } } } diff --git a/custom_components/adaptive_lighting/translations/sk.json b/custom_components/adaptive_lighting/translations/sk.json index 802e30fd..11ad8844 100644 --- a/custom_components/adaptive_lighting/translations/sk.json +++ b/custom_components/adaptive_lighting/translations/sk.json @@ -6,23 +6,51 @@ "detect_non_ha_changes": "detect_non_ha_changes: Deteguje a zastaví prispôbovanie pre zmeny mimo `light.turn_on`. Vyžaduje zapnutie `take_over_control`. 🕵️ Upozornenie: ⚠️ Niektoré svetlá môžu falošne indikovať zapnutý stav, čo spôsobí, že sa svetlo neočakávane zapne. Ak narazíte na tento problém, funkciu vypnite.", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Len pri čistom zapnutí svetiel. Pri nastavení `true` prispôsobí Adaptívne osvetlenie svetlá len pri zavolaní služby `light.turn_on` bez parametrov jasu alebo teploty svetla. ❌🌈 Napríklad: zamedzí to prispôsobovaniu ak je aktivovaná scéna. Pri nastavení `false` dôjde k prispôsobeniu nezávisle na tom či sú parametre jasu alebo teploty svetla prítomné v`service_data`. Vyžaduje zapnutie `take_over_control`. 🕵️ ", "separate_turn_on_commands": "separate_turn_on_commands: Použiť samostatné volania služby `light.turn_on` pre nastavenie teploty svetla a jasu (môže byť potrebné pre niektoré typy svetiel). 🔀", - "max_color_temp": "max_color_temp: Najstudenejšia teplota svetla v Kelvinoch. ❄️", - "prefer_rgb_color": "prefer_rgb_color: Ak je to možné, preferovať nastavenie cez RGB než nastavením teploty svetla. 🌈", + "max_color_temp": "max_color_temp: Najvyššia teplota svetla (v ˚K). ❄️", + "prefer_rgb_color": "prefer_rgb_color: Či preferovať nastavenie cez RGB než nastavením teploty svetla, ak je to možné. 🌈", "max_brightness": "max_brightness: Najvyšší jas (v %). 💡", "only_once": "only_once: Prispôsobiť svetlá iba pri zapnutí (`true`) alebo prispôsobovať ich priebežne (`false`). 🔄", "take_over_control": "take_over_control: Ak sú svetlá zapnuté a prispôsobované a niečo zavolá službu `light.turn_on`, dôjde k vypnutiu Adaptívneho osvetlenia. Poznámka: Zapnutie tejto voľby spôsobí volanie služby `homeassistant.update_entity` každý `interval`! 🔒", "lights": "svetlá: Zoznam svetiel (entity_id), ktoré majú byť ovládané (môže byť prázdny). 🌟", "min_brightness": "min_brightness: Najnižší jas (v %). 💡", - "min_color_temp": "min_color_temp: Najteplejšia teplota svetla v Kelvinoch. 🔥", - "transition_until_sleep": "transition_until_sleep: Keď je funkcia povolená, Adaptívne osvetlenie bude považovať nastavenia režimu spánku ako minimum, a na tieto hodnoty prejde po západe slnka. 🌙" + "min_color_temp": "min_color_temp: Najnižšia teplota svetla (v ˚K). 🔥", + "transition_until_sleep": "transition_until_sleep: Keď je funkcia povolená, Adaptívne osvetlenie bude považovať nastavenia režimu spánku ako minimum, a na tieto hodnoty prejde po západe slnka. 🌙", + "multi_light_intercept": "multi_light_intercept: Zachytiť a prispôsobiť volanie služby`light.turn_on`, ktoré ovlyvňuje viacero svetiel. ➗⚠️ Toto môže spôsobiť rozdelenie jedného volania `light.turn_on`na viacero volaní, napr. ak sú svetlá pod rôznymi prepínačmi adaptívneho osvetlenia. Vyžaduje zapnutie `intercept`.", + "skip_redundant_commands": "skip_redundant_commands: Preskočiť odoslanie prispôsobovacích príkazov, ktorých cieľový stav je zhodný s posledným známym stavom. Nastavenie minimalizuje sieťovú prevádzku a v niektorých prípadoch môže zlepšiť odozvu prispôsobovania. 📉 Vypnite, pokiaľ skutočný stav svetiel prestáva odpovedať stavu zaznamenanom v HA.", + "intercept": "intercept: Zachytiť a prispôsobiť volania `light.turn_on`, aby došlo k okamžitému prispôsobeniu jasu a teploty svetla. 🏎️ Vypnite pre svetlá, ktoré nepodporujú `light.turn_on` s teplotou svetla a jasom zároveň.", + "include_config_in_attributes": "include_config_in_attributes: Zobraziť všetky nastavenia ako atribúty prepínača v Home Assistant. 📝" }, "data_description": { "sunset_time": "Nastaviť pevný čas (HH:MM:SS) pre západ slnka. 🌇", - "sunrise_time": "Nastaviť pevný čas (HH:MM:SS) pre východ slnka. 🌅" + "sunrise_time": "Nastaviť pevný čas (HH:MM:SS) pre východ slnka. 🌅", + "sleep_rgb_or_color_temp": "V režime spánku použiť `\"rgb_color\"` alebo `\"color_temp\"`. 🌙", + "sleep_color_temp": "Teplota svetla (v ˚K) v režime spánku (pokiaľ `sleep_rgb_or_color_temp` je `color_temp`). 😴", + "sleep_transition": "Trvanie prechodu do alebo z režimu spánku (v sekundách). 😴", + "autoreset_control_seconds": "Automaticky ukončiť manuálne ovládanie po zadanom množtve sekúnd. Pre vypnutie nastavte 0. ⏲️", + "min_sunset_time": "Nastavte najskorší možný virtuálny západ slnka (HH:MM:SS). Umožňuje neskorší západ slnka. 🌅", + "sleep_brightness": "Jas svetiel pri režime spánku (v %). 😴", + "min_sunrise_time": "Nastavte najskorší možný virtuálny východ slnka (HH:MM:SS). Umožňuje neskorší východ slnka. 🌅", + "interval": "Frekvencia s akou prispôsobovať svetlá (v sekundách). 🔄", + "adapt_delay": "Pauza (v sekundách) medzi zapnutím svetla a aplikáciou zmien Adaptívneho osvetlenia. Môže pomôcť zabrániť blikaniu. ⏲️", + "sleep_rgb_color": "Farba svetla RGB v režime spánku (pokiaľ `sleep_rgb_or_color_temp` je `rgb_color `). 🌈", + "sunrise_offset": "Upravte čas východu slnka o sekundy vpred alebo vzad. ⏰", + "transition": "Trvanie prechodu, keď sú svetlá zmenené (v sekundách). ⏲️", + "brightness_mode": "Výber režimu jasu. Možné hodnotu sú `default`, `linear` a `tanh` (používa `brightness_mode_time_dark` a `brightness_mode_time_light`). 📈", + "brightness_mode_time_light": "(Ignorované ak `brightness_mode='default'`) Čas na zvýšenie/zníženie jasu po udalosti/pred udalosťou východu/západu slnka. 📈📉", + "sunset_offset": "Upravte čas západu slnka o sekundy vpred alebo vzad. ⏰", + "max_sunset_time": "Nastavte najneskorší možný virtuálny západ slnka (HH:MM:SS). Umožňuje skorší západ slnka. 🌅", + "initial_transition": "Trvanie prvého prechodu, keď sú svetlá zapnuté z `off` na `on` (v sekundách). ⏲️", + "brightness_mode_time_dark": "(Ignorované ak `brightness_mode='default'`) Čas na zvýšenie/zníženie jasu po udalosti/pred udalosťou východu/západu slnka. 📈📉", + "max_sunrise_time": "Nastavte najneskorší možný virtuálny východ slnka (HH:MM:SS). Umožňuje skorší východ slnka. 🌅", + "send_split_delay": "Pauza (v ms) medzi príkazmi pri zapnutom `separate_turn_on_commands` pre svetlá, ktoré nepodporujú súčasné nastavenie jasu a teploty svetla. ⏲️" }, "title": "Nastavenia Adaptívneho osvetlenia", - "description": "Nastavte komponentu Adaptívneho osvetlenia. Názvy nastavení sú zhodné s názvami v súbore YAML. Ak ste túto položku nastavili už v YAML, tak tu sa nezobrazia žiadne možnosti nastavenia. Interaktívne grafy, ktoré zobrazujú vplyv nastavení, navštívte [túto webovú aplikáciu](https://basnijholt.github.io/adaptive-lighting). Ďalšie informácie nájdete v [oficiálnej dokumentácii](https://github.com/basnijholt/adaptive-lighting#readme)." + "description": "Nastavte komponentu Adaptívneho osvetlenia. Názvy nastavení sú zhodné s názvami v súbore YAML. Ak ste túto položku nastavili už v YAML, tak tu sa nezobrazia žiadne možnosti nastavenia. Interaktívne grafy, ktoré zobrazujú vplyv nastavení, nájdete na [tejto webovej aplikácii](https://basnijholt.github.io/adaptive-lighting). Ďalšie informácie nájdete v [oficiálnej dokumentácii](https://github.com/basnijholt/adaptive-lighting#readme)." } + }, + "error": { + "option_error": "Neplatné nastavenie", + "entity_missing": "V Home Assistant chýba jedno alebo viac vybraných svetiel" } }, "title": "Adaptívne osvetlenie", @@ -30,7 +58,10 @@ "step": { "user": { "description": "Každá inštancia môže obsahovať viacero svetiel!", - "title": "Vyberte názov inštancie Adaptívneho osvetlenia" + "title": "Vyberte názov inštancie Adaptívneho osvetlenia", + "data": { + "name": "Názov" + } } }, "abort": { @@ -45,8 +76,130 @@ }, "sunset_time": { "description": "Nastaviť pevný čas (HH:MM:SS) pre západ slnka. 🌇" + }, + "sleep_brightness": { + "description": "Jas svetiel pri režime spánku (v %). 😴" + }, + "sunrise_offset": { + "description": "Upravte čas východu slnka o sekundy vpred alebo vzad. ⏰" + }, + "max_sunrise_time": { + "description": "Nastavte najneskorší možný virtuálny východ slnka (HH:MM:SS). Umožňuje skorší východ slnka. 🌅" + }, + "sleep_color_temp": { + "description": "Teplota svetla (v ˚K) v režime spánku (pokiaľ `sleep_rgb_or_color_temp` je `color_temp`). 😴" + }, + "min_brightness": { + "description": "Najnižší jas (v %). 💡" + }, + "min_color_temp": { + "description": "Najnižšia teplota svetla (v ˚K). 🔥" + }, + "sleep_rgb_or_color_temp": { + "description": "V režime spánku použiť `\"rgb_color\"` alebo `\"color_temp\"`. 🌙" + }, + "turn_on_lights": { + "description": "Či sa majú zapnúť svetlá, ktoré sú momentálne vypnuté. 🔆" + }, + "initial_transition": { + "description": "Trvanie prvého prechodu, keď sú svetlá zapnuté z `off` na `on` (v sekundách). ⏲️" + }, + "entity_id": { + "description": "ID prepínača. 📝" + }, + "include_config_in_attributes": { + "description": "Zobraziť všetky nastavenia ako atribúty prepínača v Home Assistant. 📝" + }, + "max_brightness": { + "description": "Najvyšší jas (v %). 💡" + }, + "sleep_rgb_color": { + "description": "Farba svetla RGB v režime spánku (pokiaľ `sleep_rgb_or_color_temp` je `rgb_color `). 🌈" + }, + "take_over_control": { + "description": "Ak sú svetlá zapnuté a prispôsobované a niečo zavolá službu `light.turn_on`, dôjde k vypnutiu Adaptívneho osvetlenia. Poznámka: Zapnutie tejto voľby spôsobí volanie služby `homeassistant.update_entity` každý `interval`! 🔒" + }, + "sleep_transition": { + "description": "Trvanie prechodu do alebo z režimu spánku (v sekundách). 😴" + }, + "autoreset_control_seconds": { + "description": "Automaticky ukončiť manuálne ovládanie po zadanom množtve sekúnd. Pre vypnutie nastavte 0. ⏲️" + }, + "adapt_delay": { + "description": "Pauza (v sekundách) medzi zapnutím svetla a aplikáciou zmien Adaptívneho osvetlenia. Môže pomôcť zabrániť blikaniu. ⏲️" + }, + "only_once": { + "description": "Prispôsobiť svetlá len pri zapnutí (`true`) alebo prispôsobovať ich priebežne (`false`). 🔄" + }, + "use_defaults": { + "description": "Nastaví predvolené hodnoty, ktoré nie sú špecifikované v tomto volaní služby. Možnosti: \"current\" (predvolené, zachová aktuálne hodnoty), \"factory\" (použije predvolené hodnoty z dokumentácie) alebo \"configuration\" (vráti na predvolené nastavenia prepínača). ⚙️" + }, + "separate_turn_on_commands": { + "description": "Použiť samostatné volania služby `light.turn_on` pre nastavenie teploty svetla a jasu (môže byť potrebné pre niektoré typy svetiel). 🔀" + }, + "prefer_rgb_color": { + "description": "Či preferovať nastavenie cez RGB než nastavením teploty svetla, ak je to možné. 🌈" + }, + "max_color_temp": { + "description": "Najvyššia teplota svetla (v ˚K). ❄️" + }, + "sunset_offset": { + "description": "Upravte čas západu slnka o sekundy vpred alebo vzad. ⏰" + }, + "send_split_delay": { + "description": "Pauza (v ms) medzi príkazmi pri zapnutom `separate_turn_on_commands` pre svetlá, ktoré nepodporujú súčasné nastavenie jasu a teploty svetla. ⏲️" + }, + "transition": { + "description": "Trvanie prechodu, keď sú svetlá zmenené (v sekundách). ⏲️" + }, + "min_sunset_time": { + "description": "Nastavte najskorší možný virtuálny západ slnka (HH:MM:SS). Umožňuje neskorší západ slnka. 🌅" + }, + "detect_non_ha_changes": { + "description": "Detekuje a zastaví prispôsobovanie pre iné zmeny stavov než `light.turn_on`. Vyžaduje zapnuté `take_over_control`. 🕵️ Upozornenie: ⚠️ Niektoré svetlá môžu nesprávne indikovať stav 'on', čo môže spôsobiť neočakávané zapnutie svetiel. Vypnite toto nastavenie, ak sa takéto problémy objavia." } - } + }, + "description": "Zmeňte ľubovoľné nastavenie prepínača. Všetky možnosti sú rovnaké ako v config flow." + }, + "apply": { + "fields": { + "entity_id": { + "description": "`entity_id` prepínača, na ktorý sa majú aplikovať zmeny. 📝" + }, + "adapt_brightness": { + "description": "Či prispôsobiť jas svetla. 🌞" + }, + "turn_on_lights": { + "description": "Či sa majú zapnúť svetlá, ktoré sú momentálne vypnuté. 🔆" + }, + "adapt_color": { + "description": "Či prispôsobiť teplotu svetla na podporovaných svetlách. 🌈" + }, + "prefer_rgb_color": { + "description": "Či preferovať nastavenie cez RGB než nastavením teploty svetla, ak je to možné. 🌈" + }, + "lights": { + "description": "Svetlo (alebo zoznam svetiel), na ktoré sa má nastavenie aplikovať. 💡" + }, + "transition": { + "description": "Trvanie prechodu, keď sú svetlá zmenené (v sekundách). ⏲️" + } + }, + "description": "Aplikuje na svetlá súčasné nastavenie Adaptívneho osvetlenia." + }, + "set_manual_control": { + "fields": { + "manual_control": { + "description": "Či pridať (`true`) alebo odobrať (`false`) svetlo zo zoznamu \"manuálne ovládaných\". 🔒" + }, + "entity_id": { + "description": "`entity_id` prepínača u ktorého sa majú svetlá o(d)značiť ako \"manuálne ovládané\". 📝" + }, + "lights": { + "description": "entity_id svetiel, ak nie sú špecifikované, tak sú vybrané všetky svetlá prepínača. 💡" + } + }, + "description": "Či označiť svetlo ako \"manuálne ovládané\"." } } } From 30e1c8d1904366d95da57d1a6d6e91d1f5a381ca Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 17 Feb 2024 11:00:52 -0800 Subject: [PATCH 0745/1077] Revert translations deleted by @mstefany (#933) --- .../adaptive_lighting/translations/cs.json | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/cs.json b/custom_components/adaptive_lighting/translations/cs.json index 3a7d9788..74291134 100644 --- a/custom_components/adaptive_lighting/translations/cs.json +++ b/custom_components/adaptive_lighting/translations/cs.json @@ -21,9 +21,9 @@ "description": "Všechna nastavení komponenty Adaptivního osvětlení. Názvy možností odpovídají nastavení YAML. Pokud máte v konfiguraci YAML definovánu položku 'adaptive_lighting', nezobrazí se žádné možnosti.", "data": { "lights": "lights: Seznam světel (entity_id), které mají být ovládané (může být prázdný). 🌟", - "initial_transition": "", - "sleep_transition": "", - "interval": "", + "initial_transition": "initial_transition: Prodlení pro změnu z 'vypnuto' do 'zapnuto' (sekundy)", + "sleep_transition": "sleep_transition: Prodleva pro přepnutí do „režimu spánku“ (sekundy)", + "interval": "interval: Prodleva pro změny osvětlení (v sekundách)", "max_brightness": "max_brightness: Nejvyšší jas osvětlení během cyklu. (%)", "max_color_temp": "max_color_temp: Nejchladnější odstín cyklu teploty barev. (Kelvin)", "min_brightness": "min_brightness: Nejnižší jas osvětlení během cyklu. (%)", @@ -31,17 +31,17 @@ "only_once": "only_once: Přizpůsobení osvětlení pouze při rozsvícení.", "prefer_rgb_color": "prefer_rgb_color: Upřednostněte použití 'rgb_color' před 'color_temp'.", "separate_turn_on_commands": "separate_turn_on_commands: Oddělení příkazů pro každý atribut (barva, jas, atd.) v atributu 'light.turn_on' (vyžadováno pro některá světla).", - "send_split_delay": "", - "sleep_brightness": "", - "sleep_rgb_or_color_temp": "", - "sleep_rgb_color": "", - "sleep_color_temp": "", - "sunrise_offset": "", - "sunrise_time": "", - "max_sunrise_time": "", - "sunset_offset": "", - "sunset_time": "", - "min_sunset_time": "", + "send_split_delay": "send_split_delay: prodleva mezi příkazy (milisekundy), když je použit atribut 'separate_turn_on_commands'. Může zajistit správné zpracování obou příkazů.", + "sleep_brightness": "sleep_brightness, Nastavení jasu pro režim spánku. (%)", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, použijte 'rgb_color' nebo 'color_temp'", + "sleep_rgb_color": "sleep_rgb_color, v RGB", + "sleep_color_temp": "sleep_color_temp: Nastavení teploty barev pro režim spánku. (v Kelvinech)", + "sunrise_offset": "sunrise_offset: Jak dlouho před (-) nebo po (+) definovat bod cyklu východu slunce (+/- v sekundách)", + "sunrise_time": "sunrise_time: Manuální přepsání času východu slunce, pokud je „None“, použije se skutečný čas východu slunce ve vaší lokalitě (HH:MM:SS)", + "max_sunrise_time": "max_sunrise_time: Ruční přepsání nejpozdějšího času východu slunce, pokud je „None“, použije se skutečný čas východu slunce vaší lokality (HH:MM:SS)", + "sunset_offset": "sunset_offset: Jak dlouho před (-) nebo po (+) definovat bod cyklu západu slunce (+/- v sekundách)", + "sunset_time": "sunset_time: Ruční přepsání času západu slunce, pokud je „None“, použije se skutečný čas západu slunce vaší lokality (HH:MM:SS)", + "min_sunset_time": "min_sunset_time: Ruční přepsání nejdřívějšího času západu slunce, pokud je „None“, použije se skutečný čas západu slunce vaší lokality (HH:MM:SS)", "take_over_control": "take_over_control: Je-li volán 'light.turn_on' z jiného zdroje, než Adaptivním osvětlením, když je světlo již rozsvíceno, přestaňte toto světlo ovládat, dokud není vypnuto -> zapnuto (nebo i vypínačem).", "detect_non_ha_changes": "detect_non_ha_changes: detekuje všechny změny >10% provedených pro osvětlení (také mimo HA), vyžaduje povolení atributu 'take_over_control' (každý 'interval' spouští 'homeassistant.update_entity'!)", "transition": "", From bc8c94081a9df71275ad6f29d4c2d275c462c538 Mon Sep 17 00:00:00 2001 From: saya6k <63517312+saya6k@users.noreply.github.com> Date: Sun, 18 Feb 2024 04:03:14 +0900 Subject: [PATCH 0746/1077] add Korean translation (#923) * add Korean translation * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt Co-authored-by: Bas Nijholt --- .../adaptive_lighting/translations/ko.json | 270 +++++++++++++++++- 1 file changed, 269 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/ko.json b/custom_components/adaptive_lighting/translations/ko.json index 0967ef42..f4418fd5 100644 --- a/custom_components/adaptive_lighting/translations/ko.json +++ b/custom_components/adaptive_lighting/translations/ko.json @@ -1 +1,269 @@ -{} +{ + "title": "적응형 조명", + "config": { + "step": { + "user": { + "title": "적응형 조명 인스턴스 이름 선택", + "description": "각 인스턴스는 여러 조명을 포함할 수 있습니다!", + "data": { + "name": "이름" + } + } + }, + "abort": { + "already_configured": "이 장치는 이미 구성되었습니다" + } + }, + "options": { + "step": { + "init": { + "title": "적응형 조명 옵션", + "description": "적응형 조명 구성요소를 구성합니다. 옵션 이름은 YAML 설정과 일치합니다. 이 항목을 YAML에서 정의한 경우 여기에 옵션이 표시되지 않습니다. 매개변수 효과를 시연하는 인터랙티브 그래프는 [이 웹 앱](https://basnijholt.github.io/adaptive-lighting)에서 확인할 수 있습니다. 자세한 내용은 [공식 문서](https://github.com/basnijholt/adaptive-lighting#readme)를 참조하세요.", + "data": { + "lights": "조명: 제어될 조명 entity_ids의 목록 (비어 있을 수 있음). 🌟", + "interval": "간격", + "transition": "전환", + "initial_transition": "초기 전환", + "min_brightness": "최소 밝기: 밝기 최소 퍼센트. 💡", + "max_brightness": "최대 밝기: 밝기 최대 퍼센트. 💡", + "min_color_temp": "최소 색온도: 켈빈으로 표시된 가장 따뜻한 색온도. 🔥", + "max_color_temp": "최대 색온도: 켈빈으로 표시된 가장 차가운 색온도. ❄️", + "prefer_rgb_color": "RGB 색상 선호: 가능할 경우 색온도 조정보다 RGB 색상 조정을 선호하는지 여부. 🌈", + "sleep_brightness": "수면 밝기", + "sleep_rgb_or_color_temp": "수면 rgb_or_color_temp", + "sleep_color_temp": "수면 색온도", + "sleep_rgb_color": "수면 RGB 색상", + "sleep_transition": "수면 전환", + "transition_until_sleep": "수면까지 전환: 활성화되면, 적응형 조명은 수면 설정을 최소값으로 취급하고 일몰 후 이 값으로 전환합니다. 🌙", + "sunrise_time": "일출 시간", + "min_sunrise_time": "최소 일출 시간", + "max_sunrise_time": "최대 일출 시간", + "sunrise_offset": "일출 오프셋", + "sunset_time": "일몰 시간", + "min_sunset_time": "최소 일몰 시간", + "max_sunset_time": "최대 일몰 시간", + "sunset_offset": "일몰 오프셋", + "brightness_mode": "밝기 모드", + "brightness_mode_time_dark": "어두울 때 밝기 모드 시간", + "brightness_mode_time_light": "밝을 때 밝기 모드 시간", + "take_over_control": "제어 인계: 다른 소스가 조명이 켜져 있고 조정 중일 때 `light.turn_on`을 호출하면 적응형 조명을 비활성화합니다. 이는 매 `간격`마다 `homeassistant.update_entity`를 호출합니다! 🔒", + "detect_non_ha_changes": "비HA 변경 감지: `light.turn_on`이 아닌 상태 변경을 감지하고 조정을 중단합니다. `take_over_control`이 활성화되어 있어야 합니다. 🕵️ 주의: ⚠️ 일부 조명은 잘못된 '켜짐' 상태를 나타낼 수 있으며, 이로 인해 조명이 예상치 못하게 켜질 수 있습니다. 이러한 문제가 발생하면 이 기능을 비활성화하세요.", + "autoreset_control_seconds": "자동 제어 리셋 초", + "only_once": "한 번만: 조명을 켤 때만 조정 (`true`) 또는 계속해서 조정 (`false`). 🔄", + "adapt_only_on_bare_turn_on": "초기 켜짐 시 조정만: 조명을 처음 켤 때. `true`로 설정하면 `light.turn_on`이 색상이나 밝기를 지정하지 않고 호출될 때만 AL이 조정합니다. ❌🌈 예를 들어, 장면을 활성화할 때 조정을 방지합니다. `false`로 설정하면, AL은 초기 `service_data`에 색상이나 밝기의 존재 여부와 관계없이 조정합니다. `take_over_control`이 활성화되어 있어야 합니다. 🕵️", + "separate_turn_on_commands": "분리된 켜기 명령 사용: 일부 조명 유형에 필요한 색상과 밝기에 대해 별도의 `light.turn_on` 호출을 사용합니다. 🔀", + "send_split_delay": "분할 전송 지연", + "adapt_delay": "조정 지연", + "skip_redundant_commands": "중복 명령 건너뛰기: 목표 상태가 이미 조명의 알려진 상태와 동일한 조정 명령을 보내지 않습니다. 네트워크 트래픽을 최소화하고 일부 상황에서 조정 반응성을 향상시킵니다. 📉 물리적 조명 상태가 HA의 기록된 상태와 동기화되지 않는 경우 비활성화하세요.", + "intercept": "가로채기: 색상과 밝기의 즉각적인 조정을 가능하게 하기 위해 `light.turn_on` 호출을 가로챕니다. 🏎️ 색상과 밝기를 지원하지 않는 조명에 대해 비활성화합니다.", + "multi_light_intercept": "다중 조명 가로채기: 여러 조명을 대상으로 하는 `light.turn_on` 호출을 가로채고 조정합니다. ➗⚠️ 이는 단일 `light.turn_on` 호출을 여러 호출로 분할할 수 있음을 의미합니다. 예를 들어, 조명이 다른 스위치에 있을 때. `intercept`가 활성화되어 있어야 합니다.", + "include_config_in_attributes": "속성에 구성 포함: `true`로 설정하면 Home Assistant에서 스위치의 모든 옵션을 속성으로 표시합니다. 📝" + }, + "data_description": { + "interval": "조명을 조정하는 빈도, 초 단위. 🔄", + "transition": "조명이 변경될 때 전환 기간, 초 단위. 🕑", + "initial_transition": "조명이 `off`에서 `on`으로 바뀔 때 첫 번째 전환의 지속 시간, 초 단위. ⏲️", + "sleep_brightness": "수면 모드에서 조명의 밝기 퍼센트. 😴", + "sleep_rgb_or_color_temp": "수면 모드에서 `\"rgb_color\"` 또는 `\"color_temp\"` 사용. 🌙", + "sleep_color_temp": "수면 모드에서 색온도 (sleep_rgb_or_color_temp가 `color_temp`일 때 사용) 켈빈 단위. 😴", + "sleep_rgb_color": "수면 모드에서 RGB 색상 (sleep_rgb_or_color_temp가 \"rgb_color\"일 때 사용). 🌈", + "sleep_transition": "\"수면 모드\"가 전환될 때 전환 기간, 초 단위. 😴", + "sunrise_time": "일출 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌅", + "min_sunrise_time": "가장 이른 가상 일출 시간 (HH:MM:SS)을 설정하여 더 늦은 일출을 허용. 🌅", + "max_sunrise_time": "가장 늦은 가상 일출 시간 (HH:MM:SS)을 설정하여 더 일찍 일출을 허용. 🌅", + "sunrise_offset": "양수 또는 음수 오프셋(초)으로 일출 시간을 조정. ⏰", + "sunset_time": "일몰 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌇", + "min_sunset_time": "가장 이른 가상 일몰 시간 (HH:MM:SS)을 설정하여 더 늦은 일몰을 허용. 🌇", + "max_sunset_time": "가장 늦은 가상 일몰 시간 (HH:MM:SS)을 설정하여 더 일찍 일몰을 허용. 🌇", + "sunset_offset": "양수 또는 음수 오프셋(초)으로 일몰 시간을 조정. ⏰", + "brightness_mode": "사용할 밝기 모드. 가능한 값은 `default`, `linear`, `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(`brightness_mode='default'`인 경우 무시됨) 일출/일몰 전/후에 밝기를 높이거나 낮추는 데 걸리는 시간, 초 단위. 📈📉", + "brightness_mode_time_light": "(`brightness_mode='default'`인 경우 무시됨) 일출/일몰 후/전에 밝기를 높이거나 낮추는 데 걸리는 시간, 초 단위. 📈📉.", + "autoreset_control_seconds": "특정 초 후에 수동 제어를 자동으로 재설정. 0으로 설정하면 비활성화됩니다. ⏲️", + "send_split_delay": "`separate_turn_on_commands`에 대한 호출 사이의 지연 시간(밀리초)으로, 밝기와 색상을 동시에 설정하지 않는 조명에 대한 지연. ⏲️", + "adapt_delay": "조명을 켠 후 적응형 조명이 변경 사항을 적용하기까지의 대기 시간(초). 깜박임을 피하는 데 도움이 될 수 있습니다. ⏲️" + } + } + }, + "error": { + "option_error": "잘못된 옵션", + "entity_missing": "선택한 하나 이상의 조명 엔티티가 Home Assistant에서 누락됨" + } + }, + "services": { + "apply": { + "name": "적용", + "description": "현재 적응형 조명 설정을 조명에 적용합니다.", + "fields": { + "entity_id": { + "description": "설정을 적용할 스위치의 `entity_id`. 📝", + "name": "entity_id" + }, + "lights": { + "description": "설정을 적용할 조명(또는 조명 목록). 💡", + "name": "lights" + }, + "transition": { + "description": "조명 변경 시 전환 기간, 초 단위. 🕑", + "name": "transition" + }, + "adapt_brightness": { + "description": "조명의 밝기를 조정할지 여부. 🌞", + "name": "adapt_brightness" + }, + "adapt_color": { + "description": "지원하는 조명의 색상을 조정할지 여부. 🌈", + "name": "adapt_color" + }, + "prefer_rgb_color": { + "description": "가능할 경우 색온도 조정보다 RGB 색상 조정을 선호하는지 여부. 🌈", + "name": "prefer_rgb_color" + }, + "turn_on_lights": { + "description": "현재 꺼져 있는 조명을 켤지 여부. 🔆", + "name": "turn_on_lights" + } + } + }, + "set_manual_control": { + "name": "수동 제어 설정", + "description": "조명이 '수동 제어됨'으로 표시되었는지 여부를 표시합니다.", + "fields": { + "entity_id": { + "description": "`수동 제어됨`으로 (표시 해제)할 스위치의 `entity_id`. 📝", + "name": "entity_id" + }, + "lights": { + "description": "조명의 entity_id(들), 지정하지 않으면 스위치의 모든 조명이 선택됩니다. 💡", + "name": "lights" + }, + "manual_control": { + "description": "\"수동 제어\" 목록에서 조명을 추가(\"true\") 또는 제거(\"false\")할지 여부. 🔒", + "name": "manual_control" + } + } + }, + "change_switch_settings": { + "name": "스위치 설정 변경", + "description": "스위치에서 원하는 모든 설정을 변경하세요. 여기에 있는 모든 옵션은 구성 흐름에서와 같습니다.", + "fields": { + "entity_id": { + "description": "스위치의 Entity ID. 📝", + "name": "entity_id" + }, + "use_defaults": { + "description": "이 서비스 호출에서 지정되지 않은 기본값을 설정합니다. 옵션: \"현재\"(기본값, 현재 값을 유지), \"공장\"(문서화된 기본값으로 재설정), 또는 \"구성\"(스위치 구성 기본값으로 되돌림). ⚙️", + "name": "use_defaults" + }, + "include_config_in_attributes": { + "description": "`true`로 설정하면 Home Assistant에서 스위치의 모든 옵션을 속성으로 표시합니다. 📝", + "name": "include_config_in_attributes" + }, + "turn_on_lights": { + "description": "현재 꺼져 있는 조명을 켤지 여부. 🔆", + "name": "turn_on_lights" + }, + "initial_transition": { + "description": "조명이 `off`에서 `on`으로 바뀔 때 첫 번째 전환의 지속 시간, 초 단위. ⏲️", + "name": "initial_transition" + }, + "sleep_transition": { + "description": "\"수면 모드\"가 전환될 때 전환 기간, 초 단위. 😴", + "name": "sleep_transition" + }, + "max_brightness": { + "description": "최대 밝기 퍼센트. 💡", + "name": "max_brightness" + }, + "max_color_temp": { + "description": "켈빈으로 표시된 가장 차가운 색온도. ❄️", + "name": "max_color_temp" + }, + "min_brightness": { + "description": "최소 밝기 퍼센트. 💡", + "name": "min_brightness" + }, + "min_color_temp": { + "description": "켈빈으로 표시된 가장 따뜻한 색온도. 🔥", + "name": "min_color_temp" + }, + "only_once": { + "description": "조명을 켤 때만 조정 (`true`) 또는 계속해서 조정 (`false`). 🔄", + "name": "only_once" + }, + "prefer_rgb_color": { + "description": "가능할 경우 색온도 조정보다 RGB 색상 조정을 선호하는지 여부. 🌈", + "name": "prefer_rgb_color" + }, + "separate_turn_on_commands": { + "description": "일부 조명 유형에 필요한 색상과 밝기에 대해 별도의 `light.turn_on` 호출을 사용합니다. 🔀", + "name": "separate_turn_on_commands" + }, + "send_split_delay": { + "description": "밝기와 색상을 동시에 설정하지 않는 조명에 대한 `separate_turn_on_commands` 호출 사이의 지연 시간(밀리초). ⏲️", + "name": "send_split_delay" + }, + "sleep_brightness": { + "description": "수면 모드에서 조명의 밝기 퍼센트. 😴", + "name": "sleep_brightness" + }, + "sleep_rgb_or_color_temp": { + "description": "수면 모드에서 `\"rgb_color\"` 또는 `\"color_temp\"` 사용. 🌙", + "name": "sleep_rgb_or_color_temp" + }, + "sleep_rgb_color": { + "description": "수면 모드에서 RGB 색상 (sleep_rgb_or_color_temp가 \"rgb_color\"일 때 사용). 🌈", + "name": "sleep_rgb_color" + }, + "sleep_color_temp": { + "description": "수면 모드에서 색온도 (sleep_rgb_or_color_temp가 `color_temp`일 때 사용) 켈빈 단위. 😴", + "name": "sleep_color_temp" + }, + "sunrise_offset": { + "description": "양수 또는 음수 오프셋(초)으로 일출 시간을 조정. ⏰", + "name": "sunrise_offset" + }, + "sunrise_time": { + "description": "일출 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌅", + "name": "sunrise_time" + }, + "sunset_offset": { + "description": "양수 또는 음수 오프셋(초)으로 일몰 시간을 조정. ⏰", + "name": "sunset_offset" + }, + "sunset_time": { + "description": "일몰 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌇", + "name": "sunset_time" + }, + "max_sunrise_time": { + "description": "가장 늦은 가상 일출 시간 (HH:MM:SS)을 설정하여 더 일찍 일출을 허용. 🌅", + "name": "max_sunrise_time" + }, + "min_sunset_time": { + "description": "가장 이른 가상 일몰 시간 (HH:MM:SS)을 설정하여 더 늦은 일몰을 허용. 🌇", + "name": "min_sunset_time" + }, + "take_over_control": { + "description": "다른 소스가 조명이 켜져 있고 조정 중일 때 `light.turn_on`을 호출하면 적응형 조명을 비활성화합니다. 이는 매 `간격`마다 `homeassistant.update_entity`를 호출합니다! 🔒", + "name": "take_over_control" + }, + "detect_non_ha_changes": { + "description": "`light.turn_on`이 아닌 상태 변경을 감지하고 조정을 중단합니다. `take_over_control`이 활성화되어 있어야 합니다. 🕵️ 주의: ⚠️ 일부 조명은 잘못된 '켜짐' 상태를 나타낼 수 있으며, 이로 인해 조명이 예상치 못하게 켜질 수 있습니다. 이러한 문제가 발생하면 이 기능을 비활성화하세요.", + "name": "detect_non_ha_changes" + }, + "transition": { + "description": "조명이 변경될 때 전환 기간, 초 단위. 🕑", + "name": "transition" + }, + "adapt_delay": { + "description": "조명을 켠 후 적응형 조명이 변경 사항을 적용하기까지의 대기 시간(초). 깜박임을 피하는 데 도움이 될 수 있습니다. ⏲️", + "name": "adapt_delay" + }, + "autoreset_control_seconds": { + "description": "특정 초 후에 수동 제어를 자동으로 재설정. 0으로 설정하면 비활성화됩니다. ⏲️", + "name": "autoreset_control_seconds" + } + } + } + } +} From 92aa50411a60e9779db035af5f3ad4d491336c6f Mon Sep 17 00:00:00 2001 From: droans <49721649+droans@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:10:00 -0500 Subject: [PATCH 0747/1077] Test that switch is on before updating time listeners (#936) Co-authored-by: Michael Carroll --- custom_components/adaptive_lighting/switch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 06cd2f87..cf38d21f 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -359,7 +359,8 @@ async def handle_change_switch_settings( # deep copy the defaults so we don't modify the original dicts switch._set_changeable_settings(data=data, defaults=deepcopy(defaults)) - switch._update_time_interval_listener() + if switch.is_on: + switch._update_time_interval_listener() _LOGGER.debug( "Called 'adaptive_lighting.change_switch_settings' service with '%s'", From b02c3f8024b1ea2a220aed86fa86cf3014af5ce0 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 13:10:39 -0800 Subject: [PATCH 0748/1077] docs: add droans as a contributor for code (#937) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index e4aa63a4..6ce6817a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -774,6 +774,15 @@ "contributions": [ "translation" ] + }, + { + "login": "droans", + "name": "droans", + "avatar_url": "https://avatars.githubusercontent.com/u/49721649?v=4", + "profile": "https://github.com/droans", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index b5fa6293..b59d6097 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-84-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-85-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -573,6 +573,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + + + From 8fe532748af57d65b592171f00760b27c36fd847 Mon Sep 17 00:00:00 2001 From: Jonathan Kang Date: Mon, 4 Mar 2024 03:38:31 +0800 Subject: [PATCH 0749/1077] Do not adapt lights that are turned on with an effect (#844) * Do not adapt lights that are turned on with an effect * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/switch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index cf38d21f..23ca14f9 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2654,7 +2654,10 @@ class AdaptiveLightingManager: entity_id, service_data, ) - if any(attr in service_data for attr in COLOR_ATTRS | BRIGHTNESS_ATTRS): + if any( + attr in service_data + for attr in COLOR_ATTRS | BRIGHTNESS_ATTRS | {ATTR_EFFECT} + ): self.mark_as_manual_control(entity_id) return True return False From 36a5a51405d30e36d0344ecc95a8adbb45a79755 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 3 Mar 2024 11:38:58 -0800 Subject: [PATCH 0750/1077] docs: add JonathanKang as a contributor for code (#941) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 6ce6817a..78faf634 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -783,6 +783,15 @@ "contributions": [ "code" ] + }, + { + "login": "JonathanKang", + "name": "Jonathan Kang", + "avatar_url": "https://avatars.githubusercontent.com/u/5607743?v=4", + "profile": "http://blogs.gnome.org/jonathankang/", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index b59d6097..b4c8560c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-85-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-86-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -575,6 +575,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From f2a124c30f53e67ea8606adeabd83df84c7a5c51 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 3 Mar 2024 15:09:18 -0800 Subject: [PATCH 0751/1077] Update Ruff to be used in tests (#943) * Update Ruff config * Rerun ruff --- .ruff.toml | 30 ++++- tests/conftest.py | 2 +- tests/test_adaptation_utils.py | 36 +++-- tests/test_color_and_brightness.py | 18 +-- tests/test_config_flow.py | 6 +- tests/test_init.py | 3 +- tests/test_switch.py | 204 +++++++++++++++++++---------- 7 files changed, 197 insertions(+), 102 deletions(-) diff --git a/.ruff.toml b/.ruff.toml index 6ebfec62..fe94764b 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,7 +1,7 @@ # The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml target-version = "py310" - +[lint] select = ["ALL"] # All the ones without a comment were the ones that are currently violated @@ -23,18 +23,36 @@ ignore = [ "SLF001", # Private member accessed ] -[per-file-ignores] -"tests/*.py" = ["ALL"] +[lint.per-file-ignores] +"tests/*.py" = [ + "ARG001", # Unused function argument: `call` + "D100", # Missing docstring in public module + "D103", # Missing docstring in public function + "D205", # 1 blank line required between summary line and description + "D400", # First line should end with a period + "D415", # First line should end with a period, question mark, or + "DTZ001", # The use of `datetime.datetime()` without `tzinfo` + "ERA001", # Found commented-out code + "FBT003", # Boolean positional value in function call + "FIX002", # Line contains TODO, consider resolving the issue + "G004", # Logging statement uses f-string + "PLR0915", # Too many statements (94 > 50) + "PT004", # Fixture `cleanup` does not return anything, add leading underscore + "PT007", # Wrong values type in `@pytest.mark.parametrize` expected `list` of + "S311", # Standard pseudo-random generators are not suitable for cryptographic + "TD002", # Missing author in TODO; try: `# TODO(): ...` or `# TODO + "TD003", # Missing issue link on the line following this TODO +] ".github/*py" = ["INP001"] "webapp/homeassistant_util_color.py" = ["ALL"] "webapp/app.py" = ["INP001", "DTZ011", "A002"] "custom_components/adaptive_lighting/homeassistant_util_color.py" = ["ALL"] -[flake8-pytest-style] +[lint.flake8-pytest-style] fixture-parentheses = false -[pyupgrade] +[lint.pyupgrade] keep-runtime-typing = true -[mccabe] +[lint.mccabe] max-complexity = 25 diff --git a/tests/conftest.py b/tests/conftest.py index 4b61832d..79b83fb6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,4 +17,4 @@ if "HA_CLONE" in os.environ: @pytest.fixture(autouse=True) def auto_enable_custom_integrations(enable_custom_integrations): - yield + return diff --git a/tests/test_adaptation_utils.py b/tests/test_adaptation_utils.py index 20db5037..8e36a181 100644 --- a/tests/test_adaptation_utils.py +++ b/tests/test_adaptation_utils.py @@ -2,6 +2,7 @@ from unittest.mock import Mock +import pytest from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP_KELVIN, @@ -9,7 +10,6 @@ from homeassistant.components.light import ( ) from homeassistant.const import ATTR_ENTITY_ID, STATE_ON from homeassistant.core import Context, State -import pytest from custom_components.adaptive_lighting.adaptation_utils import ( ServiceData, @@ -22,7 +22,7 @@ from custom_components.adaptive_lighting.adaptation_utils import ( @pytest.mark.parametrize( - "input_data,expected_data_list", + ("input_data", "expected_data_list"), [ ( {"foo": 1}, @@ -72,7 +72,7 @@ async def test_split_service_call_data(input_data, expected_data_list): @pytest.mark.parametrize( - "service_data,state,service_data_expected", + ("service_data", "state", "service_data_expected"), [ ( {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, @@ -97,14 +97,16 @@ async def test_split_service_call_data(input_data, expected_data_list): ], ) async def test_remove_redundant_attributes( - service_data: ServiceData, state: State | None, service_data_expected: ServiceData + service_data: ServiceData, + state: State | None, + service_data_expected: ServiceData, ): """Test filtering of service data.""" assert _remove_redundant_attributes(service_data, state) == service_data_expected @pytest.mark.parametrize( - "service_data,expected_relevant", + ("service_data", "expected_relevant"), [ ( {ATTR_ENTITY_ID: "light.test"}, @@ -125,14 +127,15 @@ async def test_remove_redundant_attributes( ], ) async def test_has_relevant_service_data_attributes( - service_data: ServiceData, expected_relevant: bool + service_data: ServiceData, + expected_relevant: bool, ): """Test the determination of relevancy of service data""" assert _has_relevant_service_data_attributes(service_data) == expected_relevant @pytest.mark.parametrize( - "service_datas,filter_by_state,service_datas_expected", + ("service_datas", "filter_by_state", "service_datas_expected"), [ ( [{ATTR_ENTITY_ID: "light.test"}], @@ -198,11 +201,12 @@ async def test_create_service_call_data_iterator( hass_states_mock, ): """Test the generator function for correct enumeration and filtering.""" - generated_service_datas = [ data async for data in _create_service_call_data_iterator( - hass_states_mock, service_datas, filter_by_state + hass_states_mock, + service_datas, + filter_by_state, ) ] @@ -215,7 +219,13 @@ async def test_create_service_call_data_iterator( @pytest.mark.parametrize( - "service_data,split,filter_by_state,service_datas_expected,sleep_time_expected", + ( + "service_data", + "split", + "filter_by_state", + "service_datas_expected", + "sleep_time_expected", + ), [ ( { @@ -230,7 +240,7 @@ async def test_create_service_call_data_iterator( ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_COLOR_TEMP_KELVIN: 4000, - } + }, ], 1.2, ), @@ -266,7 +276,7 @@ async def test_create_service_call_data_iterator( { ATTR_ENTITY_ID: "light.test", ATTR_COLOR_TEMP_KELVIN: 4000, - } + }, ], 1.2, ), @@ -282,7 +292,7 @@ async def test_create_service_call_data_iterator( { ATTR_ENTITY_ID: "light.test", ATTR_COLOR_TEMP_KELVIN: 4000, - } + }, ], 0.7, ), diff --git a/tests/test_color_and_brightness.py b/tests/test_color_and_brightness.py index a199f939..b8398a71 100644 --- a/tests/test_color_and_brightness.py +++ b/tests/test_color_and_brightness.py @@ -1,13 +1,15 @@ -import pytest -from custom_components.adaptive_lighting.color_and_brightness import ( - SunEvents, - SUN_EVENT_SUNRISE, - SUN_EVENT_NOON, -) import datetime as dt +import zoneinfo + +import pytest from astral import LocationInfo from astral.location import Location -import zoneinfo + +from custom_components.adaptive_lighting.color_and_brightness import ( + SUN_EVENT_NOON, + SUN_EVENT_SUNRISE, + SunEvents, +) # Create a mock astral_location object location = Location(LocationInfo()) @@ -31,7 +33,7 @@ def tzinfo_and_location(request): timezone=timezone, latitude=lat, longitude=long, - ) + ), ) return tzinfo, location diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 4242417a..1678e3d4 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -20,7 +20,8 @@ DEFAULT_DATA = {key: default for key, default, _ in VALIDATION_TUPLES} async def test_flow_manual_configuration(hass): """Test that config flow works.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, + context={"source": "user"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -28,7 +29,8 @@ async def test_flow_manual_configuration(hass): assert result["handler"] == "adaptive_lighting" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_NAME: "living room"} + result["flow_id"], + user_input={CONF_NAME: "living room"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "living room" diff --git a/tests/test_init.py b/tests/test_init.py index b6c82673..19710d57 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -14,7 +14,7 @@ async def test_setup_with_config(hass): config = { adaptive_lighting.DOMAIN: { adaptive_lighting.CONF_NAME: DEFAULT_NAME, - } + }, } assert await async_setup_component(hass, adaptive_lighting.DOMAIN, config) assert adaptive_lighting.DOMAIN in hass.data @@ -22,7 +22,6 @@ async def test_setup_with_config(hass): async def test_successful_config_entry(hass): """Test that Adaptive Lighting is configured successfully.""" - entry = MockConfigEntry( domain=adaptive_lighting.DOMAIN, data={CONF_NAME: DEFAULT_NAME}, diff --git a/tests/test_switch.py b/tests/test_switch.py index 8664433c..9585c960 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -2,14 +2,19 @@ # pylint: disable=protected-access import asyncio -import itertools -from copy import deepcopy +import contextlib import datetime import logging +from copy import deepcopy from random import randint from typing import Any from unittest.mock import Mock, patch +import homeassistant.config as config_util +import homeassistant.util.dt as dt_util +import pytest +import ulid_transform +import voluptuous.error from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, @@ -17,11 +22,10 @@ from homeassistant.components.light import ( ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_XY_COLOR, + SERVICE_TURN_OFF, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.light import SERVICE_TURN_OFF from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -import homeassistant.config as config_util from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_AREA_ID, @@ -41,24 +45,21 @@ from homeassistant.helpers import entity_registry from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.setup import async_setup_component from homeassistant.util.color import color_temperature_mired_to_kelvin -import homeassistant.util.dt as dt_util -import pytest from pytest_homeassistant_custom_component.common import ( MockConfigEntry, mock_area_registry, ) -import ulid_transform -import voluptuous.error from custom_components.adaptive_lighting.adaptation_utils import ( AdaptationData, _create_service_call_data_iterator, ) +from custom_components.adaptive_lighting.color_and_brightness import lerp_color_hsv from custom_components.adaptive_lighting.const import ( ADAPT_BRIGHTNESS_SWITCH, ADAPT_COLOR_SWITCH, - CONF_TAKE_OVER_CONTROL, ATTR_ADAPTIVE_LIGHTING_MANAGER, + CONF_ADAPT_ONLY_ON_BARE_TURN_ON, CONF_ADAPT_UNTIL_SLEEP, CONF_AUTORESET_CONTROL, CONF_BRIGHTNESS_MODE, @@ -69,13 +70,14 @@ from custom_components.adaptive_lighting.const import ( CONF_MANUAL_CONTROL, CONF_MAX_BRIGHTNESS, CONF_MIN_COLOR_TEMP, - CONF_PREFER_RGB_COLOR, CONF_MULTI_LIGHT_INTERCEPT, + CONF_PREFER_RGB_COLOR, CONF_SEPARATE_TURN_ON_COMMANDS, CONF_SLEEP_RGB_OR_COLOR_TEMP, CONF_SUNRISE_OFFSET, CONF_SUNRISE_TIME, CONF_SUNSET_TIME, + CONF_TAKE_OVER_CONTROL, CONF_TRANSITION, CONF_TURN_ON_LIGHTS, CONF_USE_DEFAULTS, @@ -89,20 +91,18 @@ from custom_components.adaptive_lighting.const import ( SERVICE_CHANGE_SWITCH_SETTINGS, SERVICE_SET_MANUAL_CONTROL, SLEEP_MODE_SWITCH, - CONF_ADAPT_ONLY_ON_BARE_TURN_ON, UNDO_UPDATE_LISTENER, ) from custom_components.adaptive_lighting.switch import ( CONF_INTERCEPT, + AdaptiveLightingManager, AdaptiveSwitch, _attributes_have_changed, color_difference_redmean, create_context, - AdaptiveLightingManager, is_our_context, is_our_context_id, ) -from custom_components.adaptive_lighting.color_and_brightness import lerp_color_hsv _LOGGER = logging.getLogger(__name__) @@ -206,7 +206,7 @@ async def setup_lights(hass: HomeAssistant, with_group: bool = False): "name": "Light Group", "unique_id": "light_group", "all": "false", - } + }, ) await async_setup_component( @@ -319,7 +319,10 @@ def create_transition_events( ATTR_ENTITY_ID: light, "old_state": State(light, "on", attributes=last), "new_state": State( - light, "on", attributes=attributes, context=create_random_context() + light, + "on", + attributes=attributes, + context=create_random_context(), ), } all_events.append(event_data) @@ -351,9 +354,13 @@ async def test_adaptive_lighting_switches(hass): assert len(data.keys()) == 5 -@pytest.mark.parametrize("lat,long,timezone", LAT_LONG_TZS) +@pytest.mark.parametrize(("lat", "long", "timezone"), LAT_LONG_TZS) async def test_adaptive_lighting_time_zones_with_default_settings( - hass, lat, long, timezone, reset_time_zone # pylint: disable=redefined-outer-name + hass, + lat, + long, + timezone, + reset_time_zone, # pylint: disable=redefined-outer-name ): """Test setting up the Adaptive Lighting switches with different timezones.""" await config_util.async_process_ha_core_config( @@ -363,11 +370,11 @@ async def test_adaptive_lighting_time_zones_with_default_settings( _, switch = await setup_switch(hass, {}) # Shouldn't raise an exception ever await switch._update_attrs_and_maybe_adapt_lights( - context=switch.create_context("test") + context=switch.create_context("test"), ) -@pytest.mark.parametrize("lat,long,timezone", LAT_LONG_TZS) +@pytest.mark.parametrize(("lat", "long", "timezone"), LAT_LONG_TZS) async def test_adaptive_lighting_time_zones_and_sun_settings( hass, lat, @@ -466,7 +473,7 @@ async def test_light_settings(hass): light_states = [hass.states.get(light) for light in lights] for state in light_states: assert state.attributes[ATTR_BRIGHTNESS] == round( - 255 * switch._settings[ATTR_BRIGHTNESS_PCT] / 100 + 255 * switch._settings[ATTR_BRIGHTNESS_PCT] / 100, ) last_service_data = switch.manager.last_service_data[state.entity_id] assert state.attributes[ATTR_BRIGHTNESS] == last_service_data[ATTR_BRIGHTNESS] @@ -500,7 +507,9 @@ async def test_light_settings(hass): return_value=time, ): await switch._update_attrs_and_maybe_adapt_lights( - context=context, transition=0, force=True + context=context, + transition=0, + force=True, ) await hass.async_block_till_done() return [hass.states.get(light) for light in lights] @@ -564,7 +573,7 @@ async def test_manager_not_tracking_untracked_lights(hass): blocking=True, ) await switch._update_attrs_and_maybe_adapt_lights( - context=switch.create_context("test") + context=switch.create_context("test"), ) await hass.async_block_till_done() assert light not in switch.manager.lights @@ -573,7 +582,9 @@ async def test_manager_not_tracking_untracked_lights(hass): @pytest.mark.parametrize("adapt_only_on_bare_turn_on", [True, False]) @pytest.mark.parametrize("proactive_service_call_adaptation", [True, False]) async def test_manual_control( - hass, adapt_only_on_bare_turn_on, proactive_service_call_adaptation + hass, + adapt_only_on_bare_turn_on, + proactive_service_call_adaptation, ): """Test the 'manual control' tracking.""" switch, (light, *_) = await setup_lights_and_switch( @@ -698,7 +709,8 @@ async def test_manual_control( ) ptp_kelvin = kelvin_range[1] - kelvin_range[0] await turn_light( - True, color_temp_kelvin=(light._attr_color_temp + 100) % ptp_kelvin + True, + color_temp_kelvin=(light._attr_color_temp + 100) % ptp_kelvin, ) assert manual_control[ENTITY_LIGHT_1] await switch.adapt_brightness_switch.async_turn_on() # turn on again @@ -736,10 +748,10 @@ async def test_manual_control( # Check that when no lights are specified, all are reset await change_manual_control(True, {CONF_LIGHTS: switch.lights}) - assert all([manual_control[eid] for eid in switch.lights]) + assert all(manual_control[eid] for eid in switch.lights) # do not pass "lights" so reset all await change_manual_control(False, {}) - assert all([not manual_control[eid] for eid in switch.lights]) + assert all(not manual_control[eid] for eid in switch.lights) # Turn off light and turn on using adaptive_lighting.apply await turn_light(False) @@ -760,7 +772,8 @@ async def test_manual_control( async def test_auto_reset_manual_control(hass): switch, (light, *_) = await setup_lights_and_switch( - hass, {CONF_AUTORESET_CONTROL: 0.1} + hass, + {CONF_AUTORESET_CONTROL: 0.1}, ) context = switch.create_context("test") # needs to be passed to update method manual_control = switch.manager.manual_control @@ -779,7 +792,10 @@ async def test_auto_reset_manual_control(hass): await hass.async_block_till_done() await update() _LOGGER.debug( - "Turn light %s to state %s, to %s", light.entity_id, state, kwargs + "Turn light %s to state %s, to %s", + light.entity_id, + state, + kwargs, ) _LOGGER.debug("Start test auto reset manual control") @@ -886,7 +902,8 @@ async def test_switch_off_on_off(hass): async def update(): await switch._update_attrs_and_maybe_adapt_lights( - context=switch.create_context("test"), transition=0 + context=switch.create_context("test"), + transition=0, ) await hass.async_block_till_done() @@ -943,20 +960,24 @@ def test_attributes_have_changed(): ATTR_RGB_COLOR: (255, 0, 0), ATTR_COLOR_TEMP_KELVIN: 300, } - kwargs = dict( - light="light.test", - adapt_brightness=True, - adapt_color=True, - context=Context(), - ) + kwargs = { + "light": "light.test", + "adapt_brightness": True, + "adapt_color": True, + "context": Context(), + } assert not _attributes_have_changed( - old_attributes=attributes_1, new_attributes=attributes_1, **kwargs + old_attributes=attributes_1, + new_attributes=attributes_1, + **kwargs, ) for key, value in attributes_2.items(): attrs = dict(attributes_1) attrs[key] = value assert _attributes_have_changed( - old_attributes=attributes_1, new_attributes=attrs, **kwargs + old_attributes=attributes_1, + new_attributes=attrs, + **kwargs, ) _LOGGER.debug("Test switch from color_temp to rgb_color") assert not _attributes_have_changed( @@ -985,8 +1006,7 @@ def test_attributes_have_changed(): async def test_state_change_handlers(hass): - """ - Test AdaptiveLightingManager's EVENT_STATE_CHANGED listener. + """Test AdaptiveLightingManager's EVENT_STATE_CHANGED listener. ====================== Sequence of events: 1. Transition from sleep mode to normal. @@ -1005,7 +1025,9 @@ async def test_state_change_handlers(hass): async def set_brightness(val: int): # 'Unsafe' set but we know what we're doing. hass.states.async_set( - ENTITY_LIGHT_1, "on", {ATTR_BRIGHTNESS: val, ATTR_SUPPORTED_FEATURES: 1} + ENTITY_LIGHT_1, + "on", + {ATTR_BRIGHTNESS: val, ATTR_SUPPORTED_FEATURES: 1}, ) await hass.async_block_till_done() # Call code in AdaptiveLightingManager @@ -1016,7 +1038,7 @@ async def test_state_change_handlers(hass): ATTR_ENTITY_ID: ENTITY_LIGHT_1, "state": "on", ATTR_BRIGHTNESS: val, - } + }, }, ) await hass.async_block_till_done() @@ -1057,7 +1079,9 @@ async def test_state_change_handlers(hass): # 2 Adapt from sleep with a 'transition'. await switch.sleep_mode_switch.async_turn_off() await switch._update_attrs_and_maybe_adapt_lights( - context=context, force=False, transition=0 + context=context, + force=False, + transition=0, ) await hass.async_block_till_done() current_service_data = switch.manager.last_service_data @@ -1077,7 +1101,10 @@ async def test_state_change_handlers(hass): ATTR_ENTITY_ID: light, "old_state": State(light, "on", attributes=last_service_data), "new_state": State( - light, "on", attributes=current_service_data, context=context + light, + "on", + attributes=current_service_data, + context=context, ), }, ) @@ -1131,14 +1158,16 @@ async def test_state_change_handlers(hass): await asyncio.sleep(transition_used / 3) # Ensure the timer still exists timer = listener.transition_timers.get(ENTITY_LIGHT_1) - assert timer and timer.is_running() + assert timer + assert timer.is_running() last_service_data = deepcopy(current_service_data) await update() assert not switch.manager.manual_control[ENTITY_LIGHT_1] await update() assert not switch.manager.manual_control[ENTITY_LIGHT_1] timer = listener.transition_timers.get(ENTITY_LIGHT_1) - assert timer and timer.is_running() + assert timer + assert timer.is_running() # Ensure the light did not adapt during the transition. assert last_service_data == current_service_data @@ -1231,7 +1260,7 @@ async def test_offset_too_large(hass): """Test that update fails when the offset is too large.""" _, switch = await setup_switch(hass, {CONF_SUNRISE_OFFSET: 3600 * 12}) await switch._update_attrs_and_maybe_adapt_lights( - context=switch.create_context("test") + context=switch.create_context("test"), ) await hass.async_block_till_done() @@ -1261,7 +1290,8 @@ async def test_async_update_at_interval_action(hass): async def test_separate_turn_on_commands(hass, separate_turn_on_commands): """Test 'separate_turn_on_commands' argument.""" switch, (light, *_) = await setup_lights_and_switch( - hass, {CONF_SEPARATE_TURN_ON_COMMANDS: separate_turn_on_commands} + hass, + {CONF_SEPARATE_TURN_ON_COMMANDS: separate_turn_on_commands}, ) # We just turn sleep mode on and off which should change the # brightness and color. We don't test whether the number are exactly @@ -1301,10 +1331,13 @@ async def test_area(hass): area_registry.async_create("test_area") entity = entity_registry.async_get(hass).async_get_or_create( - LIGHT_DOMAIN, "template", light.unique_id + LIGHT_DOMAIN, + "template", + light.unique_id, ) entity = entity_registry.async_get(hass).async_update_entity( - entity.entity_id, area_id="test_area" + entity.entity_id, + area_id="test_area", ) _LOGGER.debug("test_area entity: %s", entity) await hass.services.async_call( @@ -1379,7 +1412,7 @@ async def test_change_switch_settings_service(hass): # testing with "configuration" and setting a new value await change_switch_settings( - **{CONF_USE_DEFAULTS: "configuration", CONF_MIN_COLOR_TEMP: 3000} + **{CONF_USE_DEFAULTS: "configuration", CONF_MIN_COLOR_TEMP: 3000}, ) assert switch._sun_light_settings.min_color_temp == 3000 @@ -1429,16 +1462,17 @@ async def test_service_calls_task_cancellation(hass): switch.manager.cancel_ongoing_adaptation_calls(entity_id) - try: + with contextlib.suppress(asyncio.CancelledError): await task - except asyncio.CancelledError: - pass assert task.cancelled() async def _turn_on_and_track_event_contexts( - hass: HomeAssistant, context_id: str, entity_id, return_full_events: bool = False + hass: HomeAssistant, + context_id: str, + entity_id, + return_full_events: bool = False, ): context = Context(id=context_id) event_context_ids = [] @@ -1483,7 +1517,9 @@ async def test_proactive_adaptation(hass): ) event_context_ids = await _turn_on_and_track_event_contexts( - hass, "test_context", ENTITY_LIGHT_3 + hass, + "test_context", + ENTITY_LIGHT_3, ) # Expect a single service call @@ -1518,7 +1554,9 @@ async def test_proactive_adaptation_with_separate_commands(hass): ) event_context_ids = await _turn_on_and_track_event_contexts( - hass, "test_context", ENTITY_LIGHT_3 + hass, + "test_context", + ENTITY_LIGHT_3, ) # Expect two service calls @@ -1575,7 +1613,9 @@ async def test_proactive_adaptation_transition_override(hass): ) with patch.object( - light3, "async_turn_on", wraps=light3.async_turn_on + light3, + "async_turn_on", + wraps=light3.async_turn_on, ) as patched_async_turn_on: await hass.services.async_call( LIGHT_DOMAIN, @@ -1604,7 +1644,7 @@ async def test_proactive_adaptation_transition_override(hass): async def setup_proactive_multiple_lights_two_switches(hass): - lights_instances = await setup_lights(hass) + await setup_lights(hass) # Setup switches lights = [ ENTITY_LIGHT_1, @@ -1628,10 +1668,12 @@ async def setup_proactive_multiple_lights_two_switches(hass): CONF_INTERCEPT: True, } _, switch1 = await setup_switch( - hass, {CONF_NAME: "switch1", CONF_LIGHTS: [ENTITY_LIGHT_1], **defaults} + hass, + {CONF_NAME: "switch1", CONF_LIGHTS: [ENTITY_LIGHT_1], **defaults}, ) _, switch2 = await setup_switch( - hass, {CONF_NAME: "switch2", CONF_LIGHTS: [ENTITY_LIGHT_2], **defaults} + hass, + {CONF_NAME: "switch2", CONF_LIGHTS: [ENTITY_LIGHT_2], **defaults}, ) assert hass.states.get(switch1.entity_id).state == STATE_ON assert hass.states.get(switch2.entity_id).state == STATE_ON @@ -1645,7 +1687,10 @@ async def test_proactive_multiple_lights_all_at_once(hass): _LOGGER.debug("Start test_proactive_multiple_lights_all_at_once") # Setup demo lights and turn on events = await _turn_on_and_track_event_contexts( - hass, "test1", lights, return_full_events=True + hass, + "test1", + lights, + return_full_events=True, ) assert len(events) == 3, events @@ -1670,7 +1715,10 @@ async def test_proactive_multiple_lights_all_at_once(hass): # Turn on second time even though already on events = await _turn_on_and_track_event_contexts( - hass, "test2", lights, return_full_events=True + hass, + "test2", + lights, + return_full_events=True, ) assert len(events) == 1, events assert events[0].context.id == "test2" @@ -1704,7 +1752,10 @@ async def test_proactive_multiple_lights_turn_on_managed_lights_only(hass): _LOGGER.debug("Start test_proactive_multiple_lights_all_at_once") # Setup demo lights and turn on events = await _turn_on_and_track_event_contexts( - hass, "test1", lights[:-1], return_full_events=True + hass, + "test1", + lights[:-1], + return_full_events=True, ) assert len(events) == 2, events @@ -1725,7 +1776,10 @@ async def test_proactive_multiple_lights_one_switch_and_one_skipped(hass): _LOGGER.debug("Start test_proactive_multiple_lights_all_at_once") # Setup demo lights and turn on events = await _turn_on_and_track_event_contexts( - hass, "test1", two_lights, return_full_events=True + hass, + "test1", + two_lights, + return_full_events=True, ) assert len(events) == 2, events @@ -1752,10 +1806,14 @@ async def test_two_switches_for_single_light(hass): """ extra_conf = {CONF_INTERCEPT: True} switch1, (light1, *_) = await setup_lights_and_switch( - hass, extra_conf | {CONF_NAME: "switch1"}, all_lights=True + hass, + extra_conf | {CONF_NAME: "switch1"}, + all_lights=True, ) switch2, (light2, *_) = await setup_lights_and_switch( - hass, extra_conf | {CONF_NAME: "switch2"}, all_lights=True + hass, + extra_conf | {CONF_NAME: "switch2"}, + all_lights=True, ) assert light1 is light2 @@ -1946,7 +2004,7 @@ async def test_light_group( await hass.async_block_till_done() await switch._update_attrs_and_maybe_adapt_lights( - context=switch.create_context("test") + context=switch.create_context("test"), ) await hass.async_block_till_done() @@ -1968,7 +2026,10 @@ async def test_light_group( assert not switch.manager.manual_control["light.light_4"] assert not switch.manager.manual_control["light.light_5"] events = await _turn_on_and_track_event_contexts( - hass, "testing", "light.light_group", return_full_events=True + hass, + "testing", + "light.light_group", + return_full_events=True, ) if proactive_service_call_adaptation and multi_light_intercept: await asyncio.gather(*switch.manager.adaptation_tasks) @@ -2022,7 +2083,10 @@ async def test_light_group( # light_group is expanded, with a :skpp: context_id, this goes trhough another iteration, # and then the light_group is adapted. events = await _turn_on_and_track_event_contexts( - hass, "testing", entity_ids, return_full_events=True + hass, + "testing", + entity_ids, + return_full_events=True, ) if proactive_service_call_adaptation and multi_light_intercept: await asyncio.gather(*switch.manager.adaptation_tasks) @@ -2047,7 +2111,7 @@ async def test_light_group( @pytest.mark.parametrize("brightness_mode", ["linear", "tanh"]) -@pytest.mark.parametrize("dark,light", ([900, 1800], [1800, 900], [1800, 1800])) +@pytest.mark.parametrize(("dark", "light"), ([900, 1800], [1800, 900], [1800, 1800])) async def test_brightness_mode(hass, brightness_mode, dark, light): """Test brightness mode. From e4e1aa7d37e86e6adb654808db0a04a87ce9c931 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 6 Apr 2024 10:13:13 +0200 Subject: [PATCH 0752/1077] Use Python 3.12 in .github/workflows/update-readme.yml (#961) --- .github/workflows/update-readme.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index 8fca3d77..74f5e242 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -20,7 +20,7 @@ jobs: - name: Install Home Assistant uses: ./.github/workflows/install_dependencies with: - python-version: "3.11" + python-version: "3.12" - name: Install markdown-code-runner and README code dependencies run: | From 9eae1501d3fcda2cfdb479b25d8864e7dfb75833 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 6 Apr 2024 10:14:54 +0200 Subject: [PATCH 0753/1077] Test HA core v2023.6 until v2024.2 (#942) * Test HA core v2023.6 until v2024.2 * Use Python 3.12 * Add components.ffmpeg * Revert "VS Code Dev Container (dev & test environment) (#605)" This reverts commit 6283158ff730644f76d8007c70edbf0ffd6846fe. * Fix * revert * fi * simplify * Fix * fixes * fix * rev * Rename test * fix * fix all * fix * revert --- .github/workflows/pytest.yaml | 29 +++++++-- Dockerfile | 7 +-- test_dependencies.py | 1 + tests/conftest.py | 20 ------ tests/test_adaptation_utils.py | 19 +++--- tests/test_color_and_brightness.py | 3 +- tests/test_config_flow.py | 10 +-- tests/test_hass_utils.py | 9 ++- tests/test_init.py | 9 ++- tests/test_switch.py | 98 ++++++++++++++++-------------- 10 files changed, 104 insertions(+), 101 deletions(-) delete mode 100644 tests/conftest.py diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index ba7eb1a7..750fba04 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -27,10 +27,26 @@ jobs: - python-version: "3.10" core-version: "2023.4.6" - python-version: "3.10" - core-version: "2023.5.2" + core-version: "2023.5.4" - python-version: "3.11" - core-version: "2023.6.1" + core-version: "2023.6.3" - python-version: "3.11" + core-version: "2023.7.3" + - python-version: "3.11" + core-version: "2023.8.4" + - python-version: "3.11" + core-version: "2023.9.3" + - python-version: "3.11" + core-version: "2023.10.5" + - python-version: "3.11" + core-version: "2023.11.3" + - python-version: "3.11" + core-version: "2023.12.4" + - python-version: "3.11" + core-version: "2024.1.6" + - python-version: "3.11" + core-version: "2024.2.5" + - python-version: "3.12" core-version: "dev" steps: - name: Check out code from GitHub @@ -56,6 +72,11 @@ jobs: run: | cd core + # Link homeassitant.components.adaptive_lighting + cd homeassistant/components + ln -fs ../../../custom_components/adaptive_lighting adaptive_lighting + cd - + # Link adaptive_lighting tests cd tests/components/ ln -fs ../../../tests adaptive_lighting @@ -71,10 +92,8 @@ jobs: -qq \ --timeout=9 \ --durations=10 \ - --cov="custom_components.adaptive_lighting" \ + --cov="homeassistant" \ --cov-report=xml \ -o console_output_style=count \ -p no:sugar \ tests/components/adaptive_lighting - env: - HA_CLONE: true diff --git a/Dockerfile b/Dockerfile index 4baac59a..916d1021 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,8 @@ RUN pip3 install -r /core/requirements.txt --use-pep517 && \ COPY . /app/ # Setup symlinks in core -RUN ln -s /app/tests /core/tests/components/adaptive_lighting && \ +RUN ln -s /app/custom_components/adaptive_lighting /core/homeassistant/components/adaptive_lighting && \ + ln -s /app/tests /core/tests/components/adaptive_lighting && \ # For test_dependencies.py ln -s /core /app/core @@ -38,8 +39,6 @@ WORKDIR /core # Make 'custom_components/adaptive_lighting' imports available to tests ENV PYTHONPATH="${PYTHONPATH}:/app" -# Enable testing against HA clone (instead of pytest_homeassistant_custom_component) -ENV HA_CLONE=true ENTRYPOINT ["python3", \ # Enable Python development mode @@ -53,7 +52,7 @@ ENTRYPOINT ["python3", \ # Print the 10 slowest tests "--durations=10", \ # Measure code coverage for the 'homeassistant' package - "--cov=custom_components.adaptive_lighting", \ + "--cov='homeassistant'", \ # Generate an XML report of the code coverage "--cov-report=xml", \ # Generate an HTML report of the code coverage diff --git a/test_dependencies.py b/test_dependencies.py index a07f9ee6..96377cee 100644 --- a/test_dependencies.py +++ b/test_dependencies.py @@ -36,6 +36,7 @@ required = [ "components.stream", "components.conversation", # only available after HA≥2023.2 "components.cloud", + "components.ffmpeg", # needed since 2024.1 ] to_install = [package for r in required for package in deps[r]] diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 79b83fb6..00000000 --- a/tests/conftest.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Fixtures for testing.""" - -import os -import sys - -import pytest - -# Tests in the dev enviromentment use the pytest_homeassistant_custom_component instead of -# a cloned HA core repo for a simple and clean structure. To still test against a HA core -# clone (e.g. the dev branch for which no pytest_homeassistant_custom_component exists -# because HA does not publish dev snapshot packages), set the HA_CLONE env variable. -if "HA_CLONE" in os.environ: - # Rewire the testing package to the cloned test modules. See the test `Dockerfile` - # for setup details. - sys.modules["pytest_homeassistant_custom_component"] = __import__("tests") - - -@pytest.fixture(autouse=True) -def auto_enable_custom_integrations(enable_custom_integrations): - return diff --git a/tests/test_adaptation_utils.py b/tests/test_adaptation_utils.py index 8e36a181..10774fa2 100644 --- a/tests/test_adaptation_utils.py +++ b/tests/test_adaptation_utils.py @@ -3,15 +3,7 @@ from unittest.mock import Mock import pytest -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP_KELVIN, - ATTR_TRANSITION, -) -from homeassistant.const import ATTR_ENTITY_ID, STATE_ON -from homeassistant.core import Context, State - -from custom_components.adaptive_lighting.adaptation_utils import ( +from homeassistant.components.adaptive_lighting.adaptation_utils import ( ServiceData, _create_service_call_data_iterator, _has_relevant_service_data_attributes, @@ -19,6 +11,13 @@ from custom_components.adaptive_lighting.adaptation_utils import ( _split_service_call_data, prepare_adaptation_data, ) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP_KELVIN, + ATTR_TRANSITION, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_ON +from homeassistant.core import Context, State @pytest.mark.parametrize( @@ -130,7 +129,7 @@ async def test_has_relevant_service_data_attributes( service_data: ServiceData, expected_relevant: bool, ): - """Test the determination of relevancy of service data""" + """Test the determination of relevancy of service data.""" assert _has_relevant_service_data_attributes(service_data) == expected_relevant diff --git a/tests/test_color_and_brightness.py b/tests/test_color_and_brightness.py index b8398a71..6c76175f 100644 --- a/tests/test_color_and_brightness.py +++ b/tests/test_color_and_brightness.py @@ -4,8 +4,7 @@ import zoneinfo import pytest from astral import LocationInfo from astral.location import Location - -from custom_components.adaptive_lighting.color_and_brightness import ( +from homeassistant.components.adaptive_lighting.color_and_brightness import ( SUN_EVENT_NOON, SUN_EVENT_SUNRISE, SunEvents, diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 1678e3d4..1a9e8fbf 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,11 +1,7 @@ """Test Adaptive Lighting config flow.""" from homeassistant import data_entry_flow -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_NAME -from pytest_homeassistant_custom_component.common import MockConfigEntry - -from custom_components.adaptive_lighting.const import ( +from homeassistant.components.adaptive_lighting.const import ( CONF_SUNRISE_TIME, CONF_SUNSET_TIME, DEFAULT_NAME, @@ -13,6 +9,10 @@ from custom_components.adaptive_lighting.const import ( NONE_STR, VALIDATION_TUPLES, ) +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_NAME + +from tests.common import MockConfigEntry DEFAULT_DATA = {key: default for key, default, _ in VALIDATION_TUPLES} diff --git a/tests/test_hass_utils.py b/tests/test_hass_utils.py index 9f044d8a..4be4c88d 100644 --- a/tests/test_hass_utils.py +++ b/tests/test_hass_utils.py @@ -2,16 +2,15 @@ from unittest.mock import AsyncMock +from homeassistant.components.adaptive_lighting.adaptation_utils import ServiceData +from homeassistant.components.adaptive_lighting.hass_utils import ( + setup_service_call_interceptor, +) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.const import SERVICE_TURN_ON from homeassistant.core import ServiceCall from homeassistant.util.read_only_dict import ReadOnlyDict -from custom_components.adaptive_lighting.adaptation_utils import ServiceData -from custom_components.adaptive_lighting.hass_utils import ( - setup_service_call_interceptor, -) - async def test_setup_service_call_interceptor(hass): """Test setup and removal of service call interceptor.""" diff --git a/tests/test_init.py b/tests/test_init.py index 19710d57..6bbfd599 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,12 +1,15 @@ """Tests for Adaptive Lighting integration.""" +from homeassistant.components import adaptive_lighting +from homeassistant.components.adaptive_lighting.const import ( + DEFAULT_NAME, + UNDO_UPDATE_LISTENER, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_NAME from homeassistant.setup import async_setup_component -from pytest_homeassistant_custom_component.common import MockConfigEntry -from custom_components import adaptive_lighting -from custom_components.adaptive_lighting.const import DEFAULT_NAME, UNDO_UPDATE_LISTENER +from tests.common import MockConfigEntry async def test_setup_with_config(hass): diff --git a/tests/test_switch.py b/tests/test_switch.py index 9585c960..9c2f254f 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -15,47 +15,14 @@ import homeassistant.util.dt as dt_util import pytest import ulid_transform import voluptuous.error -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT, - ATTR_COLOR_TEMP_KELVIN, - ATTR_RGB_COLOR, - ATTR_TRANSITION, - ATTR_XY_COLOR, - SERVICE_TURN_OFF, -) -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ( - ATTR_AREA_ID, - ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, - CONF_LIGHTS, - CONF_NAME, - EVENT_CALL_SERVICE, - EVENT_STATE_CHANGED, - SERVICE_TOGGLE, - SERVICE_TURN_ON, - STATE_OFF, - STATE_ON, -) -from homeassistant.core import Context, Event, HomeAssistant, State -from homeassistant.helpers import entity_registry -from homeassistant.helpers.entity_platform import async_get_platforms -from homeassistant.setup import async_setup_component -from homeassistant.util.color import color_temperature_mired_to_kelvin -from pytest_homeassistant_custom_component.common import ( - MockConfigEntry, - mock_area_registry, -) - -from custom_components.adaptive_lighting.adaptation_utils import ( +from homeassistant.components.adaptive_lighting.adaptation_utils import ( AdaptationData, _create_service_call_data_iterator, ) -from custom_components.adaptive_lighting.color_and_brightness import lerp_color_hsv -from custom_components.adaptive_lighting.const import ( +from homeassistant.components.adaptive_lighting.color_and_brightness import ( + lerp_color_hsv, +) +from homeassistant.components.adaptive_lighting.const import ( ADAPT_BRIGHTNESS_SWITCH, ADAPT_COLOR_SWITCH, ATTR_ADAPTIVE_LIGHTING_MANAGER, @@ -93,7 +60,7 @@ from custom_components.adaptive_lighting.const import ( SLEEP_MODE_SWITCH, UNDO_UPDATE_LISTENER, ) -from custom_components.adaptive_lighting.switch import ( +from homeassistant.components.adaptive_lighting.switch import ( CONF_INTERCEPT, AdaptiveLightingManager, AdaptiveSwitch, @@ -103,6 +70,39 @@ from custom_components.adaptive_lighting.switch import ( is_our_context, is_our_context_id, ) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_TEMP_KELVIN, + ATTR_RGB_COLOR, + ATTR_TRANSITION, + ATTR_XY_COLOR, + SERVICE_TURN_OFF, +) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.template.light import LightTemplate +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + ATTR_AREA_ID, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_LIGHTS, + CONF_NAME, + EVENT_CALL_SERVICE, + EVENT_STATE_CHANGED, + SERVICE_TOGGLE, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import Context, Event, HomeAssistant, State +from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity_platform import async_get_platforms +from homeassistant.setup import async_setup_component +from homeassistant.util.color import color_temperature_mired_to_kelvin + +from tests.common import MockConfigEntry, mock_area_registry _LOGGER = logging.getLogger(__name__) @@ -234,7 +234,11 @@ async def setup_lights(hass: HomeAssistant, with_group: bool = False): return lights -async def setup_lights_and_switch(hass, extra_conf=None, all_lights: bool = False): +async def setup_lights_and_switch( + hass, + extra_conf=None, + all_lights: bool = False, +) -> tuple[AdaptiveSwitch, list[LightTemplate]]: """Create switch and demo lights.""" # Setup demo lights and turn on lights_instances = await setup_lights(hass) @@ -411,7 +415,7 @@ async def test_adaptive_lighting_time_zones_and_sun_settings( async def patch_time_and_update(time): with patch( - "custom_components.adaptive_lighting.color_and_brightness.utcnow", + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", return_value=time, ): await switch._update_attrs_and_maybe_adapt_lights(context=context) @@ -503,7 +507,7 @@ async def test_light_settings(hass): async def patch_time_and_get_updated_states(time): with patch( - "custom_components.adaptive_lighting.color_and_brightness.utcnow", + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", return_value=time, ): await switch._update_attrs_and_maybe_adapt_lights( @@ -1324,7 +1328,7 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands): assert sleep_color_temp != color_temp -async def test_area(hass): +async def test_light_switch_in_specific_area(hass): switch, (light, *_) = await setup_lights_and_switch(hass) area_registry = mock_area_registry(hass) @@ -1611,7 +1615,6 @@ async def test_proactive_adaptation_transition_override(hass): }, True, ) - with patch.object( light3, "async_turn_on", @@ -1632,6 +1635,7 @@ async def test_proactive_adaptation_transition_override(hass): ) # Assert that default is used when no transition is specified in service call + assert patched_async_turn_on.call_args_list, patched_async_turn_on.call_args_list kwargs = patched_async_turn_on.call_args_list[0].kwargs assert set({ATTR_TRANSITION: 123}.items()).issubset(kwargs.items()) @@ -1815,7 +1819,7 @@ async def test_two_switches_for_single_light(hass): extra_conf | {CONF_NAME: "switch2"}, all_lights=True, ) - assert light1 is light2 + assert light1.entity_id == light2.entity_id # One switch controls brightness the other color await switch1.adapt_color_switch.async_turn_off() @@ -1897,7 +1901,7 @@ async def test_adapt_until_sleep_and_rgb_colors(hass): async def patch_time_and_update(time): with patch( - "custom_components.adaptive_lighting.color_and_brightness.utcnow", + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", return_value=time, ): await switch._update_attrs_and_maybe_adapt_lights(context=context) @@ -2160,7 +2164,7 @@ async def test_brightness_mode(hass, brightness_mode, dark, light): async def patch_time_and_update(time): with patch( - "custom_components.adaptive_lighting.color_and_brightness.utcnow", + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", return_value=time, ): await switch._update_attrs_and_maybe_adapt_lights(context=context) From 2f31b42cb2a852aab8e14c222cc296d79fc23a36 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 6 Apr 2024 11:25:47 +0200 Subject: [PATCH 0754/1077] Fix `devcontainer` and testing `Dockerfile` (#962) * Fix permissions for `scripts/setup` * Add new script * Use script * Update Dockerfile * Rename and clone * Perms * Fix * fi --- .devcontainer.json | 2 +- .../workflows/install_dependencies/action.yml | 9 +-------- Dockerfile | 13 ++++--------- scripts/install_ha | 16 ++++++++++++++++ scripts/setup | 8 -------- scripts/setup-devcontainer | 11 +++++++++++ 6 files changed, 33 insertions(+), 26 deletions(-) create mode 100755 scripts/install_ha delete mode 100644 scripts/setup create mode 100755 scripts/setup-devcontainer diff --git a/.devcontainer.json b/.devcontainer.json index d842d1dd..14778469 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -1,7 +1,7 @@ { "name": "basnijholt/adaptive_lighting", "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.11-bullseye", - "postCreateCommand": "scripts/setup", + "postCreateCommand": "scripts/setup-devcontainer", "forwardPorts": [ 8123 ], diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index 3041698b..16c0a6ec 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -34,11 +34,4 @@ runs: - name: Install dependencies shell: bash run: | - echo "::warning::### WARNING! Deprecation warnings muted with option '--use-pep517' please address this at some point in pytest.yaml. ###" - pip install -r core/requirements.txt --use-pep517 - # because they decided to pull codecov the package from PyPI... - sed -i '/codecov/d' core/requirements_test.txt - pip install -r core/requirements_test.txt --use-pep517 - pip install -e core/ --use-pep517 - pip install ulid-transform # this is in Adaptive-lighting's manifest.json - pip install $(python test_dependencies.py) --use-pep517 + ./scripts/install_ha diff --git a/Dockerfile b/Dockerfile index 916d1021..d3c5c79b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ # Optionally build the image yourself with: # docker build -t basnijholt/adaptive-lighting:latest . -FROM python:3.11-buster +FROM python:3.12-bookworm RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y \ @@ -16,12 +16,7 @@ RUN apt-get update && \ && rm -rf /var/lib/apt/lists/* # Clone home-assistant/core -RUN git clone --depth 1 https://github.com/home-assistant/core.git /core - -# Install home-assistant/core dependencies -RUN pip3 install -r /core/requirements.txt --use-pep517 && \ - pip3 install -r /core/requirements_test.txt --use-pep517 && \ - pip3 install -e /core/ --use-pep517 +RUN git clone --depth 1 --branch dev https://github.com/home-assistant/core.git /core # Copy the Adaptive Lighting repository COPY . /app/ @@ -32,8 +27,8 @@ RUN ln -s /app/custom_components/adaptive_lighting /core/homeassistant/component # For test_dependencies.py ln -s /core /app/core -# Install dependencies of components that Adaptive Lighting depends on -RUN pip3 install $(python3 /app/test_dependencies.py) --use-pep517 +# Install home-assistant/core dependencies +RUN /app/scripts/install_ha WORKDIR /core diff --git a/scripts/install_ha b/scripts/install_ha new file mode 100755 index 00000000..c38fe351 --- /dev/null +++ b/scripts/install_ha @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -ex +cd "$(dirname "$0")/.." + +pip install -r core/requirements.txt --use-pep517 + +if grep -q 'codecov' core/requirements_test.txt; then + # Older HA versions still have `codecov` in `requirements_test.txt` + # however it is removed from PyPI, so we cannot install it + sed -i '/codecov/d' core/requirements_test.txt +fi +pip install -r core/requirements_test.txt --use-pep517 + +pip install -e core/ --use-pep517 +pip install ulid-transform # this is in Adaptive-lighting's manifest.json +pip install $(python test_dependencies.py) --use-pep517 diff --git a/scripts/setup b/scripts/setup deleted file mode 100644 index 0688d70d..00000000 --- a/scripts/setup +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -python3 -m pip install --requirement requirements.txt -pre-commit install-hooks diff --git a/scripts/setup-devcontainer b/scripts/setup-devcontainer new file mode 100755 index 00000000..9571972b --- /dev/null +++ b/scripts/setup-devcontainer @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e +cd "$(dirname "$0")/.." + +# Clone only if the folder doesn't exist +if [[ ! -d "core" ]]; then + git clone --depth 1 --branch dev https://github.com/home-assistant/core.git +fi + +./scripts/install_ha +pre-commit install-hooks From a38865644814b7c634cbec0884626324a8b1b01c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 6 Apr 2024 11:31:30 +0200 Subject: [PATCH 0755/1077] Remove `--use-pep517` from `pip install` and change Python version in devcontainer (#963) * Remove --use-pep517 from pip install * Update Python version of devcontainer --- .devcontainer.json | 2 +- scripts/install_ha | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.devcontainer.json b/.devcontainer.json index 14778469..9b9ba58b 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -1,6 +1,6 @@ { "name": "basnijholt/adaptive_lighting", - "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.11-bullseye", + "image": "mcr.microsoft.com/vscode/devcontainers/python:3.12", "postCreateCommand": "scripts/setup-devcontainer", "forwardPorts": [ 8123 diff --git a/scripts/install_ha b/scripts/install_ha index c38fe351..efe0bd25 100755 --- a/scripts/install_ha +++ b/scripts/install_ha @@ -2,15 +2,15 @@ set -ex cd "$(dirname "$0")/.." -pip install -r core/requirements.txt --use-pep517 +pip install -r core/requirements.txt if grep -q 'codecov' core/requirements_test.txt; then # Older HA versions still have `codecov` in `requirements_test.txt` # however it is removed from PyPI, so we cannot install it sed -i '/codecov/d' core/requirements_test.txt fi -pip install -r core/requirements_test.txt --use-pep517 +pip install -r core/requirements_test.txt -pip install -e core/ --use-pep517 +pip install -e core/ pip install ulid-transform # this is in Adaptive-lighting's manifest.json -pip install $(python test_dependencies.py) --use-pep517 +pip install $(python test_dependencies.py) From 14bb0b4c5f7550538751e3a604bc2d5dbbd7dd35 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 6 Apr 2024 04:04:46 -0700 Subject: [PATCH 0756/1077] docs: add scuricvladimir as a contributor for translation (#964) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 78faf634..d8769f64 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -792,6 +792,15 @@ "contributions": [ "code" ] + }, + { + "login": "scuricvladimir", + "name": "scuricvladimir", + "avatar_url": "https://avatars.githubusercontent.com/u/46634162?v=4", + "profile": "https://github.com/scuricvladimir", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index b4c8560c..19e82ad8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-86-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-87-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -576,6 +576,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 7eb2d00ab9c6eddf613d9de424505adaaf2a4440 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 6 Apr 2024 04:05:28 -0700 Subject: [PATCH 0757/1077] docs: add Welsyntoffie as a contributor for translation (#965) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index d8769f64..18da4e3a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -801,6 +801,15 @@ "contributions": [ "translation" ] + }, + { + "login": "Welsyntoffie", + "name": "Pieter", + "avatar_url": "https://avatars.githubusercontent.com/u/47089904?v=4", + "profile": "https://github.com/Welsyntoffie", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 19e82ad8..6fa47a25 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-87-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-88-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -577,6 +577,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 6dae5641a2613c991aca3455d8deaa740991f986 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 6 Apr 2024 04:05:48 -0700 Subject: [PATCH 0758/1077] docs: add san80068259 as a contributor for translation (#966) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 18da4e3a..a6bf2e8c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -810,6 +810,15 @@ "contributions": [ "translation" ] + }, + { + "login": "san80068259", + "name": "san80068259", + "avatar_url": "https://avatars.githubusercontent.com/u/68324107?v=4", + "profile": "https://github.com/san80068259", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 6fa47a25..3044445d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-88-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-89-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -578,6 +578,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From a36b0c343099bf47aa813ae8d9be1354d06d2adb Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sat, 6 Apr 2024 13:06:17 +0200 Subject: [PATCH 0759/1077] Translations update from Hosted Weblate (#946) * Translated using Weblate (Croatian) Currently translated at 44.4% (68 of 153 strings) Added translation using Weblate (Croatian) Co-authored-by: Hosted Weblate Co-authored-by: scuricvladimir Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/hr/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Afrikaans) Currently translated at 49.0% (75 of 153 strings) Added translation using Weblate (Afrikaans) Co-authored-by: Hosted Weblate Co-authored-by: Pieter Bezuidenhout Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/af/ Translation: Adaptive Lighting/Adaptive Lighting * Added translation using Weblate (Chinese (Traditional)) Co-authored-by: Hosted Weblate Co-authored-by: chris lin --------- Co-authored-by: scuricvladimir Co-authored-by: Pieter Bezuidenhout Co-authored-by: chris lin --- .../adaptive_lighting/translations/af.json | 40 +++++++++++++++++++ .../adaptive_lighting/translations/hr.json | 19 +++++++++ .../translations/zh_Hant.json | 1 + 3 files changed, 60 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/af.json create mode 100644 custom_components/adaptive_lighting/translations/hr.json create mode 100644 custom_components/adaptive_lighting/translations/zh_Hant.json diff --git a/custom_components/adaptive_lighting/translations/af.json b/custom_components/adaptive_lighting/translations/af.json new file mode 100644 index 00000000..4a0cd87a --- /dev/null +++ b/custom_components/adaptive_lighting/translations/af.json @@ -0,0 +1,40 @@ +{ + "services": { + "apply": { + "description": "Pas die huidige Adaptive Lighting-instellings op ligte toe.", + "fields": { + "lights": { + "description": "'n Lig (of lys van ligte) om die instellings op toe te pas. 💡" + } + } + }, + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Pas ligte net aan wanneer hulle aangeskakel is (`true`) of hou aan om dit aan te pas (`false`)" + }, + "sunrise_offset": { + "description": "Pas sonsopkomstyd aan met 'n positiewe of negatiewe afwyking in sekondes. ⏰" + }, + "sunset_offset": { + "description": "Pas sonsondergangtyd aan met 'n positiewe of negatiewe afwyking in sekondes. ⏰" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Aanpasbare beligting opsies", + "data": { + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Wanneer ligte aanvanklik aangeskakel word. As dit op \"true\" gestel is, pas AL slegs aan as \"light.turn_on\" opgeroep word sonder om kleur of helderheid te spesifiseer. ❌🌈 Dit verhoed bv. aanpassing wanneer 'n toneel geaktiveer word. As `onwaar`, pas AL aan ongeag die teenwoordigheid van kleur of helderheid in die aanvanklike `diens_data`. Moet `oorname_beheer` geaktiveer moet word. 🕵️ " + }, + "data_description": { + "sunrise_offset": "Pas sonsopkomstyd aan met 'n positiewe of negatiewe afwyking in sekondes. ⏰", + "sunset_offset": "Pas sonsondergangtyd aan met 'n positiewe of negatiewe afwyking in sekondes. ⏰" + } + } + } + }, + "title": "Aanpasbare beligting" +} diff --git a/custom_components/adaptive_lighting/translations/hr.json b/custom_components/adaptive_lighting/translations/hr.json new file mode 100644 index 00000000..7347cda1 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/hr.json @@ -0,0 +1,19 @@ +{ + "options": { + "step": { + "init": { + "title": "Opcije prilagodljivog osvjetljenja" + } + } + }, + "title": "prilagodi svjetlinu", + "services": { + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Prilagodi svjetla samo kada su uključena (true) ili ih neprestano prilagođavaj (false). 🔄" + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/zh_Hant.json b/custom_components/adaptive_lighting/translations/zh_Hant.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/zh_Hant.json @@ -0,0 +1 @@ +{} From 47cbda5c5e484a203328184f2d871b1722cd2107 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 6 Apr 2024 19:05:18 +0200 Subject: [PATCH 0760/1077] Add versions to matrix and fix area test (#967) --- .github/workflows/pytest.yaml | 4 +++ test_dependencies.py | 1 + tests/test_switch.py | 61 ++++++++++++++++++++++++++++++++--- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 750fba04..59a46c1c 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -46,6 +46,10 @@ jobs: core-version: "2024.1.6" - python-version: "3.11" core-version: "2024.2.5" + - python-version: "3.12" + core-version: "2024.3.3" + - python-version: "3.12" + core-version: "2024.4.1" - python-version: "3.12" core-version: "dev" steps: diff --git a/test_dependencies.py b/test_dependencies.py index 96377cee..b92ff1fb 100644 --- a/test_dependencies.py +++ b/test_dependencies.py @@ -39,5 +39,6 @@ required = [ "components.ffmpeg", # needed since 2024.1 ] to_install = [package for r in required for package in deps[r]] +to_install.append("flaky") print(" ".join(to_install)) # noqa: T201 diff --git a/tests/test_switch.py b/tests/test_switch.py index 9c2f254f..a6c4a2a8 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -5,6 +5,7 @@ import asyncio import contextlib import datetime import logging +from collections import OrderedDict from copy import deepcopy from random import randint from typing import Any @@ -15,6 +16,7 @@ import homeassistant.util.dt as dt_util import pytest import ulid_transform import voluptuous.error +from flaky import flaky from homeassistant.components.adaptive_lighting.adaptation_utils import ( AdaptationData, _create_service_call_data_iterator, @@ -96,13 +98,15 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) +from homeassistant.const import __version__ as ha_version from homeassistant.core import Context, Event, HomeAssistant, State +from homeassistant.helpers import area_registry as ar from homeassistant.helpers import entity_registry from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.setup import async_setup_component from homeassistant.util.color import color_temperature_mired_to_kelvin -from tests.common import MockConfigEntry, mock_area_registry +from tests.common import MockConfigEntry _LOGGER = logging.getLogger(__name__) @@ -774,6 +778,7 @@ async def test_manual_control( assert not manual_control[ENTITY_LIGHT_1] +@flaky(max_runs=3, min_passes=1) async def test_auto_reset_manual_control(hass): switch, (light, *_) = await setup_lights_and_switch( hass, @@ -1328,11 +1333,57 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands): assert sleep_color_temp != color_temp +# Vendored in this function as it was broken +# https://github.com/home-assistant/core/pull/112150 (my PR and reported issue) +# Then removed: https://github.com/home-assistant/core/pull/112172 +# Then re-added: https://github.com/home-assistant/core/pull/113453 +# This version is no longer the same as the one in HA because of the many changes +# that have been made in 2024. +def mock_area_registry( + hass: HomeAssistant, +) -> ar.AreaRegistry: + """Mock the Area Registry.""" + registry = ar.AreaRegistry(hass) + registry._area_data = {} + area_kwargs = { + "name": "Test Area", + "normalized_name": "test-area", + "id": "test-area", + "picture": None, + } + year, month = (int(x) for x in ha_version.split(".")[:2]) + dt = datetime.date(year, month, 1) + if dt >= datetime.date(2023, 1, 1): + area_kwargs["aliases"] = {} + if dt >= datetime.date(2024, 2, 1): + area_kwargs["icon"] = None + if dt >= datetime.date(2024, 3, 1): + area_kwargs["floor_id"] = "test-floor" + + # This mess... 🤯 + if dt >= datetime.date(2024, 2, 1) and dt != datetime.date(2024, 4, 1): + # 2024.4 removed AreaRegistryItems and then added it back in 2024.5: + # https://github.com/home-assistant/core/pull/114777 + registry.areas = ar.AreaRegistryItems() + elif dt == datetime.date(2024, 4, 1): + from homeassistant.helpers.normalized_name_base_registry import ( + NormalizedNameBaseRegistryItems, + ) + + registry.areas = NormalizedNameBaseRegistryItems() + else: + registry.areas = OrderedDict() + + area = ar.AreaEntry(**area_kwargs) + registry.areas[area.id] = area + hass.data[ar.DATA_REGISTRY] = registry + return registry + + async def test_light_switch_in_specific_area(hass): switch, (light, *_) = await setup_lights_and_switch(hass) - area_registry = mock_area_registry(hass) - area_registry.async_create("test_area") + mock_area_registry(hass) entity = entity_registry.async_get(hass).async_get_or_create( LIGHT_DOMAIN, @@ -1341,9 +1392,9 @@ async def test_light_switch_in_specific_area(hass): ) entity = entity_registry.async_get(hass).async_update_entity( entity.entity_id, - area_id="test_area", + area_id="test-area", ) - _LOGGER.debug("test_area entity: %s", entity) + _LOGGER.debug("test-area entity: %s", entity) await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, From d23f6ec63a713e9428328023bd405fcf2b7c4fd2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 7 Apr 2024 14:01:05 +0200 Subject: [PATCH 0761/1077] Fix `test_proactive_adaptation_with_separate_commands` (#970) * Add link script * Set run_immediately=False * Add await hass.async_block_till_done() * Use scripts/link * Rename scripts * Links in devcontainer * Install from setup script --- .../workflows/install_dependencies/action.yml | 3 ++- .github/workflows/pytest.yaml | 24 ------------------- .github/workflows/update-readme.yml | 5 ---- Dockerfile | 7 ++---- custom_components/adaptive_lighting/switch.py | 3 +++ requirements.txt | 10 -------- scripts/{install_ha => setup-dependencies} | 0 scripts/setup-devcontainer | 9 ++++++- scripts/setup-symlinks | 13 ++++++++++ tests/test_switch.py | 1 + 10 files changed, 29 insertions(+), 46 deletions(-) delete mode 100644 requirements.txt rename scripts/{install_ha => setup-dependencies} (100%) create mode 100755 scripts/setup-symlinks diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index 16c0a6ec..badc6566 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -34,4 +34,5 @@ runs: - name: Install dependencies shell: bash run: | - ./scripts/install_ha + ./scripts/setup-dependencies + ./scripts/setup-symlinks diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 59a46c1c..ed5de50c 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -62,30 +62,6 @@ jobs: python-version: ${{ matrix.python-version }} core-version: ${{ matrix.core-version }} - - name: Click here for troubleshooting steps if tests break again. - run: | - echo "::notice::### If tests fail, try these debug steps: ###" - echo "::notice::### 1. Replace '-qq' from .github/workflow/pytest.yaml. with '-v' for extra verbosity. ###" - echo "::notice::### 2. Push or run action again. ###" - echo "::notice::### 3. Check for any log messages in github actions resembling the following using CTRL+F ### - echo "::notice::### 4. ERROR:homeassistant.setup:Setup failed for 'component': Unable to import component: No module named ''module'' ###" - echo "::notice::### 5. add 'component'.'module' (without the '') from the above log into the 'required' list inside of 'test_dependencies.py' ###" - echo "::notice::### 6. Try again! If more issues persist they should be easily solvable by reading the verbose logs now. ###" - - - name: Link custom_components/adaptive_lighting - run: | - cd core - - # Link homeassitant.components.adaptive_lighting - cd homeassistant/components - ln -fs ../../../custom_components/adaptive_lighting adaptive_lighting - cd - - - # Link adaptive_lighting tests - cd tests/components/ - ln -fs ../../../tests adaptive_lighting - cd - - - name: Run pytest timeout-minutes: 60 run: | diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index 74f5e242..bf85fce3 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -26,11 +26,6 @@ jobs: run: | pip install markdown-code-runner==1.0.0 pandas tabulate - - name: Link custom_components/adaptive_lighting - run: | - cd core/homeassistant/components - ln -fs ../../../custom_components/adaptive_lighting adaptive_lighting - - name: Run markdown-code-runner run: markdown-code-runner --debug README.md diff --git a/Dockerfile b/Dockerfile index d3c5c79b..5232b4d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,13 +22,10 @@ RUN git clone --depth 1 --branch dev https://github.com/home-assistant/core.git COPY . /app/ # Setup symlinks in core -RUN ln -s /app/custom_components/adaptive_lighting /core/homeassistant/components/adaptive_lighting && \ - ln -s /app/tests /core/tests/components/adaptive_lighting && \ - # For test_dependencies.py - ln -s /core /app/core +RUN ln -s /core /app/core && /app/scripts/setup-symlinks # Install home-assistant/core dependencies -RUN /app/scripts/install_ha +RUN /app/scripts/setup-dependencies WORKDIR /core diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 23ca14f9..9408d90b 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -952,6 +952,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STARTED, self._setup_listeners, + run_immediately=False, ) last_state: State | None = await self.async_get_last_state() is_new_entry = last_state is None # newly added to HA @@ -1658,10 +1659,12 @@ class AdaptiveLightingManager: self.hass.bus.async_listen( EVENT_CALL_SERVICE, self.turn_on_off_event_listener, + run_immediately=False, ), self.hass.bus.async_listen( EVENT_STATE_CHANGED, self.state_changed_event_listener, + run_immediately=False, ), ] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index c560a858..00000000 --- a/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -colorlog==6.7.0 -pip>=21.0,<23.2 -ruff==0.0.265 -pre-commit - -# Install HA and test dependencies (pytest, coverage) -# To pin the dev container to a specific HA version, set this dependency -# to the adequate version (add `==`) and rebuild the dev container. -# See https://github.com/MatthewFlamm/pytest-homeassistant-custom-component/releases for version mappings. -pytest-homeassistant-custom-component diff --git a/scripts/install_ha b/scripts/setup-dependencies similarity index 100% rename from scripts/install_ha rename to scripts/setup-dependencies diff --git a/scripts/setup-devcontainer b/scripts/setup-devcontainer index 9571972b..563d26ef 100755 --- a/scripts/setup-devcontainer +++ b/scripts/setup-devcontainer @@ -7,5 +7,12 @@ if [[ ! -d "core" ]]; then git clone --depth 1 --branch dev https://github.com/home-assistant/core.git fi -./scripts/install_ha +pip install \ + colorlog==6.7.0 \ + pip>=21.0,<23.2 \ + ruff==0.0.265 \ + pre-commit + +./scripts/setup-dependencies +./scripts/setup-symlinks pre-commit install-hooks diff --git a/scripts/setup-symlinks b/scripts/setup-symlinks new file mode 100755 index 00000000..91026b3a --- /dev/null +++ b/scripts/setup-symlinks @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -ex +cd "$(dirname "$0")/.." + +# Link custom components +cd core/homeassistant/components/ +ln -fs ../../../custom_components/adaptive_lighting adaptive_lighting +cd - + +# Link tests +cd core/tests/components/ +ln -fs ../../../tests/ adaptive_lighting +cd - diff --git a/tests/test_switch.py b/tests/test_switch.py index a6c4a2a8..e582a031 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1684,6 +1684,7 @@ async def test_proactive_adaptation_transition_override(hass): {ATTR_ENTITY_ID: ENTITY_LIGHT_3, ATTR_TRANSITION: 456}, blocking=True, ) + await hass.async_block_till_done() # Assert that default is used when no transition is specified in service call assert patched_async_turn_on.call_args_list, patched_async_turn_on.call_args_list From d427b18b483786b524d64bef762d2a223b8a1813 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 7 Apr 2024 14:22:22 +0200 Subject: [PATCH 0762/1077] [pre-commit.ci] pre-commit autoupdate (#935) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.2.1 → v0.3.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.1...v0.3.5) - [github.com/psf/black: 24.2.0 → 24.3.0](https://github.com/psf/black/compare/24.2.0...24.3.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index acb6dd76..9a7a0a9e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,11 +8,11 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.1 + rev: v0.3.5 hooks: - id: ruff args: ["--fix"] - repo: https://github.com/psf/black - rev: 24.2.0 + rev: 24.3.0 hooks: - id: black From edf04271315a66199d02590082e3c8f21cd3095e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 7 Apr 2024 14:25:47 +0200 Subject: [PATCH 0763/1077] Update version in manifest.json (1.21.0) (#971) --- custom_components/adaptive_lighting/manifest.json | 2 +- scripts/setup-devcontainer | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 7461e73d..96cec4ac 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.20.0" + "version": "1.21.0" } diff --git a/scripts/setup-devcontainer b/scripts/setup-devcontainer index 563d26ef..11fdd106 100755 --- a/scripts/setup-devcontainer +++ b/scripts/setup-devcontainer @@ -10,8 +10,7 @@ fi pip install \ colorlog==6.7.0 \ pip>=21.0,<23.2 \ - ruff==0.0.265 \ - pre-commit + ruff==0.0.265 ./scripts/setup-dependencies ./scripts/setup-symlinks From 1a2cd7399d99a8dece62d6ad1c99cc7a5448c855 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 9 Apr 2024 08:59:18 +0200 Subject: [PATCH 0764/1077] Fix for HA <= 2024.3, closes #973 (#974) * Fix for HA <= 2024.3, closes #973 * Link --- custom_components/adaptive_lighting/manifest.json | 2 +- custom_components/adaptive_lighting/switch.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 96cec4ac..2ec04e27 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.21.0" + "version": "1.21.1" } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 9408d90b..2b23c71c 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -60,6 +60,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) +from homeassistant.const import __version__ as ha_version from homeassistant.core import ( CALLBACK_TYPE, Context, @@ -949,10 +950,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self.hass.is_running: await self._setup_listeners() else: + kw = {} + year, month = (int(x) for x in ha_version.split(".")[:2]) + if (year, month) >= (2024, 4): + # Added in https://github.com/home-assistant/core/pull/113020 + kw["run_immediately"] = False self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STARTED, self._setup_listeners, - run_immediately=False, + **kw, ) last_state: State | None = await self.async_get_last_state() is_new_entry = last_state is None # newly added to HA From b746e5f19ccc34346f34cd97f54197e7c534ded9 Mon Sep 17 00:00:00 2001 From: Frosh Date: Sun, 12 May 2024 20:32:46 +0200 Subject: [PATCH 0765/1077] Remove deprecated 'run_immediately' (#991) --- custom_components/adaptive_lighting/switch.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 2b23c71c..23ca14f9 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -60,7 +60,6 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.const import __version__ as ha_version from homeassistant.core import ( CALLBACK_TYPE, Context, @@ -950,15 +949,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self.hass.is_running: await self._setup_listeners() else: - kw = {} - year, month = (int(x) for x in ha_version.split(".")[:2]) - if (year, month) >= (2024, 4): - # Added in https://github.com/home-assistant/core/pull/113020 - kw["run_immediately"] = False self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STARTED, self._setup_listeners, - **kw, ) last_state: State | None = await self.async_get_last_state() is_new_entry = last_state is None # newly added to HA @@ -1665,12 +1658,10 @@ class AdaptiveLightingManager: self.hass.bus.async_listen( EVENT_CALL_SERVICE, self.turn_on_off_event_listener, - run_immediately=False, ), self.hass.bus.async_listen( EVENT_STATE_CHANGED, self.state_changed_event_listener, - run_immediately=False, ), ] From 82f2b8f82edfb5805a5f86ed27159d0616f7f514 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 12 May 2024 11:33:39 -0700 Subject: [PATCH 0766/1077] docs: add erdnaxela02 as a contributor for code (#992) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index a6bf2e8c..efaf4bcd 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -819,6 +819,15 @@ "contributions": [ "translation" ] + }, + { + "login": "erdnaxela02", + "name": "Frosh", + "avatar_url": "https://avatars.githubusercontent.com/u/21007415?v=4", + "profile": "https://github.com/erdnaxela02", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 3044445d..a22769fb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-89-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-90-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -579,6 +579,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 514bb4de2ad945297084d28ac26be624414aa4c9 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Mon, 13 May 2024 17:49:43 +0200 Subject: [PATCH 0767/1077] Translations update from Hosted Weblate (#981) * Added translation using Weblate (Romanian) Co-authored-by: Vlad Radu * Translated using Weblate (Portuguese) Currently translated at 56.8% (87 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Rafael Miranda Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pt/ Translation: Adaptive Lighting/Adaptive Lighting --------- Co-authored-by: Vlad Radu Co-authored-by: Rafael Miranda --- custom_components/adaptive_lighting/translations/pt.json | 2 +- custom_components/adaptive_lighting/translations/ro.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 custom_components/adaptive_lighting/translations/ro.json diff --git a/custom_components/adaptive_lighting/translations/pt.json b/custom_components/adaptive_lighting/translations/pt.json index 11184a20..bc4c82d2 100644 --- a/custom_components/adaptive_lighting/translations/pt.json +++ b/custom_components/adaptive_lighting/translations/pt.json @@ -7,7 +7,7 @@ "description": "Ajustar a hora do nascer do sol com um offset positivo ou negativo em segundos. ⏰" }, "only_once": { - "description": "Adaptar as luzes apenas quando estão ligadas (`true`) ou continuar a adaptá-las (`false`)." + "description": "Adaptar as luzes apenas quando estas estão ligadas (`true`) ou continuar a adaptá-las (`false`)." }, "sunset_offset": { "description": "Ajustar a hora do pôr do sol com um offset positivo ou negativo em segundos. ⏰" diff --git a/custom_components/adaptive_lighting/translations/ro.json b/custom_components/adaptive_lighting/translations/ro.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/ro.json @@ -0,0 +1 @@ +{} From 470b245c6ec59444fb50df8a08d19ba31a26e30e Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 08:50:07 -0700 Subject: [PATCH 0768/1077] docs: add rafaeltmiranda as a contributor for translation (#993) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index efaf4bcd..e0347277 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -828,6 +828,15 @@ "contributions": [ "code" ] + }, + { + "login": "rafaeltmiranda", + "name": "Rafael Miranda", + "avatar_url": "https://avatars.githubusercontent.com/u/47206949?v=4", + "profile": "https://www.linkedin.com/in/rafaeltmiranda/", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index a22769fb..2ef03c14 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-90-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-91-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -580,6 +580,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From cb289a0548ab065ff63ca4e1e8543fd727f50e92 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 08:50:34 -0700 Subject: [PATCH 0769/1077] docs: add rVlad93 as a contributor for translation (#994) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index e0347277..8d713542 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -837,6 +837,15 @@ "contributions": [ "translation" ] + }, + { + "login": "rVlad93", + "name": "rVlad93", + "avatar_url": "https://avatars.githubusercontent.com/u/60452666?v=4", + "profile": "https://github.com/rVlad93", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 2ef03c14..d11d2734 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-91-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-92-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -582,6 +582,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + + + From b1f8df6f142c83620beb282c2d67ee076bc55b19 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 13 May 2024 08:51:23 -0700 Subject: [PATCH 0770/1077] Bump to 1.21.2 in manifest.json (#995) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 2ec04e27..de44df73 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.21.1" + "version": "1.21.2" } From 01f639f5bb877d3afae4846e7d895808fc1a635e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 15 May 2024 09:02:37 -0700 Subject: [PATCH 0771/1077] Use `after_dependencies` to fix #950 (#999) * Use `after_dependencies` to fix #950 * Sort manifest.json * sort --- custom_components/adaptive_lighting/manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index de44df73..37d77e4a 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -1,6 +1,7 @@ { "domain": "adaptive_lighting", "name": "Adaptive Lighting", + "after_dependencies": ["light"], "codeowners": ["@basnijholt", "@RubenKelevra", "@th3w1zard1", "@protyposis"], "config_flow": true, "dependencies": [], @@ -8,5 +9,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.21.2" + "version": "1.21.3" } From 618e2ccf4a966f52725e62cbaf3fb5097a4fa1ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ebbinghaus?= Date: Wed, 15 May 2024 18:07:59 +0200 Subject: [PATCH 0772/1077] Add service to group switches (#998) --- custom_components/adaptive_lighting/switch.py | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 23ca14f9..b4af97ed 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -54,6 +54,8 @@ from homeassistant.const import ( EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_STARTED, EVENT_STATE_CHANGED, + MAJOR_VERSION, + MINOR_VERSION, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -70,6 +72,12 @@ from homeassistant.core import ( callback, ) from homeassistant.helpers import entity_platform, entity_registry + +if [MAJOR_VERSION, MINOR_VERSION] < [2023, 9]: + from homeassistant.helpers.entity import DeviceInfo +else: + from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.event import ( async_track_state_change_event, async_track_time_interval, @@ -944,6 +952,17 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Return true if adaptive lighting is on.""" return self._state + @property + def device_info(self) -> DeviceInfo: + """Return the device info, used to group this and adjacent entities in the UI.""" + return DeviceInfo( + identifiers={ + (DOMAIN, self._name), + }, + name=self._name, + entry_type=DeviceEntryType.SERVICE, + ) + async def async_added_to_hass(self) -> None: """Call when entity about to be added to hass.""" if self.hass.is_running: @@ -1563,9 +1582,9 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): self._icon = icon self._state: bool | None = None self._which = which - name = data[CONF_NAME] - self._unique_id = f"{name}_{slugify(self._which)}" - self._name = f"Adaptive Lighting {which}: {name}" + self._config_name = data[CONF_NAME] + self._unique_id = f"{self._config_name}_{slugify(self._which)}" + self._name = f"Adaptive Lighting {which}: {self._config_name}" self._initial_state = initial_state @property @@ -1588,6 +1607,17 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): """Return true if adaptive lighting is on.""" return self._state + @property + def device_info(self) -> DeviceInfo: + """Return the device info, used to group this and adjacent entities in the UI.""" + return DeviceInfo( + identifiers={ + (DOMAIN, self._config_name), + }, + name=f"Adaptive Lighting: {self._config_name}", + entry_type=DeviceEntryType.SERVICE, + ) + async def async_added_to_hass(self) -> None: """Call when entity about to be added to hass.""" last_state = await self.async_get_last_state() From 18d0186e33b0a5903653ba4eb818ce5beae1139f Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 09:08:49 -0700 Subject: [PATCH 0773/1077] docs: add MrEbbinghaus as a contributor for code (#1000) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 8d713542..a1412330 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -846,6 +846,15 @@ "contributions": [ "translation" ] + }, + { + "login": "MrEbbinghaus", + "name": "Björn Ebbinghaus", + "avatar_url": "https://avatars.githubusercontent.com/u/2965273?v=4", + "profile": "https://blog.ebbinghaus.me/", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index d11d2734..3093391f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-92-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-93-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -584,6 +584,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 36115327da1646beaa54c9ec5ef739346d5cc87f Mon Sep 17 00:00:00 2001 From: Marck <18088281+Marck@users.noreply.github.com> Date: Mon, 20 May 2024 17:14:11 +0200 Subject: [PATCH 0774/1077] Added light dependencies (#1003) --- custom_components/adaptive_lighting/manifest.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 37d77e4a..accd3ff0 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -1,10 +1,9 @@ { "domain": "adaptive_lighting", "name": "Adaptive Lighting", - "after_dependencies": ["light"], "codeowners": ["@basnijholt", "@RubenKelevra", "@th3w1zard1", "@protyposis"], "config_flow": true, - "dependencies": [], + "dependencies": ["light"], "documentation": "https://github.com/basnijholt/adaptive-lighting#readme", "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", From a0476632a50e85e369f95dffe474ec83425f0467 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 20 May 2024 08:15:00 -0700 Subject: [PATCH 0775/1077] Bump to 1.21.4 --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index accd3ff0..08e7a20c 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.21.3" + "version": "1.21.4" } From 5a5529962858e84847e6702e2a352769e94804e9 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 20 May 2024 08:17:41 -0700 Subject: [PATCH 0776/1077] Bump to 1.22.0 (#1004) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 08e7a20c..6d8c0f5f 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.21.4" + "version": "1.22.0" } From 5aa763fef3c08c7ebbf2408df52b47f8bef8b26c Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 08:23:12 -0700 Subject: [PATCH 0777/1077] docs: add Marck as a contributor for code (#1005) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index a1412330..1f6e9028 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -855,6 +855,15 @@ "contributions": [ "code" ] + }, + { + "login": "Marck", + "name": "Marck", + "avatar_url": "https://avatars.githubusercontent.com/u/18088281?v=4", + "profile": "https://github.com/Marck", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 3093391f..02adcde9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-93-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-94-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -585,6 +585,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 7987a4a825e8cd7724fa3708dacc0d0439ba2ba7 Mon Sep 17 00:00:00 2001 From: Lucho Gizdov Date: Thu, 6 Jun 2024 18:15:25 +0300 Subject: [PATCH 0778/1077] Add Translations file for Bulgarian (#987) --- .../adaptive_lighting/translations/bg.json | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 custom_components/adaptive_lighting/translations/bg.json diff --git a/custom_components/adaptive_lighting/translations/bg.json b/custom_components/adaptive_lighting/translations/bg.json new file mode 100644 index 00000000..b88bbaae --- /dev/null +++ b/custom_components/adaptive_lighting/translations/bg.json @@ -0,0 +1,269 @@ +{ + "title": "Адаптивно осветление", + "config": { + "step": { + "user": { + "title": "Изберете име за инстанцията на Адаптивно осветление", + "description": "Всяка инстанция може да съдържа множество лампи!", + "data": { + "name": "Име" + } + } + }, + "abort": { + "already_configured": "Това устройство е вече конфигурирано" + } + }, + "options": { + "step": { + "init": { + "title": "Настройки на Адаптивно осветление", + "description": "Конфигурирайте компонент за Адаптивно осветление. Имената на опциите съвпадат с настройките на YAML. Ако сте дефинирали този запис в YAML, няма да се появят опции тук. За интерактивни графики, които демонстрират ефектите на параметрите, посетете [това уеб приложение](https://basnijholt.github.io/adaptive-lighting). За повече подробности, вижте [официалната документация](https://github.com/basnijholt/adaptive-lighting#readme).", + "data": { + "lights": "lights: Списък от entity_ids на лампи за контрол (може да е празен). 🌟", + "interval": "интервал", + "transition": "преход", + "initial_transition": "начален преход", + "min_brightness": "min_brightness: Минимален процент на яркост. 💡", + "max_brightness": "max_brightness: Максимален процент на яркост. 💡", + "min_color_temp": "min_color_temp: Най-топла цветова температура в Келвини. 🔥", + "max_color_temp": "max_color_temp: Най-студена цветова температура в Келвини. ❄️", + "prefer_rgb_color": "prefer_rgb_color: Дали да се предпочита RGB цветова корекция пред температура на светлината, когато е възможно. 🌈", + "sleep_brightness": "яркост при сън", + "sleep_rgb_or_color_temp": "RGB или цветова температура при сън", + "sleep_color_temp": "цветова температура при сън", + "sleep_rgb_color": "RGB цвят при сън", + "sleep_transition": "преход при сън", + "transition_until_sleep": "transition_until_sleep: Когато е активирано, Adaptive Lighting ще третира настройките за сън като минимум, преминавайки към тези стойности след залез. 🌙", + "sunrise_time": "време на изгрев", + "min_sunrise_time": "минимално време на изгрев", + "max_sunrise_time": "максимално време на изгрев", + "sunrise_offset": "отместване на изгрева", + "sunset_time": "време на залез", + "min_sunset_time": "минимално време на залез", + "max_sunset_time": "максимално време на залез", + "sunset_offset": "отместване на залеза", + "brightness_mode": "режим на яркост", + "brightness_mode_time_dark": "време на режим на яркост при тъмно", + "brightness_mode_time_light": "време на режим на яркост при светло", + "take_over_control": "take_over_control: Деактивира Adaptive Lighting, ако друг източник извика \"light.turn_on\", докато лампите са включени и се адаптират. Имайте предвид, че това извиква \"homeassistant.update_entity\" на всеки \"interval\"! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Открива и спира адаптации за промени в състоянието, които не са \"light.turn_on\". Изисква \"take_over_control\" активиран. 🕵️ Внимание: ⚠️ Някои лампиможе лъжливо да указват 'включено' състояние, което може да доведе до неочаквано включване на лампите. Деактивирайте тази функция, ако се сблъскате с такива проблеми.", + "autoreset_control_seconds": "секунди за автоматично нулиране на контрола", + "only_once": "only_once: Адаптира лампите само когато са включени (\"true\") или продължава да ги адаптира (\"false\"). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: При първоначално включване на лампите. Ако е зададено на \"true\", Адаптивно Осветление се адаптира само ако е извикано \"light.turn_on\" без указване на цвят или яркост. ❌🌈 Това например предотвратява адаптация при активиране на сцена. Ако е \"false\", Адаптивно Осветление се адаптира независимо от наличието на цвят или яркост в първоначалните \"service_data\". Изисква \"take_over_control\" активиран. 🕵️ ", + "separate_turn_on_commands": "separate_turn_on_commands: Използва отделни \"light.turn_on\" команди за цвят и яркост, необходими за някои типове светлини. 🔀", + "send_split_delay": "забавяне при изпращане на разделени", + "adapt_delay": "забавяне при адаптация", + "skip_redundant_commands": "skip_redundant_commands: Пропуска изпращането на команди за адаптация, чиято целева състояние вече е равно на известното състояние на светлината. Минимизира мрежовия трафик и подобрява отговорността на адаптацията в някои ситуации. 📉Деактивирайте, ако физическите състояния на лампите се разминават с записаното състояние на HA.", + "intercept": "intercept: Прихваща и адаптира \"light.turn_on\" повиквания, позволявайки моментална адаптация на цвета и яркостта. 🏎️ Деактивирайте за светлини, които не поддържат \"light.turn_on\" с цвят и яркост.", + "multi_light_intercept": "multi_light_intercept: Прихваща и адаптира \"light.turn_on\" повиквания, които целят множество светлини. ➗⚠️ Това може да доведе до разделяне на едно \"light.turn_on\" повикване на множество повиквания, например когато лампите са в различни превключватели. Изисква \"intercept\" да бъде активиран.", + "include_config_in_attributes": "include_config_in_attributes: Показва всички опции като атрибути на ключа в Home Assistant, когато е зададено на \"true\". 📝" + }, + "data_description": { + "interval": "Честота за адаптиране на лампите, в секунди. 🔄", + "transition": "Продължителност на прехода, когато лампите се променят, в секунди. 🕑", + "initial_transition": "Продължителност на първия преход, когато лампите преминават от \"off\" на \"on\" в секунди. ⏲️", + "sleep_brightness": "Процент на яркостта на лампите в режим на сън. 😴", + "sleep_rgb_or_color_temp": "Използвайте или \"\"rgb_color\"\" или \"\"color_temp\"\" в режим на сън. 🌙", + "sleep_color_temp": "Цветова температура в режим на сън (използва се, когато \"sleep_rgb_or_color_temp\" е \"color_temp\") в Келвин. 😴", + "sleep_rgb_color": "RGB цвят в режим на сън (използва се, когато \"sleep_rgb_or_color_temp\" е \"rgb_color\"). 🌈", + "sleep_transition": "Продължителност на прехода, когато се превключва \"режим на сън\" в секунди. 😴", + "sunrise_time": "Задайте фиксирано време (HH:MM:SS) за изгрев. 🌅", + "min_sunrise_time": "Задайте най-ранното виртуално време за изгрев (HH:MM:SS), позволяващо по-късни изгреви. 🌅", + "max_sunrise_time": "Задайте най-късното виртуално време за изгрев (HH:MM:SS), позволяващо по-ранни изгреви. 🌅", + "sunrise_offset": "Регулирайте времето на изгрев с положителен или отрицателен отместване в секунди. ⏰", + "sunset_time": "Задайте фиксирано време (HH:MM:SS) за залез. 🌇", + "min_sunset_time": "Задайте най-ранното виртуално време за залез (HH:MM:SS), позволяващо по-късни залези. 🌇", + "max_sunset_time": "Задайте най-късното виртуално време за залез (HH:MM:SS), позволяващо по-ранни залези. 🌇", + "sunset_offset": "Регулирайте времето на залез с положителен или отрицателен отместване в секунди. ⏰", + "brightness_mode": "Режим на яркост за използване. Възможни стойности са \"default\", \"linear\" и \"tanh\" (използва \"brightness_mode_time_dark\" и \"brightness_mode_time_light\"). 📈", + "brightness_mode_time_dark": "(Игнорира се, ако \"brightness_mode='default'\") Продължителност в секунди за увеличаване/намаляване на яркостта преди/след изгрев/залез. 📈📉", + "brightness_mode_time_light": "(Игнорира се, ако \"brightness_mode='default'\") Продължителност в секунди за увеличаване/намаляване на яркостта след/преди изгрев/залез. 📈📉.", + "autoreset_control_seconds": "Автоматично нулиране на ръчния контрол след определен брой секунди. Задайте на 0 за деактивиране. ⏲️", + "send_split_delay": "Забавяне (ms) между \"separate_turn_on_commands\" за светлини, които не поддържат едновременна настройка на яркост и цвят. ⏲️", + "adapt_delay": "Време за изчакване (секунди) между включване на светлината и прилагане на промени от Адаптивно Осветление. Може да помогне за избягване на трептене. ⏲️" + } + } + }, + "error": { + "option_error": "Невалидна опция", + "entity_missing": "Една или повече избрани entity-та на лампилипсват от Home Assistant" + } + }, + "services": { + "apply": { + "name": "приложи", + "description": "Прилага текущите настройки за Адаптивно осветление към лампите", + "fields": { + "entity_id": { + "description": "entity_id на ключа с настройките за прилагане. 📝", + "name": "entity_id" + }, + "lights": { + "description": "Светлина (или списък от светлини), към които да се приложат настройките. 💡", + "name": "lights" + }, + "transition": { + "description": "Продължителност на прехода, когато лампите се променят, в секунди. 🕑", + "name": "transition" + }, + "adapt_brightness": { + "description": "Дали да се адаптира яркостта на светлината. 🌞", + "name": "adapt_brightness" + }, + "adapt_color": { + "description": "Дали да се адаптира цветът на лампите, които го поддържат. 🌈", + "name": "adapt_color" + }, + "prefer_rgb_color": { + "description": "Дали да се предпочита RGB цветова корекция пред температура на светлината, когато е възможно. 🌈", + "name": "prefer_rgb_color" + }, + "turn_on_lights": { + "description": "Дали да се включат лампите, които в момента са изключени. 🔆", + "name": "turn_on_lights" + } + } + }, + "set_manual_control": { + "name": "set_manual_control", + "description": "Маркирай дали дадена светлина е с 'ръчно контролиранe'.", + "fields": { + "entity_id": { + "description": "entity_id на ключа, в който да се маркира или демаркира светлината като ръчно контролирана. 📝", + "name": "entity_id" + }, + "lights": { + "description": "entity_id(та) на лампите, ако не са посочени, всички лампи в ключа се избират. 💡", + "name": "lights" + }, + "manual_control": { + "description": "Дали да се добави (\"true\") или премахне (\"false\") светлината от списъка \"manual_control\". 🔒", + "name": "manual_control" + } + } + }, + "change_switch_settings": { + "name": "change_switch_settings", + "description": "Променете всички настройки, които искате в ключа. Всички опции тук са същите като в потока на конфигурацията.", + "fields": { + "entity_id": { + "description": "Entity ID на ключа. 📝", + "name": "entity_id" + }, + "use_defaults": { + "description": "Задава стойностите по подразбиране, които не са посочени в този обаждане на услугата. Опции: \"current\" (по подразбиране, запазва текущите стойности), \"factory\" (нулира до документирани стойности по подразбиране) или \"configuration\" (връща към стойностите по подразбиране на конфигурацията на превключвателя). ⚙️", + "name": "use_defaults" + }, + "include_config_in_attributes": { + "description": "Показва всички опции като атрибути на ключа в Home Assistant, когато е зададено на \"true\". 📝", + "name": "include_config_in_attributes" + }, + "turn_on_lights": { + "description": "Дали да се включат лампите, които в момента са изключени. 🔆", + "name": "turn_on_lights" + }, + "initial_transition": { + "description": "Продължителност на първия преход, когато лампите преминават от 'изключено' на 'включено' в секунди. ⏲️", + "name": "initial_transition" + }, + "sleep_transition": { + "description": "Продължителност на прехода, когато се превключва \"режим на сън\" в секунди. 😴", + "name": "sleep_transition" + }, + "max_brightness": { + "description": "Максимален процент на яркост. 💡", + "name": "max_brightness" + }, + "max_color_temp": { + "description": "Най-студена цветова температура в Келвини. ❄️", + "name": "max_color_temp" + }, + "min_brightness": { + "description": "Минимален процент на яркост. 💡", + "name": "min_brightness" + }, + "min_color_temp": { + "description": "Най-топла цветова температура в Келвини. 🔥", + "name": "min_color_temp" + }, + "only_once": { + "description": "Адаптира лампите само когато са включени ('true') или продължава да ги адаптира ('false'). 🔄", + "name": "only_once" + }, + "prefer_rgb_color": { + "description": "Дали да се предпочита RGB цветова корекция пред температура на светлината, когато е възможно. 🌈", + "name": "prefer_rgb_color" + }, + "separate_turn_on_commands": { + "description": "Използвайте отделни light.turn_on повиквания за цвят и яркост, необходими за някои типове светлини. 🔀", + "name": "separate_turn_on_commands" + }, + "send_split_delay": { + "description": "Забавяне (милисекунди) между \"separate_turn_on_commands\" за лампи, които не поддържат едновременна настройка на яркост и цвят. ⏲️", + "name": "send_split_delay" + }, + "sleep_brightness": { + "description": "Процент на яркостта на лампите в режим на сън. 😴", + "name": "sleep_brightness" + }, + "sleep_rgb_or_color_temp": { + "description": "Използвай \"rgb_color\" или \"color_temp\" в режим на сън. 🌙", + "name": "sleep_rgb_or_color_temp" + }, + "sleep_rgb_color": { + "description": "RGB цвят в режим на сън (използва се, когато sleep_rgb_or_color_temp е \"rgb_color\"). 🌈", + "name": "sleep_rgb_color" + }, + "sleep_color_temp": { + "description": "Цветова температура в режим на сън (използва се, когато sleep_rgb_or_color_temp е \"color_temp\") в Келвини. 😴", + "name": "sleep_color_temp" + }, + "sunrise_offset": { + "description": "Коригирайте времето на изгрев с положително или отрицателно отместване в секунди. ⏰", + "name": "sunrise_offset" + }, + "sunrise_time": { + "description": "Задайте фиксирано време (HH:MM:SS) за изгрев. 🌅", + "name": "sunrise_time" + }, + "sunset_offset": { + "description": "Коригирайте времето на залез с положително или отрицателно отместване в секунди. ⏰", + "name": "sunset_offset" + }, + "sunset_time": { + "description": "Задайте фиксирано време (HH:MM:SS) за залез. 🌇", + "name": "sunset_time" + }, + "max_sunrise_time": { + "description": "Задайте най-късното виртуално време за изгрев (HH:MM:SS), което позволява по-ранни изгреви. 🌅", + "name": "max_sunrise_time" + }, + "min_sunset_time": { + "description": "Задайте най-ранното виртуално време за изгрев (HH:MM:SS), което позволява по-ранни залези. 🌇", + "name": "min_sunset_time" + }, + "take_over_control": { + "description": "Деактивирайте адаптивното осветление, ако друг източник извика 'light.turn_on', докато лампите са включени и се адаптират. Обърнете внимание, че това извиква homeassistant.update_entity на всеки interval! 🔒", + "name": "take_over_control" + }, + "detect_non_ha_changes": { + "description": "Открива и спира адаптации за промени в състоянието, които не са 'light.turn_on'. Изисква активиран 'take_over_control'. 🕵️ Внимание: ⚠️ Някои лампи може лъжливо да указват състояние ‘включено’, което може да доведе до неочаквано включване на лампите. Деактивирайте тази функция, ако се сблъскате с такива проблеми.", + "name": "detect_non_ha_changes" + }, + "transition": { + "description": "Продължителност на прехода, когато лампите се променят, в секунди. 🕑", + "name": "transition" + }, + "adapt_delay": { + "description": "Време за изчакване (в секунди) между включването на светлината и прилагането на промените от адаптивното осветление. Може да помогне за избягване на премигване. ⏲️", + "name": "adapt_delay" + }, + "autoreset_control_seconds": { + "description": "Автоматично нулиране на ръчното управление след определен брой секунди. Задайте на 0, за да деактивирате. ⏲️", + "name": "autoreset_control_seconds" + } + } + } + } +} From ca0c79e3df853326e0f147791ff825782537646c Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 6 Jun 2024 08:18:44 -0700 Subject: [PATCH 0779/1077] docs: add lachezar-gizdov as a contributor for translation (#1013) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 1f6e9028..7ac2c652 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -864,6 +864,15 @@ "contributions": [ "code" ] + }, + { + "login": "lachezar-gizdov", + "name": "Lucho Gizdov", + "avatar_url": "https://avatars.githubusercontent.com/u/11273726?v=4", + "profile": "https://carmodsheaven.com", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 02adcde9..37b29dc3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-94-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-95-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -586,6 +586,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 8e8c520dad81fdcf4359b1c2b6ce7665b99286a6 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 6 Jun 2024 17:22:10 +0200 Subject: [PATCH 0780/1077] Translated using Weblate (Romanian) (#997) --- .../adaptive_lighting/translations/ro.json | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/ro.json b/custom_components/adaptive_lighting/translations/ro.json index 0967ef42..e0f7ec02 100644 --- a/custom_components/adaptive_lighting/translations/ro.json +++ b/custom_components/adaptive_lighting/translations/ro.json @@ -1 +1,72 @@ -{} +{ + "config": { + "step": { + "user": { + "description": "Fiecare instanţă poate conţine mai multe lumini!" + } + } + }, + "options": { + "step": { + "init": { + "data_description": { + "brightness_mode_time_light": "(Se ignoră dacă `modul_de_luminozitate='implicit'`) Durată în secunde a modificării luminozităţii în sus/jos cand poziţia sorelui este înainte sau după răsărit/apus.", + "sunrise_offset": "Ajustați ora răsăritului cu un decalaj pozitiv sau negativ în secunde.⏰", + "autoreset_control_seconds": "Resetare automată al controlului manual după un număr de secunde. Setaţi la 0 pentru a dezactiva.", + "brightness_mode": "Mod de luminozitate de utilizat. Valorile posibile sunt: 'implicit', 'liniar' şi 'hiperbolic' ( ultilizează 'mod_luminozitate_timp_de_noapte' şi 'mod_luminozitate_timp_de_zi').", + "sleep_brightness": "Procentul luminozităţii luminilor în modul 'somn'.", + "interval": "Frecvenţa adaptării luminilor, în secunde.", + "sunset_offset": "Ajustați ora răsăritului cu un decalaj pozitiv sau negativ în secunde." + }, + "title": "Opţiuni Iluminare Adaptivă", + "data": { + "adapt_only_on_bare_turn_on": "adaptează_doar_la_comanda_de_arpindere: La aprinderea iniţială a luminilor. Dacă este activat, IA va adapta luminile doar dacă se invocă 'light.turn_on' fără a specifica culoarea sau luminozitatea. Aceasta, de exemplu, previne adaptarea atunci când se activează o scenă. Dacă este dezactivat,IA va adaptata luminile indiferent de prezența valorilor culorii sau luminozității în service_data. Necesită activarea opţiunii 'preia_controlul. " + } + } + } + }, + "title": "Iluminare Adaptivă", + "services": { + "apply": { + "description": "Aplicaţi luminilor setările curente ale Iluminiării Adaptive luminilor.", + "fields": { + "lights": { + "description": "O lumină (sau listă de lumini) pentru care să se aplice setările." + } + } + }, + "change_switch_settings": { + "fields": { + "entity_id": { + "description": "Numele entităţii." + }, + "sleep_brightness": { + "description": "Procentul luminozităţii luminilor în modul 'somn'." + }, + "sleep_transition": { + "description": "Durata de tranziție (în secunde) atunci când modul de „somn” este activat." + }, + "autoreset_control_seconds": { + "description": "Resetare automată al controlului manual după un număr de secunde. Setaţi la 0 pentru a dezactiva." + }, + "only_once": { + "description": "Adaptează luminile doar la pornire ('activat') sau adaptează continuu ('dezactivat')." + }, + "sunrise_offset": { + "description": "Ajustați ora răsăritului cu un decalaj pozitiv sau negativ în secunde.⏰" + }, + "sunset_offset": { + "description": "Ajustați ora apusului cu un decalaj pozitiv sau negativ în secunde." + } + }, + "description": "Schimbați orice setări dorită în comutator. Toate opțiunile de aici sunt la fel ca în fluxul de configurare." + }, + "set_manual_control": { + "fields": { + "lights": { + "description": "Numele luminii (luminilor) , dacă nu sunt specificate, sunt selectate toate luminile ce aparţin de comutator." + } + } + } + } +} From d3b2d00bf1568853fc963880a1203ef606cc5f88 Mon Sep 17 00:00:00 2001 From: MizterB <5458030+MizterB@users.noreply.github.com> Date: Sun, 25 Aug 2024 12:00:14 -0400 Subject: [PATCH 0781/1077] Treat Hue groups as individual lights (#1037) --- custom_components/adaptive_lighting/switch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index b4af97ed..3b9448a9 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -638,7 +638,9 @@ def _expand_light_groups( def _is_light_group(state: State) -> bool: - return "entity_id" in state.attributes + return "entity_id" in state.attributes and not state.attributes.get( + "is_hue_group", False + ) @bind_hass From 318d921b91706b2513b4aa2074af2f30e3f6d2f3 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 25 Aug 2024 09:00:56 -0700 Subject: [PATCH 0782/1077] docs: add MizterB as a contributor for code (#1039) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- custom_components/adaptive_lighting/switch.py | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 7ac2c652..55053f56 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -873,6 +873,15 @@ "contributions": [ "translation" ] + }, + { + "login": "MizterB", + "name": "MizterB", + "avatar_url": "https://avatars.githubusercontent.com/u/5458030?v=4", + "profile": "https://github.com/MizterB", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 37b29dc3..4b4031ec 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-95-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-96-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -587,6 +587,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 3b9448a9..98a33021 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -639,7 +639,8 @@ def _expand_light_groups( def _is_light_group(state: State) -> bool: return "entity_id" in state.attributes and not state.attributes.get( - "is_hue_group", False + "is_hue_group", + False, ) From fc46ed73b68f12146a9782b867e32d4cfe72b813 Mon Sep 17 00:00:00 2001 From: rwjack <59068073+rwjack@users.noreply.github.com> Date: Sun, 25 Aug 2024 20:03:13 +0200 Subject: [PATCH 0783/1077] Fix #1017 (#1038) --- custom_components/adaptive_lighting/switch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 98a33021..893e7565 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -72,6 +72,7 @@ from homeassistant.core import ( callback, ) from homeassistant.helpers import entity_platform, entity_registry +from homeassistant.helpers.entity_component import async_update_entity if [MAJOR_VERSION, MINOR_VERSION] < [2023, 9]: from homeassistant.helpers.entity import DeviceInfo @@ -2492,7 +2493,7 @@ class AdaptiveLightingManager: # Ensure HASS is correctly updating your light's state with # light.turn_on calls if any problems arise. This # can happen e.g. using zigbee2mqtt with 'report: false' in device settings. - await self.hass.helpers.entity_component.async_update_entity(light) + await async_update_entity(self.hass, light) refreshed_state = self.hass.states.get(light) assert refreshed_state is not None From 7d265e9928fa6b2a223146ac81cdafa20735df5f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 25 Aug 2024 11:21:01 -0700 Subject: [PATCH 0784/1077] Add more versions to testing matrix (#1040) * Add more versions to testing matrix * Update mypy-dev dep * fix sed --- .github/workflows/pytest.yaml | 10 +++- README.md | 102 +++++++++++++++++----------------- scripts/setup-dependencies | 5 ++ 3 files changed, 65 insertions(+), 52 deletions(-) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index ed5de50c..b03f0023 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -49,7 +49,15 @@ jobs: - python-version: "3.12" core-version: "2024.3.3" - python-version: "3.12" - core-version: "2024.4.1" + core-version: "2024.4.4" + - python-version: "3.12" + core-version: "2024.5.5" + - python-version: "3.12" + core-version: "2024.6.4" + - python-version: "3.12" + core-version: "2024.7.4" + - python-version: "3.12" + core-version: "2024.8.3" - python-version: "3.12" core-version: "dev" steps: diff --git a/README.md b/README.md index 4b4031ec..94393aab 100644 --- a/README.md +++ b/README.md @@ -103,46 +103,46 @@ The YAML and frontend configuration methods support all of the options listed be -| Variable name | Description | Default | Type | -|:-------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:---------------------------------------| -| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | -| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | -| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | -| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | -| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | -| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | -| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | -| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | -| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | -| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | -| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | -| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | -| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | -| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | -| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | -| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | -| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | -| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | -| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | -| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | -| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | -| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | +| Variable name | Description | Default | Type | +|:-------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:---------------------------------------| +| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | +| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | +| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | +| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | +| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | +| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | | `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | | `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | -| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | -| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` | -| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | -| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | +| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | | `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | -| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | -| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | -| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | -| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | -| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | -| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | -| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | +| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | +| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | +| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | +| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | @@ -186,15 +186,15 @@ adaptive_lighting: -| Service data attribute | Description | Required | Type | -|:-------------------------|:-------------------------------------------------------------------------------------|:-----------|:---------------------| -| `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ✅ | list of `entity_id`s | -| `lights` | A light (or list of lights) to apply the settings to. 💡 | ❌ | list of `entity_id`s | -| `transition` | Duration of transition when lights change, in seconds. 🕑 | ❌ | `float` 0-6553 | -| `adapt_brightness` | Whether to adapt the brightness of the light. 🌞 | ❌ | bool | -| `adapt_color` | Whether to adapt the color on supporting lights. 🌈 | ❌ | bool | -| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | ❌ | bool | -| `turn_on_lights` | Whether to turn on lights that are currently off. 🔆 | ❌ | bool | +| Service data attribute | Description | Required | Type | +|:-------------------------|:--------------------------------------------------------------------------------------|:-----------|:---------------------| +| `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ✅ | list of `entity_id`s | +| `lights` | A light (or list of lights) to apply the settings to. 💡 | ❌ | list of `entity_id`s | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | ❌ | `float` 0-6553 | +| `adapt_brightness` | Whether to adapt the brightness of the light. 🌞 | ❌ | bool | +| `adapt_color` | Whether to adapt the color on supporting lights. 🌈 | ❌ | bool | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | ❌ | bool | +| `turn_on_lights` | Whether to turn on lights that are currently off. 🔆 | ❌ | bool | #### `adaptive_lighting.set_manual_control` @@ -208,11 +208,11 @@ adaptive_lighting: -| Service data attribute | Description | Required | Type | -|:-------------------------|:-----------------------------------------------------------------------------------------------|:-----------|:---------------------| -| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s | -| `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s | -| `manual_control` | Whether to add ("true") or remove ("false") the light from the "manual_control" list. 🔒 | ❌ | bool | +| Service data attribute | Description | Required | Type | +|:-------------------------|:------------------------------------------------------------------------------------------------|:-----------|:---------------------| +| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s | +| `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s | +| `manual_control` | Whether to add ("true") or remove ("false") the light from the "manual_control" list. 🔒 | ❌ | bool | #### `adaptive_lighting.change_switch_settings` diff --git a/scripts/setup-dependencies b/scripts/setup-dependencies index efe0bd25..ca14045d 100755 --- a/scripts/setup-dependencies +++ b/scripts/setup-dependencies @@ -9,6 +9,11 @@ if grep -q 'codecov' core/requirements_test.txt; then # however it is removed from PyPI, so we cannot install it sed -i '/codecov/d' core/requirements_test.txt fi + +if grep -q 'mypy-dev==1.10.0a3' core/requirements_test.txt; then + # mypy-dev==1.10.0a3 seems to not be available anymore, HA 2024.4 and 2024.5 are affected + sed -i 's/mypy-dev==1.10.0a3/mypy-dev==1.10.0b1/' core/requirements_test.txt +fi pip install -r core/requirements_test.txt pip install -e core/ From ac37b50330c87016bb6a50b7089c7511aa30acc3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 25 Aug 2024 11:34:34 -0700 Subject: [PATCH 0785/1077] Update manifest.json with version --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 6d8c0f5f..bd92e6bd 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.22.0" + "version": "1.23.0" } From 981cc420ecf35fdfeed23fe40142545e629e4afb Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 25 Aug 2024 12:24:23 -0700 Subject: [PATCH 0786/1077] docs: add brietman as a contributor for translation (#1042) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 55053f56..d8934e03 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -882,6 +882,15 @@ "contributions": [ "code" ] + }, + { + "login": "brietman", + "name": "brietman", + "avatar_url": "https://avatars.githubusercontent.com/u/17436537?v=4", + "profile": "https://github.com/brietman", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 94393aab..296b4aca 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-96-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-97-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -588,6 +588,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From ccf465251c5c17b8b2f021b5f57322f8f89baed7 Mon Sep 17 00:00:00 2001 From: chpego <38792705+chpego@users.noreply.github.com> Date: Mon, 7 Oct 2024 07:42:03 +0200 Subject: [PATCH 0787/1077] Update bug-report.md (#1046) fix url --- .github/ISSUE_TEMPLATE/bug-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 385259a6..548276d0 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -18,7 +18,7 @@ If you need help with using or configuring Adaptive Lighting, please [open a Q&A Please confirm that you have completed the following steps: - [ ] I have updated to the [latest Adaptive Lighting version](https://github.com/basnijholt/adaptive-lighting/releases) available in [HACS](https://hacs.xyz/). -- [ ] I have reviewed the [Troubleshooting Section](https://github.com/basnijholt/adaptive-lighting#troubleshooting) in the [README](https://github.com/basnijholt/adaptive-lighting#readme). +- [ ] I have reviewed the [Troubleshooting Section](https://github.com/basnijholt/adaptive-lighting#sos-troubleshooting) in the [README](https://github.com/basnijholt/adaptive-lighting#readme). - [ ] (If using Zigbee2MQTT) I have read the [Zigbee2MQTT troubleshooting guide](https://github.com/basnijholt/adaptive-lighting#zigbee2mqtt) in the [README](https://github.com/basnijholt/adaptive-lighting#readme). - [ ] I have checked the [V2 Roadmap](https://github.com/basnijholt/adaptive-lighting/discussions/291) and [open issues](https://github.com/basnijholt/adaptive-lighting/issues) to ensure my issue isn't a duplicate. From 8899d0f5f41a38806dc5cf56cb5c4d9389c2dccd Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 5 Dec 2024 11:14:08 -0800 Subject: [PATCH 0788/1077] Add Mend Renovate bot (#1077) * Add Mend Renovate bot * Add extends --- .github/renovate.json | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/renovate.json diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 00000000..d27d86ea --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "rebaseWhen": "behind-base-branch", + "dependencyDashboard": true, + "labels": [ + "dependencies", + "no-stale" + ], + "commitMessagePrefix": "⬆️", + "commitMessageTopic": "{{depName}}", + "prBodyDefinitions": { + "Release": "yes" + }, + "packageRules": [ + { + "matchManagers": [ + "github-actions" + ], + "addLabels": [ + "github_actions" + ], + "rangeStrategy": "pin" + }, + { + "matchManagers": [ + "github-actions" + ], + "matchUpdateTypes": [ + "minor", + "patch" + ], + "automerge": true + } + ], + "extends": [ + "config:recommended" + ] +} From 09e047345714b44142728627e05cb5def046b6a0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 11:20:00 -0800 Subject: [PATCH 0789/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20python-?= =?UTF-8?q?multipart=20to=20v0.0.18=20[SECURITY]=20(#1078)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ⬆️ Update python-multipart to v0.0.18 [SECURITY] * Update README.md, strings.json, and services.yaml --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- README.md | 102 ++++++++++++++++----------------- webapp/requirements-locked.txt | 2 +- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 296b4aca..1fc735e0 100644 --- a/README.md +++ b/README.md @@ -103,46 +103,46 @@ The YAML and frontend configuration methods support all of the options listed be -| Variable name | Description | Default | Type | -|:-------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:---------------------------------------| -| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | -| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | -| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | -| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | -| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | -| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | -| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | -| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | -| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | -| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | -| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | -| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | -| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | -| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | -| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | -| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | -| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | -| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | -| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | -| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | -| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | -| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | +| Variable name | Description | Default | Type | +|:-------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:---------------------------------------| +| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | +| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | +| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | +| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | +| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | +| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | | `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | | `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | -| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | -| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` | -| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | -| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | +| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | | `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | -| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | -| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | -| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | -| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | -| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | -| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | -| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | +| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | +| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | +| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | +| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | @@ -186,15 +186,15 @@ adaptive_lighting: -| Service data attribute | Description | Required | Type | -|:-------------------------|:--------------------------------------------------------------------------------------|:-----------|:---------------------| -| `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ✅ | list of `entity_id`s | -| `lights` | A light (or list of lights) to apply the settings to. 💡 | ❌ | list of `entity_id`s | -| `transition` | Duration of transition when lights change, in seconds. 🕑 | ❌ | `float` 0-6553 | -| `adapt_brightness` | Whether to adapt the brightness of the light. 🌞 | ❌ | bool | -| `adapt_color` | Whether to adapt the color on supporting lights. 🌈 | ❌ | bool | -| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | ❌ | bool | -| `turn_on_lights` | Whether to turn on lights that are currently off. 🔆 | ❌ | bool | +| Service data attribute | Description | Required | Type | +|:-------------------------|:-------------------------------------------------------------------------------------|:-----------|:---------------------| +| `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ✅ | list of `entity_id`s | +| `lights` | A light (or list of lights) to apply the settings to. 💡 | ❌ | list of `entity_id`s | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | ❌ | `float` 0-6553 | +| `adapt_brightness` | Whether to adapt the brightness of the light. 🌞 | ❌ | bool | +| `adapt_color` | Whether to adapt the color on supporting lights. 🌈 | ❌ | bool | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | ❌ | bool | +| `turn_on_lights` | Whether to turn on lights that are currently off. 🔆 | ❌ | bool | #### `adaptive_lighting.set_manual_control` @@ -208,11 +208,11 @@ adaptive_lighting: -| Service data attribute | Description | Required | Type | -|:-------------------------|:------------------------------------------------------------------------------------------------|:-----------|:---------------------| -| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s | -| `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s | -| `manual_control` | Whether to add ("true") or remove ("false") the light from the "manual_control" list. 🔒 | ❌ | bool | +| Service data attribute | Description | Required | Type | +|:-------------------------|:-----------------------------------------------------------------------------------------------|:-----------|:---------------------| +| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s | +| `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s | +| `manual_control` | Whether to add ("true") or remove ("false") the light from the "manual_control" list. 🔒 | ❌ | bool | #### `adaptive_lighting.change_switch_settings` diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index e965a1b5..64ed6570 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -43,7 +43,7 @@ packaging==23.2 # via # htmltools # shinyswatch -python-multipart==0.0.6 +python-multipart==0.0.18 # via shiny pytz==2023.3.post1 # via astral From 9a190e625af9b08a3a176f39b610a960f83058da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 11:20:07 -0800 Subject: [PATCH 0790/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20idna=20?= =?UTF-8?q?to=20v3.7=20[SECURITY]=20(#1079)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index 64ed6570..8e544136 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -27,7 +27,7 @@ htmltools==0.5.1 # via # shiny # shinyswatch -idna==3.6 +idna==3.7 # via anyio linkify-it-py==2.0.2 # via shiny From 43015d1df16c0ea1a38dfaacb6ea57e20d0ab701 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 11:20:12 -0800 Subject: [PATCH 0791/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20starlet?= =?UTF-8?q?te=20to=20v0.40.0=20[SECURITY]=20(#1080)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index 8e544136..4db3cb83 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -58,7 +58,7 @@ shinyswatch==0.3.1 # via -r requirements.txt sniffio==1.3.0 # via anyio -starlette==0.34.0 +starlette==0.40.0 # via shiny typing-extensions==4.9.0 # via From 5c6fc91c6676256c0eb73968b117224a7e4bd03f Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:27:19 +0200 Subject: [PATCH 0792/1077] docs: add TamilNeram as a contributor for translation (#1089) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index d8934e03..492cdaab 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -891,6 +891,15 @@ "contributions": [ "translation" ] + }, + { + "login": "TamilNeram", + "name": "தமிழ் நேரம்", + "avatar_url": "https://avatars.githubusercontent.com/u/67970539?v=4", + "profile": "https://github.com/TamilNeram", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 1fc735e0..2b0f646e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-97-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-98-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -589,6 +589,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 111d9fc6b10adb8cb8fc4071a13b50d5a5277a8d Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 5 Dec 2024 20:27:50 +0100 Subject: [PATCH 0793/1077] Translations update from Hosted Weblate (#1016) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Tamil) Currently translated at 100.0% (153 of 153 strings) Added translation using Weblate (Tamil) Co-authored-by: Hosted Weblate Co-authored-by: தமிழ்நேரம் Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ta/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Greek) Currently translated at 44.4% (68 of 153 strings) Added translation using Weblate (Greek) Co-authored-by: Hosted Weblate Co-authored-by: Thunderstrike116 Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/el/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Japanese) Currently translated at 49.0% (75 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Meteor2 Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ja/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Portuguese) Currently translated at 60.7% (93 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Patrick Bassut Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pt/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Dutch) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: brietman Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Finnish) Currently translated at 99.3% (152 of 153 strings) Co-authored-by: Ricky Tigg Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/fi/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Slovak) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Milan Šalka Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/sk/ Translation: Adaptive Lighting/Adaptive Lighting --------- Co-authored-by: தமிழ்நேரம் Co-authored-by: Thunderstrike116 Co-authored-by: Meteor2 Co-authored-by: Patrick Bassut Co-authored-by: brietman Co-authored-by: Ricky Tigg Co-authored-by: Milan Šalka --- .../adaptive_lighting/translations/el.json | 19 ++ .../adaptive_lighting/translations/fi.json | 139 +++++++++++- .../adaptive_lighting/translations/ja.json | 3 + .../adaptive_lighting/translations/nl.json | 4 +- .../adaptive_lighting/translations/pt.json | 16 +- .../adaptive_lighting/translations/sk.json | 6 +- .../adaptive_lighting/translations/ta.json | 202 ++++++++++++++++++ 7 files changed, 377 insertions(+), 12 deletions(-) create mode 100644 custom_components/adaptive_lighting/translations/el.json create mode 100644 custom_components/adaptive_lighting/translations/ta.json diff --git a/custom_components/adaptive_lighting/translations/el.json b/custom_components/adaptive_lighting/translations/el.json new file mode 100644 index 00000000..8794a46a --- /dev/null +++ b/custom_components/adaptive_lighting/translations/el.json @@ -0,0 +1,19 @@ +{ + "title": "Adaptive Lighting", + "options": { + "step": { + "init": { + "title": "Επιλογές Adaptive Lighting" + } + } + }, + "services": { + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Προσαρμογή των φώτων μόνο όταν είναι αναμμένα (`true`) ή συνεχής προσαρμογή αυτών (`false`). 🔄" + } + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/fi.json b/custom_components/adaptive_lighting/translations/fi.json index 7acd9578..15b5b18f 100644 --- a/custom_components/adaptive_lighting/translations/fi.json +++ b/custom_components/adaptive_lighting/translations/fi.json @@ -28,8 +28,69 @@ }, "transition": { "description": "Valojen siirtymän kesto sekunneissa, kun valaistusta muutetaan." + }, + "sleep_color_temp": { + "description": "Värilämpötila lepotilassa (käytetään, kun `sleep_rgb_or_color_temp` on `color_temp`) kelvineinä. 😴" + }, + "detect_non_ha_changes": { + "description": "Havaitsee ja pysäyttää mukautukset ei-\"light.turn_on\" -tilanmuutoksille. Vaatii \"take_over_control\":n käyttöönoton. 🕵️ Varoitus: ⚠️ Jotkut valot saatavat osoittaa virheellisesti 'on'-tilan, mikä voi johtaa valojen syttymiseen odottamatta. Poista tämä ominaisuus käytöstä, jos kohtaat tällaisia ongelmia." + }, + "sunrise_time": { + "description": "Aseta kiinteä aika (TT:MM:SS) auringonnousulle. 🌅" + }, + "use_defaults": { + "description": "Asettaa oletusarvot, joita ei ole määritetty tässä palvelukutsussa. Vaihtoehdot: \"nykyinen\" (oletus, säilyttää nykyiset arvot), \"tehdas\" (palauttaa dokumentoituihin oletusasetuksiin) tai \"kokoonpano\" (palaa kokoonpanon oletusasetusten vaihtamiseksi). ⚙️" + }, + "max_sunrise_time": { + "description": "Aseta viimeisin virtuaalinen auringonnousuaika (TT:MM:SS), aikaisempia auringonnousuja sallien. 🌅" + }, + "separate_turn_on_commands": { + "description": "Käytä erillisiä \"light.turn_on\" -kutsuja värin ja kirkkauden määrittämiseksi, joita tarvitaan joissakin valotyypeissä. 🔀" + }, + "entity_id": { + "description": "Kytkimen entiteettitunnus. 📝" + }, + "turn_on_lights": { + "description": "Sytytetäänkö valot, jotka ovat tällä hetkellä pois päältä. 🔆" + }, + "include_config_in_attributes": { + "description": "Näytä kaikki vaihtoehdot attribuutteina kotiavustajan kytkimellä, kun sen arvo on \"true\". 📝" + }, + "sleep_transition": { + "description": "Siirtymän kesto, kun \"lepotila\" vaihdetaan sekunneiksi. 😴" + }, + "max_brightness": { + "description": "Enimmäiskirkkausprosentti. 💡" + }, + "min_brightness": { + "description": "Vähittäiskirkkausprosentti. 💡" + }, + "sleep_rgb_color": { + "description": "RGB-väri lepotilassa (käytetään, kun `sleep_rgb_or_color_temp` on \"rgb_color\"). 🌈" + }, + "sunset_time": { + "description": "Aseta kiinteä aika (TT:MM:SS) auringonlaskulle. 🌇" + }, + "sleep_rgb_or_color_temp": { + "description": "Käytä joko `\"rgb_color\"` tai `\"color_temp\"` lepotilassa. 🌙" + }, + "min_color_temp": { + "description": "Lämpimin värilämpötila kelvineissä. 🔥" + }, + "prefer_rgb_color": { + "description": "Halutaanko RGB-värinsäätö mieluummin valon värilämpötilan sijaan, kun mahdollista. 🌈" + }, + "take_over_control": { + "description": "Poista Adaptive Lighting käytöstä, jos toinen lähde kutsuu `light.turn_on`, kun valot ovat päällä ja niitä mukautetaan. Huomaa, että tämä kutsuu `homeassistant.update_entity` joka `interval`! 🔒" + }, + "min_sunset_time": { + "description": "Aseta aikaisin virtuaalinen auringonlaskuaika (TT:MM:SS), myöhempiä auringonlaskuja sallien. 🌇" + }, + "adapt_delay": { + "description": "Odotusaika (sekunteina) valon syttymisen ja Adaptive Lightingin muutosten käyttöönoton välillä. Saattaa auttaa välttämään välkkymistä. ⏲️" } - } + }, + "description": "Muuta haluamiasi asetuksia kytkimessä. Kaikki vaihtoehdot ovat samat kuin kokoonpanon kulussa." }, "apply": { "description": "Asettaa nykyiset Adaptiivisen Valaistuksen asetukset valoihin.", @@ -39,8 +100,37 @@ }, "transition": { "description": "Valojen siirtymän kesto sekunneissa, kun valaistusta muutetaan." + }, + "entity_id": { + "description": "Kytkimen `entity_id` ja käytettävät asetukset. 📝" + }, + "adapt_brightness": { + "description": "Mukautetaanko valon kirkkautta. 🌞" + }, + "adapt_color": { + "description": "Mukautetaanko tukivalojen väriä. 🌈" + }, + "prefer_rgb_color": { + "description": "Halutaanko RGB-värinsäätö mieluummin valon värilämpötilan sijaan, kun mahdollista. 🌈" + }, + "turn_on_lights": { + "description": "Sytytetäänkö valot, jotka ovat tällä hetkellä pois päältä. 🔆" } } + }, + "set_manual_control": { + "fields": { + "lights": { + "description": "Valojen entity_id(s), jos sitä ei ole määritetty, kaikki kytkimen valot valitaan. 💡" + }, + "entity_id": { + "description": "Kytkimen `entity_id`, jolla valo määritetään `manuaalisesti ohjattavaksi`. 📝" + }, + "manual_control": { + "description": "Lisätäänkö (\"true\") vai poistetaanko (\"false\") valo \"manual_control\"-luettelosta. 🔒" + } + }, + "description": "Merkitse, onko valo 'manuaalisesti ohjattu'." } }, "title": "Adaptiivinen valaistus", @@ -55,16 +145,57 @@ "brightness_mode": "Kirkkaus-moodi jota käytetään. Mahdolliset arvot ovat `default`, `linear`, and `tanh` (käyttää arvoja `brightness_mode_time_dark` ja `brightness_mode_time_light`).", "sunset_offset": "Muuta auringonlaskun aikaa positiivisella tai negatiivisella korjauksella määritettynä sekunneissa.", "initial_transition": "Ensimmäisen siirtymän kesto sekunneissa, kun valot kytketään 'off'-tilasta 'on'-tilaan.", - "send_split_delay": "Viive (ms) `separate_turn_on_commands` välillä valoille, jotka eivät tue yhtäaikaista kirkkauden ja värilämpötilan säätöä." - } + "send_split_delay": "Viive (ms) `separate_turn_on_commands` välillä valoille, jotka eivät tue yhtäaikaista kirkkauden ja värilämpötilan säätöä.", + "sleep_color_temp": "Värilämpötila lepotilassa (käytetään, kun `sleep_rgb_or_color_temp` on `color_temp`) kelvineinä. 😴", + "brightness_mode_time_dark": "(Ohitetaan, jos `brightness_mode='default'`) Kesto sekunteina kirkkauden lisäämiseen/vähentämiseen auringonnousun/auringonlaskun jälkeen/ennen. 📈📉.", + "adapt_delay": "Odotusaika (sekunteina) valon syttymisen ja Adaptive Lightingin muutosten käyttöönoton välillä. Saattaa auttaa välttämään välkkymistä. ⏲️", + "sleep_transition": "Siirtymän kesto, kun \"lepotila\" vaihdetaan sekunneiksi. 😴", + "interval": "Tiheys valojen mukauttamiseen sekunneissa. 🔄", + "brightness_mode_time_light": "(Ohitetaan, jos `brightness_mode='default'`) Kesto sekunteina kirkkauden lisäämiseen/vähentämiseen auringonnousun/auringonlaskun jälkeen/ennen. 📈📉.", + "sleep_rgb_color": "RGB-väri lepotilassa (käytetään, kun `sleep_rgb_or_color_temp` on \"rgb_color\"). 🌈", + "sunrise_time": "Aseta kiinteä aika (TT:MM:SS) auringonnousulle. 🌅", + "sunset_time": "Aseta kiinteä aika (TT:MM:SS) auringonlaskulle. 🌇", + "min_sunset_time": "Aseta aikaisin virtuaalinen auringonlaskuaika (TT:MM:SS), myöhempiä auringonlaskuja sallien. 🌅", + "min_sunrise_time": "Aseta aikaisin virtuaalinen auringonnousuaika (TT:MM:SS), myöhempiä auringonnousuja sallien. 🌅", + "max_sunrise_time": "Aseta aikaisin virtuaalinen auringonnousuaika (TT:MM:SS), aikaisempia auringonnousuja sallien. 🌅", + "sleep_rgb_or_color_temp": "Käytä joko `\"rgb_color\"` tai `\"color_temp\"` lepotilassa. 🌙" + }, + "description": "Määritä Adaptive Lighting -komponentti. Vaihtoehtojen nimet vastaavat YAML-asetuksia. Jos olet määrittänyt tämän merkinnän YAML:ssa, tässä ei näy vaihtoehtoja. Interaktiiviset kaaviot, jotka esittelevät parametrien vaikutuksia, on [tässä verkkosovelluksessa](https://basnijholt.github.io/adaptive-lighting). Lisätietoja löytyy [virallisesta dokumentaatiosta](https://github.com/basnijholt/adaptive-lighting#readme).", + "data": { + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Kun valot sytytetään ensimmäisen kerran. Jos asetuksena on \"true\", Adaptive Lighting mukautuu vain, jos \"light.turn_on\" kutsutaan määrittelemättä väriä tai kirkkautta. ❌🌈 Tämä esimerkiksi estää mukautumisen näkymää aktivoitaessa. Jos \"false\", Adaptive Lighting mukautuu riippumatta siitä, onko alkuperäisessä \"service_data\"-arvossa väri tai kirkkaus. Vaatii \"take_over_control\":n käyttöönoton. 🕵️ ", + "multi_light_intercept": "multi_light_intercept: sieppaa ja mukauta light.turn_on-kutsut, jotka kohdistuvat useisiin valoihin. ➗⚠️ Tämä saattaa johtaa yksittäisen light.turn_on-kutsun jakamiseen useiksi kutsuiksi, esimerkiksi kun valot ovat eri kytkimissä. Vaadi `intercept`:n käyttöönotto.", + "only_once": "only_once: Mukauta valot vain, kun ne ovat päällä (\"true\") tai mukauta niitä jatkuvasti (\"false\"). 🔄", + "skip_redundant_commands": "skip_redundant_commands: Ohita mukautuskomentojen lähettäminen, joiden kohdetila on jo yhtä suuri kuin valon tunnettu tila. Minimoi verkkoliikenteen ja parantaa mukautumisvastetta joissain tilanteissa. 📉 Poista käytöstä, jos fyysiset valotilat eivät ole synkronoitu kotiavustajan tallennetun tilan kanssa.", + "take_over_control": "take_over_control: Poista mukautuva valaistus käytöstä, jos toinen lähde kutsuu `light.turn_on`, kun valot ovat päällä ja niitä mukautetaan. Huomaa, että tämä kutsuu `homeassistant.update_entity` joka `interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Havaitsee ja pysäyttää mukautukset ei-\"light.turn_on\" -tilanmuutoksille. Vaatii \"take_over_control\" käyttöönoton. 🕵️ Varoitus: ⚠️ Jotkut valot saatavat osoittaa virheellisesti 'on'-tilan, mikä voi johtaa valojen syttymiseen odottamatta. Poista tämä ominaisuus käytöstä, jos kohtaat tällaisia ongelmia.", + "lights": "lights: Luettelo ohjattavista valon entity_ids:stä (voi olla tyhjä). 🌟", + "max_brightness": "max_brightness: Enimmäiskirkkausprosentti. 💡", + "max_color_temp": "max_color_temp: Kylmin värilämpötila kelvineinä. ❄️", + "min_brightness": "min_brightness: Vähittäiskirkkausprosentti. 💡", + "min_color_temp": "min_color_temp: Lämpimin värilämpötila kelvineinä. 🔥", + "prefer_rgb_color": "prefer_rgb_color: valitaanko RGB-värien säätö valon värilämpötilan sijaan, kun mahdollista. 🌈", + "transition_until_sleep": "shift_until_sleep: Kun käytössä, Adaptive Lighting käsittelee lepoasetukset miniminä ja siirtyy näihin arvoihin auringonlaskun jälkeen. 🌙", + "include_config_in_attributes": "include_config_in_attributes: Näytä kaikki vaihtoehdot attribuutteina Kotiavustajan kytkimessä, kun asetuksena on \"true\". 📝", + "intercept": "intercept: sieppaa ja mukauta \"light.turn_on\"-kutsut mahdollistamaan välitön värin ja kirkkauden mukauttaminen. 🏎️ Poista käytöstä valot, jotka eivät tue \"light.turn_on\" värin ja kirkkauden kanssa.", + "separate_turn_on_commands": "separate_turn_on_commands: Käytä erillisiä `light.turn_on`-kutsuja värin ja kirkkauden määrittämiseksi, joita tarvitaan joissakin valotyypeissä. 🔀" + }, + "title": "Adaptive Lightingin vaihtoehdot" } + }, + "error": { + "option_error": "Virheellinen vaihtoehto", + "entity_missing": "Kotiavustajasta puuttuu yksi tai useampi valittu valoentiteetti" } }, "config": { "step": { "user": { - "title": "Valitse nimi tälle Adaptiivisen Valaistuksen esiintymälle" + "title": "Valitse nimi tälle Adaptiivisen Valaistuksen esiintymälle", + "description": "Jokainen esiintymä voi sisältää useita valoja!" } + }, + "abort": { + "already_configured": "Tämä laite on jo määritetty" } } } diff --git a/custom_components/adaptive_lighting/translations/ja.json b/custom_components/adaptive_lighting/translations/ja.json index 20cc082c..065d8b3b 100644 --- a/custom_components/adaptive_lighting/translations/ja.json +++ b/custom_components/adaptive_lighting/translations/ja.json @@ -11,6 +11,9 @@ }, "sunset_offset": { "description": "日の入時間を基準に秒単位で正値もしくは負値で調整する。⏰" + }, + "entity_id": { + "description": "スイッチのエンティティID。 📝" } } }, diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json index 16fe3059..49fd1019 100644 --- a/custom_components/adaptive_lighting/translations/nl.json +++ b/custom_components/adaptive_lighting/translations/nl.json @@ -43,7 +43,7 @@ "sunset_time": "sunset_time: handmatige onderdrukking van de zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", "min_sunset_time": "min_sunset_time: handmatige onderdrukking van de minimale zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsondergangstijd op uw locatie gebruikt (UU:MM:SS)", "take_over_control": "take_over_control: Als iets anders dan Adaptieve verlichting 'light.turn_on' roept wanneer een lamp al aan is, stop dan met het aanpassen van het licht totdat het (of de schakelaar) uit -> aan gaat.", - "detect_non_ha_changes": "detect_non_ha_changes: detecteert alle >10% wijzigingen aan de lichten (ook buiten HA), vereist dat 'take_over_control' is ingeschakeld (heet 'homeassistant.update_entity' elke 'interval'!)", + "detect_non_ha_changes": "detect_non_ha_changes: Detecteert en stopt aanpassingen voor`light.turn_on` statuswijzigingen. Vereist dat`take_over_control` is ingeschakeld. 🕵️ Voorzichtig: ⚠️ Sommige lampen kunnen een 'aan' status vals aangeven, wat kan leiden tot onverwacht inschakelen van lampen. Schakel deze functie uit als je dergelijke problemen tegenkomt.", "transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)", "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Bij het initieel inschakelen van de lampen. Als dit op `true` is ingesteld, past Av alleen aan als `light.turn_on` wordt aangeroepen zonder een kleur of helderheid te specificeren. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Als het `false` is, past Av aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. `take_over_control` moet ingeschakeld zijn. 🕵️ ", @@ -121,7 +121,7 @@ "description": "Duur van de eerste overgang wanneer de lampen van `uit` naar `aan`gaan, in seconden. ⏲️" }, "take_over_control": { - "description": "Schakel Adaptieve verlichting uit wanneer een andere bron `light.turn_on` aanroept terwijl de lampen aan staan en worden aangepast. N.b. dit zal `homeassistant.update_entity` elke `interval` uitvoeren." + "description": "Schakel Adaptieve verlichting uit als een andere bron `light.turn_on` aanroept terwijl de lampen aan zijn en worden aangepast. Let op dit roept`homeassistant.update_entity` elke `interval`aan." }, "detect_non_ha_changes": { "description": "Detecteert en stopt aanpassingen voor niet-`light.turn_on` state veranderingen. `take_over_control` moet actief zijn. 🕵️ Let op:⚠️Sommige lampen kunnen incorrect een 'on' state weergeven, wat resulteert in lampen die onverwacht aan gaan. Schakel deze feature uit wanneer deze fout zich voordoet." diff --git a/custom_components/adaptive_lighting/translations/pt.json b/custom_components/adaptive_lighting/translations/pt.json index bc4c82d2..d8b72689 100644 --- a/custom_components/adaptive_lighting/translations/pt.json +++ b/custom_components/adaptive_lighting/translations/pt.json @@ -26,8 +26,15 @@ }, "transition": { "description": "Duração da transição quando as luzes mudam, em segundos. 🕑" + }, + "max_color_temp": { + "description": "Cor mais fria em Kelvin. ❄️" + }, + "sleep_brightness": { + "description": "Porcentagem do brilho da lâmpadas no modo \"sleep mode\"." } - } + }, + "description": "Muda alguma configuração que você quiser no interruptor. Todas as opções aqui são as mesmas que estão no processo de configuração." }, "apply": { "description": "Aplica as definições atuais da Iluminação Adaptativa às luzes.", @@ -60,9 +67,12 @@ "sunset_offset": "Ajustar a hora do pôr do sol com um offset positivo ou negativo em segundos. ⏰", "sleep_transition": "Duração da transição quando o \"modo dormir\" é alternado em segundos. 😴", "autoreset_control_seconds": "Reiniciar o controlo manual automaticamente após um número de segundos. Definir 0 para desativar. ⏲️", - "transition": "Duração da transição quando as luzes mudam, em segundos. 🕑" + "transition": "Duração da transição quando as luzes mudam, em segundos. 🕑", + "sleep_brightness": "Porcentagem do brilho da lâmpadas no modo \"sleep mode\".", + "brightness_mode": "Brilho que irá ser usado. Possíveis valores são `default`, `linear` e `tanh`(usa `brightness_mode_time_dark` e `brightness_mode_time_light`). 📈" }, - "title": "Opções da Iluminação Adaptativa" + "title": "Opções da Iluminação Adaptativa", + "description": "Configure um componente da Iluminação Adaptativa. O nome das opções são as mesmas que as do YML. Se você já definiu essa configuração no YAML, nenhuma opção vai aparecer aqui. Para acessar um gráfico que demonstra o efeito dos parâmetros, acesse [esse app](https://basnijholt.github.io/adaptive-lighting). Para mais detalhes, veja a [documentação oficial](https://github.com/basnijholt/adaptive-lighting#readme)." } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/sk.json b/custom_components/adaptive_lighting/translations/sk.json index 11ad8844..e1cf499f 100644 --- a/custom_components/adaptive_lighting/translations/sk.json +++ b/custom_components/adaptive_lighting/translations/sk.json @@ -36,7 +36,7 @@ "sunrise_offset": "Upravte čas východu slnka o sekundy vpred alebo vzad. ⏰", "transition": "Trvanie prechodu, keď sú svetlá zmenené (v sekundách). ⏲️", "brightness_mode": "Výber režimu jasu. Možné hodnotu sú `default`, `linear` a `tanh` (používa `brightness_mode_time_dark` a `brightness_mode_time_light`). 📈", - "brightness_mode_time_light": "(Ignorované ak `brightness_mode='default'`) Čas na zvýšenie/zníženie jasu po udalosti/pred udalosťou východu/západu slnka. 📈📉", + "brightness_mode_time_light": "(Ignorovaný, ak `brightness_mode = 'predvolené') Trvanie v sekundách na rampu / vypnutie jasu po / pred východ slnka/sunset. 📈📉.", "sunset_offset": "Upravte čas západu slnka o sekundy vpred alebo vzad. ⏰", "max_sunset_time": "Nastavte najneskorší možný virtuálny západ slnka (HH:MM:SS). Umožňuje skorší západ slnka. 🌅", "initial_transition": "Trvanie prvého prechodu, keď sú svetlá zapnuté z `off` na `on` (v sekundách). ⏲️", @@ -132,7 +132,7 @@ "description": "Prispôsobiť svetlá len pri zapnutí (`true`) alebo prispôsobovať ich priebežne (`false`). 🔄" }, "use_defaults": { - "description": "Nastaví predvolené hodnoty, ktoré nie sú špecifikované v tomto volaní služby. Možnosti: \"current\" (predvolené, zachová aktuálne hodnoty), \"factory\" (použije predvolené hodnoty z dokumentácie) alebo \"configuration\" (vráti na predvolené nastavenia prepínača). ⚙️" + "description": "Stanovuje predvolené hodnoty nie sú uvedené v tomto servisnom hovore. Možnosti: \"current\" (predvolené, zachováva aktuálne hodnoty), \"chôdzky na zdokumentované predvolené nastavenia), alebo \"konfigurácia\" (odkazy prepínať predvolené nastavenia). ⚙️" }, "separate_turn_on_commands": { "description": "Použiť samostatné volania služby `light.turn_on` pre nastavenie teploty svetla a jasu (môže byť potrebné pre niektoré typy svetiel). 🔀" @@ -196,7 +196,7 @@ "description": "`entity_id` prepínača u ktorého sa majú svetlá o(d)značiť ako \"manuálne ovládané\". 📝" }, "lights": { - "description": "entity_id svetiel, ak nie sú špecifikované, tak sú vybrané všetky svetlá prepínača. 💡" + "description": "subjekt_id(s) svietidiel, ak nie je špecifikované, všetky svetlá v prepínači sú vybrané. 💡" } }, "description": "Či označiť svetlo ako \"manuálne ovládané\"." diff --git a/custom_components/adaptive_lighting/translations/ta.json b/custom_components/adaptive_lighting/translations/ta.json new file mode 100644 index 00000000..66115d64 --- /dev/null +++ b/custom_components/adaptive_lighting/translations/ta.json @@ -0,0 +1,202 @@ +{ + "services": { + "set_manual_control": { + "description": "ஒரு ஒளி 'கைமுறையாக கட்டுப்படுத்தப்பட்டதா' என்பதைக் குறிக்கவும்.", + "fields": { + "entity_id": { + "description": "சுவிட்சின் `நிறுவனம்_ஐடி`, இதில் (அன்) ஒளியை` கைமுறையாக கட்டுப்படுத்தப்படுகிறது 'என்று குறிக்கவும். ." + }, + "lights": { + "description": "விளக்குகளின் entity_id (கள்), குறிப்பிடப்படாவிட்டால், சுவிட்சில் உள்ள அனைத்து விளக்குகளும் தேர்ந்தெடுக்கப்படுகின்றன. ." + }, + "manual_control": { + "description": "\"கையேடு_ கன்ட்ரோல்\" பட்டியலிலிருந்து ஒளியை சேர்க்க வேண்டுமா அல்லது அகற்ற வேண்டுமா அல்லது அகற்ற வேண்டுமா அல்லது அகற்ற வேண்டுமா? ." + } + } + }, + "change_switch_settings": { + "fields": { + "sunrise_offset": { + "description": "விநாடிகளில் நேர்மறை அல்லது எதிர்மறை ஆஃப்செட் மூலம் சூரிய தோன்றுகை நேரத்தை சரிசெய்யவும். ." + }, + "sunrise_time": { + "description": "சூரிய உதயத்திற்கு ஒரு நிலையான நேரத்தை (HH: MM: SS) அமைக்கவும். ." + }, + "entity_id": { + "description": "சுவிட்சின் நிறுவன ஐடி. ." + }, + "use_defaults": { + "description": "இந்த பணி அழைப்பில் குறிப்பிடப்படாத இயல்புநிலை மதிப்புகளை அமைக்கிறது. விருப்பங்கள்: \"நடப்பு\" (இயல்புநிலை, தற்போதைய மதிப்புகளைத் தக்க வைத்துக் கொள்கிறது), \"தொழிற்சாலை\" (ஆவணப்படுத்தப்பட்ட இயல்புநிலைகளுக்கு மீட்டமைக்கிறது) அல்லது \"உள்ளமைவு\" (கட்டமைப்பு இயல்புநிலைகளை மாற்றுவதற்கு மாற்றுகிறது). ." + }, + "include_config_in_attributes": { + "description": "`உண்மை` என அமைக்கப்பட்டிருக்கும் போது வீட்டு உதவியாளரின் சுவிட்சில் உள்ள அனைத்து விருப்பங்களையும் பண்புகளாகக் காட்டுங்கள். ." + }, + "turn_on_lights": { + "description": "தற்போது முடக்கப்பட்ட விளக்குகளை இயக்க வேண்டுமா. ." + }, + "initial_transition": { + "description": "விளக்குகள் `ஆஃப்` முதல்` ஆன் `வரை நொடிகளில் மாறும் போது முதல் மாற்றத்தின் காலம். ." + }, + "sleep_transition": { + "description": "\"தூக்க பயன்முறை\" நொடிகளில் மாற்றப்படும்போது மாற்றத்தின் காலம். ." + }, + "max_brightness": { + "description": "அதிகபட்ச ஒளி விழுக்காடு. ." + }, + "max_color_temp": { + "description": "கெல்வினில் குளிரான வண்ண வெப்பநிலை. ." + }, + "min_brightness": { + "description": "குறைந்தபட்ச ஒளி விழுக்காடு. ." + }, + "min_color_temp": { + "description": "கெல்வினில் வெப்பமான வண்ண வெப்பநிலை. ." + }, + "only_once": { + "description": "விளக்குகள் இயக்கப்படும்போது மட்டுமே (`உண்மை`) மாற்றியமைக்கும்போது அல்லது அவற்றைத் தழுவிக்கொள்ளுங்கள் (` பொய்`). ." + }, + "prefer_rgb_color": { + "description": "முடிந்தவரை ஒளி வண்ண வெப்பநிலையை விட RGB வண்ண சரிசெய்தலை விரும்பலாமா. ." + }, + "separate_turn_on_commands": { + "description": "சில ஒளி வகைகளுக்கு தேவைப்படும் வண்ணம் மற்றும் பிரகாசத்திற்கான தனித்தனி `லைட்.டர்ன்_ஓஎன்` அழைப்புகளைப் பயன்படுத்தவும். ." + }, + "send_split_delay": { + "description": "ஒரே நேரத்தில் ஒளி மற்றும் வண்ண அமைப்பை ஆதரிக்காத விளக்குகளுக்கு `தனி_டர்ன்_ஆன்_காமண்ட்ச்` இடையே நேரந்தவறுகை (எம்.எச்). ." + }, + "sleep_brightness": { + "description": "தூக்க பயன்முறையில் விளக்குகளின் ஒளி விழுக்காடு. ." + }, + "sleep_rgb_or_color_temp": { + "description": "தூக்க பயன்முறையில் `\" rgb_color \"` அல்லது `\" Color_Temp \"key ஐப் பயன்படுத்தவும். ." + }, + "sleep_rgb_color": { + "description": "தூக்க பயன்முறையில் RGB வண்ணம் (`SLEEP_RGB_OR_COLOR_TEMP`\" RGB_COLOR \"ஆக இருக்கும்போது பயன்படுத்தப்படுகிறது). ." + }, + "sleep_color_temp": { + "description": "ச்லீப் பயன்முறையில் வண்ண வெப்பநிலை (கெல்வினில் `SLEEP_RGB_OR_COLOR_TEMP` என்பது` color_temp` ஆக இருக்கும்போது பயன்படுத்தப்படுகிறது). ." + }, + "sunset_offset": { + "description": "வினாடிகளில் நேர்மறை அல்லது எதிர்மறை ஆஃப்செட் மூலம் சூரிய மறைவு நேரத்தை சரிசெய்யவும். ." + }, + "sunset_time": { + "description": "சூரிய அச்தமனத்திற்கு ஒரு நிலையான நேரத்தை (HH: MM: SS) அமைக்கவும். ." + }, + "max_sunrise_time": { + "description": "ஆரம்பகால சூரிய உதயத்தை அனுமதிக்கும் அண்மைக் கால மெய்நிகர் சூரிய தோன்றுகை நேரத்தை (HH: MM: SS) அமைக்கவும். ." + }, + "min_sunset_time": { + "description": "ஆரம்பகால மெய்நிகர் சூரிய மறைவு நேரத்தை (HH: MM: SS) அமைக்கவும், பின்னர் சூரிய அச்தமனங்களை அனுமதிக்கிறது. ." + }, + "take_over_control": { + "description": "விளக்குகள் இயக்கத்தில் இருக்கும்போது மற்றொரு சான்று `லைட்.டர்ன்_ஒன்` என்று அழைத்தால் தகவமைப்பு விளக்குகளை முடக்கு. இது `ஓமாசிச்டன்ட்.பிடேட்_என்டிட்டி` ஒவ்வொரு` இடைவெளியையும் 'என்று அழைக்கிறது என்பதை நினைவில் கொள்க! ." + }, + "detect_non_ha_changes": { + "description": "`விளக்கு அல்லாத. டர்ன்_ஓஎன்` மாநில மாற்றங்களுக்கான தழுவல்களைக் கண்டறிந்து நிறுத்துகிறது. `Take_over_control` இயக்கப்பட்டது. 🕵œ எச்சரிக்கை: ⚠œ சில விளக்குகள் ஒரு 'ஆன்' நிலையை பொய்யாகக் குறிக்கக்கூடும், இதனால் விளக்குகள் எதிர்பாராத விதமாக இயக்கப்படலாம். இதுபோன்ற சிக்கல்களை நீங்கள் சந்தித்தால் இந்த அம்சத்தை முடக்கு." + }, + "transition": { + "description": "விளக்குகள் மாறும்போது, நொடிகளில் மாற்றத்தின் காலம். ." + }, + "adapt_delay": { + "description": "லைட் டர்ன் மற்றும் தகவமைப்பு விளக்குகள் இடையே காத்திருப்பு நேரம் (விநாடிகள்) மாற்றங்களைப் பயன்படுத்துகிறது. ஒளிரும் தவிர்க்க உதவலாம். ." + }, + "autoreset_control_seconds": { + "description": "பல விநாடிகளுக்குப் பிறகு தானாகவே கையேடு கட்டுப்பாட்டை மீட்டமைக்கவும். முடக்க 0 என அமைக்கவும். ." + } + }, + "description": "சுவிட்சில் நீங்கள் விரும்பும் எந்த அமைப்புகளையும் மாற்றவும். இங்குள்ள அனைத்து விருப்பங்களும் கட்டமைப்பு ஓட்டத்தில் உள்ளதைப் போலவே இருக்கும்." + }, + "apply": { + "description": "தற்போதைய தகவமைப்பு லைட்டிங் அமைப்புகளை விளக்குகளுக்கு பயன்படுத்துகிறது.", + "fields": { + "entity_id": { + "description": "விண்ணப்பிக்க அமைப்புகளுடன் சுவிட்சின் `ENTITY_ID`. ." + }, + "lights": { + "description": "அமைப்புகளைப் பயன்படுத்த ஒரு ஒளி (அல்லது விளக்குகளின் பட்டியல்). ." + }, + "transition": { + "description": "விளக்குகள் மாறும்போது, நொடிகளில் மாற்றத்தின் காலம். ." + }, + "adapt_brightness": { + "description": "ஒளியின் பிரகாசத்தை மாற்றியமைக்க வேண்டுமா. ." + }, + "adapt_color": { + "description": "துணை விளக்குகளில் வண்ணத்தை மாற்றியமைக்க வேண்டுமா. ." + }, + "prefer_rgb_color": { + "description": "முடிந்தவரை ஒளி வண்ண வெப்பநிலையை விட RGB வண்ண சரிசெய்தலை விரும்பலாமா. ." + }, + "turn_on_lights": { + "description": "தற்போது முடக்கப்பட்ட விளக்குகளை இயக்க வேண்டுமா. ." + } + } + } + }, + "title": "தகவமைப்பு விளக்குகள்", + "config": { + "step": { + "user": { + "title": "தகவமைப்பு விளக்கு உதாரணத்திற்கு ஒரு பெயரைத் தேர்வுசெய்க", + "description": "ஒவ்வொரு நிகழ்விலும் பல விளக்குகள் இருக்கலாம்!" + } + }, + "abort": { + "already_configured": "இந்த சாதனம் ஏற்கனவே கட்டமைக்கப்பட்டுள்ளது" + } + }, + "options": { + "step": { + "init": { + "title": "தகவமைப்பு விளக்கு விருப்பங்கள்", + "description": "தகவமைப்பு விளக்கு கூறுகளை உள்ளமைக்கவும். விருப்பப் பெயர்கள் YAML அமைப்புகளுடன் சீரமைக்கப்படுகின்றன. இந்த உள்ளீட்டை நீங்கள் YAML இல் வரையறுத்திருந்தால், இங்கே எந்த விருப்பங்களும் தோன்றாது. அளவுரு விளைவுகளை நிரூபிக்கும் ஊடாடும் வரைபடங்களுக்கு, [இந்த வலை பயன்பாடு] (https://basnijholt.github.io/adaptive-lighting) ஐப் பார்வையிடவும். மேலும் விவரங்களுக்கு, [அதிகாரப்பூர்வ ஆவணங்கள்] (https://github.com/basnijholt/adaptive-lighting#readme) ஐப் பார்க்கவும்.", + "data": { + "lights": "விளக்குகள்: கட்டுப்படுத்தப்பட வேண்டிய ஒளி நிறுவனம்_டுகளின் பட்டியல் (காலியாக இருக்கலாம்). .", + "min_brightness": "min_brightness: குறைந்தபட்ச ஒளி விழுக்காடு. .", + "max_brightness": "அதிகபட்ச பிரகாசம்: அதிகபட்ச ஒளி விழுக்காடு. .", + "min_color_temp": "min_color_temp: கெல்வினில் வெப்பமான வண்ண வெப்பநிலை. .", + "max_color_temp": "MAX_COLOR_TEMP: கெல்வினில் குளிரான வண்ண வெப்பநிலை. .", + "prefer_rgb_color": "bey_rgb_color: முடிந்தவரை ஒளி வண்ண வெப்பநிலையை விட RGB வண்ண சரிசெய்தலை விரும்பலாமா. .", + "transition_until_sleep": "Transition_until_sleep: இயக்கப்பட்டால், தகவமைப்பு விளக்குகள் தூக்க அமைப்புகளை குறைந்தபட்சமாகக் கருதும், சூரிய அச்தமனத்திற்குப் பிறகு இந்த மதிப்புகளுக்கு மாறும். .", + "take_over_control": "Take_over_control: விளக்குகள் இயக்கத்தில் இருக்கும்போது மற்றொரு சான்று `லைட்.டர்ன்_ஓஎன்` என்று அழைத்தால் தகவமைப்பு விளக்குகளை முடக்கு. இது `ஓமாசிச்டன்ட்.பிடேட்_என்டிட்டி` ஒவ்வொரு` இடைவெளியையும் 'என்று அழைக்கிறது என்பதை நினைவில் கொள்க! .", + "detect_non_ha_changes": "கண்டறிதல்_நான்_ஆ_சேஞ்ச்ச்: `விளக்கு அல்லாத. டர்ன்_ஓஎன்` மாநில மாற்றங்களுக்கான தழுவல்களைக் கண்டறிந்து நிறுத்துகிறது. `Take_over_control` இயக்கப்பட்டது. 🕵œ எச்சரிக்கை: ⚠œ சில விளக்குகள் ஒரு 'ஆன்' நிலையை பொய்யாகக் குறிக்கக்கூடும், இதனால் விளக்குகள் எதிர்பாராத விதமாக இயக்கப்படலாம். இதுபோன்ற சிக்கல்களை நீங்கள் சந்தித்தால் இந்த அம்சத்தை முடக்கு.", + "only_once": "மட்டும்_இன்: விளக்குகள் இயக்கப்படும்போது மட்டுமே (`உண்மை`) மாற்றியமைக்கும்போது அல்லது அவற்றைத் தழுவிக்கொள்ளுங்கள் (` தவறு`). .", + "adapt_only_on_bare_turn_on": "Sadve_only_on_bare_turn_on: ஆரம்பத்தில் விளக்குகளை இயக்கும்போது. `உண்மை` என அமைக்கப்பட்டால், வண்ணம் அல்லது பிரகாசத்தைக் குறிப்பிடாமல்` லைட்.டர்ன்_ஓஎன்` செயல்படுத்தப்பட்டால் மட்டுமே அல் மாற்றியமைக்கிறது. ❌🌈 இது எ.கா., ஒரு காட்சியை செயல்படுத்தும்போது தழுவலைத் தடுக்கிறது. `தவறு` என்றால், ஆரம்ப` சேவை_டா` இல் நிறம் அல்லது ஒளி இருப்பதைப் பொருட்படுத்தாமல் AL மாற்றியமைக்கிறது. `Take_over_control` இயக்கப்பட்டது. . ", + "separate_turn_on_commands": "தனித்தனி_டர்ன்_ஆன்_காமண்ட்ச்: சில ஒளி வகைகளுக்கு தேவைப்படும் வண்ணம் மற்றும் பிரகாசத்திற்கான தனித்தனி `லைட்.டர்ன்_ஓஎன்` அழைப்புகளைப் பயன்படுத்தவும். .", + "skip_redundant_commands": "Skip_redundant_commands: தழுவல் கட்டளைகளை அனுப்புவதைத் தவிர்க்கவும், அதன் இலக்கு நிலை ஏற்கனவே ஒளியின் அறியப்பட்ட நிலைக்கு சமம். பிணையம் போக்குவரத்தை குறைக்கிறது மற்றும் சில சூழ்நிலைகளில் தழுவல் மறுமொழியை மேம்படுத்துகிறது. ஆ இன் பதிவு செய்யப்பட்ட நிலையுடன் இயற்பியல் ஒளி நிலைகள் ஒத்திசைவிலிருந்து வெளியேறினால் அது காணக்கூடியது.", + "intercept": "இடைமறிப்பு: உடனடி வண்ணம் மற்றும் பிரகாசமான தழுவலை செயல்படுத்த `ஒளி. Color வண்ணம் மற்றும் பிரகாசத்துடன் `ஒளி.", + "multi_light_intercept": "Mulli_light_intect: பல விளக்குகளை குறிவைக்கும் `light.turn_on` அழைப்புகளை இடைமறிக்கவும் மாற்றவும். ➗⚠œ இது ஒரு `லைட்.டர்ன்_ஒன்` அழைப்பை பல அழைப்புகளாக பிரிக்கக்கூடும், எ.கா., விளக்குகள் வெவ்வேறு சுவிட்சுகளில் இருக்கும்போது. இயக்கப்பட வேண்டும் `இடைமறிப்பு` தேவை.", + "include_config_in_attributes": "அடங்கும்_கான்ஃபிக்_இன்_அட்ரிபியூட்: `உண்மை` என அமைக்கப்பட்டிருக்கும் போது வீட்டு உதவியாளரின் சுவிட்சில் உள்ள பண்புகளாக அனைத்து விருப்பங்களையும் காட்டுங்கள். ." + }, + "data_description": { + "interval": "விளக்குகளை மாற்றியமைக்க அதிர்வெண், நொடிகளில். .", + "transition": "விளக்குகள் மாறும்போது, நொடிகளில் மாற்றத்தின் காலம். .", + "initial_transition": "விளக்குகள் `ஆஃப்` முதல்` ஆன் `வரை நொடிகளில் மாறும் போது முதல் மாற்றத்தின் காலம். .", + "sleep_brightness": "தூக்க பயன்முறையில் விளக்குகளின் ஒளி விழுக்காடு. .", + "sleep_rgb_or_color_temp": "தூக்க பயன்முறையில் `\" rgb_color \"` அல்லது `\" Color_Temp \"key ஐப் பயன்படுத்தவும். .", + "sleep_color_temp": "ச்லீப் பயன்முறையில் வண்ண வெப்பநிலை (கெல்வினில் `SLEEP_RGB_OR_COLOR_TEMP` என்பது` color_temp` ஆக இருக்கும்போது பயன்படுத்தப்படுகிறது). .", + "sleep_rgb_color": "தூக்க பயன்முறையில் RGB வண்ணம் (`SLEEP_RGB_OR_COLOR_TEMP`\" RGB_COLOR \"ஆக இருக்கும்போது பயன்படுத்தப்படுகிறது). .", + "sleep_transition": "\"தூக்க பயன்முறை\" நொடிகளில் மாற்றப்படும்போது மாற்றத்தின் காலம். .", + "sunrise_time": "சூரிய உதயத்திற்கு ஒரு நிலையான நேரத்தை (HH: MM: SS) அமைக்கவும். .", + "min_sunrise_time": "ஆரம்பகால மெய்நிகர் சூரிய உதய நேரத்தை (HH: MM: SS) அமைக்கவும், பின்னர் சூரிய உதயங்களை அனுமதிக்கிறது. .", + "max_sunrise_time": "ஆரம்பகால சூரிய உதயத்தை அனுமதிக்கும் அண்மைக் கால மெய்நிகர் சூரிய தோன்றுகை நேரத்தை (HH: MM: SS) அமைக்கவும். .", + "sunrise_offset": "விநாடிகளில் நேர்மறை அல்லது எதிர்மறை ஆஃப்செட் மூலம் சூரிய தோன்றுகை நேரத்தை சரிசெய்யவும். .", + "sunset_time": "சூரிய அச்தமனத்திற்கு ஒரு நிலையான நேரத்தை (HH: MM: SS) அமைக்கவும். .", + "min_sunset_time": "ஆரம்பகால மெய்நிகர் சூரிய மறைவு நேரத்தை (HH: MM: SS) அமைக்கவும், பின்னர் சூரிய அச்தமனங்களை அனுமதிக்கிறது. .", + "max_sunset_time": "முந்தைய சூரிய அச்தமனங்களை அனுமதிக்கும் அண்மைக் கால மெய்நிகர் சன்செட் நேரத்தை (HH: MM: SS) அமைக்கவும். .", + "sunset_offset": "விநாடிகளில் நேர்மறை அல்லது எதிர்மறை ஆஃப்செட் மூலம் சூரிய மறைவு நேரத்தை சரிசெய்யவும். .", + "send_split_delay": "ஒரே நேரத்தில் ஒளி மற்றும் வண்ண அமைப்பை ஆதரிக்காத விளக்குகளுக்கு `தனி_டர்ன்_ஆன்_காமண்ட்ச்` இடையே நேரந்தவறுகை (எம்.எச்). .", + "adapt_delay": "லைட் டர்ன் மற்றும் தகவமைப்பு விளக்குகள் இடையே காத்திருப்பு நேரம் (விநாடிகள்) மாற்றங்களைப் பயன்படுத்துகிறது. ஒளிரும் தவிர்க்க உதவலாம். .", + "brightness_mode": "பயன்படுத்த பிரகாசமான முறை. சாத்தியமான மதிப்புகள் `இயல்புநிலை`,` லீனியர்`, மற்றும் `டான்` (` பிரகாசம்_மோட்_ நேரம்_டார்க்` மற்றும் `பிரகாசம்_மோட்_மட்_லிட்` ஆகியவற்றைப் பயன்படுத்துகின்றன). .", + "brightness_mode_time_dark": ". .", + "brightness_mode_time_light": ". ..", + "autoreset_control_seconds": "பல விநாடிகளுக்குப் பிறகு தானாகவே கையேடு கட்டுப்பாட்டை மீட்டமைக்கவும். முடக்க 0 என அமைக்கவும். ." + } + } + }, + "error": { + "option_error": "தவறான விருப்பம்", + "entity_missing": "ஒன்று அல்லது அதற்கு மேற்பட்ட தேர்ந்தெடுக்கப்பட்ட ஒளி நிறுவனங்கள் வீட்டு உதவியாளரிடமிருந்து காணவில்லை" + } + } +} From 54f43076495ee0ab7e76e17f81029791d3b74ca1 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:28:01 +0200 Subject: [PATCH 0794/1077] docs: add Thunderstrike116 as a contributor for translation (#1095) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 492cdaab..cdf03c35 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -900,6 +900,15 @@ "contributions": [ "translation" ] + }, + { + "login": "Thunderstrike116", + "name": "Thunderstrike116", + "avatar_url": "https://avatars.githubusercontent.com/u/23220766?v=4", + "profile": "https://github.com/Thunderstrike116", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 2b0f646e..0fa68d93 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-98-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-99-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -591,6 +591,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + + + From 4006fe0b1565a2cc4cf8814ceec7068f57b259d5 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:28:32 +0200 Subject: [PATCH 0795/1077] docs: add immeteor2 as a contributor for translation (#1105) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index cdf03c35..1edee2ae 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -909,6 +909,15 @@ "contributions": [ "translation" ] + }, + { + "login": "immeteor2", + "name": "immeteor2", + "avatar_url": "https://avatars.githubusercontent.com/u/125735487?v=4", + "profile": "https://github.com/immeteor2", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 0fa68d93..5c0213b5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-99-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-100-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -593,6 +593,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 4e2f02fbc2b4e18b6f56b55f8e7f535f10331938 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:28:58 +0200 Subject: [PATCH 0796/1077] docs: add pbassut as a contributor for translation (#1110) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 1edee2ae..c01e17a5 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -918,6 +918,15 @@ "contributions": [ "translation" ] + }, + { + "login": "pbassut", + "name": "Patrick Bassut", + "avatar_url": "https://avatars.githubusercontent.com/u/1500037?v=4", + "profile": "https://github.com/pbassut", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 5c0213b5..71cc87a8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-100-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-101-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -594,6 +594,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 33a7f56a3177e8d3f2f88fe6b14b31349f965acb Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:29:38 +0200 Subject: [PATCH 0797/1077] docs: add Ricky-Tigg as a contributor for translation (#1120) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index c01e17a5..a78192bc 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -927,6 +927,15 @@ "contributions": [ "translation" ] + }, + { + "login": "Ricky-Tigg", + "name": "Ricky Tigg", + "avatar_url": "https://avatars.githubusercontent.com/u/26058215?v=4", + "profile": "https://github.com/Ricky-Tigg", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 71cc87a8..78e8e260 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-101-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-102-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -595,6 +595,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 7e98bdd46a4fdaf474b1f934e3243ba78c07d896 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:31:36 +0200 Subject: [PATCH 0798/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20websock?= =?UTF-8?q?ets=20to=20v14=20(#1124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index 4db3cb83..b1c274b3 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -71,7 +71,7 @@ uvicorn==0.25.0 # via shiny watchfiles==0.21.0 # via shiny -websockets==12.0 +websockets==14.1 # via shiny xstatic-bootswatch==3.3.7.0 # via shinyswatch From 3811df7c573a8c963543725c58d1b773bbf2b993 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:40:26 +0200 Subject: [PATCH 0799/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20watchfi?= =?UTF-8?q?les=20to=20v1=20(#1123)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index b1c274b3..99b384ad 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -69,7 +69,7 @@ uc-micro-py==1.0.2 # via linkify-it-py uvicorn==0.25.0 # via shiny -watchfiles==0.21.0 +watchfiles==1.0.0 # via shiny websockets==14.1 # via shiny From 2a3acd52065a6a5f822d7b6407b8707337a02052 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:40:36 +0200 Subject: [PATCH 0800/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20ubuntu?= =?UTF-8?q?=20to=20v24=20(#1122)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/pytest.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index b03f0023..6c4c176a 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -8,7 +8,7 @@ on: jobs: pytest: name: Run pytest - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 timeout-minutes: 60 strategy: fail-fast: false From 8855f3313e9729da2e521f5ca1effcc44c42d627 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:41:15 +0200 Subject: [PATCH 0801/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20shiny?= =?UTF-8?q?=20to=20v1=20(#1121)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- webapp/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index 99b384ad..daac1fae 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -47,7 +47,7 @@ python-multipart==0.0.18 # via shiny pytz==2023.3.post1 # via astral -shiny==0.5.0 +shiny==1.2.1 # via # -r requirements.txt # shinylive diff --git a/webapp/requirements.txt b/webapp/requirements.txt index 54732c09..84e45334 100644 --- a/webapp/requirements.txt +++ b/webapp/requirements.txt @@ -1,4 +1,4 @@ shinylive==0.1.1 astral==2.2 shinyswatch==0.3.1 -shiny==0.5.0 +shiny==1.2.1 From 2a24c519fe723a7d3aad350d45696c910003d1e2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:41:23 +0200 Subject: [PATCH 0802/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20release?= =?UTF-8?q?-drafter/release-drafter=20action=20to=20v6=20(#1119)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release-drafter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 12b1f6c2..e3badf0f 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -17,6 +17,6 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: release-drafter/release-drafter@v5 + - uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 7d418be392fbc845baf5e77c1c2b4dd6b6b1965f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:41:34 +0200 Subject: [PATCH 0803/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20pytz=20?= =?UTF-8?q?to=20v2024=20(#1118)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index daac1fae..75c07a61 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -45,7 +45,7 @@ packaging==23.2 # shinyswatch python-multipart==0.0.18 # via shiny -pytz==2023.3.post1 +pytz==2024.2 # via astral shiny==1.2.1 # via From 0d29b1d60c9d0954217abd4313439c9e89e03ee6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:41:41 +0200 Subject: [PATCH 0804/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20packagi?= =?UTF-8?q?ng=20to=20v24=20(#1117)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index 75c07a61..9f1ce3d4 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -39,7 +39,7 @@ mdit-py-plugins==0.4.0 # via shiny mdurl==0.1.2 # via markdown-it-py -packaging==23.2 +packaging==24.2 # via # htmltools # shinyswatch From 584368c1a484b49b513d5da167d417e853bbf21b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:41:49 +0200 Subject: [PATCH 0805/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20docker/?= =?UTF-8?q?setup-qemu-action=20action=20to=20v3=20(#1116)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 1f6099d0..426f15dd 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -15,7 +15,7 @@ jobs: - linux/arm64 steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Login to Docker Hub From b8306c2b2189ded2bd7fd755862677186d8f64da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:41:54 +0200 Subject: [PATCH 0806/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20docker/?= =?UTF-8?q?setup-buildx-action=20action=20to=20v3=20(#1115)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 426f15dd..4a39d531 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -17,7 +17,7 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub uses: docker/login-action@v2 with: From dd1090cf52d88884f98e6045892a1cb127ede2e1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:42:07 +0200 Subject: [PATCH 0807/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20docker/?= =?UTF-8?q?login-action=20action=20to=20v3=20(#1114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 4a39d531..5b91de29 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} From 57c36133f7b02151e7d1851713259afe9da84d70 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:42:14 +0200 Subject: [PATCH 0808/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20docker/?= =?UTF-8?q?build-push-action=20action=20to=20v6=20(#1113)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 5b91de29..3e088e96 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -24,7 +24,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v6 with: # Only push on the master branch push: ${{ github.ref == 'refs/heads/master' }} From e4f5163f164255b7015d9a50051432de88fb9109 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:42:48 +0200 Subject: [PATCH 0809/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/upload-pages-artifact=20action=20to=20v3=20(#1111)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/deploy-webapp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml index 51cbab81..38f83fee 100644 --- a/.github/workflows/deploy-webapp.yml +++ b/.github/workflows/deploy-webapp.yml @@ -52,7 +52,7 @@ jobs: uses: actions/configure-pages@v3 - name: Upload artifact - uses: actions/upload-pages-artifact@v2 + uses: actions/upload-pages-artifact@v3 with: # Upload the 'site' directory, where your app has been built path: "site" From 577e6b21387533e5bc9544ea11fae68771a8d046 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:43:09 +0200 Subject: [PATCH 0810/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/setup-python=20action=20to=20v5=20(#1109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/deploy-webapp.yml | 2 +- .github/workflows/install_dependencies/action.yml | 2 +- .github/workflows/pre-commit.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml index 38f83fee..7dc8b8bc 100644 --- a/.github/workflows/deploy-webapp.yml +++ b/.github/workflows/deploy-webapp.yml @@ -33,7 +33,7 @@ jobs: uses: actions/checkout@v3 - name: Set Up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: 3.x diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index badc6566..37379c41 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -28,7 +28,7 @@ runs: ref: ${{ inputs.core-version }} - name: Set up Python ${{ inputs.python-version }} id: python - uses: actions/setup-python@v4.1.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ inputs.python-version }} - name: Install dependencies diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index f46e01d4..050cf6b0 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -10,5 +10,5 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v5 - uses: pre-commit/action@v3.0.0 From 9082ab349d65b15b9b260fb5d2c7d6d9c729c0e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:43:52 +0200 Subject: [PATCH 0811/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/deploy-pages=20action=20to=20v4=20(#1108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/deploy-webapp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml index 7dc8b8bc..e86a8c93 100644 --- a/.github/workflows/deploy-webapp.yml +++ b/.github/workflows/deploy-webapp.yml @@ -59,4 +59,4 @@ jobs: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v4 From aab8d1bd6d2ffb0caadfd804aee0d3a0408ff9df Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:44:39 +0200 Subject: [PATCH 0812/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/configure-pages=20action=20to=20v5=20(#1107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/deploy-webapp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml index e86a8c93..aacb861d 100644 --- a/.github/workflows/deploy-webapp.yml +++ b/.github/workflows/deploy-webapp.yml @@ -49,7 +49,7 @@ jobs: shinylive export webapp site - name: Setup Pages - uses: actions/configure-pages@v3 + uses: actions/configure-pages@v5 - name: Upload artifact uses: actions/upload-pages-artifact@v3 From dfac56ca89382c8ec90cb0e9d236296ca4eb86d1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:44:52 +0200 Subject: [PATCH 0813/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20uvicorn?= =?UTF-8?q?=20to=20v0.32.1=20(#1103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index 9f1ce3d4..b35744b8 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -67,7 +67,7 @@ typing-extensions==4.9.0 # shinyswatch uc-micro-py==1.0.2 # via linkify-it-py -uvicorn==0.25.0 +uvicorn==0.32.1 # via shiny watchfiles==1.0.0 # via shiny From d5f992a12d0d80f60d90eccd75cc74c312c9d30a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:45:31 +0200 Subject: [PATCH 0814/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20typing-?= =?UTF-8?q?extensions=20to=20v4.12.2=20(#1102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index b35744b8..4e06fdcf 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -60,7 +60,7 @@ sniffio==1.3.0 # via anyio starlette==0.40.0 # via shiny -typing-extensions==4.9.0 +typing-extensions==4.12.2 # via # htmltools # shiny From 186da900aaddc11c8224292431684b35e74df2ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:45:39 +0200 Subject: [PATCH 0815/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20starlet?= =?UTF-8?q?te=20to=20v0.41.3=20(#1101)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index 4e06fdcf..28773147 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -58,7 +58,7 @@ shinyswatch==0.3.1 # via -r requirements.txt sniffio==1.3.0 # via anyio -starlette==0.40.0 +starlette==0.41.3 # via shiny typing-extensions==4.12.2 # via From 5450e0d3adc2bdad3bf22acef70e76d0b858c267 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:46:17 +0200 Subject: [PATCH 0816/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20shinyli?= =?UTF-8?q?ve=20to=20v0.7.1=20(#1099)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- webapp/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index 28773147..45db72aa 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -52,7 +52,7 @@ shiny==1.2.1 # -r requirements.txt # shinylive # shinyswatch -shinylive==0.1.1 +shinylive==0.7.1 # via -r requirements.txt shinyswatch==0.3.1 # via -r requirements.txt diff --git a/webapp/requirements.txt b/webapp/requirements.txt index 84e45334..518357b1 100644 --- a/webapp/requirements.txt +++ b/webapp/requirements.txt @@ -1,4 +1,4 @@ -shinylive==0.1.1 +shinylive==0.7.1 astral==2.2 shinyswatch==0.3.1 shiny==1.2.1 From 8b5a48f4eef5299ceb7fd9e75f97f5ff324f471a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:46:26 +0200 Subject: [PATCH 0817/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20python?= =?UTF-8?q?=20Docker=20tag=20to=20v3.13=20(#1096)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5232b4d9..6e871795 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ # Optionally build the image yourself with: # docker build -t basnijholt/adaptive-lighting:latest . -FROM python:3.12-bookworm +FROM python:3.13-bookworm RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y \ From 8c5d65f9551fed9b62466e475ae5f4d63900e166 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:46:38 +0200 Subject: [PATCH 0818/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20idna=20?= =?UTF-8?q?to=20v3.10=20(#1094)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index 45db72aa..c507652d 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -27,7 +27,7 @@ htmltools==0.5.1 # via # shiny # shinyswatch -idna==3.7 +idna==3.10 # via anyio linkify-it-py==2.0.2 # via shiny From af09a6f3a3093c24f581fac64c4f2399b0e57077 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:46:46 +0200 Subject: [PATCH 0819/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20htmltoo?= =?UTF-8?q?ls=20to=20v0.6.0=20(#1093)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index c507652d..1ad1a3cc 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -23,7 +23,7 @@ click==8.1.7 # uvicorn h11==0.14.0 # via uvicorn -htmltools==0.5.1 +htmltools==0.6.0 # via # shiny # shinyswatch From 0c8feba1cf82c8050dec9e544b238243afdb3500 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:46:51 +0200 Subject: [PATCH 0820/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20asgiref?= =?UTF-8?q?=20to=20v3.8.1=20(#1092)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index 1ad1a3cc..bf1b9a2d 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -12,7 +12,7 @@ appdirs==1.4.4 # via # shiny # shinylive -asgiref==3.7.2 +asgiref==3.8.1 # via shiny astral==2.2 # via -r requirements.txt From ed434e6b489943fe56a5f092f7741c84536767c2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:46:57 +0200 Subject: [PATCH 0821/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20anyio?= =?UTF-8?q?=20to=20v4.7.0=20(#1091)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index bf1b9a2d..4e17a984 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -4,7 +4,7 @@ # # pip-compile --output-file=requirements-locked.txt requirements.txt # -anyio==4.2.0 +anyio==4.7.0 # via # starlette # watchfiles From d8255b2baddf6e3ab0d4403aa11c62a5b8222681 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:47:44 +0200 Subject: [PATCH 0822/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/checkout=20action=20to=20v3.6.0=20(#1088)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/hassfest.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml index cc16d185..de29b939 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/hassfest.yaml @@ -11,5 +11,5 @@ jobs: validate_hassfest: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v3.0.2" + - uses: "actions/checkout@v3.6.0" - uses: home-assistant/actions/hassfest@master From 56497176df35fe4cc7daa9d84afdc61de3324452 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:48:06 +0200 Subject: [PATCH 0823/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20shinysw?= =?UTF-8?q?atch=20to=20v0.8.0=20(#1100)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- webapp/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index 4e17a984..ea2f299e 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -54,7 +54,7 @@ shiny==1.2.1 # shinyswatch shinylive==0.7.1 # via -r requirements.txt -shinyswatch==0.3.1 +shinyswatch==0.8.0 # via -r requirements.txt sniffio==1.3.0 # via anyio diff --git a/webapp/requirements.txt b/webapp/requirements.txt index 518357b1..413a85d1 100644 --- a/webapp/requirements.txt +++ b/webapp/requirements.txt @@ -1,4 +1,4 @@ shinylive==0.7.1 astral==2.2 -shinyswatch==0.3.1 +shinyswatch==0.8.0 shiny==1.2.1 From c2d699555ef9798fbccb831ee1f128c64bcb8a01 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:48:13 +0200 Subject: [PATCH 0824/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20uc-micr?= =?UTF-8?q?o-py=20to=20v1.0.3=20(#1087)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index ea2f299e..8f6fc45b 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -65,7 +65,7 @@ typing-extensions==4.12.2 # htmltools # shiny # shinyswatch -uc-micro-py==1.0.2 +uc-micro-py==1.0.3 # via linkify-it-py uvicorn==0.32.1 # via shiny From cee0ae56c45161703d82ee6d0b017f35b818ac27 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:48:19 +0200 Subject: [PATCH 0825/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20sniffio?= =?UTF-8?q?=20to=20v1.3.1=20(#1086)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index 8f6fc45b..16f72e2a 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -56,7 +56,7 @@ shinylive==0.7.1 # via -r requirements.txt shinyswatch==0.8.0 # via -r requirements.txt -sniffio==1.3.0 +sniffio==1.3.1 # via anyio starlette==0.41.3 # via shiny From 32e3bbdeba2996c321fefdda7b3e3739a79484db Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:48:26 +0200 Subject: [PATCH 0826/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20python-?= =?UTF-8?q?multipart=20to=20v0.0.19=20(#1085)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index 16f72e2a..169e590a 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -43,7 +43,7 @@ packaging==24.2 # via # htmltools # shinyswatch -python-multipart==0.0.18 +python-multipart==0.0.19 # via shiny pytz==2024.2 # via astral From 0ba1fa554bd2f391c988381f2508b5b338b913c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:48:40 +0200 Subject: [PATCH 0827/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20pre-com?= =?UTF-8?q?mit/action=20action=20to=20v3.0.1=20(#1084)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/pre-commit.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 050cf6b0..b5dd0c0e 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -11,4 +11,4 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v5 - - uses: pre-commit/action@v3.0.0 + - uses: pre-commit/action@v3.0.1 From 5bd1adf3d2423e6cce04093bd986fcc76fb01c59 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:48:45 +0200 Subject: [PATCH 0828/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20mdit-py?= =?UTF-8?q?-plugins=20to=20v0.4.2=20(#1083)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index 169e590a..6e32b2fa 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -35,7 +35,7 @@ markdown-it-py==3.0.0 # via # mdit-py-plugins # shiny -mdit-py-plugins==0.4.0 +mdit-py-plugins==0.4.2 # via shiny mdurl==0.1.2 # via markdown-it-py From 6b567565ab616acb9fdd101d1c9e7d328cc57ffa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:48:51 +0200 Subject: [PATCH 0829/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20linkify?= =?UTF-8?q?-it-py=20to=20v2.0.3=20(#1082)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- webapp/requirements-locked.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index 6e32b2fa..e788711f 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -29,7 +29,7 @@ htmltools==0.6.0 # shinyswatch idna==3.10 # via anyio -linkify-it-py==2.0.2 +linkify-it-py==2.0.3 # via shiny markdown-it-py==3.0.0 # via From 4739863fb242fa781c98b8bea48165244cf15091 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:49:56 +0200 Subject: [PATCH 0830/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/checkout=20action=20to=20v4=20(#1106)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/deploy-webapp.yml | 2 +- .github/workflows/hassfest.yaml | 2 +- .github/workflows/install_dependencies/action.yml | 4 ++-- .github/workflows/main-to-master-sync.yml | 2 +- .github/workflows/pre-commit.yaml | 2 +- .github/workflows/pytest.yaml | 2 +- .github/workflows/update-readme.yml | 2 +- .github/workflows/validate.yml | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml index aacb861d..44d3d0df 100644 --- a/.github/workflows/deploy-webapp.yml +++ b/.github/workflows/deploy-webapp.yml @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set Up Python uses: actions/setup-python@v5 diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml index de29b939..4d141e56 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/hassfest.yaml @@ -11,5 +11,5 @@ jobs: validate_hassfest: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v3.6.0" + - uses: "actions/checkout@v4.2.2" - uses: home-assistant/actions/hassfest@master diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index 37379c41..c1f2ddd8 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -14,14 +14,14 @@ runs: using: "composite" steps: - name: Check out code from GitHub - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: ${{ github.repository }} ref: ${{ github.ref }} persist-credentials: false fetch-depth: 0 - name: Check out code from GitHub - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: home-assistant/core path: core diff --git a/.github/workflows/main-to-master-sync.yml b/.github/workflows/main-to-master-sync.yml index 99cd85b3..d5c915c8 100644 --- a/.github/workflows/main-to-master-sync.yml +++ b/.github/workflows/main-to-master-sync.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: ref: main fetch-depth: 0 diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index b5dd0c0e..7f579afa 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -9,6 +9,6 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 6c4c176a..4d7472de 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -62,7 +62,7 @@ jobs: core-version: "dev" steps: - name: Check out code from GitHub - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Home Assistant uses: ./.github/workflows/install_dependencies diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index bf85fce3..456e6626 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code from GitHub - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Home Assistant uses: ./.github/workflows/install_dependencies diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 2bb88b96..79c7fe00 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -11,7 +11,7 @@ jobs: validate_hacs: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v2" + - uses: "actions/checkout@v4" - name: HACS validation uses: "hacs/action@main" with: From c12b54e265b253dce4724ca6352e287fd1432eb4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 5 Dec 2024 11:55:44 -0800 Subject: [PATCH 0831/1077] Use uv pip compile --- webapp/requirements-locked.txt | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt index e788711f..b946c919 100644 --- a/webapp/requirements-locked.txt +++ b/webapp/requirements-locked.txt @@ -1,9 +1,5 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile --output-file=requirements-locked.txt requirements.txt -# +# This file was autogenerated by uv via the following command: +# uv pip compile --output-file=requirements-locked.txt requirements.txt anyio==4.7.0 # via # starlette @@ -16,11 +12,15 @@ asgiref==3.8.1 # via shiny astral==2.2 # via -r requirements.txt +chevron==0.14.0 + # via shinylive click==8.1.7 # via # shiny # shinylive # uvicorn +future==1.0.0 + # via lzstring h11==0.14.0 # via uvicorn htmltools==0.6.0 @@ -31,6 +31,8 @@ idna==3.10 # via anyio linkify-it-py==2.0.3 # via shiny +lzstring==1.0.4 + # via shinylive markdown-it-py==3.0.0 # via # mdit-py-plugins @@ -39,14 +41,29 @@ mdit-py-plugins==0.4.2 # via shiny mdurl==0.1.2 # via markdown-it-py +narwhals==1.15.2 + # via shiny +orjson==3.10.12 + # via shiny packaging==24.2 # via # htmltools + # shiny # shinyswatch +prompt-toolkit==3.0.36 + # via + # questionary + # shiny python-multipart==0.0.19 # via shiny pytz==2024.2 # via astral +questionary==2.0.1 + # via shiny +setuptools==75.6.0 + # via + # shiny + # shinylive shiny==1.2.1 # via # -r requirements.txt @@ -64,6 +81,7 @@ typing-extensions==4.12.2 # via # htmltools # shiny + # shinylive # shinyswatch uc-micro-py==1.0.3 # via linkify-it-py @@ -71,7 +89,7 @@ uvicorn==0.32.1 # via shiny watchfiles==1.0.0 # via shiny +wcwidth==0.2.13 + # via prompt-toolkit websockets==14.1 # via shiny -xstatic-bootswatch==3.3.7.0 - # via shinyswatch From 8fb9493a40a33155ceac7aea96200ee68a8831c1 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 5 Dec 2024 14:35:19 -0800 Subject: [PATCH 0832/1077] Fix Shiny WebApp (#1127) * Use requirements.txt.in * Include webapp/color_and_brightness.py * Fix Shiny WebApp * Install shinylive in CI --- .github/workflows/deploy-webapp.yml | 3 +- webapp/app.py | 14 +- webapp/color_and_brightness.py | 521 ++++++++++++++++++++++++++++ webapp/requirements-locked.txt | 77 ---- webapp/requirements.txt | 8 +- webapp/requirements.txt.in | 1 + 6 files changed, 535 insertions(+), 89 deletions(-) create mode 100644 webapp/color_and_brightness.py delete mode 100644 webapp/requirements-locked.txt create mode 100644 webapp/requirements.txt.in diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml index 44d3d0df..f0dedea3 100644 --- a/.github/workflows/deploy-webapp.yml +++ b/.github/workflows/deploy-webapp.yml @@ -39,7 +39,8 @@ jobs: - name: Install Dependencies run: | - pip install -r webapp/requirements-locked.txt + pip install -r webapp/requirements.txt + pip install shinylive - name: Build the WebAssembly app run: | diff --git a/webapp/app.py b/webapp/app.py index ab896687..68dbb2f9 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -217,10 +217,9 @@ Dive into the simulator, experiment with different settings, and fine-tune the b # Shiny UI app_ui = ui.page_fluid( - shinyswatch.theme.sandstone(), ui.panel_title("🌞 Adaptive Lighting Simulator WebApp 🌛"), ui.layout_sidebar( - ui.panel_sidebar( + ui.sidebar( ui.input_switch("adapt_until_sleep", "adapt_until_sleep", value=False), ui.input_switch("sleep_mode", "sleep_mode", value=False), ui.input_slider("min_brightness", "min_brightness", 1, 100, 30, post="%"), @@ -277,13 +276,12 @@ app_ui = ui.page_fluid( post=" hr", ), ), - ui.panel_main( - ui.markdown(desc_top), - ui.output_plot(id="brightness_plot"), - ui.output_plot(id="color_temp_plot"), - ui.markdown(desc_bottom), - ), + ui.markdown(desc_top), + ui.output_plot(id="brightness_plot"), + ui.output_plot(id="color_temp_plot"), + ui.markdown(desc_bottom), ), + theme=shinyswatch.theme.sandstone, ) diff --git a/webapp/color_and_brightness.py b/webapp/color_and_brightness.py new file mode 100644 index 00000000..ba804df3 --- /dev/null +++ b/webapp/color_and_brightness.py @@ -0,0 +1,521 @@ +"""Switch for the Adaptive Lighting integration.""" + +from __future__ import annotations + +import bisect +import colorsys +import datetime +import logging +import math +from dataclasses import dataclass +from datetime import timedelta +from functools import cached_property, partial +from typing import TYPE_CHECKING, Any, Literal, cast + +from homeassistant_util_color import ( + color_RGB_to_xy, + color_temperature_to_rgb, + color_xy_to_hs, +) + +if TYPE_CHECKING: + import astral + +# Same as homeassistant.const.SUN_EVENT_SUNRISE and homeassistant.const.SUN_EVENT_SUNSET +# We re-define them here to not depend on homeassistant in this file. +SUN_EVENT_SUNRISE = "sunrise" +SUN_EVENT_SUNSET = "sunset" + +SUN_EVENT_NOON = "solar_noon" +SUN_EVENT_MIDNIGHT = "solar_midnight" + +_ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT) +_ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))} + +UTC = datetime.timezone.utc +utcnow: partial[datetime.datetime] = partial(datetime.datetime.now, UTC) +utcnow.__doc__ = "Get now in UTC time." + +_LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class SunEvents: + """Track the state of the sun and associated light settings.""" + + name: str + astral_location: astral.Location + sunrise_time: datetime.time | None + min_sunrise_time: datetime.time | None + max_sunrise_time: datetime.time | None + sunset_time: datetime.time | None + min_sunset_time: datetime.time | None + max_sunset_time: datetime.time | None + sunrise_offset: datetime.timedelta = datetime.timedelta() + sunset_offset: datetime.timedelta = datetime.timedelta() + timezone: datetime.tzinfo = UTC + + def sunrise(self, dt: datetime.date) -> datetime.datetime: + """Return the (adjusted) sunrise time for the given datetime.""" + sunrise = ( + self.astral_location.sunrise(dt, local=False) + if self.sunrise_time is None + else self._replace_time(dt, self.sunrise_time) + ) + self.sunrise_offset + if self.min_sunrise_time is not None: + min_sunrise = self._replace_time(dt, self.min_sunrise_time) + if min_sunrise > sunrise: + sunrise = min_sunrise + if self.max_sunrise_time is not None: + max_sunrise = self._replace_time(dt, self.max_sunrise_time) + if max_sunrise < sunrise: + sunrise = max_sunrise + return sunrise + + def sunset(self, dt: datetime.date) -> datetime.datetime: + """Return the (adjusted) sunset time for the given datetime.""" + sunset = ( + self.astral_location.sunset(dt, local=False) + if self.sunset_time is None + else self._replace_time(dt, self.sunset_time) + ) + self.sunset_offset + if self.min_sunset_time is not None: + min_sunset = self._replace_time(dt, self.min_sunset_time) + if min_sunset > sunset: + sunset = min_sunset + if self.max_sunset_time is not None: + max_sunset = self._replace_time(dt, self.max_sunset_time) + if max_sunset < sunset: + sunset = max_sunset + return sunset + + def _replace_time( + self, + dt: datetime.date, + time: datetime.time, + ) -> datetime.datetime: + date_time = datetime.datetime.combine(dt, time) + dt_with_tz = date_time.replace(tzinfo=self.timezone) + return dt_with_tz.astimezone(UTC) + + def noon_and_midnight( + self, + dt: datetime.datetime, + sunset: datetime.datetime | None = None, + sunrise: datetime.datetime | None = None, + ) -> tuple[datetime.datetime, datetime.datetime]: + """Return the (adjusted) noon and midnight times for the given datetime.""" + if ( + self.sunrise_time is None + and self.sunset_time is None + and self.min_sunrise_time is None + and self.max_sunrise_time is None + and self.min_sunset_time is None + and self.max_sunset_time is None + ): + solar_noon = self.astral_location.noon(dt, local=False) + solar_midnight = self.astral_location.midnight(dt, local=False) + return solar_noon, solar_midnight + + if sunset is None: + sunset = self.sunset(dt) + if sunrise is None: + sunrise = self.sunrise(dt) + + middle = abs(sunset - sunrise) / 2 + if sunset > sunrise: + noon = sunrise + middle + midnight = noon + timedelta(hours=12) * (1 if noon.hour < 12 else -1) + else: + midnight = sunset + middle + noon = midnight + timedelta(hours=12) * (1 if midnight.hour < 12 else -1) + return noon, midnight + + def sun_events(self, dt: datetime.datetime) -> list[tuple[str, float]]: + """Get the four sun event's timestamps at 'dt'.""" + sunrise = self.sunrise(dt) + sunset = self.sunset(dt) + solar_noon, solar_midnight = self.noon_and_midnight(dt, sunset, sunrise) + events = [ + (SUN_EVENT_SUNRISE, sunrise.timestamp()), + (SUN_EVENT_SUNSET, sunset.timestamp()), + (SUN_EVENT_NOON, solar_noon.timestamp()), + (SUN_EVENT_MIDNIGHT, solar_midnight.timestamp()), + ] + self._validate_sun_event_order(events) + return events + + def _validate_sun_event_order(self, events: list[tuple[str, float]]) -> None: + """Check if the sun events are in the expected order.""" + events = sorted(events, key=lambda x: x[1]) + events_names, _ = zip(*events, strict=True) + if events_names not in _ALLOWED_ORDERS: + msg = ( + f"{self.name}: The sun events {events_names} are not in the expected" + " order. The Adaptive Lighting integration will not work!" + " This might happen if your sunrise/sunset offset is too large or" + " your manually set sunrise/sunset time is past/before noon/midnight." + ) + _LOGGER.error(msg) + raise ValueError(msg) + + def prev_and_next_events(self, dt: datetime.datetime) -> list[tuple[str, float]]: + """Get the previous and next sun event.""" + events = [ + event + for days in [-1, 0, 1] + for event in self.sun_events(dt + timedelta(days=days)) + ] + events = sorted(events, key=lambda x: x[1]) + i_now = bisect.bisect([ts for _, ts in events], dt.timestamp()) + return events[i_now - 1 : i_now + 1] + + def sun_position(self, dt: datetime.datetime) -> float: + """Calculate the position of the sun, between [-1, 1].""" + target_ts = dt.timestamp() + (_, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt) + h, x = ( + (prev_ts, next_ts) + if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE) + else (next_ts, prev_ts) + ) + # k = -1 between sunset and sunrise (sun below horizon) + # k = 1 between sunrise and sunset (sun above horizon) + k = 1 if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_NOON) else -1 + return k * (1 - ((target_ts - h) / (h - x)) ** 2) + + def closest_event(self, dt: datetime.datetime) -> tuple[str, float]: + """Get the closest sunset or sunrise event.""" + (prev_event, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt) + if SUN_EVENT_SUNRISE in (prev_event, next_event): + ts_event = prev_ts if prev_event == SUN_EVENT_SUNRISE else next_ts + return SUN_EVENT_SUNRISE, ts_event + if SUN_EVENT_SUNSET in (prev_event, next_event): + ts_event = prev_ts if prev_event == SUN_EVENT_SUNSET else next_ts + return SUN_EVENT_SUNSET, ts_event + msg = "No sunrise or sunset event found." + raise ValueError(msg) + + +@dataclass(frozen=True) +class SunLightSettings: + """Track the state of the sun and associated light settings.""" + + name: str + astral_location: astral.Location + adapt_until_sleep: bool + max_brightness: int + max_color_temp: int + min_brightness: int + min_color_temp: int + sleep_brightness: int + sleep_rgb_or_color_temp: Literal["color_temp", "rgb_color"] + sleep_color_temp: int + sleep_rgb_color: tuple[int, int, int] + sunrise_time: datetime.time | None + min_sunrise_time: datetime.time | None + max_sunrise_time: datetime.time | None + sunset_time: datetime.time | None + min_sunset_time: datetime.time | None + max_sunset_time: datetime.time | None + brightness_mode_time_dark: datetime.timedelta + brightness_mode_time_light: datetime.timedelta + brightness_mode: Literal["default", "linear", "tanh"] = "default" + sunrise_offset: datetime.timedelta = datetime.timedelta() + sunset_offset: datetime.timedelta = datetime.timedelta() + timezone: datetime.tzinfo = UTC + + @cached_property + def sun(self) -> SunEvents: + """Return the SunEvents object.""" + return SunEvents( + name=self.name, + astral_location=self.astral_location, + sunrise_time=self.sunrise_time, + sunrise_offset=self.sunrise_offset, + min_sunrise_time=self.min_sunrise_time, + max_sunrise_time=self.max_sunrise_time, + sunset_time=self.sunset_time, + sunset_offset=self.sunset_offset, + min_sunset_time=self.min_sunset_time, + max_sunset_time=self.max_sunset_time, + timezone=self.timezone, + ) + + def _brightness_pct_default(self, dt: datetime.datetime) -> float: + """Calculate the brightness percentage using the default method.""" + sun_position = self.sun.sun_position(dt) + if sun_position > 0: + return self.max_brightness + delta_brightness = self.max_brightness - self.min_brightness + return (delta_brightness * (1 + sun_position)) + self.min_brightness + + def _brightness_pct_tanh(self, dt: datetime.datetime) -> float: + event, ts_event = self.sun.closest_event(dt) + dark = self.brightness_mode_time_dark.total_seconds() + light = self.brightness_mode_time_light.total_seconds() + if event == SUN_EVENT_SUNRISE: + brightness = scaled_tanh( + dt.timestamp() - ts_event, + x1=-dark, + x2=+light, + y1=0.05, # be at 5% of range at x1 + y2=0.95, # be at 95% of range at x2 + y_min=self.min_brightness, + y_max=self.max_brightness, + ) + elif event == SUN_EVENT_SUNSET: + brightness = scaled_tanh( + dt.timestamp() - ts_event, + x1=-light, # shifted timestamp for the start of sunset + x2=+dark, # shifted timestamp for the end of sunset + y1=0.95, # be at 95% of range at the start of sunset + y2=0.05, # be at 5% of range at the end of sunset + y_min=self.min_brightness, + y_max=self.max_brightness, + ) + return clamp(brightness, self.min_brightness, self.max_brightness) + + def _brightness_pct_linear(self, dt: datetime.datetime) -> float: + event, ts_event = self.sun.closest_event(dt) + # at ts_event - dt_start, brightness == start_brightness + # at ts_event + dt_end, brightness == end_brightness + dark = self.brightness_mode_time_dark.total_seconds() + light = self.brightness_mode_time_light.total_seconds() + if event == SUN_EVENT_SUNRISE: + brightness = lerp( + dt.timestamp() - ts_event, + x1=-dark, + x2=+light, + y1=self.min_brightness, + y2=self.max_brightness, + ) + elif event == SUN_EVENT_SUNSET: + brightness = lerp( + dt.timestamp() - ts_event, + x1=-light, + x2=+dark, + y1=self.max_brightness, + y2=self.min_brightness, + ) + return clamp(brightness, self.min_brightness, self.max_brightness) + + def brightness_pct(self, dt: datetime.datetime, is_sleep: bool) -> float: + """Calculate the brightness in %.""" + if is_sleep: + return self.sleep_brightness + assert self.brightness_mode in ("default", "linear", "tanh") + if self.brightness_mode == "default": + return self._brightness_pct_default(dt) + if self.brightness_mode == "linear": + return self._brightness_pct_linear(dt) + if self.brightness_mode == "tanh": + return self._brightness_pct_tanh(dt) + return None + + def color_temp_kelvin(self, sun_position: float) -> int: + """Calculate the color temperature in Kelvin.""" + if sun_position > 0: + delta = self.max_color_temp - self.min_color_temp + ct = (delta * sun_position) + self.min_color_temp + return 5 * round(ct / 5) # round to nearest 5 + if sun_position == 0 or not self.adapt_until_sleep: + return self.min_color_temp + if self.adapt_until_sleep and sun_position < 0: + delta = abs(self.min_color_temp - self.sleep_color_temp) + ct = (delta * abs(1 + sun_position)) + self.sleep_color_temp + return 5 * round(ct / 5) # round to nearest 5 + msg = "Should not happen" + raise ValueError(msg) + + def brightness_and_color( + self, + dt: datetime.datetime, + is_sleep: bool, + ) -> dict[str, Any]: + """Calculate the brightness and color.""" + sun_position = self.sun.sun_position(dt) + rgb_color: tuple[float, float, float] + # Variable `force_rgb_color` is needed for RGB color after sunset (if enabled) + force_rgb_color = False + brightness_pct = self.brightness_pct(dt, is_sleep) + if is_sleep: + color_temp_kelvin = self.sleep_color_temp + rgb_color = self.sleep_rgb_color + elif ( + self.sleep_rgb_or_color_temp == "rgb_color" + and self.adapt_until_sleep + and sun_position < 0 + ): + # Feature requested in + # https://github.com/basnijholt/adaptive-lighting/issues/624 + # This will result in a perceptible jump in color at sunset and sunrise + # because the `color_temperature_to_rgb` function is not 100% accurate. + min_color_rgb = color_temperature_to_rgb(self.min_color_temp) + rgb_color = lerp_color_hsv( + min_color_rgb, + self.sleep_rgb_color, + sun_position, + ) + color_temp_kelvin = self.color_temp_kelvin(sun_position) + force_rgb_color = True + else: + color_temp_kelvin = self.color_temp_kelvin(sun_position) + rgb_color = color_temperature_to_rgb(color_temp_kelvin) + # backwards compatibility for versions < 1.3.1 - see #403 + color_temp_mired: float = math.floor(1000000 / color_temp_kelvin) + xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color) + hs_color: tuple[float, float] = color_xy_to_hs(*xy_color) + return { + "brightness_pct": brightness_pct, + "color_temp_kelvin": color_temp_kelvin, + "color_temp_mired": color_temp_mired, + "rgb_color": rgb_color, + "xy_color": xy_color, + "hs_color": hs_color, + "sun_position": sun_position, + "force_rgb_color": force_rgb_color, + } + + def get_settings( + self, + is_sleep, + transition, + ) -> dict[str, float | int | tuple[float, float] | tuple[float, float, float]]: + """Get all light settings. + + Calculating all values takes <0.5ms. + """ + dt = utcnow() + timedelta(seconds=transition or 0) + return self.brightness_and_color(dt, is_sleep) + + +def find_a_b(x1: float, x2: float, y1: float, y2: float) -> tuple[float, float]: + """Compute the values of 'a' and 'b' for a scaled and shifted tanh function. + + Given two points (x1, y1) and (x2, y2), this function calculates the coefficients 'a' and 'b' + for a tanh function of the form y = 0.5 * (tanh(a * (x - b)) + 1) that passes through these points. + + The derivation is as follows: + + 1. Start with the equation of the tanh function: + y = 0.5 * (tanh(a * (x - b)) + 1) + + 2. Rearrange the equation to isolate tanh: + tanh(a * (x - b)) = 2*y - 1 + + 3. Take the inverse tanh (or artanh) on both sides to solve for 'a' and 'b': + a * (x - b) = artanh(2*y - 1) + + 4. Plug in the points (x1, y1) and (x2, y2) to get two equations. + Using these, we can solve for 'a' and 'b' as: + a = (artanh(2*y2 - 1) - artanh(2*y1 - 1)) / (x2 - x1) + b = x1 - (artanh(2*y1 - 1) / a) + + Parameters + ---------- + x1 + x-coordinate of the first point. + x2 + x-coordinate of the second point. + y1 + y-coordinate of the first point (should be between 0 and 1). + y2 + y-coordinate of the second point (should be between 0 and 1). + + Returns + ------- + a + Coefficient 'a' for the tanh function. + b + Coefficient 'b' for the tanh function. + + Notes + ----- + The values of y1 and y2 should lie between 0 and 1, inclusive. + + """ + a = (math.atanh(2 * y2 - 1) - math.atanh(2 * y1 - 1)) / (x2 - x1) + b = x1 - (math.atanh(2 * y1 - 1) / a) + return a, b + + +def scaled_tanh( + x: float, + x1: float, + x2: float, + y1: float = 0.05, + y2: float = 0.95, + y_min: float = 0.0, + y_max: float = 100.0, +) -> float: + """Apply a scaled and shifted tanh function to a given input. + + This function represents a transformation of the tanh function that scales and shifts + the output to lie between y_min and y_max. For values of 'x' close to 'x1' and 'x2' + (used to calculate 'a' and 'b'), the output of this function will be close to 'y_min' + and 'y_max', respectively. + + The equation of the function is as follows: + y = y_min + (y_max - y_min) * 0.5 * (tanh(a * (x - b)) + 1) + + Parameters + ---------- + x + The input to the function. + x1 + x-coordinate of the first point. + x2 + x-coordinate of the second point. + y1 + y-coordinate of the first point (should be between 0 and 1). Defaults to 0.05. + y2 + y-coordinate of the second point (should be between 0 and 1). Defaults to 0.95. + y_min + The minimum value of the output range. Defaults to 0. + y_max + The maximum value of the output range. Defaults to 100. + + Returns + ------- + float: The output of the function, which lies in the range [y_min, y_max]. + + """ + a, b = find_a_b(x1, x2, y1, y2) + return y_min + (y_max - y_min) * 0.5 * (math.tanh(a * (x - b)) + 1) + + +def lerp_color_hsv( + rgb1: tuple[float, float, float], + rgb2: tuple[float, float, float], + t: float, +) -> tuple[int, int, int]: + """Linearly interpolate between two RGB colors in HSV color space.""" + t = abs(t) + assert 0 <= t <= 1 + + # Convert RGB to HSV + hsv1 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb1]) + hsv2 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb2]) + + # Linear interpolation in HSV space + hsv = ( + hsv1[0] + t * (hsv2[0] - hsv1[0]), + hsv1[1] + t * (hsv2[1] - hsv1[1]), + hsv1[2] + t * (hsv2[2] - hsv1[2]), + ) + + # Convert back to RGB + rgb = tuple(int(round(x * 255)) for x in colorsys.hsv_to_rgb(*hsv)) + assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}" + return cast(tuple[int, int, int], rgb) + + +def lerp(x, x1, x2, y1, y2): + """Linearly interpolate between two values.""" + return y1 + (x - x1) * (y2 - y1) / (x2 - x1) + + +def clamp(value: float, minimum: float, maximum: float) -> float: + """Clamp value between minimum and maximum.""" + return max(minimum, min(value, maximum)) diff --git a/webapp/requirements-locked.txt b/webapp/requirements-locked.txt deleted file mode 100644 index e788711f..00000000 --- a/webapp/requirements-locked.txt +++ /dev/null @@ -1,77 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile --output-file=requirements-locked.txt requirements.txt -# -anyio==4.7.0 - # via - # starlette - # watchfiles -appdirs==1.4.4 - # via - # shiny - # shinylive -asgiref==3.8.1 - # via shiny -astral==2.2 - # via -r requirements.txt -click==8.1.7 - # via - # shiny - # shinylive - # uvicorn -h11==0.14.0 - # via uvicorn -htmltools==0.6.0 - # via - # shiny - # shinyswatch -idna==3.10 - # via anyio -linkify-it-py==2.0.3 - # via shiny -markdown-it-py==3.0.0 - # via - # mdit-py-plugins - # shiny -mdit-py-plugins==0.4.2 - # via shiny -mdurl==0.1.2 - # via markdown-it-py -packaging==24.2 - # via - # htmltools - # shinyswatch -python-multipart==0.0.19 - # via shiny -pytz==2024.2 - # via astral -shiny==1.2.1 - # via - # -r requirements.txt - # shinylive - # shinyswatch -shinylive==0.7.1 - # via -r requirements.txt -shinyswatch==0.8.0 - # via -r requirements.txt -sniffio==1.3.1 - # via anyio -starlette==0.41.3 - # via shiny -typing-extensions==4.12.2 - # via - # htmltools - # shiny - # shinyswatch -uc-micro-py==1.0.3 - # via linkify-it-py -uvicorn==0.32.1 - # via shiny -watchfiles==1.0.0 - # via shiny -websockets==14.1 - # via shiny -xstatic-bootswatch==3.3.7.0 - # via shinyswatch diff --git a/webapp/requirements.txt b/webapp/requirements.txt index 413a85d1..cbcadafc 100644 --- a/webapp/requirements.txt +++ b/webapp/requirements.txt @@ -1,4 +1,6 @@ -shinylive==0.7.1 +# This file was autogenerated by uv via the following command: +# uv pip compile --output-file=requirements.txt requirements.txt.in astral==2.2 -shinyswatch==0.8.0 -shiny==1.2.1 + # via -r requirements.txt.in +pytz==2023.3.post1 + # via astral diff --git a/webapp/requirements.txt.in b/webapp/requirements.txt.in new file mode 100644 index 00000000..39c93a83 --- /dev/null +++ b/webapp/requirements.txt.in @@ -0,0 +1 @@ +astral==2.2 From 66b93eb3de7ee04993f47da97c02e78883d0df9d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 5 Dec 2024 14:55:25 -0800 Subject: [PATCH 0833/1077] Add `webapp/__init__.py` (#1129) --- webapp/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 webapp/__init__.py diff --git a/webapp/__init__.py b/webapp/__init__.py new file mode 100644 index 00000000..e69de29b From b3e4d09b6b01115dd74b6cd73c27af57ffa6741c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 7 Dec 2024 11:29:05 -0800 Subject: [PATCH 0834/1077] Include core 2024.9.3 until 2024.12.0 in the tests (#1130) --- .github/workflows/pytest.yaml | 8 ++++++++ webapp/__init__.py | 1 + 2 files changed, 9 insertions(+) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 4d7472de..68d4ff08 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -58,6 +58,14 @@ jobs: core-version: "2024.7.4" - python-version: "3.12" core-version: "2024.8.3" + - python-version: "3.12" + core-version: "2024.9.3" + - python-version: "3.12" + core-version: "2024.10.4" + - python-version: "3.12" + core-version: "2024.11.3" + - python-version: "3.12" + core-version: "2024.12.0" - python-version: "3.12" core-version: "dev" steps: diff --git a/webapp/__init__.py b/webapp/__init__.py index e69de29b..33dc1b66 100644 --- a/webapp/__init__.py +++ b/webapp/__init__.py @@ -0,0 +1 @@ +"""Shiny webapp for Adaptive Lighting.""" From 6d517fc913bea88e800ffdba6cad4cd953b18b98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Mar=C3=A1z?= Date: Wed, 1 Jan 2025 21:22:38 +0100 Subject: [PATCH 0835/1077] Remove trailing space from string in const.py (#1146) --- custom_components/adaptive_lighting/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 79d571e2..5f78c8fe 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -28,7 +28,7 @@ CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = ( ) DOCS[CONF_DETECT_NON_HA_CHANGES] = ( "Detects and halts adaptations for non-`light.turn_on` state changes. " - "Needs `take_over_control` enabled. 🕵️ " + "Needs `take_over_control` enabled. 🕵️" "Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result " "in lights turning on unexpectedly. " "Disable this feature if you encounter such issues." From 66561058ecd05f3c3fed3a6233d5fce4c4fa1aa3 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 1 Jan 2025 12:23:22 -0800 Subject: [PATCH 0836/1077] docs: add marazmarci as a contributor for code (#1147) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index a78192bc..4fbe022e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -936,6 +936,15 @@ "contributions": [ "translation" ] + }, + { + "login": "marazmarci", + "name": "Márton Maráz", + "avatar_url": "https://avatars.githubusercontent.com/u/1349654?v=4", + "profile": "https://github.com/marazmarci", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 78e8e260..5000fc36 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-102-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-103-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -596,6 +596,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 2e2b9e55ef69384679b40c993688e316de969634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Mar=C3=A1z?= Date: Wed, 1 Jan 2025 23:18:54 +0100 Subject: [PATCH 0837/1077] Fix trailing spaces in const.py: restore correct one, remove incorrect one (#1148) * Fix trailing spaces in const.py: restore correct one, remove incorrect one * Also remove trailing space from translations/en.json * Also remove trailing space from strings.json --- custom_components/adaptive_lighting/const.py | 4 ++-- custom_components/adaptive_lighting/strings.json | 2 +- custom_components/adaptive_lighting/translations/en.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 5f78c8fe..3fc9c5d4 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -28,7 +28,7 @@ CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = ( ) DOCS[CONF_DETECT_NON_HA_CHANGES] = ( "Detects and halts adaptations for non-`light.turn_on` state changes. " - "Needs `take_over_control` enabled. 🕵️" + "Needs `take_over_control` enabled. 🕵️ " "Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result " "in lights turning on unexpectedly. " "Disable this feature if you encounter such issues." @@ -84,7 +84,7 @@ DOCS[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] = ( "invoked without specifying color or brightness. ❌🌈 " "This e.g., prevents adaptation when activating a scene. " "If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. " - "Needs `take_over_control` enabled. 🕵️ " + "Needs `take_over_control` enabled. 🕵️" ) CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index f6f6890b..a2724f6b 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -49,7 +49,7 @@ "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ ", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay", "adapt_delay": "adapt_delay", diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index f55a6d88..9991d232 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -50,7 +50,7 @@ "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ ", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay", "adapt_delay": "adapt_delay", From bf7d109c9833c4404d212c3bab38b54ec7a25021 Mon Sep 17 00:00:00 2001 From: Daniel <49846893+danielbrunt57@users.noreply.github.com> Date: Wed, 1 Jan 2025 14:19:31 -0800 Subject: [PATCH 0838/1077] fix: async entry setup (#1141) * Update __init__.py * Update strings.json --- custom_components/adaptive_lighting/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index a235d70f..13c2d7d1 100644 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -65,10 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): undo_listener = config_entry.add_update_listener(async_update_options) data[config_entry.entry_id] = {UNDO_UPDATE_LISTENER: undo_listener} - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform), - ) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True From 2b6c42d4a864fa1888324d9234a50e66010991e1 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Wed, 1 Jan 2025 23:22:24 +0100 Subject: [PATCH 0839/1077] Translations update from Hosted Weblate (#1145) * Translated using Weblate (Croatian) Currently translated at 47.7% (73 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: dex girl Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/hr/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Korean) Currently translated at 99.3% (152 of 153 strings) Co-authored-by: Anonymous Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ko/ Translation: Adaptive Lighting/Adaptive Lighting --------- Co-authored-by: dex girl --- .../adaptive_lighting/translations/hr.json | 15 +- .../adaptive_lighting/translations/ko.json | 528 +++++++++--------- 2 files changed, 278 insertions(+), 265 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/hr.json b/custom_components/adaptive_lighting/translations/hr.json index 7347cda1..2f8e65ca 100644 --- a/custom_components/adaptive_lighting/translations/hr.json +++ b/custom_components/adaptive_lighting/translations/hr.json @@ -2,7 +2,14 @@ "options": { "step": { "init": { - "title": "Opcije prilagodljivog osvjetljenja" + "title": "Opcije prilagodljivog osvjetljenja", + "data_description": { + "sunrise_offset": "Podesite vrijeme izlaska sunca s pozitivnim ili negativnim pomakom u sekundama. ⏰", + "sunset_offset": "Podesite vrijeme izlaska sunca s pozitivnim ili negativnim pomakom u sekundama. ⏰" + }, + "data": { + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Prilikom početnog paljenja svjetla. Ako je postavljeno na \"true\", AL se prilagođava samo ako se \"light.turn_on\" pozove bez navođenja boje ili svjetline. ❌🌈 Ovo npr. sprječava prilagodbu prilikom aktiviranja scene. Ako je \"false\", AL se prilagođava bez obzira na prisutnost boje ili svjetline u početnim \"service_data\". Potrebno je omogućiti `take_over_control`. 🕵️ " + } } } }, @@ -12,6 +19,12 @@ "fields": { "only_once": { "description": "Prilagodi svjetla samo kada su uključena (true) ili ih neprestano prilagođavaj (false). 🔄" + }, + "sunrise_offset": { + "description": "Podesite vrijeme izlaska sunca s pozitivnim ili negativnim pomakom u sekundama. ⏰" + }, + "sunset_offset": { + "description": "Podesite vrijeme izlaska sunca s pozitivnim ili negativnim pomakom u sekundama. ⏰" } } } diff --git a/custom_components/adaptive_lighting/translations/ko.json b/custom_components/adaptive_lighting/translations/ko.json index f4418fd5..8a628f70 100644 --- a/custom_components/adaptive_lighting/translations/ko.json +++ b/custom_components/adaptive_lighting/translations/ko.json @@ -1,269 +1,269 @@ { - "title": "적응형 조명", - "config": { - "step": { - "user": { - "title": "적응형 조명 인스턴스 이름 선택", - "description": "각 인스턴스는 여러 조명을 포함할 수 있습니다!", - "data": { - "name": "이름" - } - } - }, - "abort": { - "already_configured": "이 장치는 이미 구성되었습니다" + "title": "적응형 조명", + "config": { + "step": { + "user": { + "title": "적응형 조명 인스턴스 이름 선택", + "description": "각 인스턴스는 여러 조명을 포함할 수 있습니다!", + "data": { + "name": "이름" } + } }, - "options": { - "step": { - "init": { - "title": "적응형 조명 옵션", - "description": "적응형 조명 구성요소를 구성합니다. 옵션 이름은 YAML 설정과 일치합니다. 이 항목을 YAML에서 정의한 경우 여기에 옵션이 표시되지 않습니다. 매개변수 효과를 시연하는 인터랙티브 그래프는 [이 웹 앱](https://basnijholt.github.io/adaptive-lighting)에서 확인할 수 있습니다. 자세한 내용은 [공식 문서](https://github.com/basnijholt/adaptive-lighting#readme)를 참조하세요.", - "data": { - "lights": "조명: 제어될 조명 entity_ids의 목록 (비어 있을 수 있음). 🌟", - "interval": "간격", - "transition": "전환", - "initial_transition": "초기 전환", - "min_brightness": "최소 밝기: 밝기 최소 퍼센트. 💡", - "max_brightness": "최대 밝기: 밝기 최대 퍼센트. 💡", - "min_color_temp": "최소 색온도: 켈빈으로 표시된 가장 따뜻한 색온도. 🔥", - "max_color_temp": "최대 색온도: 켈빈으로 표시된 가장 차가운 색온도. ❄️", - "prefer_rgb_color": "RGB 색상 선호: 가능할 경우 색온도 조정보다 RGB 색상 조정을 선호하는지 여부. 🌈", - "sleep_brightness": "수면 밝기", - "sleep_rgb_or_color_temp": "수면 rgb_or_color_temp", - "sleep_color_temp": "수면 색온도", - "sleep_rgb_color": "수면 RGB 색상", - "sleep_transition": "수면 전환", - "transition_until_sleep": "수면까지 전환: 활성화되면, 적응형 조명은 수면 설정을 최소값으로 취급하고 일몰 후 이 값으로 전환합니다. 🌙", - "sunrise_time": "일출 시간", - "min_sunrise_time": "최소 일출 시간", - "max_sunrise_time": "최대 일출 시간", - "sunrise_offset": "일출 오프셋", - "sunset_time": "일몰 시간", - "min_sunset_time": "최소 일몰 시간", - "max_sunset_time": "최대 일몰 시간", - "sunset_offset": "일몰 오프셋", - "brightness_mode": "밝기 모드", - "brightness_mode_time_dark": "어두울 때 밝기 모드 시간", - "brightness_mode_time_light": "밝을 때 밝기 모드 시간", - "take_over_control": "제어 인계: 다른 소스가 조명이 켜져 있고 조정 중일 때 `light.turn_on`을 호출하면 적응형 조명을 비활성화합니다. 이는 매 `간격`마다 `homeassistant.update_entity`를 호출합니다! 🔒", - "detect_non_ha_changes": "비HA 변경 감지: `light.turn_on`이 아닌 상태 변경을 감지하고 조정을 중단합니다. `take_over_control`이 활성화되어 있어야 합니다. 🕵️ 주의: ⚠️ 일부 조명은 잘못된 '켜짐' 상태를 나타낼 수 있으며, 이로 인해 조명이 예상치 못하게 켜질 수 있습니다. 이러한 문제가 발생하면 이 기능을 비활성화하세요.", - "autoreset_control_seconds": "자동 제어 리셋 초", - "only_once": "한 번만: 조명을 켤 때만 조정 (`true`) 또는 계속해서 조정 (`false`). 🔄", - "adapt_only_on_bare_turn_on": "초기 켜짐 시 조정만: 조명을 처음 켤 때. `true`로 설정하면 `light.turn_on`이 색상이나 밝기를 지정하지 않고 호출될 때만 AL이 조정합니다. ❌🌈 예를 들어, 장면을 활성화할 때 조정을 방지합니다. `false`로 설정하면, AL은 초기 `service_data`에 색상이나 밝기의 존재 여부와 관계없이 조정합니다. `take_over_control`이 활성화되어 있어야 합니다. 🕵️", - "separate_turn_on_commands": "분리된 켜기 명령 사용: 일부 조명 유형에 필요한 색상과 밝기에 대해 별도의 `light.turn_on` 호출을 사용합니다. 🔀", - "send_split_delay": "분할 전송 지연", - "adapt_delay": "조정 지연", - "skip_redundant_commands": "중복 명령 건너뛰기: 목표 상태가 이미 조명의 알려진 상태와 동일한 조정 명령을 보내지 않습니다. 네트워크 트래픽을 최소화하고 일부 상황에서 조정 반응성을 향상시킵니다. 📉 물리적 조명 상태가 HA의 기록된 상태와 동기화되지 않는 경우 비활성화하세요.", - "intercept": "가로채기: 색상과 밝기의 즉각적인 조정을 가능하게 하기 위해 `light.turn_on` 호출을 가로챕니다. 🏎️ 색상과 밝기를 지원하지 않는 조명에 대해 비활성화합니다.", - "multi_light_intercept": "다중 조명 가로채기: 여러 조명을 대상으로 하는 `light.turn_on` 호출을 가로채고 조정합니다. ➗⚠️ 이는 단일 `light.turn_on` 호출을 여러 호출로 분할할 수 있음을 의미합니다. 예를 들어, 조명이 다른 스위치에 있을 때. `intercept`가 활성화되어 있어야 합니다.", - "include_config_in_attributes": "속성에 구성 포함: `true`로 설정하면 Home Assistant에서 스위치의 모든 옵션을 속성으로 표시합니다. 📝" - }, - "data_description": { - "interval": "조명을 조정하는 빈도, 초 단위. 🔄", - "transition": "조명이 변경될 때 전환 기간, 초 단위. 🕑", - "initial_transition": "조명이 `off`에서 `on`으로 바뀔 때 첫 번째 전환의 지속 시간, 초 단위. ⏲️", - "sleep_brightness": "수면 모드에서 조명의 밝기 퍼센트. 😴", - "sleep_rgb_or_color_temp": "수면 모드에서 `\"rgb_color\"` 또는 `\"color_temp\"` 사용. 🌙", - "sleep_color_temp": "수면 모드에서 색온도 (sleep_rgb_or_color_temp가 `color_temp`일 때 사용) 켈빈 단위. 😴", - "sleep_rgb_color": "수면 모드에서 RGB 색상 (sleep_rgb_or_color_temp가 \"rgb_color\"일 때 사용). 🌈", - "sleep_transition": "\"수면 모드\"가 전환될 때 전환 기간, 초 단위. 😴", - "sunrise_time": "일출 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌅", - "min_sunrise_time": "가장 이른 가상 일출 시간 (HH:MM:SS)을 설정하여 더 늦은 일출을 허용. 🌅", - "max_sunrise_time": "가장 늦은 가상 일출 시간 (HH:MM:SS)을 설정하여 더 일찍 일출을 허용. 🌅", - "sunrise_offset": "양수 또는 음수 오프셋(초)으로 일출 시간을 조정. ⏰", - "sunset_time": "일몰 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌇", - "min_sunset_time": "가장 이른 가상 일몰 시간 (HH:MM:SS)을 설정하여 더 늦은 일몰을 허용. 🌇", - "max_sunset_time": "가장 늦은 가상 일몰 시간 (HH:MM:SS)을 설정하여 더 일찍 일몰을 허용. 🌇", - "sunset_offset": "양수 또는 음수 오프셋(초)으로 일몰 시간을 조정. ⏰", - "brightness_mode": "사용할 밝기 모드. 가능한 값은 `default`, `linear`, `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", - "brightness_mode_time_dark": "(`brightness_mode='default'`인 경우 무시됨) 일출/일몰 전/후에 밝기를 높이거나 낮추는 데 걸리는 시간, 초 단위. 📈📉", - "brightness_mode_time_light": "(`brightness_mode='default'`인 경우 무시됨) 일출/일몰 후/전에 밝기를 높이거나 낮추는 데 걸리는 시간, 초 단위. 📈📉.", - "autoreset_control_seconds": "특정 초 후에 수동 제어를 자동으로 재설정. 0으로 설정하면 비활성화됩니다. ⏲️", - "send_split_delay": "`separate_turn_on_commands`에 대한 호출 사이의 지연 시간(밀리초)으로, 밝기와 색상을 동시에 설정하지 않는 조명에 대한 지연. ⏲️", - "adapt_delay": "조명을 켠 후 적응형 조명이 변경 사항을 적용하기까지의 대기 시간(초). 깜박임을 피하는 데 도움이 될 수 있습니다. ⏲️" - } - } - }, - "error": { - "option_error": "잘못된 옵션", - "entity_missing": "선택한 하나 이상의 조명 엔티티가 Home Assistant에서 누락됨" - } - }, - "services": { - "apply": { - "name": "적용", - "description": "현재 적응형 조명 설정을 조명에 적용합니다.", - "fields": { - "entity_id": { - "description": "설정을 적용할 스위치의 `entity_id`. 📝", - "name": "entity_id" - }, - "lights": { - "description": "설정을 적용할 조명(또는 조명 목록). 💡", - "name": "lights" - }, - "transition": { - "description": "조명 변경 시 전환 기간, 초 단위. 🕑", - "name": "transition" - }, - "adapt_brightness": { - "description": "조명의 밝기를 조정할지 여부. 🌞", - "name": "adapt_brightness" - }, - "adapt_color": { - "description": "지원하는 조명의 색상을 조정할지 여부. 🌈", - "name": "adapt_color" - }, - "prefer_rgb_color": { - "description": "가능할 경우 색온도 조정보다 RGB 색상 조정을 선호하는지 여부. 🌈", - "name": "prefer_rgb_color" - }, - "turn_on_lights": { - "description": "현재 꺼져 있는 조명을 켤지 여부. 🔆", - "name": "turn_on_lights" - } - } - }, - "set_manual_control": { - "name": "수동 제어 설정", - "description": "조명이 '수동 제어됨'으로 표시되었는지 여부를 표시합니다.", - "fields": { - "entity_id": { - "description": "`수동 제어됨`으로 (표시 해제)할 스위치의 `entity_id`. 📝", - "name": "entity_id" - }, - "lights": { - "description": "조명의 entity_id(들), 지정하지 않으면 스위치의 모든 조명이 선택됩니다. 💡", - "name": "lights" - }, - "manual_control": { - "description": "\"수동 제어\" 목록에서 조명을 추가(\"true\") 또는 제거(\"false\")할지 여부. 🔒", - "name": "manual_control" - } - } - }, - "change_switch_settings": { - "name": "스위치 설정 변경", - "description": "스위치에서 원하는 모든 설정을 변경하세요. 여기에 있는 모든 옵션은 구성 흐름에서와 같습니다.", - "fields": { - "entity_id": { - "description": "스위치의 Entity ID. 📝", - "name": "entity_id" - }, - "use_defaults": { - "description": "이 서비스 호출에서 지정되지 않은 기본값을 설정합니다. 옵션: \"현재\"(기본값, 현재 값을 유지), \"공장\"(문서화된 기본값으로 재설정), 또는 \"구성\"(스위치 구성 기본값으로 되돌림). ⚙️", - "name": "use_defaults" - }, - "include_config_in_attributes": { - "description": "`true`로 설정하면 Home Assistant에서 스위치의 모든 옵션을 속성으로 표시합니다. 📝", - "name": "include_config_in_attributes" - }, - "turn_on_lights": { - "description": "현재 꺼져 있는 조명을 켤지 여부. 🔆", - "name": "turn_on_lights" - }, - "initial_transition": { - "description": "조명이 `off`에서 `on`으로 바뀔 때 첫 번째 전환의 지속 시간, 초 단위. ⏲️", - "name": "initial_transition" - }, - "sleep_transition": { - "description": "\"수면 모드\"가 전환될 때 전환 기간, 초 단위. 😴", - "name": "sleep_transition" - }, - "max_brightness": { - "description": "최대 밝기 퍼센트. 💡", - "name": "max_brightness" - }, - "max_color_temp": { - "description": "켈빈으로 표시된 가장 차가운 색온도. ❄️", - "name": "max_color_temp" - }, - "min_brightness": { - "description": "최소 밝기 퍼센트. 💡", - "name": "min_brightness" - }, - "min_color_temp": { - "description": "켈빈으로 표시된 가장 따뜻한 색온도. 🔥", - "name": "min_color_temp" - }, - "only_once": { - "description": "조명을 켤 때만 조정 (`true`) 또는 계속해서 조정 (`false`). 🔄", - "name": "only_once" - }, - "prefer_rgb_color": { - "description": "가능할 경우 색온도 조정보다 RGB 색상 조정을 선호하는지 여부. 🌈", - "name": "prefer_rgb_color" - }, - "separate_turn_on_commands": { - "description": "일부 조명 유형에 필요한 색상과 밝기에 대해 별도의 `light.turn_on` 호출을 사용합니다. 🔀", - "name": "separate_turn_on_commands" - }, - "send_split_delay": { - "description": "밝기와 색상을 동시에 설정하지 않는 조명에 대한 `separate_turn_on_commands` 호출 사이의 지연 시간(밀리초). ⏲️", - "name": "send_split_delay" - }, - "sleep_brightness": { - "description": "수면 모드에서 조명의 밝기 퍼센트. 😴", - "name": "sleep_brightness" - }, - "sleep_rgb_or_color_temp": { - "description": "수면 모드에서 `\"rgb_color\"` 또는 `\"color_temp\"` 사용. 🌙", - "name": "sleep_rgb_or_color_temp" - }, - "sleep_rgb_color": { - "description": "수면 모드에서 RGB 색상 (sleep_rgb_or_color_temp가 \"rgb_color\"일 때 사용). 🌈", - "name": "sleep_rgb_color" - }, - "sleep_color_temp": { - "description": "수면 모드에서 색온도 (sleep_rgb_or_color_temp가 `color_temp`일 때 사용) 켈빈 단위. 😴", - "name": "sleep_color_temp" - }, - "sunrise_offset": { - "description": "양수 또는 음수 오프셋(초)으로 일출 시간을 조정. ⏰", - "name": "sunrise_offset" - }, - "sunrise_time": { - "description": "일출 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌅", - "name": "sunrise_time" - }, - "sunset_offset": { - "description": "양수 또는 음수 오프셋(초)으로 일몰 시간을 조정. ⏰", - "name": "sunset_offset" - }, - "sunset_time": { - "description": "일몰 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌇", - "name": "sunset_time" - }, - "max_sunrise_time": { - "description": "가장 늦은 가상 일출 시간 (HH:MM:SS)을 설정하여 더 일찍 일출을 허용. 🌅", - "name": "max_sunrise_time" - }, - "min_sunset_time": { - "description": "가장 이른 가상 일몰 시간 (HH:MM:SS)을 설정하여 더 늦은 일몰을 허용. 🌇", - "name": "min_sunset_time" - }, - "take_over_control": { - "description": "다른 소스가 조명이 켜져 있고 조정 중일 때 `light.turn_on`을 호출하면 적응형 조명을 비활성화합니다. 이는 매 `간격`마다 `homeassistant.update_entity`를 호출합니다! 🔒", - "name": "take_over_control" - }, - "detect_non_ha_changes": { - "description": "`light.turn_on`이 아닌 상태 변경을 감지하고 조정을 중단합니다. `take_over_control`이 활성화되어 있어야 합니다. 🕵️ 주의: ⚠️ 일부 조명은 잘못된 '켜짐' 상태를 나타낼 수 있으며, 이로 인해 조명이 예상치 못하게 켜질 수 있습니다. 이러한 문제가 발생하면 이 기능을 비활성화하세요.", - "name": "detect_non_ha_changes" - }, - "transition": { - "description": "조명이 변경될 때 전환 기간, 초 단위. 🕑", - "name": "transition" - }, - "adapt_delay": { - "description": "조명을 켠 후 적응형 조명이 변경 사항을 적용하기까지의 대기 시간(초). 깜박임을 피하는 데 도움이 될 수 있습니다. ⏲️", - "name": "adapt_delay" - }, - "autoreset_control_seconds": { - "description": "특정 초 후에 수동 제어를 자동으로 재설정. 0으로 설정하면 비활성화됩니다. ⏲️", - "name": "autoreset_control_seconds" - } - } - } + "abort": { + "already_configured": "이 장치는 이미 구성되었습니다" } + }, + "options": { + "step": { + "init": { + "title": "적응형 조명 옵션", + "description": "적응형 조명 구성요소를 구성합니다. 옵션 이름은 YAML 설정과 일치합니다. 이 항목을 YAML에서 정의한 경우 여기에 옵션이 표시되지 않습니다. 매개변수 효과를 시연하는 인터랙티브 그래프는 [이 웹 앱](https://basnijholt.github.io/adaptive-lighting)에서 확인할 수 있습니다. 자세한 내용은 [공식 문서](https://github.com/basnijholt/adaptive-lighting#readme)를 참조하세요.", + "data": { + "lights": "조명: 제어될 조명 entity_ids의 목록 (비어 있을 수 있음). 🌟", + "interval": "간격", + "transition": "전환", + "initial_transition": "초기 전환", + "min_brightness": "최소 밝기: 밝기 최소 퍼센트. 💡", + "max_brightness": "최대 밝기: 밝기 최대 퍼센트. 💡", + "min_color_temp": "최소 색온도: 켈빈으로 표시된 가장 따뜻한 색온도. 🔥", + "max_color_temp": "최대 색온도: 켈빈으로 표시된 가장 차가운 색온도. ❄️", + "prefer_rgb_color": "RGB 색상 선호: 가능할 경우 색온도 조정보다 RGB 색상 조정을 선호하는지 여부. 🌈", + "sleep_brightness": "수면 밝기", + "sleep_rgb_or_color_temp": "수면 rgb_or_color_temp", + "sleep_color_temp": "수면 색온도", + "sleep_rgb_color": "수면 RGB 색상", + "sleep_transition": "수면 전환", + "transition_until_sleep": "수면까지 전환: 활성화되면, 적응형 조명은 수면 설정을 최소값으로 취급하고 일몰 후 이 값으로 전환합니다. 🌙", + "sunrise_time": "일출 시간", + "min_sunrise_time": "최소 일출 시간", + "max_sunrise_time": "최대 일출 시간", + "sunrise_offset": "일출 오프셋", + "sunset_time": "일몰 시간", + "min_sunset_time": "최소 일몰 시간", + "max_sunset_time": "최대 일몰 시간", + "sunset_offset": "일몰 오프셋", + "brightness_mode": "밝기 모드", + "brightness_mode_time_dark": "어두울 때 밝기 모드 시간", + "brightness_mode_time_light": "밝을 때 밝기 모드 시간", + "take_over_control": "제어 인계: 다른 소스가 조명이 켜져 있고 조정 중일 때 `light.turn_on`을 호출하면 적응형 조명을 비활성화합니다. 이는 매 `간격`마다 `homeassistant.update_entity`를 호출합니다! 🔒", + "detect_non_ha_changes": "비HA 변경 감지: `light.turn_on`이 아닌 상태 변경을 감지하고 조정을 중단합니다. `take_over_control`이 활성화되어 있어야 합니다. 🕵️ 주의: ⚠️ 일부 조명은 잘못된 '켜짐' 상태를 나타낼 수 있으며, 이로 인해 조명이 예상치 못하게 켜질 수 있습니다. 이러한 문제가 발생하면 이 기능을 비활성화하세요.", + "autoreset_control_seconds": "자동 제어 리셋 초", + "only_once": "한 번만: 조명을 켤 때만 조정 (`true`) 또는 계속해서 조정 (`false`). 🔄", + "adapt_only_on_bare_turn_on": "초기 켜짐 시 조정만: 조명을 처음 켤 때. `true`로 설정하면 `light.turn_on`이 색상이나 밝기를 지정하지 않고 호출될 때만 AL이 조정합니다. ❌🌈 예를 들어, 장면을 활성화할 때 조정을 방지합니다. `false`로 설정하면, AL은 초기 `service_data`에 색상이나 밝기의 존재 여부와 관계없이 조정합니다. `take_over_control`이 활성화되어 있어야 합니다. 🕵️", + "separate_turn_on_commands": "분리된 켜기 명령 사용: 일부 조명 유형에 필요한 색상과 밝기에 대해 별도의 `light.turn_on` 호출을 사용합니다. 🔀", + "send_split_delay": "분할 전송 지연", + "adapt_delay": "조정 지연", + "skip_redundant_commands": "중복 명령 건너뛰기: 목표 상태가 이미 조명의 알려진 상태와 동일한 조정 명령을 보내지 않습니다. 네트워크 트래픽을 최소화하고 일부 상황에서 조정 반응성을 향상시킵니다. 📉 물리적 조명 상태가 HA의 기록된 상태와 동기화되지 않는 경우 비활성화하세요.", + "intercept": "가로채기: 색상과 밝기의 즉각적인 조정을 가능하게 하기 위해 `light.turn_on` 호출을 가로챕니다. 🏎️ 색상과 밝기를 지원하지 않는 조명에 대해 비활성화합니다.", + "multi_light_intercept": "다중 조명 가로채기: 여러 조명을 대상으로 하는 `light.turn_on` 호출을 가로채고 조정합니다. ➗⚠️ 이는 단일 `light.turn_on` 호출을 여러 호출로 분할할 수 있음을 의미합니다. 예를 들어, 조명이 다른 스위치에 있을 때. `intercept`가 활성화되어 있어야 합니다.", + "include_config_in_attributes": "속성에 구성 포함: `true`로 설정하면 Home Assistant에서 스위치의 모든 옵션을 속성으로 표시합니다. 📝" + }, + "data_description": { + "interval": "조명을 조정하는 빈도, 초 단위. 🔄", + "transition": "조명이 변경될 때 전환 기간, 초 단위. 🕑", + "initial_transition": "조명이 `off`에서 `on`으로 바뀔 때 첫 번째 전환의 지속 시간, 초 단위. ⏲️", + "sleep_brightness": "수면 모드에서 조명의 밝기 퍼센트. 😴", + "sleep_rgb_or_color_temp": "수면 모드에서 `\"rgb_color\"` 또는 `\"color_temp\"` 사용. 🌙", + "sleep_color_temp": "수면 모드에서 색온도 (sleep_rgb_or_color_temp가 `color_temp`일 때 사용) 켈빈 단위. 😴", + "sleep_rgb_color": "수면 모드에서 RGB 색상 (sleep_rgb_or_color_temp가 \"rgb_color\"일 때 사용). 🌈", + "sleep_transition": "\"수면 모드\"가 전환될 때 전환 기간, 초 단위. 😴", + "sunrise_time": "일출 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌅", + "min_sunrise_time": "가장 이른 가상 일출 시간 (HH:MM:SS)을 설정하여 더 늦은 일출을 허용. 🌅", + "max_sunrise_time": "가장 늦은 가상 일출 시간 (HH:MM:SS)을 설정하여 더 일찍 일출을 허용. 🌅", + "sunrise_offset": "양수 또는 음수 오프셋(초)으로 일출 시간을 조정. ⏰", + "sunset_time": "일몰 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌇", + "min_sunset_time": "가장 이른 가상 일몰 시간 (HH:MM:SS)을 설정하여 더 늦은 일몰을 허용. 🌇", + "max_sunset_time": "가장 늦은 가상 일몰 시간 (HH:MM:SS)을 설정하여 더 일찍 일몰을 허용. 🌇", + "sunset_offset": "양수 또는 음수 오프셋(초)으로 일몰 시간을 조정. ⏰", + "brightness_mode": "사용할 밝기 모드. 가능한 값은 `default`, `linear`, `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(`brightness_mode='default'`인 경우 무시됨) 일출/일몰 전/후에 밝기를 높이거나 낮추는 데 걸리는 시간, 초 단위. 📈📉", + "brightness_mode_time_light": "(`brightness_mode='default'`인 경우 무시됨) 일출/일몰 후/전에 밝기를 높이거나 낮추는 데 걸리는 시간, 초 단위. 📈📉.", + "autoreset_control_seconds": "특정 초 후에 수동 제어를 자동으로 재설정. 0으로 설정하면 비활성화됩니다. ⏲️", + "send_split_delay": "`separate_turn_on_commands`에 대한 호출 사이의 지연 시간(밀리초)으로, 밝기와 색상을 동시에 설정하지 않는 조명에 대한 지연. ⏲️", + "adapt_delay": "조명을 켠 후 적응형 조명이 변경 사항을 적용하기까지의 대기 시간(초). 깜박임을 피하는 데 도움이 될 수 있습니다. ⏲️" + } + } + }, + "error": { + "option_error": "잘못된 옵션", + "entity_missing": "선택한 하나 이상의 조명 엔티티가 Home Assistant에서 누락됨" + } + }, + "services": { + "apply": { + "name": "적용", + "description": "현재 적응형 조명 설정을 조명에 적용합니다.", + "fields": { + "entity_id": { + "description": "설정을 적용할 스위치의 `entity_id`. 📝", + "name": "entity_id" + }, + "lights": { + "description": "설정을 적용할 조명(또는 조명 목록). 💡", + "name": "lights" + }, + "transition": { + "description": "조명 변경 시 전환 기간, 초 단위. 🕑", + "name": "transition" + }, + "adapt_brightness": { + "description": "조명의 밝기를 조정할지 여부. 🌞", + "name": "adapt_brightness" + }, + "adapt_color": { + "description": "지원하는 조명의 색상을 조정할지 여부. 🌈", + "name": "adapt_color" + }, + "prefer_rgb_color": { + "description": "가능할 경우 색온도 조정보다 RGB 색상 조정을 선호하는지 여부. 🌈", + "name": "prefer_rgb_color" + }, + "turn_on_lights": { + "description": "현재 꺼져 있는 조명을 켤지 여부. 🔆", + "name": "turn_on_lights" + } + } + }, + "set_manual_control": { + "name": "수동 제어 설정", + "description": "조명이 '수동 제어됨'으로 표시되었는지 여부를 표시합니다.", + "fields": { + "entity_id": { + "description": "`수동 제어됨`으로 (표시 해제)할 스위치의 `entity_id`. 📝", + "name": "entity_id" + }, + "lights": { + "description": "조명의 entity_id(들), 지정하지 않으면 스위치의 모든 조명이 선택됩니다. 💡", + "name": "lights" + }, + "manual_control": { + "description": "\"수동 제어\" 목록에서 조명을 추가(\"true\") 또는 제거(\"false\")할지 여부. 🔒", + "name": "manual_control" + } + } + }, + "change_switch_settings": { + "name": "스위치 설정 변경", + "description": "스위치에서 원하는 모든 설정을 변경하세요. 여기에 있는 모든 옵션은 구성 흐름에서와 같습니다.", + "fields": { + "entity_id": { + "description": "스위치의 Entity ID. 📝", + "name": "entity_id" + }, + "use_defaults": { + "description": "이 서비스 호출에서 지정되지 않은 기본값을 설정합니다. 옵션: \"현재\"(기본값, 현재 값을 유지), \"공장\"(문서화된 기본값으로 재설정), 또는 \"구성\"(스위치 구성 기본값으로 되돌림). ⚙️", + "name": "use_defaults" + }, + "include_config_in_attributes": { + "description": "`true`로 설정하면 Home Assistant에서 스위치의 모든 옵션을 속성으로 표시합니다. 📝", + "name": "include_config_in_attributes" + }, + "turn_on_lights": { + "description": "현재 꺼져 있는 조명을 켤지 여부. 🔆", + "name": "turn_on_lights" + }, + "initial_transition": { + "description": "조명이 `off`에서 `on`으로 바뀔 때 첫 번째 전환의 지속 시간, 초 단위. ⏲️", + "name": "initial_transition" + }, + "sleep_transition": { + "description": "\"수면 모드\"가 전환될 때 전환 기간, 초 단위. 😴", + "name": "sleep_transition" + }, + "max_brightness": { + "description": "최대 밝기 퍼센트. 💡", + "name": "max_brightness" + }, + "max_color_temp": { + "description": "켈빈으로 표시된 가장 차가운 색온도. ❄️", + "name": "max_color_temp" + }, + "min_brightness": { + "description": "최소 밝기 퍼센트. 💡", + "name": "min_brightness" + }, + "min_color_temp": { + "description": "켈빈으로 표시된 가장 따뜻한 색온도. 🔥", + "name": "min_color_temp" + }, + "only_once": { + "description": "조명을 켤 때만 조정 (`true`) 또는 계속해서 조정 (`false`). 🔄", + "name": "only_once" + }, + "prefer_rgb_color": { + "description": "가능할 경우 색온도 조정보다 RGB 색상 조정을 선호하는지 여부. 🌈", + "name": "prefer_rgb_color" + }, + "separate_turn_on_commands": { + "description": "일부 조명 유형에 필요한 색상과 밝기에 대해 별도의 `light.turn_on` 호출을 사용합니다. 🔀", + "name": "separate_turn_on_commands" + }, + "send_split_delay": { + "description": "밝기와 색상을 동시에 설정하지 않는 조명에 대한 `separate_turn_on_commands` 호출 사이의 지연 시간(밀리초). ⏲️", + "name": "send_split_delay" + }, + "sleep_brightness": { + "description": "수면 모드에서 조명의 밝기 퍼센트. 😴", + "name": "sleep_brightness" + }, + "sleep_rgb_or_color_temp": { + "description": "수면 모드에서 `\"rgb_color\"` 또는 `\"color_temp\"` 사용. 🌙", + "name": "sleep_rgb_or_color_temp" + }, + "sleep_rgb_color": { + "description": "수면 모드에서 RGB 색상 (sleep_rgb_or_color_temp가 \"rgb_color\"일 때 사용). 🌈", + "name": "sleep_rgb_color" + }, + "sleep_color_temp": { + "description": "수면 모드에서 색온도 (sleep_rgb_or_color_temp가 `color_temp`일 때 사용) 켈빈 단위. 😴", + "name": "sleep_color_temp" + }, + "sunrise_offset": { + "description": "양수 또는 음수 오프셋(초)으로 일출 시간을 조정. ⏰", + "name": "sunrise_offset" + }, + "sunrise_time": { + "description": "일출 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌅", + "name": "sunrise_time" + }, + "sunset_offset": { + "description": "양수 또는 음수 오프셋(초)으로 일몰 시간을 조정. ⏰", + "name": "sunset_offset" + }, + "sunset_time": { + "description": "일몰 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌇", + "name": "sunset_time" + }, + "max_sunrise_time": { + "description": "가장 늦은 가상 일출 시간 (HH:MM:SS)을 설정하여 더 일찍 일출을 허용. 🌅", + "name": "max_sunrise_time" + }, + "min_sunset_time": { + "description": "가장 이른 가상 일몰 시간 (HH:MM:SS)을 설정하여 더 늦은 일몰을 허용. 🌇", + "name": "min_sunset_time" + }, + "take_over_control": { + "description": "다른 소스가 조명이 켜져 있고 조정 중일 때 `light.turn_on`을 호출하면 적응형 조명을 비활성화합니다. 이는 매 `간격`마다 `homeassistant.update_entity`를 호출합니다! 🔒", + "name": "take_over_control" + }, + "detect_non_ha_changes": { + "description": "`light.turn_on`이 아닌 상태 변경을 감지하고 조정을 중단합니다. `take_over_control`이 활성화되어 있어야 합니다. 🕵️ 주의: ⚠️ 일부 조명은 잘못된 '켜짐' 상태를 나타낼 수 있으며, 이로 인해 조명이 예상치 못하게 켜질 수 있습니다. 이러한 문제가 발생하면 이 기능을 비활성화하세요.", + "name": "detect_non_ha_changes" + }, + "transition": { + "description": "조명이 변경될 때 전환 기간, 초 단위. 🕑", + "name": "transition" + }, + "adapt_delay": { + "description": "조명을 켠 후 적응형 조명이 변경 사항을 적용하기까지의 대기 시간(초). 깜박임을 피하는 데 도움이 될 수 있습니다. ⏲️", + "name": "adapt_delay" + }, + "autoreset_control_seconds": { + "description": "특정 초 후에 수동 제어를 자동으로 재설정. 0으로 설정하면 비활성화됩니다. ⏲️", + "name": "autoreset_control_seconds" + } + } + } + } } From 06dab257e3396d9849c78166562780b1e29d0aff Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 1 Jan 2025 14:22:50 -0800 Subject: [PATCH 0840/1077] docs: add Sara492 as a contributor for translation (#1149) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 4fbe022e..c049869a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -945,6 +945,15 @@ "contributions": [ "code" ] + }, + { + "login": "Sara492", + "name": "Sara492", + "avatar_url": "https://avatars.githubusercontent.com/u/63058202?v=4", + "profile": "https://github.com/Sara492", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 5000fc36..5cf5c579 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-103-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-104-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -597,6 +597,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From cdd8ca3952853e3cc674f50226ea4442b8125ed2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Jan 2025 14:26:53 -0800 Subject: [PATCH 0841/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20mcr.mic?= =?UTF-8?q?rosoft.com/vscode/devcontainers/python=20Docker=20tag=20to=20v3?= =?UTF-8?q?.13=20(#1137)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ⬆️ Update mcr.microsoft.com/vscode/devcontainers/python Docker tag to v3.13 * Update README.md, strings.json, and services.yaml --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- .devcontainer.json | 2 +- README.md | 2 +- custom_components/adaptive_lighting/services.yaml | 2 +- custom_components/adaptive_lighting/strings.json | 4 ++-- custom_components/adaptive_lighting/translations/en.json | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.devcontainer.json b/.devcontainer.json index 9b9ba58b..941547f4 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -1,6 +1,6 @@ { "name": "basnijholt/adaptive_lighting", - "image": "mcr.microsoft.com/vscode/devcontainers/python:3.12", + "image": "mcr.microsoft.com/vscode/devcontainers/python:3.13", "postCreateCommand": "scripts/setup-devcontainer", "forwardPorts": [ 8123 diff --git a/README.md b/README.md index 5cf5c579..382ad711 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ The YAML and frontend configuration methods support all of the options listed be | `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | | `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | | `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | -| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` | +| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` | | `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | | `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | | `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 4b79f88c..07363fc7 100644 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -226,7 +226,7 @@ change_switch_settings: selector: boolean: null detect_non_ha_changes: - description: 'Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an ''on'' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.' + description: 'Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️Caution: ⚠️ Some lights might falsely indicate an ''on'' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.' required: false example: false selector: diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index a2724f6b..86a05e28 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -46,7 +46,7 @@ "brightness_mode_time_dark": "brightness_mode_time_dark", "brightness_mode_time_light": "brightness_mode_time_light", "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", - "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", + "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", @@ -247,7 +247,7 @@ "name": "take_over_control" }, "detect_non_ha_changes": { - "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", + "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "name": "detect_non_ha_changes" }, "transition": { diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 9991d232..44626e29 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -47,7 +47,7 @@ "brightness_mode_time_dark": "brightness_mode_time_dark", "brightness_mode_time_light": "brightness_mode_time_light", "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", - "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", + "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", @@ -248,7 +248,7 @@ "name": "take_over_control" }, "detect_non_ha_changes": { - "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", + "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "name": "detect_non_ha_changes" }, "transition": { From 60d07ecd028e37e6a4af2d261e361d9874215ea5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 1 Jan 2025 14:37:22 -0800 Subject: [PATCH 0842/1077] Add FUNDING.yml (#1150) From f8719994a62efb54b599098ea86841278fa2a3cf Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 1 Jan 2025 16:11:22 -0800 Subject: [PATCH 0843/1077] =?UTF-8?q?use=20`uv`,=20fix=20`devcontainer`,?= =?UTF-8?q?=20and=20drop=20support=20for=20HA=20=E2=89=A42023.6=20(#1151)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update .devcontainer * Drop support for ≤2023.6 * Add .vscode/settings.json * Use async_process_ha_core_config * Fix for HA ≤2023.10 * pop normalized_name * Add comments * use . instead of source * set pytest args * set country * use py3.13 for dev branch of core * set country in test_adaptive_lighting_time_zones_with_default_settings * Add comment --- .devcontainer.json | 8 +++----- .github/workflows/pytest.yaml | 19 ++----------------- .vscode/settings.json | 20 ++++++++++++++++++++ hacs.json | 2 +- scripts/setup-dependencies | 14 +++++++++----- scripts/setup-devcontainer | 10 +++++----- tests/test_config_flow.py | 16 ++++++++++------ tests/test_switch.py | 29 ++++++++++++++++++++++------- 8 files changed, 72 insertions(+), 46 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.devcontainer.json b/.devcontainer.json index 941547f4..0a09ed54 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -1,7 +1,7 @@ { "name": "basnijholt/adaptive_lighting", - "image": "mcr.microsoft.com/vscode/devcontainers/python:3.13", - "postCreateCommand": "scripts/setup-devcontainer", + "image": "mcr.microsoft.com/devcontainers/python:1-3.13", + "postCreateCommand": "./scripts/setup-devcontainer && . .venv/bin/activate", "forwardPorts": [ 8123 ], @@ -36,7 +36,5 @@ } }, "remoteUser": "vscode", - "features": { - "ghcr.io/devcontainers/features/rust:1": {} - } + "features": {} } diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 68d4ff08..411cfdd5 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -14,22 +14,6 @@ jobs: fail-fast: false matrix: include: - - python-version: "3.10" - core-version: "2022.11.5" - - python-version: "3.10" - core-version: "2022.12.9" - - python-version: "3.10" - core-version: "2023.1.7" - - python-version: "3.10" - core-version: "2023.2.5" - - python-version: "3.10" - core-version: "2023.3.6" - - python-version: "3.10" - core-version: "2023.4.6" - - python-version: "3.10" - core-version: "2023.5.4" - - python-version: "3.11" - core-version: "2023.6.3" - python-version: "3.11" core-version: "2023.7.3" - python-version: "3.11" @@ -66,7 +50,7 @@ jobs: core-version: "2024.11.3" - python-version: "3.12" core-version: "2024.12.0" - - python-version: "3.12" + - python-version: "3.13" core-version: "dev" steps: - name: Check out code from GitHub @@ -82,6 +66,7 @@ jobs: timeout-minutes: 60 run: | export PYTHONPATH=${PYTHONPATH}:${PWD} + source .venv/bin/activate cd core python3 -X dev -m pytest \ -vvv \ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e59dc709 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "files.associations": { + "*.yaml": "home-assistant" + }, + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "python.testing.pytestEnabled": true, + "python.testing.pytestArgs": [ + "-vvv", + "-qq", + "--timeout=9", + "--durations=10", + "--cov=homeassistant", + "--cov-report=xml", + "-o", + "console_output_style=count", + "-p", + "no:sugar", + "core/tests/components/adaptive_lighting" + ] +} diff --git a/hacs.json b/hacs.json index f21e6d60..9c9035ef 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { "name": "Adaptive Lighting", "render_readme": true, - "homeassistant": "2022.11.0" + "homeassistant": "2023.7.0" } diff --git a/scripts/setup-dependencies b/scripts/setup-dependencies index ca14045d..9409ac0c 100755 --- a/scripts/setup-dependencies +++ b/scripts/setup-dependencies @@ -2,7 +2,11 @@ set -ex cd "$(dirname "$0")/.." -pip install -r core/requirements.txt +pip install uv +uv venv +source .venv/bin/activate + +uv pip install -r core/requirements.txt if grep -q 'codecov' core/requirements_test.txt; then # Older HA versions still have `codecov` in `requirements_test.txt` @@ -14,8 +18,8 @@ if grep -q 'mypy-dev==1.10.0a3' core/requirements_test.txt; then # mypy-dev==1.10.0a3 seems to not be available anymore, HA 2024.4 and 2024.5 are affected sed -i 's/mypy-dev==1.10.0a3/mypy-dev==1.10.0b1/' core/requirements_test.txt fi -pip install -r core/requirements_test.txt +uv pip install -r core/requirements_test.txt -pip install -e core/ -pip install ulid-transform # this is in Adaptive-lighting's manifest.json -pip install $(python test_dependencies.py) +uv pip install -e core/ +uv pip install ulid-transform # this is in Adaptive-lighting's manifest.json +uv pip install $(python test_dependencies.py) diff --git a/scripts/setup-devcontainer b/scripts/setup-devcontainer index 11fdd106..bdb82534 100755 --- a/scripts/setup-devcontainer +++ b/scripts/setup-devcontainer @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -e +set -ex cd "$(dirname "$0")/.." # Clone only if the folder doesn't exist @@ -8,10 +8,10 @@ if [[ ! -d "core" ]]; then fi pip install \ - colorlog==6.7.0 \ - pip>=21.0,<23.2 \ - ruff==0.0.265 + colorlog \ + pip \ + ruff ./scripts/setup-dependencies ./scripts/setup-symlinks -pre-commit install-hooks +uv run pre-commit install-hooks diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 1a9e8fbf..af01fc11 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,6 +1,5 @@ """Test Adaptive Lighting config flow.""" -from homeassistant import data_entry_flow from homeassistant.components.adaptive_lighting.const import ( CONF_SUNRISE_TIME, CONF_SUNSET_TIME, @@ -11,6 +10,7 @@ from homeassistant.components.adaptive_lighting.const import ( ) from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_NAME +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -24,7 +24,7 @@ async def test_flow_manual_configuration(hass): context={"source": "user"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["handler"] == "adaptive_lighting" @@ -32,7 +32,7 @@ async def test_flow_manual_configuration(hass): result["flow_id"], user_input={CONF_NAME: "living room"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "living room" @@ -46,7 +46,7 @@ async def test_import_success(hass): data=data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME for key, value in data.items(): assert result["data"][key] == value @@ -65,7 +65,7 @@ async def test_options(hass): await hass.config_entries.async_setup(entry.entry_id) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" data = DEFAULT_DATA.copy() @@ -75,7 +75,7 @@ async def test_options(hass): result["flow_id"], user_input=data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY for key, value in data.items(): assert result["data"][key] == value @@ -114,6 +114,9 @@ async def test_import_twice(hass): ) +# TODO: Fix, broken for all supported versions +# But in ≤2024.5 it gives homeassistant.config_entries.UnknownEntry: cd69dbda65bd3f86e9a32d974cdfa23f +# and ≥2024.6 it times out async def test_changing_options_when_using_yaml(hass): """Test changing options when using YAML.""" entry = MockConfigEntry( @@ -125,6 +128,7 @@ async def test_changing_options_when_using_yaml(hass): ) entry.add_to_hass(hass) + await hass.block_till_done() await hass.config_entries.async_setup(entry.entry_id) result = await hass.config_entries.options.async_init(entry.entry_id) diff --git a/tests/test_switch.py b/tests/test_switch.py index e582a031..afc720e5 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -11,7 +11,6 @@ from random import randint from typing import Any from unittest.mock import Mock, patch -import homeassistant.config as config_util import homeassistant.util.dt as dt_util import pytest import ulid_transform @@ -362,6 +361,19 @@ async def test_adaptive_lighting_switches(hass): assert len(data.keys()) == 5 +def async_process_ha_core_config(hass, config): + """Set up the Home Assistant configuration.""" + try: + # ha >= "2023.11.0" + from homeassistant.core_config import async_process_ha_core_config + + return async_process_ha_core_config(hass, config) + except ModuleNotFoundError: + import homeassistant.config as config_util + + return config_util.async_process_ha_core_config(hass, config) + + @pytest.mark.parametrize(("lat", "long", "timezone"), LAT_LONG_TZS) async def test_adaptive_lighting_time_zones_with_default_settings( hass, @@ -371,9 +383,9 @@ async def test_adaptive_lighting_time_zones_with_default_settings( reset_time_zone, # pylint: disable=redefined-outer-name ): """Test setting up the Adaptive Lighting switches with different timezones.""" - await config_util.async_process_ha_core_config( + await async_process_ha_core_config( hass, - {"latitude": lat, "longitude": long, "time_zone": timezone}, + {"latitude": lat, "longitude": long, "time_zone": timezone, "country": "US"}, ) _, switch = await setup_switch(hass, {}) # Shouldn't raise an exception ever @@ -394,9 +406,9 @@ async def test_adaptive_lighting_time_zones_and_sun_settings( Also test the (sleep) brightness and color temperature settings. """ - await config_util.async_process_ha_core_config( + await async_process_ha_core_config( hass, - {"latitude": lat, "longitude": long, "time_zone": timezone}, + {"latitude": lat, "longitude": long, "time_zone": timezone, "country": "US"}, ) _, switch = await setup_switch( hass, @@ -1359,6 +1371,8 @@ def mock_area_registry( area_kwargs["icon"] = None if dt >= datetime.date(2024, 3, 1): area_kwargs["floor_id"] = "test-floor" + if dt >= datetime.date(2024, 11, 1): + area_kwargs.pop("normalized_name") # This mess... 🤯 if dt >= datetime.date(2024, 2, 1) and dt != datetime.date(2024, 4, 1): @@ -1588,6 +1602,7 @@ async def test_proactive_adaptation(hass): assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 3448 +# TODO: Breaks since 2024.5.0! async def test_proactive_adaptation_with_separate_commands(hass): """Validate that a split proactive adaptation yields one additional service call.""" switch, _ = await setup_lights_and_switch( @@ -1927,9 +1942,9 @@ async def test_adapt_until_sleep_and_rgb_colors(hass): Also test the (sleep) brightness and color temperature settings. """ lat, long, timezone = (32.87336, -117.22743, "US/Pacific") - await config_util.async_process_ha_core_config( + await async_process_ha_core_config( hass, - {"latitude": lat, "longitude": long, "time_zone": timezone}, + {"latitude": lat, "longitude": long, "time_zone": timezone, "country": "US"}, ) switch, lights = await setup_lights_and_switch( hass, From 9a2466cc6f1377e4df4c75c7362c513ed3757051 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 1 Jan 2025 16:19:08 -0800 Subject: [PATCH 0844/1077] Use `markdown-code-runner==2.1.0` (#1152) * Use `markdown-code-runner==2.1.0` * Use correct arg * uv run * Update README.md, strings.json, and services.yaml --- .github/workflows/update-readme.yml | 8 ++--- README.md | 32 +++++++++---------- .../adaptive_lighting/services.yaml | 2 +- .../adaptive_lighting/strings.json | 4 +-- .../adaptive_lighting/translations/en.json | 4 +-- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index 456e6626..1df50935 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -24,16 +24,16 @@ jobs: - name: Install markdown-code-runner and README code dependencies run: | - pip install markdown-code-runner==1.0.0 pandas tabulate + uv pip install markdown-code-runner==2.1.0 pandas tabulate - name: Run markdown-code-runner - run: markdown-code-runner --debug README.md + run: uv run markdown-code-runner --verbose README.md - name: Run update services.yaml - run: python .github/update-services.py + run: uv run python .github/update-services.py - name: Run update strings.json - run: python .github/update-strings.py + run: uv run python .github/update-strings.py - name: Commit updated README.md, strings.json, and services.yaml id: commit diff --git a/README.md b/README.md index 382ad711..d4019252 100644 --- a/README.md +++ b/README.md @@ -96,13 +96,13 @@ Transform your home's atmosphere with Adaptive Lighting 🏠, and experience the All of the configuration options are listed below, along with their default values. The YAML and frontend configuration methods support all of the options listed below. - + - + - - + + | Variable name | Description | Default | Type | |:-------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:---------------------------------------| | `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | @@ -132,7 +132,7 @@ The YAML and frontend configuration methods support all of the options listed be | `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | | `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | | `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | -| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` | +| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` | | `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | | `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | | `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | @@ -144,7 +144,7 @@ The YAML and frontend configuration methods support all of the options listed be | `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | | `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | - + Full example: @@ -179,13 +179,13 @@ adaptive_lighting: `adaptive_lighting.apply` applies Adaptive Lighting settings to lights on demand. - + - + - - + + | Service data attribute | Description | Required | Type | |:-------------------------|:-------------------------------------------------------------------------------------|:-----------|:---------------------| | `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ✅ | list of `entity_id`s | @@ -196,25 +196,25 @@ adaptive_lighting: | `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | ❌ | bool | | `turn_on_lights` | Whether to turn on lights that are currently off. 🔆 | ❌ | bool | - + #### `adaptive_lighting.set_manual_control` `adaptive_lighting.set_manual_control` can mark (or unmark) whether a light is "manually controlled", meaning that when a light has `manual_control`, the light is not adapted. - + - + - - + + | Service data attribute | Description | Required | Type | |:-------------------------|:-----------------------------------------------------------------------------------------------|:-----------|:---------------------| | `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s | | `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s | | `manual_control` | Whether to add ("true") or remove ("false") the light from the "manual_control" list. 🔒 | ❌ | bool | - + #### `adaptive_lighting.change_switch_settings` `adaptive_lighting.change_switch_settings` (new in 1.7.0) Change any of the above configuration options of Adaptive Lighting (such as `sunrise_time` or `prefer_rgb_color`) with a service call directly from your script/automation. diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 07363fc7..4b79f88c 100644 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -226,7 +226,7 @@ change_switch_settings: selector: boolean: null detect_non_ha_changes: - description: 'Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️Caution: ⚠️ Some lights might falsely indicate an ''on'' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.' + description: 'Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an ''on'' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.' required: false example: false selector: diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 86a05e28..a2724f6b 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -46,7 +46,7 @@ "brightness_mode_time_dark": "brightness_mode_time_dark", "brightness_mode_time_light": "brightness_mode_time_light", "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", - "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", + "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", @@ -247,7 +247,7 @@ "name": "take_over_control" }, "detect_non_ha_changes": { - "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", + "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "name": "detect_non_ha_changes" }, "transition": { diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 44626e29..9991d232 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -47,7 +47,7 @@ "brightness_mode_time_dark": "brightness_mode_time_dark", "brightness_mode_time_light": "brightness_mode_time_light", "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", - "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", + "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", @@ -248,7 +248,7 @@ "name": "take_over_control" }, "detect_non_ha_changes": { - "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", + "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", "name": "detect_non_ha_changes" }, "transition": { From ad4e7bd7754eb030e3b119866aa58c3483d6268d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 1 Jan 2025 16:34:35 -0800 Subject: [PATCH 0845/1077] Add 2025.1.0b5 to testing (#1154) --- .github/workflows/pytest.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 411cfdd5..098a33af 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -49,7 +49,9 @@ jobs: - python-version: "3.12" core-version: "2024.11.3" - python-version: "3.12" - core-version: "2024.12.0" + core-version: "2024.12.5" + - python-version: "3.12" + core-version: "2025.1.0b5" - python-version: "3.13" core-version: "dev" steps: From e6888cdc786b0b0c52a97d74a0f1b44339b44d7e Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 1 Jan 2025 16:37:08 -0800 Subject: [PATCH 0846/1077] docs: add enpaga as a contributor for translation (#1156) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index c049869a..43c49beb 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -954,6 +954,15 @@ "contributions": [ "translation" ] + }, + { + "login": "enpaga", + "name": "enpaga", + "avatar_url": "https://avatars.githubusercontent.com/u/180730931?v=4", + "profile": "https://github.com/enpaga", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index d4019252..71ffa60d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-104-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-105-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -598,6 +598,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 32482c4e0449face455f73fa9997d34993556e3f Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 2 Jan 2025 01:37:23 +0100 Subject: [PATCH 0847/1077] Translated using Weblate (Catalan) (#1153) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 91.5% (140 of 153 strings) Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ca/ Translation: Adaptive Lighting/Adaptive Lighting Co-authored-by: Enric Pagès i Gassull --- .../adaptive_lighting/translations/ca.json | 106 +++++++++++++++++- 1 file changed, 101 insertions(+), 5 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/ca.json b/custom_components/adaptive_lighting/translations/ca.json index 11e6d43d..ff8814b0 100644 --- a/custom_components/adaptive_lighting/translations/ca.json +++ b/custom_components/adaptive_lighting/translations/ca.json @@ -11,15 +11,42 @@ "autoreset_control_seconds": "Restableix automàticament el control manual al cap d'uns segons. Posar a 0 per desactivar. ⏲️", "brightness_mode": "Mode de brillantor a utilitzar. Els valors possibles són `default`, \"linear\" i \"tanh\" (utilitza `brightness_mode_time_dark` i `brightness_mode_time_light`). 📈", "sleep_color_temp": "Temperatura de color en mode nit (s'utilitza quan `sleep_rgb_or_color_temp` és `color_temp`) en Kelvin. 😴", - "sleep_brightness": "Percentatge de brillantor de les llums en mode nit. 😴" + "sleep_brightness": "Percentatge de brillantor de les llums en mode nit. 😴", + "interval": "Freqüència d'adaptació de les llums, en segons. 🔄", + "sleep_transition": "Durada de la transició en commutar el \"mode nocturn\", en segons. 🕑", + "sleep_rgb_color": "Color RGB en mode nocturn (s'utilitza quan `sleep_rgb_or_color_temp` és \"rgb_color\"). 🌈", + "transition": "Durada de la transició en canviar les llums, en segons. 🕑", + "sunrise_time": "Indica una hora fixa (HH:MM:SS) per a la sortida del sol. 🌅", + "sleep_rgb_or_color_temp": "Utilitza `\"rgb_color\"` o `\"color_temp\"` durant el mode nocturn. 🌙", + "sunset_time": "Indica una hora fixa (HH:MM:SS) per a la posta de sol. 🌇", + "brightness_mode_time_dark": "(S'ignora si `brightness_mode='default'`) La durada en segons de la variació de la brillantor abans/despres de la sortida/posta de sol. 📈📉.", + "brightness_mode_time_light": "(S'ignora si `brightness_mode='default'`) La durada en segons de la variació de la brillantor abans/despres de la sortida/posta de sol. 📈📉.", + "adapt_delay": "Temps d'espera (en segons) entre l'encesa de la llum i els canvis per part d'Adaptive Lighting. Pot ajudar a evitar els parpalleigs. ⏲️" }, "title": "Opcions Il·luminació Adaptativa", "data": { - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quan s'encenen les llums inicialment. Si el valor és `true`, AL adapta només si s'ha cridat `light.turn_on` sense especificar color o brillantor. ❌🌈 Això impedeix l'adaptació quan s'activa una escena. Si el valor és `false`, AL adapta independentment de la presencia de color o brillantor en les dades inicials `service_data`. Necessita `take_over_control` habilitat. 🕵️ ", - "detect_non_ha_changes": "detect_non_ha_changes: Detecta i atura les adaptacions per als canvis d'estat diferents a `light.turn_on`. Necessita `take_over_control` habilitat. 🕵️ Precaució: ⚠️ Alguns llums poden indicar falsament un estat \"encès\", cosa que podria provocar que els llums s'encenguessin inesperadament. Desactiva aquesta funció si trobes aquests problemes." + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quan s'encenen les llums inicialment. Si el valor és `true`, AL adapta només si s'ha cridat `light.turn_on` sense especificar color o brillantor. ❌🌈 Això impedeix l'adaptació quan s'activa una escena. Si el valor és `false`, AL adapta independentment de la presencia de color o brillantor en les dades inicials `service_data`. Necessita `take_over_control` habilitat. 🕵️", + "detect_non_ha_changes": "detect_non_ha_changes: Detecta i atura les adaptacions per als canvis d'estat diferents a `light.turn_on`. Necessita que `take_over_control` estigui habilitat. 🕵️ Precaució: ⚠️ Alguns llums poden indicar falsament un estat \"encès\", cosa que podria provocar que els llums s'encenguin inesperadament. Desactiva aquesta funció si trobes aquests problemes.", + "lights": "lights: Llista d'entity_ids dels llums a controlar (pot estar buida). 🌟", + "min_brightness": "min_brightness: Percentatge mínim de brillantor. 💡", + "max_brightness": "max_brightness: Percentatge màxim de brillantor. 💡", + "min_color_temp": "min_color_temp: Temperatura de color més càlida en graus Kelvin. 🔥", + "max_color_temp": "max_color_temp: Temperatura de color més freda en graus Kelvin. ❄️", + "prefer_rgb_color": "prefer_rgb_color: Si prefereixes l'ajustament del color RGB en lloc de la temperatura de color, quan sigui possible. 🌈", + "take_over_control": "take_over_control: Inhabilita Adaptive Lighting si una altra font crida `light.turn_on` quan les llums estan enceses i en procés d'adaptació. Tingues present que això cridarà `homeassistant.update_entity` cada `interval`! 🔒", + "only_once": "only_once: Adapta els llums només quan s'encenen (`true`) o segueix adaptant-les (`false`). 🔄", + "separate_turn_on_commands": "separate_turn_on_commands: Separa les crides de `light.turn_on` per a color i brillantor; necessari per alguns tipus de llums. 🔀", + "include_config_in_attributes": "include_config_in_attributes: Mostra totes les opcions com atributs a l'interruptor de Home Assistant quan s'estableix com a `true`. 📝", + "multi_light_intercept": "multi_light_intercept: Intercepta i adapta les crides `light.turn_on` dirigides a múltiples llums. ➗⚠️ Pot provocar la divisió d'una crida única `light.turn_on` en múltiples crides, com ara, quan les llums són en interruptors diferents. Necessita que `intercept` estigui habilitat.", + "transition_until_sleep": "transition_until_sleep: Si s'activa, Adaptive Lighting considerarà els ajustaments del mode nocturn com a mínims, fent una transició cap aquests valors després de la posta de sol. 🌙", + "intercept": "intercept: Intercepta i adapta les crides `light.turn_on` per permetre canvis instantanis de color i brillantor. 🏎️ Inhabilita-ho per a llums que no admeten `light.turn_on` amb color i brillantor." }, "description": "Configura un component d'Adaptive Lighting. Els noms de les opcions s'alineen amb la configuració de YAML. Si has definit aquesta entrada a YAML, aquí no apareixerà cap opció. Per veure gràfics interactius que demostren efectes de paràmetres, visita [aquesta aplicació web] (https://basnijholt.github.io/adaptive-lighting). Per a més detalls, pots veure la [documentació oficial] (https://github.com/basnijholt/adaptive-lighting#readme)." } + }, + "error": { + "option_error": "Opció invàlida", + "entity_missing": "Una o més de les entitats de llum seleccionades no es troba a Home Assistant" } }, "services": { @@ -50,10 +77,58 @@ "description": "Retard (ms) entre `separate_turn_on_commands` per als llums que no admeten la configuració simultània de brillantor i color. ⏲️" }, "detect_non_ha_changes": { - "description": "Detecta i atura les adaptacions per als canvis d'estat diferents a `light.turn_on`. Necessita `take_over_control` habilitat. 🕵️ Precaució: ⚠️ Alguns llums poden indicar falsament un estat \"encès\", cosa que podria provocar que els llums s'encenguessin inesperadament. Desactiva aquesta funció si trobes aquests problemes." + "description": "Detecta i atura les adaptacions per als canvis d'estat diferents a `light.turn_on`. Necessita que `take_over_control` estigui habilitat. 🕵️ Precaució: ⚠️ Alguns llums poden indicar falsament un estat \"encès\", cosa que podria provocar que els llums s'encenguessin inesperadament. Desactiva aquesta funció si trobes aquests problemes." }, "take_over_control": { "description": "Desactiva Adaptive Lighting si una altra font crida `light.turn_on` mentre els llums estan encesos i adaptats. Tingues en compte que això crida `homeassistant.update_entity` cada `interval`! 🔒" + }, + "entity_id": { + "description": "ID de la entitat de l'interruptor. 📝" + }, + "turn_on_lights": { + "description": "Si s'encenen les llums que estan apagades en aquest moment. 🔆" + }, + "initial_transition": { + "description": "Durada de la primera transició quan les llums canvien `off` cap a `on` en segons. ⏲️" + }, + "sleep_transition": { + "description": "Durada de la transició en commutar el \"mode nocturn\", en segons. 🕑" + }, + "max_brightness": { + "description": "Percentatge màxim de brillantor. 💡" + }, + "min_brightness": { + "description": "Percentatge mínim de brillantor. 💡" + }, + "prefer_rgb_color": { + "description": "Si es prefereix ajustar el color RGB en lloc de la temperatura de color, quan sigui possible. 🌈" + }, + "min_color_temp": { + "description": "Temperatura de color més càlida, en graus Kelvin. 🔥" + }, + "separate_turn_on_commands": { + "description": "Utilitza crides independents per a `light.turn_on` per color i brillantor; necessari per alguns tipus de llums. 🔀" + }, + "sleep_rgb_or_color_temp": { + "description": "Utilitza `\"rgb_color\"` o `\"color_temp\"` durant el mode nocturn. 🌙" + }, + "sleep_rgb_color": { + "description": "Color RGB en mode nocturn (s'utilitza quan `sleep_rgb_or_color_temp` és \"rgb_color\"). 🌈" + }, + "sunrise_time": { + "description": "Indica una hora fixa (HH:MM:SS) per a la sortida del sol. 🌅" + }, + "sunset_time": { + "description": "Indica una hora fixa (HH:MM:SS) per a la posta de sol. 🌇" + }, + "transition": { + "description": "Durada de la transició en canviar les llums, en segons. 🕑" + }, + "adapt_delay": { + "description": "Temps d'espera (en segons) entre l'encesa de la llum i els canvis per part d'Adaptive Lighting. Pot ajudar a evitar els parpalleigs. ⏲️" + }, + "use_defaults": { + "description": "Defineix els valors per defecte que no s'especifiquin a la crida del servei. Opcions: \"current\" (per defecte, manté els valors actuals), \"factory\" (restaura els valors documentats per defecte), o \"configuration\" (retorna als valors per defecte de l'interruptor). ⚙️" } }, "description": "Canvia les opcions de configuració que vulguis al commutador. Totes les opcions d'aquí són les mateixes que en el flux de configuració." @@ -62,16 +137,37 @@ "fields": { "lights": { "description": "Una llum (o una llista de llums) a la qual aplicar la configuració. 💡" + }, + "transition": { + "description": "Durada de la transició en canviar les llums, en segons. 🕑" + }, + "prefer_rgb_color": { + "description": "Si es prefereix ajustar el color RGB en lloc de la temperatura de color, quan sigui possible. 🌈" + }, + "turn_on_lights": { + "description": "Si s'encenen les llums que estan apagades en aquest moment. 🔆" } }, "description": "Aplica la configuració actual d'Adaptive Lighting a les llums." + }, + "set_manual_control": { + "description": "Indica quan una llum està 'controlada manualment'.", + "fields": { + "lights": { + "description": "entity_id(s) de les llums; si no s'especifica es seleccionaran totes les llums de l'interruptor. 💡" + } + } } }, "config": { "step": { "user": { - "title": "Tria un nom per a la instància d'Adaptive Lighting" + "title": "Tria un nom per a la instància d'Adaptive Lighting", + "description": "Cada instància pot contenir múltiples llums!" } + }, + "abort": { + "already_configured": "Aquest dispositiu ja està configurat" } } } From 72eb848a794b4df92c8b6ac45ae0f73d49c0a123 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 1 Jan 2025 16:46:11 -0800 Subject: [PATCH 0848/1077] =?UTF-8?q?Fix=20setting=20config=5Fentry=20dire?= =?UTF-8?q?ctly=20in=20=E2=89=A52024.12=20(#1155)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix setting config_entry directly in ≥2024.12 FAILED tests/components/adaptive_lighting/test_config_flow.py::test_incorrect_options - RuntimeError: Detected that integration 'adaptive_lighting' sets option flow config_entry explicitly, which is deprecated at homeassistant/components/adaptive_lighting/config_flow.py, line 86: self.config_entry = config_entry. Please create a bug report at https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+adaptive_lighting%22 * compatiblity --- custom_components/adaptive_lighting/config_flow.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index f0098937..3922d000 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -5,7 +5,7 @@ import logging import homeassistant.helpers.config_validation as cv import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, MAJOR_VERSION, MINOR_VERSION from homeassistant.core import callback from .const import ( # pylint: disable=unused-import @@ -58,6 +58,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @callback def async_get_options_flow(config_entry): """Get the options flow for this handler.""" + if (MAJOR_VERSION, MINOR_VERSION) >= (2024, 12): + # https://github.com/home-assistant/core/pull/129651 + return OptionsFlowHandler() return OptionsFlowHandler(config_entry) @@ -81,9 +84,13 @@ def validate_options(user_input, errors): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for Adaptive Lighting.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, *args, **kwargs) -> None: """Initialize options flow.""" - self.config_entry = config_entry + if (MAJOR_VERSION, MINOR_VERSION) >= (2024, 12): + super().__init__(*args, **kwargs) + # https://github.com/home-assistant/core/pull/129651 + else: + self.config_entry = args[0] async def async_step_init(self, user_input=None): """Handle options flow.""" From 69c3e3503c30a4298b41c0f5eb1309bb37e2f143 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 1 Jan 2025 20:02:46 -0800 Subject: [PATCH 0849/1077] Release v1.25.0 (#1157) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index bd92e6bd..4b0cac4f 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.23.0" + "version": "1.25.0" } From 935b913bc7802e091f9b10e82545b66b56045d9e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 1 Jan 2025 22:49:58 -0800 Subject: [PATCH 0850/1077] Fix test_proactive_adaptation_with_separate_commands (#1158) --- tests/test_switch.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_switch.py b/tests/test_switch.py index afc720e5..34554872 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1602,7 +1602,6 @@ async def test_proactive_adaptation(hass): assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 3448 -# TODO: Breaks since 2024.5.0! async def test_proactive_adaptation_with_separate_commands(hass): """Validate that a split proactive adaptation yields one additional service call.""" switch, _ = await setup_lights_and_switch( @@ -1623,11 +1622,16 @@ async def test_proactive_adaptation_with_separate_commands(hass): }, ) - event_context_ids = await _turn_on_and_track_event_contexts( + events = await _turn_on_and_track_event_contexts( hass, "test_context", ENTITY_LIGHT_3, + return_full_events=True, ) + # Wait for all adaptation tasks to complete + await asyncio.gather(*switch.manager.adaptation_tasks) + await hass.async_block_till_done() + event_context_ids = [event.context.id for event in events] # Expect two service calls assert len(event_context_ids) == 2, event_context_ids From 9aee2349555ad25937ba3b7b614b7776e2d3bce5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 1 Jan 2025 23:28:15 -0800 Subject: [PATCH 0851/1077] [pre-commit.ci] pre-commit autoupdate (#972) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v5.0.0) - [github.com/astral-sh/ruff-pre-commit: v0.3.5 → v0.8.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.5...v0.8.4) - [github.com/psf/black: 24.3.0 → 24.10.0](https://github.com/psf/black/compare/24.3.0...24.10.0) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix issues --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .github/update-strings.py | 2 +- .pre-commit-config.yaml | 6 +++--- .../adaptive_lighting/adaptation_utils.py | 4 ++-- .../adaptive_lighting/color_and_brightness.py | 12 ++++-------- custom_components/adaptive_lighting/switch.py | 2 +- tests/test_switch.py | 7 ++----- webapp/color_and_brightness.py | 12 ++++-------- 7 files changed, 17 insertions(+), 28 deletions(-) diff --git a/.github/update-strings.py b/.github/update-strings.py index d25a7af2..94fce459 100644 --- a/.github/update-strings.py +++ b/.github/update-strings.py @@ -22,7 +22,7 @@ data = {} data_description = {} for k, _, typ in const.VALIDATION_TUPLES: desc = const.DOCS[k] - if len(desc) > 40 and typ != bool and typ != cv.entity_ids: + if len(desc) > 40 and typ not in (bool, cv.entity_ids): data[k] = k data_description[k] = desc else: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9a7a0a9e..fcaebb75 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: check-added-large-files - id: trailing-whitespace @@ -8,11 +8,11 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.5 + rev: v0.8.4 hooks: - id: ruff args: ["--fix"] - repo: https://github.com/psf/black - rev: 24.3.0 + rev: 24.10.0 hooks: - id: black diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py index 595911f0..aea061d1 100644 --- a/custom_components/adaptive_lighting/adaptation_utils.py +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -69,8 +69,8 @@ def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]: if service_datas and (transition := service_data.get(ATTR_TRANSITION)) is not None: transition /= len(service_datas) - for service_data in service_datas: - service_data[ATTR_TRANSITION] = transition + for _service_data in service_datas: + _service_data[ATTR_TRANSITION] = transition return service_datas diff --git a/custom_components/adaptive_lighting/color_and_brightness.py b/custom_components/adaptive_lighting/color_and_brightness.py index 52386bf7..215a9a70 100644 --- a/custom_components/adaptive_lighting/color_and_brightness.py +++ b/custom_components/adaptive_lighting/color_and_brightness.py @@ -64,12 +64,10 @@ class SunEvents: ) + self.sunrise_offset if self.min_sunrise_time is not None: min_sunrise = self._replace_time(dt, self.min_sunrise_time) - if min_sunrise > sunrise: - sunrise = min_sunrise + sunrise = max(min_sunrise, sunrise) if self.max_sunrise_time is not None: max_sunrise = self._replace_time(dt, self.max_sunrise_time) - if max_sunrise < sunrise: - sunrise = max_sunrise + sunrise = min(max_sunrise, sunrise) return sunrise def sunset(self, dt: datetime.date) -> datetime.datetime: @@ -81,12 +79,10 @@ class SunEvents: ) + self.sunset_offset if self.min_sunset_time is not None: min_sunset = self._replace_time(dt, self.min_sunset_time) - if min_sunset > sunset: - sunset = min_sunset + sunset = max(min_sunset, sunset) if self.max_sunset_time is not None: max_sunset = self._replace_time(dt, self.max_sunset_time) - if max_sunset < sunset: - sunset = max_sunset + sunset = min(max_sunset, sunset) return sunset def _replace_time( diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 893e7565..29955d0a 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2552,7 +2552,7 @@ class AdaptiveLightingManager: and id_off_to_on == turn_on_event.context.id ) - async def just_turned_off( # noqa: PLR0911, PLR0912 + async def just_turned_off( # noqa: PLR0911 self, entity_id: str, ) -> bool: diff --git a/tests/test_switch.py b/tests/test_switch.py index 34554872..607f17ef 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -2131,11 +2131,8 @@ async def test_light_group( assert events[1].context.id == "testing" e1 = events[2].data["service_data"][ATTR_ENTITY_ID] e2 = events[3].data["service_data"][ATTR_ENTITY_ID] - assert ( - e1 == "light.light_4" - and e2 == "light.light_5" - or e1 == "light.light_5" - and e2 == "light.light_4" + assert (e1 == "light.light_4" and e2 == "light.light_5") or ( + e1 == "light.light_5" and e2 == "light.light_4" ) assert ":lght:" in events[2].context.id assert ":lght:" in events[3].context.id diff --git a/webapp/color_and_brightness.py b/webapp/color_and_brightness.py index ba804df3..b632ba8e 100644 --- a/webapp/color_and_brightness.py +++ b/webapp/color_and_brightness.py @@ -64,12 +64,10 @@ class SunEvents: ) + self.sunrise_offset if self.min_sunrise_time is not None: min_sunrise = self._replace_time(dt, self.min_sunrise_time) - if min_sunrise > sunrise: - sunrise = min_sunrise + sunrise = max(min_sunrise, sunrise) if self.max_sunrise_time is not None: max_sunrise = self._replace_time(dt, self.max_sunrise_time) - if max_sunrise < sunrise: - sunrise = max_sunrise + sunrise = min(max_sunrise, sunrise) return sunrise def sunset(self, dt: datetime.date) -> datetime.datetime: @@ -81,12 +79,10 @@ class SunEvents: ) + self.sunset_offset if self.min_sunset_time is not None: min_sunset = self._replace_time(dt, self.min_sunset_time) - if min_sunset > sunset: - sunset = min_sunset + sunset = max(min_sunset, sunset) if self.max_sunset_time is not None: max_sunset = self._replace_time(dt, self.max_sunset_time) - if max_sunset < sunset: - sunset = max_sunset + sunset = min(max_sunset, sunset) return sunset def _replace_time( From da4f3c16eb071e250ccd86bc2c69378569f41368 Mon Sep 17 00:00:00 2001 From: Alex Whiteside <1505496+alexw23@users.noreply.github.com> Date: Mon, 16 Jun 2025 11:54:59 +1000 Subject: [PATCH 0852/1077] Added a HACS button for quicker install path via Google/Github (#1211) --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 71ffa60d..8d01acfc 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ [Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) is a custom component for [Home Assistant](https://www.home-assistant.io/) that intelligently adjusts the brightness and color of your lights 💡 based on the sun's position, while still allowing for manual control. -Download and install directly through [HACS (Home Assistant Community Store)](https://hacs.xyz/) +Download and install directly through [HACS (Home Assistant Community Store)](https://hacs.xyz/): + +[![Open your Home Assistant instance and open the Adaptive Lighting integration inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=basnijholt&repository=adaptive-lighting&category=integration) By automatically adapting the settings of your lights throughout the day, Adaptive Lighting helps maintain your natural circadian rhythm 😴, which can lead to improved sleep, mood, and overall well-being. Experience cooler color temperatures at noon, gradually transitioning to warmer colors at sunset and sunrise. From ded439e6f784f05ce4a319f2dc596ae39b85603d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 15 Jun 2025 21:31:51 -0700 Subject: [PATCH 0853/1077] Fix Docker setup and CI installation (#1212) --- .../workflows/install_dependencies/action.yml | 3 + .github/workflows/pytest.yaml | 56 ++++++------------- .github/workflows/update-readme.yml | 2 +- Dockerfile | 13 ++--- scripts/develop | 2 +- scripts/setup-dependencies | 19 ++----- scripts/setup-devcontainer | 1 + tests/README.md | 2 +- 8 files changed, 32 insertions(+), 66 deletions(-) diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index c1f2ddd8..8f1732d6 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -31,8 +31,11 @@ runs: uses: actions/setup-python@v5.3.0 with: python-version: ${{ inputs.python-version }} + - name: Set up UV + uses: astral-sh/setup-uv@v6 - name: Install dependencies shell: bash run: | + uv venv --python ${{ inputs.python-version }} ./scripts/setup-dependencies ./scripts/setup-symlinks diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 098a33af..fb25eff0 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -14,46 +14,22 @@ jobs: fail-fast: false matrix: include: - - python-version: "3.11" - core-version: "2023.7.3" - - python-version: "3.11" - core-version: "2023.8.4" - - python-version: "3.11" - core-version: "2023.9.3" - - python-version: "3.11" - core-version: "2023.10.5" - - python-version: "3.11" - core-version: "2023.11.3" - - python-version: "3.11" - core-version: "2023.12.4" - - python-version: "3.11" - core-version: "2024.1.6" - - python-version: "3.11" - core-version: "2024.2.5" - - python-version: "3.12" - core-version: "2024.3.3" - - python-version: "3.12" - core-version: "2024.4.4" - - python-version: "3.12" - core-version: "2024.5.5" - - python-version: "3.12" - core-version: "2024.6.4" - - python-version: "3.12" - core-version: "2024.7.4" - - python-version: "3.12" - core-version: "2024.8.3" - - python-version: "3.12" - core-version: "2024.9.3" - - python-version: "3.12" - core-version: "2024.10.4" - - python-version: "3.12" - core-version: "2024.11.3" - - python-version: "3.12" - core-version: "2024.12.5" - - python-version: "3.12" - core-version: "2025.1.0b5" - - python-version: "3.13" - core-version: "dev" + - core-version: "2024.12.5" + python-version: "3.12" + - core-version: "2025.1.4" + python-version: "3.12" + - core-version: "2025.2.5" + python-version: "3.13" + - core-version: "2025.3.4" + python-version: "3.13" + - core-version: "2025.4.4" + python-version: "3.13" + - core-version: "2025.5.3" + python-version: "3.13" + - core-version: "2025.6.1" + python-version: "3.13" + - core-version: "dev" + python-version: "3.13" steps: - name: Check out code from GitHub uses: actions/checkout@v4 diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index 1df50935..a4422130 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -20,7 +20,7 @@ jobs: - name: Install Home Assistant uses: ./.github/workflows/install_dependencies with: - python-version: "3.12" + python-version: "3.13" - name: Install markdown-code-runner and README code dependencies run: | diff --git a/Dockerfile b/Dockerfile index 6e871795..87f791e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,13 +7,7 @@ # Optionally build the image yourself with: # docker build -t basnijholt/adaptive-lighting:latest . -FROM python:3.13-bookworm - -RUN apt-get update && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y \ - git \ - build-essential libssl-dev libffi-dev python3-dev \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/astral-sh/uv:debian # Clone home-assistant/core RUN git clone --depth 1 --branch dev https://github.com/home-assistant/core.git /core @@ -25,9 +19,12 @@ COPY . /app/ RUN ln -s /core /app/core && /app/scripts/setup-symlinks # Install home-assistant/core dependencies +RUN mkdir -p /venv +ENV UV_PROJECT_ENVIRONMENT=/venv UV_PYTHON=3.13 PATH="/venv/bin:$PATH" +RUN uv venv RUN /app/scripts/setup-dependencies -WORKDIR /core +WORKDIR /app/core # Make 'custom_components/adaptive_lighting' imports available to tests ENV PYTHONPATH="${PYTHONPATH}:/app" diff --git a/scripts/develop b/scripts/develop index e7ce50cd..5382e0fc 100644 --- a/scripts/develop +++ b/scripts/develop @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -e +set -ex cd "$(dirname "$0")/.." diff --git a/scripts/setup-dependencies b/scripts/setup-dependencies index 9409ac0c..b9e2de1f 100755 --- a/scripts/setup-dependencies +++ b/scripts/setup-dependencies @@ -2,24 +2,13 @@ set -ex cd "$(dirname "$0")/.." -pip install uv -uv venv -source .venv/bin/activate +if grep -q 'mypy-dev==1.14.0a3' core/requirements_test.txt; then + # mypy-dev==1.14.0a3 seems to not be available anymore, HA 2024.12 is affected + sed -i 's/mypy-dev==1.14.0a3/mypy-dev==1.14.0a7/' core/requirements_test.txt +fi uv pip install -r core/requirements.txt - -if grep -q 'codecov' core/requirements_test.txt; then - # Older HA versions still have `codecov` in `requirements_test.txt` - # however it is removed from PyPI, so we cannot install it - sed -i '/codecov/d' core/requirements_test.txt -fi - -if grep -q 'mypy-dev==1.10.0a3' core/requirements_test.txt; then - # mypy-dev==1.10.0a3 seems to not be available anymore, HA 2024.4 and 2024.5 are affected - sed -i 's/mypy-dev==1.10.0a3/mypy-dev==1.10.0b1/' core/requirements_test.txt -fi uv pip install -r core/requirements_test.txt - uv pip install -e core/ uv pip install ulid-transform # this is in Adaptive-lighting's manifest.json uv pip install $(python test_dependencies.py) diff --git a/scripts/setup-devcontainer b/scripts/setup-devcontainer index bdb82534..f16c2cbd 100755 --- a/scripts/setup-devcontainer +++ b/scripts/setup-devcontainer @@ -12,6 +12,7 @@ pip install \ pip \ ruff +uv venv --python 3.13 ./scripts/setup-dependencies ./scripts/setup-symlinks uv run pre-commit install-hooks diff --git a/tests/README.md b/tests/README.md index dd125d97..a5b60839 100644 --- a/tests/README.md +++ b/tests/README.md @@ -20,7 +20,7 @@ This command will download the Docker image from [the adaptive-lighting Docker H If you prefer to build the image yourself, use the following command: ```bash -docker build -t basnijholt/adaptive-lighting:latest --no-cache . +docker build -t basnijholt/adaptive-lighting:latest --no-cache --progress=plain . ``` This might be necessary if the image on Docker Hub is outdated or if the [`test_dependencies.py`](../test_dependencies.py) file is updated. From 640b634074b8f7a9ff7b9c7773a428082943fbdb Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Mon, 16 Jun 2025 06:38:10 +0200 Subject: [PATCH 0854/1077] Translations update from Hosted Weblate (#1160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Galician) Currently translated at 46.4% (71 of 153 strings) Translated using Weblate (Galician) Currently translated at 45.0% (69 of 153 strings) Added translation using Weblate (Galician) Co-authored-by: Hosted Weblate Co-authored-by: Yago Raña Gayoso Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/gl/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Tamil) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: தமிழ்நேரம் Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ta/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Romanian) Currently translated at 57.5% (88 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: tinutac Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ro/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Ukrainian) Currently translated at 79.7% (122 of 153 strings) Translated using Weblate (Ukrainian) Currently translated at 61.4% (94 of 153 strings) Translated using Weblate (Ukrainian) Currently translated at 59.4% (91 of 153 strings) Translated using Weblate (Ukrainian) Currently translated at 58.8% (90 of 153 strings) Co-authored-by: Ada Melentyeva Co-authored-by: Artem Co-authored-by: Hosted Weblate Co-authored-by: Rostyslav Dudka Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/uk/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Catalan) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Enric Pagès i Gassull Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ca/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Portuguese) Currently translated at 66.0% (101 of 153 strings) Co-authored-by: Helder Ferreira Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pt/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Polish) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Piotr Laszczkowski Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pl/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Indonesian) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Reza Almanda Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/id/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Dutch) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: renout Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Spanish) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Yago Raña Gayoso Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/es/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (French) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: KosmoMoustache Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/fr/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Swedish) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: bittin1ddc447d824349b2 Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/sv/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Finnish) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Ricky Tigg Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/fi/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: gmkeebiy Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/zh_Hans/ Translation: Adaptive Lighting/Adaptive Lighting --------- Co-authored-by: Yago Raña Gayoso Co-authored-by: தமிழ்நேரம் Co-authored-by: tinutac Co-authored-by: Ada Melentyeva Co-authored-by: Artem Co-authored-by: Rostyslav Dudka Co-authored-by: Enric Pagès i Gassull Co-authored-by: Helder Ferreira Co-authored-by: Piotr Laszczkowski Co-authored-by: Reza Almanda Co-authored-by: renout Co-authored-by: KosmoMoustache Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: Ricky Tigg Co-authored-by: gmkeebiy --- .../adaptive_lighting/translations/ca.json | 47 ++++++-- .../adaptive_lighting/translations/es.json | 2 +- .../adaptive_lighting/translations/fi.json | 5 +- .../adaptive_lighting/translations/fr.json | 114 +++++++++--------- .../adaptive_lighting/translations/gl.json | 32 +++++ .../adaptive_lighting/translations/id.json | 2 +- .../adaptive_lighting/translations/nl.json | 2 +- .../adaptive_lighting/translations/pl.json | 2 +- .../adaptive_lighting/translations/pt.json | 12 +- .../adaptive_lighting/translations/ro.json | 6 +- .../adaptive_lighting/translations/sv.json | 2 +- .../adaptive_lighting/translations/ta.json | 2 +- .../adaptive_lighting/translations/uk.json | 95 ++++++++++++++- .../translations/zh-Hans.json | 2 +- 14 files changed, 246 insertions(+), 79 deletions(-) create mode 100644 custom_components/adaptive_lighting/translations/gl.json diff --git a/custom_components/adaptive_lighting/translations/ca.json b/custom_components/adaptive_lighting/translations/ca.json index ff8814b0..3b3a9b36 100644 --- a/custom_components/adaptive_lighting/translations/ca.json +++ b/custom_components/adaptive_lighting/translations/ca.json @@ -4,14 +4,14 @@ "step": { "init": { "data_description": { - "initial_transition": "Durada de la primera transició quan els llums s'encenen de `off` a `on` en segons. ⏲️", + "initial_transition": "Durada de la primera transició quan els llums canvien de `off` a `on` en segons. ⏲️", "sunset_offset": "Ajusta l'hora de la posta del sol amb una compensació positiva o negativa en segons. ⏰", "send_split_delay": "Retard (ms) entre `separate_turn_on_commands` per als llums que no admeten la configuració simultània de brillantor i color. ⏲️", "sunrise_offset": "Ajusta l'hora de sortida del sol amb una compensació positiva o negativa en segons. ⏰", "autoreset_control_seconds": "Restableix automàticament el control manual al cap d'uns segons. Posar a 0 per desactivar. ⏲️", "brightness_mode": "Mode de brillantor a utilitzar. Els valors possibles són `default`, \"linear\" i \"tanh\" (utilitza `brightness_mode_time_dark` i `brightness_mode_time_light`). 📈", - "sleep_color_temp": "Temperatura de color en mode nit (s'utilitza quan `sleep_rgb_or_color_temp` és `color_temp`) en Kelvin. 😴", - "sleep_brightness": "Percentatge de brillantor de les llums en mode nit. 😴", + "sleep_color_temp": "Temperatura de color en mode nocturn (s'utilitza quan `sleep_rgb_or_color_temp` és `color_temp`) en Kelvin. 😴", + "sleep_brightness": "Percentatge de brillantor dels llums en mode nocturn. 😴", "interval": "Freqüència d'adaptació de les llums, en segons. 🔄", "sleep_transition": "Durada de la transició en commutar el \"mode nocturn\", en segons. 🕑", "sleep_rgb_color": "Color RGB en mode nocturn (s'utilitza quan `sleep_rgb_or_color_temp` és \"rgb_color\"). 🌈", @@ -21,7 +21,11 @@ "sunset_time": "Indica una hora fixa (HH:MM:SS) per a la posta de sol. 🌇", "brightness_mode_time_dark": "(S'ignora si `brightness_mode='default'`) La durada en segons de la variació de la brillantor abans/despres de la sortida/posta de sol. 📈📉.", "brightness_mode_time_light": "(S'ignora si `brightness_mode='default'`) La durada en segons de la variació de la brillantor abans/despres de la sortida/posta de sol. 📈📉.", - "adapt_delay": "Temps d'espera (en segons) entre l'encesa de la llum i els canvis per part d'Adaptive Lighting. Pot ajudar a evitar els parpalleigs. ⏲️" + "adapt_delay": "Temps d'espera (en segons) entre l'encesa de la llum i els canvis per part d'Adaptive Lighting. Pot ajudar a evitar els parpalleigs. ⏲️", + "min_sunrise_time": "Defineix la sortida de sol virtual més primerenca (HH:MM:SS), tot permetent sortides de sol posteriors. 🌅", + "max_sunrise_time": "Defineix la sortida de sol virtual més tardana (HH:MM:SS), tot permetent sortides de sol abans. 🌅", + "max_sunset_time": "Defineix la sortida virtual de sol virtual més tardana (HH:MM:SS), tot permetent sortides de sol abans. 🌇", + "min_sunset_time": "Defineix la posta de sol virtual més primerenca (HH:MM:SS), tot permetent postes de sol més tard. 🌇" }, "title": "Opcions Il·luminació Adaptativa", "data": { @@ -39,7 +43,8 @@ "include_config_in_attributes": "include_config_in_attributes: Mostra totes les opcions com atributs a l'interruptor de Home Assistant quan s'estableix com a `true`. 📝", "multi_light_intercept": "multi_light_intercept: Intercepta i adapta les crides `light.turn_on` dirigides a múltiples llums. ➗⚠️ Pot provocar la divisió d'una crida única `light.turn_on` en múltiples crides, com ara, quan les llums són en interruptors diferents. Necessita que `intercept` estigui habilitat.", "transition_until_sleep": "transition_until_sleep: Si s'activa, Adaptive Lighting considerarà els ajustaments del mode nocturn com a mínims, fent una transició cap aquests valors després de la posta de sol. 🌙", - "intercept": "intercept: Intercepta i adapta les crides `light.turn_on` per permetre canvis instantanis de color i brillantor. 🏎️ Inhabilita-ho per a llums que no admeten `light.turn_on` amb color i brillantor." + "intercept": "intercept: Intercepta i adapta les crides `light.turn_on` per permetre canvis instantanis de color i brillantor. 🏎️ Inhabilita-ho per a llums que no admeten `light.turn_on` amb color i brillantor.", + "skip_redundant_commands": "skip_redundant_commands: Evita l'enviament de d'ordres d'adaptació als objectius on el seu estat ja és el conegut del llum. Minimitza el trànsit de la xarxa i millora la resposta de l'adaptació en alguns casos. 📉 Inhabilita-ho si l'estat físic del llum queda desincronitzat amb l'estat registrat a Home Assistant." }, "description": "Configura un component d'Adaptive Lighting. Els noms de les opcions s'alineen amb la configuració de YAML. Si has definit aquesta entrada a YAML, aquí no apareixerà cap opció. Per veure gràfics interactius que demostren efectes de paràmetres, visita [aquesta aplicació web] (https://basnijholt.github.io/adaptive-lighting). Per a més detalls, pots veure la [documentació oficial] (https://github.com/basnijholt/adaptive-lighting#readme)." } @@ -56,7 +61,7 @@ "description": "Ajustar les llums només quan s'encenguin (`true`) o ajustar contínuament(`false`). 🔄" }, "sleep_color_temp": { - "description": "Temperatura de color en mode nit (s'utilitza quan `sleep_rgb_or_color_temp` és `color_temp`) en Kelvin. 😴" + "description": "Temperatura de color en mode nocturn (s'utilitza quan `sleep_rgb_or_color_temp` és `color_temp`) en Kelvin. 😴" }, "sunrise_offset": { "description": "Ajusta l'hora de sortida del sol amb una compensació positiva o negativa en segons. ⏰" @@ -68,7 +73,7 @@ "description": "Restableix automàticament el control manual al cap d'uns segons. Posar a 0 per desactivar. ⏲️" }, "sleep_brightness": { - "description": "Percentatge de brillantor de les llums en mode nit. 😴" + "description": "Percentatge de brillantor dels llums en mode nocturn. 😴" }, "max_color_temp": { "description": "Temperatura de color més freda en Kelvin. ❄️" @@ -77,7 +82,7 @@ "description": "Retard (ms) entre `separate_turn_on_commands` per als llums que no admeten la configuració simultània de brillantor i color. ⏲️" }, "detect_non_ha_changes": { - "description": "Detecta i atura les adaptacions per als canvis d'estat diferents a `light.turn_on`. Necessita que `take_over_control` estigui habilitat. 🕵️ Precaució: ⚠️ Alguns llums poden indicar falsament un estat \"encès\", cosa que podria provocar que els llums s'encenguessin inesperadament. Desactiva aquesta funció si trobes aquests problemes." + "description": "Detecta i atura les adaptacions per als canvis d'estat diferents a `light.turn_on`. Necessita que `take_over_control` estigui habilitat. 🕵️ Precaució: ⚠️ Alguns llums poden indicar falsament un estat \"encès\", cosa que podria provocar que els llums s'encenguin inesperadament. Desactiva aquesta funció si trobes aquests problemes." }, "take_over_control": { "description": "Desactiva Adaptive Lighting si una altra font crida `light.turn_on` mentre els llums estan encesos i adaptats. Tingues en compte que això crida `homeassistant.update_entity` cada `interval`! 🔒" @@ -89,7 +94,7 @@ "description": "Si s'encenen les llums que estan apagades en aquest moment. 🔆" }, "initial_transition": { - "description": "Durada de la primera transició quan les llums canvien `off` cap a `on` en segons. ⏲️" + "description": "Durada de la primera transició quan els llums canvien `off` a `on` en segons. ⏲️" }, "sleep_transition": { "description": "Durada de la transició en commutar el \"mode nocturn\", en segons. 🕑" @@ -129,6 +134,15 @@ }, "use_defaults": { "description": "Defineix els valors per defecte que no s'especifiquin a la crida del servei. Opcions: \"current\" (per defecte, manté els valors actuals), \"factory\" (restaura els valors documentats per defecte), o \"configuration\" (retorna als valors per defecte de l'interruptor). ⚙️" + }, + "include_config_in_attributes": { + "description": "Mostra totes les opcions com a atributs de l'interruptor a Home Assistant quan es defineixi com a `true`. 📝" + }, + "max_sunrise_time": { + "description": "Defineix la sortida de sol virtual més tardana (HH:MM:SS), tot permetent sortides de sol abans. 🌅" + }, + "min_sunset_time": { + "description": "Defineix la posta de sol virtual més primerenca (HH:MM:SS), tot permetent postes de sol més tard. 🌇" } }, "description": "Canvia les opcions de configuració que vulguis al commutador. Totes les opcions d'aquí són les mateixes que en el flux de configuració." @@ -146,6 +160,15 @@ }, "turn_on_lights": { "description": "Si s'encenen les llums que estan apagades en aquest moment. 🔆" + }, + "entity_id": { + "description": "L'`entity_id` de l'interruptor amb els paràmetres per aplicar. 📝" + }, + "adapt_brightness": { + "description": "Si cal adaptar la brillantor del llum. 🌞" + }, + "adapt_color": { + "description": "Si cal adaptar el color a les llums que ho admetin. 🌈" } }, "description": "Aplica la configuració actual d'Adaptive Lighting a les llums." @@ -155,6 +178,12 @@ "fields": { "lights": { "description": "entity_id(s) de les llums; si no s'especifica es seleccionaran totes les llums de l'interruptor. 💡" + }, + "entity_id": { + "description": "L'`entity_id` de l'interruptor al qual (des)marcar el llum com a `manually controlled`. 📝" + }, + "manual_control": { + "description": "Si cal afegir (\"true\") o treure (\"false\") el llum de la llista de \"manual_control\". 🔒" } } } diff --git a/custom_components/adaptive_lighting/translations/es.json b/custom_components/adaptive_lighting/translations/es.json index c90b842e..66d6a7a8 100644 --- a/custom_components/adaptive_lighting/translations/es.json +++ b/custom_components/adaptive_lighting/translations/es.json @@ -29,7 +29,7 @@ "adapt_delay": "Tiempo de espera (segundos) entre el encendido de la luz y Adaptive Lighting aplicando cambios. Puede ayudar a evitar parpadeos. ⏲️" }, "data": { - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Al encender las luces. Si el valor es `true`, AL adapta sólo si se llama `light.turn_on` sin especificar el color o brillo. ❌🌈 Esto, por ejemplo, previene la adaptación al activar una escena. Si el valor es `false`, AL adapta independientemente de la presencia de color o brillo en `service_data` inicial. Necesita `take_over_control` habilitado. 🕵️ ", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Al encender las luces. Si el valor es `true`, AL adapta sólo si se llama `light.turn_on` sin especificar el color o brillo. ❌🌈 Esto, por ejemplo, previene la adaptación al activar una escena. Si el valor es `false`, AL adapta independientemente de la presencia de color o brillo en `service_data` inicial. Necesita `take_over_control` habilitado. 🕵️", "detect_non_ha_changes": "detect_non_ha_changes: Detecta e interrumpe adaptaciones para cambios de estado no `light.turn_on`. Necesita `take_over_control` habilitado. 🕵️ Precaución: ⚠️ Algunas luces pueden indicar de forma errónea un estado 'on', que puede resultar en luces que se enciendan de forma no esperada. Deshabilita esta función si encuentras dichos problemas.", "intercept": "intercept: Intercepta y adapta llamadas a `light.turn_on` para habilitar adaptaciones instantáneas de color y brillo. 🏎️ Deshabilitar para luces que no soporten `light.turn_on` con color y brillo.", "min_color_temp": "min_color_temp: Temperatura de color más cálida en grados Kelvin. 🔥", diff --git a/custom_components/adaptive_lighting/translations/fi.json b/custom_components/adaptive_lighting/translations/fi.json index 15b5b18f..6fd5f528 100644 --- a/custom_components/adaptive_lighting/translations/fi.json +++ b/custom_components/adaptive_lighting/translations/fi.json @@ -158,11 +158,12 @@ "min_sunset_time": "Aseta aikaisin virtuaalinen auringonlaskuaika (TT:MM:SS), myöhempiä auringonlaskuja sallien. 🌅", "min_sunrise_time": "Aseta aikaisin virtuaalinen auringonnousuaika (TT:MM:SS), myöhempiä auringonnousuja sallien. 🌅", "max_sunrise_time": "Aseta aikaisin virtuaalinen auringonnousuaika (TT:MM:SS), aikaisempia auringonnousuja sallien. 🌅", - "sleep_rgb_or_color_temp": "Käytä joko `\"rgb_color\"` tai `\"color_temp\"` lepotilassa. 🌙" + "sleep_rgb_or_color_temp": "Käytä joko `\"rgb_color\"` tai `\"color_temp\"` lepotilassa. 🌙", + "max_sunset_time": "Aseta viimeisin virtuaalinen auringonlaskuaika (TT:MM:SS), jotta aikaisemmat auringonlaskut ovat mahdollisia. 🌇" }, "description": "Määritä Adaptive Lighting -komponentti. Vaihtoehtojen nimet vastaavat YAML-asetuksia. Jos olet määrittänyt tämän merkinnän YAML:ssa, tässä ei näy vaihtoehtoja. Interaktiiviset kaaviot, jotka esittelevät parametrien vaikutuksia, on [tässä verkkosovelluksessa](https://basnijholt.github.io/adaptive-lighting). Lisätietoja löytyy [virallisesta dokumentaatiosta](https://github.com/basnijholt/adaptive-lighting#readme).", "data": { - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Kun valot sytytetään ensimmäisen kerran. Jos asetuksena on \"true\", Adaptive Lighting mukautuu vain, jos \"light.turn_on\" kutsutaan määrittelemättä väriä tai kirkkautta. ❌🌈 Tämä esimerkiksi estää mukautumisen näkymää aktivoitaessa. Jos \"false\", Adaptive Lighting mukautuu riippumatta siitä, onko alkuperäisessä \"service_data\"-arvossa väri tai kirkkaus. Vaatii \"take_over_control\":n käyttöönoton. 🕵️ ", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Kun valot sytytetään ensimmäisen kerran. Jos asetuksena on \"true\", Adaptive Lighting mukautuu vain, jos \"light.turn_on\" kutsutaan määrittelemättä väriä tai kirkkautta. ❌🌈 Tämä esimerkiksi estää mukautumisen näkymää aktivoitaessa. Jos \"false\", Adaptive Lighting mukautuu riippumatta siitä, onko alkuperäisessä \"service_data\"-arvossa väri tai kirkkaus. Vaatii \"take_over_control\":n käyttöönoton. 🕵️", "multi_light_intercept": "multi_light_intercept: sieppaa ja mukauta light.turn_on-kutsut, jotka kohdistuvat useisiin valoihin. ➗⚠️ Tämä saattaa johtaa yksittäisen light.turn_on-kutsun jakamiseen useiksi kutsuiksi, esimerkiksi kun valot ovat eri kytkimissä. Vaadi `intercept`:n käyttöönotto.", "only_once": "only_once: Mukauta valot vain, kun ne ovat päällä (\"true\") tai mukauta niitä jatkuvasti (\"false\"). 🔄", "skip_redundant_commands": "skip_redundant_commands: Ohita mukautuskomentojen lähettäminen, joiden kohdetila on jo yhtä suuri kuin valon tunnettu tila. Minimoi verkkoliikenteen ja parantaa mukautumisvastetta joissain tilanteissa. 📉 Poista käytöstä, jos fyysiset valotilat eivät ole synkronoitu kotiavustajan tallennetun tilan kanssa.", diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json index a3bc6bc5..c749ba6b 100644 --- a/custom_components/adaptive_lighting/translations/fr.json +++ b/custom_components/adaptive_lighting/translations/fr.json @@ -4,7 +4,7 @@ "step": { "user": { "title": "Choisissez un nom pour cette instance d'éclairage adaptatif", - "description": "Choisissez un nom pour cette instance. Vous pouvez configurer plusieurs instances d'éclairage adaptatif, chacune pouvant contrôler plusieurs lampes !", + "description": "Chaque instance peut contenir plusieurs lumières", "data": { "name": "Nom" } @@ -18,71 +18,71 @@ "step": { "init": { "title": "Options d'éclairage adaptatif", - "description": "Tous les paramètres de l'instance d'éclairage adaptatif. Les noms des options correspondent aux paramètres YAML. Aucune option n'est affichée si l'entrée adaptive_lighting est définie dans votre configuration YAML.", + "description": "Configurer un composant d'éclairage adaptatif. Les noms correspondent aux paramètres YAML. Si vous avez défini cette entrée en YAML, aucune option n'apparaît ici. Pour les graphiques interactifs qui montrent les effets des paramètres, visiter [cette application web](https://basnijholt.github.io/adaptive-lighting). Pour plus de détail, voir la [documentation](https://github.com/basnijholt/adaptive-lighting#readme)", "data": { - "lights": "lights : Les lampes à contrôler", + "lights": "lights : Liste d'\"entity_ids\" de lumières à controller (peu être vide). 🌟", "initial_transition": "initial_transition : Transition (en secondes) lorsque l'état d'une lampe passe d'« éteinte » à « allumée ».", "sleep_transition": "sleep_transition : Transition (en secondes) lorsque « sleep_state » est commuté.", "interval": "interval : Temps (en secondes) entre deux mises à jour du commutateur.", - "max_brightness": "max_brightness : Luminosité maximale des lampes (en pourcentage) au cours d'un cycle.", - "max_color_temp": "max_color_temp : Couleur la plus froide (en kelvins) du cycle de température de couleur.", - "min_brightness": "min_brightness : Luminosité minimale des lampes (en pourcentage) au cours d'un cycle.", - "min_color_temp": "min_color_temp : Couleur la plus chaude (en kelvins) du cycle de température de couleur.", - "only_once": "only_once : Adapter les lampes uniquement au moment où elles sont allumées.", - "prefer_rgb_color": "prefer_rgb_color : Utiliser « rgb_color » plutôt que « color_temp » lorsque cela est possible.", - "separate_turn_on_commands": "separate_turn_on_commands : Séparer les commandes pour chaque attribut (couleur, luminosité, etc.) de « light.turn_on » (nécessaire pour certaines lampes).", + "max_brightness": "max_brightness : Luminosité maximum (en pourcentage). 💡", + "max_color_temp": "max_color_temp : Couleur la plus froide (en Kelvins). ❄️", + "min_brightness": "min_brightness : Luminosité minimale en pourcentage. 💡", + "min_color_temp": "min_color_temp : Couleur de température la plus chaude en kelvins. 🔥", + "only_once": "only_once : Adapter les lampes uniquement au moment où elles sont allumées. 🔄", + "prefer_rgb_color": "prefer_rgb_color : Indique s'il est préférable d'utiliser le réglage de couleur RBG plutôt que la température de couleur lorsque cela est possible. 🌈", + "separate_turn_on_commands": "separate_turn_on_commands : Utiliser des appels \"light.turn_on\" séparés pour la couleur et la luminosité, nécessaires pour certains types de lumière. 🔀", "sleep_brightness": "sleep_brightness : Luminosité (en pourcentage) du mode nuit.", "sleep_color_temp": "sleep_color_temp : Température de couleur (en kelvins) du mode nuit.", "sunrise_offset": "sunrise_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au lever du soleil.", "sunrise_time": "sunrise_time : Heure (HH:MM:SS) du lever du soleil. Si « None », utilise l'heure correspondant à votre emplacement.", "sunset_offset": "sunset_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au coucher du soleil.", "sunset_time": "sunset_time : Heure (HH:MM:SS) du coucher du soleil. Si « None », utilise l'heure correspondant à votre emplacement.", - "take_over_control": "take_over_control : Si quelque chose d'autre que l'éclairage adaptatif appelle « light.turn_on » alors qu'une lampe est déjà allumée, cesser d'adapter cette lampe jusqu'à ce qu'elle (ou le commutateur) soit éteinte puis rallumée.", - "detect_non_ha_changes": "detect_non_ha_changes : Détecter tout changement de plus de 10 % appliqué aux lampes (même en dehors de HA). Nécessite que « take_over_control » soit activé. (Appelle « homeassistant.update_entity » tous les « interval » !)", + "take_over_control": "take_over_control : Désactive l'éclairage adaptatif si une autre source appelle \"light.turn_on\" pendant que la lumière est allumée ou adoptée. Notez que cela appelle \"homeassistant.update_entity\" chaque \"interval\"! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes : Détecter et arrête les changement d'états autre que \"light.turn_on\". Nécessite que \"take_over_control\" soit activé. 🕵️ Attention : ⚠️ Certaines lumière peuvent faussement indiqué un état \"on\", ce qui pourrait occasionner des lumières s'allumant de façon inattendu. Désactivez cette fonctionnalité si vous rencontrez de tels problème.", "transition": "transition : Durée de la transition (en secondes) des changements appliqués aux lampes.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quand on allume les lumières au départ. Si le paramètre est < < vrai > > , AL s ' adapte uniquement si l ' on invoque < < light.turn_on > > sans préciser la couleur ou la luminosité. ❌ Ceci, par exemple, empêche l'adaptation lors de l'activation d'une scène. Si `false`, AL s'adapte indépendamment de la présence de couleur ou de luminosité dans le `service_data' initial. Besoins `take_over_control` activé. 🕵∫ ", - "multi_light_intercept": "multi_light_intercept : Intercepte et adapte les appels à `light.turn_on` qui ciblent plusieurs lumières. ➗⚠️ Cela peut entraîner la division d'un seul appel `light.turn_on` en plusieurs appels, par exemple, lorsque les lumières sont dans différents interrupteurs. Nécessite que `intercept` soit activé.", - "intercept": "intercept : Intercepter et adapter les appels à `light.turn_on` pour permettre une adaptation instantanée de la couleur et de la luminosité. 🏎️ Désactivez cette option pour les lumières qui ne prennent pas en charge `light.turn_on` avec couleur et luminosité.", - "include_config_in_attributes": "include_config_in_attributes : Afficher toutes les options en tant qu'attributs sur l'interrupteur dans Home Assistant lorsqu'il est défini sur `true`. 📝", - "skip_redundant_commands": "skip_redundant_commands : Évite d'envoyer des commandes d'adaptation lorsque l'état cible est déjà égal à l'état connu de la lumière. Minimise le trafic réseau et améliore la réactivité de l'adaptation dans certaines situations. 📉 Désactivez si les états physiques des lumières ne correspondent pas à l'état enregistré de Home Assistant.", - "transition_until_sleep": "transition_until_sleep : Lorsqu'activée, l'Éclairage Adaptatif considérera les paramètres de sommeil comme le minimum, effectuant la transition vers ces valeurs après le coucher du soleil. 🌙" + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Lors de l'allumage initiale des lumières. Si le paramètre est \"vrai\", AL s'adapte uniquement si l'on invoque \"light.turn_on\" sans préciser la couleur ou la luminosité. ❌🌈 Ceci, par exemple, empêche l'adaptation lors de l'activation d'une scène. Si \"false\", AL adapte indépendamment de la présence de couleur ou de luminosité dans le \"service_data\" initial. \"take_over_control\" doit être activé. 🕵", + "multi_light_intercept": "multi_light_intercept : Intercepte et adapte les appels à \"light.turn_on\" qui ciblent plusieurs lumières. ➗⚠️ Cela peut entraîner la division d'un seul appel \"light.turn_on\" en plusieurs appels, par exemple, lorsque les lumières sont dans différents interrupteurs. Nécessite que \"intercept\" soit activé.", + "intercept": "intercept : Intercepter et adapter les appels à \"light.turn_on\" pour permettre une adaptation instantanée de la couleur et de la luminosité. 🏎️ Désactivez cette option pour les lumières qui ne prennent pas en charge \"light.turn_on\" avec couleur et luminosité.", + "include_config_in_attributes": "include_config_in_attributes : Afficher toutes les options en tant qu'attributs sur l'interrupteur dans Home Assistant lorsqu'il est défini sur \"true\". 📝", + "skip_redundant_commands": "skip_redundant_commands : Évite d'envoyer des commandes d'adaptation lorsque l'état cible est déjà égal à l'état connu de la lumière. Minimise le trafic réseau et améliore la réactivité de l'adaptation dans certaines situations. 📉 Désactivez si les états physiques des lumières ne correspondent pas à l'état enregistré de Home Assistant.", + "transition_until_sleep": "transition_until_sleep : Lorsque cela est activée, l'éclairage Adaptatif considérera les paramètres du mode nuit comme le minimum, effectuant la transition vers ces valeurs après le coucher du soleil. 🌙" }, "data_description": { "interval": "Fréquence d'adaptation des lumières, en secondes. 🔄", - "sleep_brightness": "Pourcentage de luminosité des lumières en mode sommeil. 😴", - "autoreset_control_seconds": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲∫", - "sunset_offset": "Réglez le temps de coucher avec un décalage positif ou négatif en quelques secondes. ⏰", - "brightness_mode": "Mode de luminosité à utiliser. Les valeurs possibles sont < < par défaut > > , < < linéaire > > et < < parois > > , < < par défaut > > , et < < par coup > > , < < par défaut > > et par > par > . 📈", - "send_split_delay": "Délai (ms) entre \"separate_turn_on_commands\" pour les lumières qui ne supportent pas la commande de luminosité et le réglage de couleur en même temps. ⏲", - "sleep_color_temp": "Température de couleur en mode sommeil (utilisée lorsque `sleep_rgb_or_color_temp` est `color_temp`) en Kelvin. 😴", + "sleep_brightness": "Pourcentage de luminosité des lumières en mode nuit. 😴", + "autoreset_control_seconds": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲️", + "sunset_offset": "Réglez le l'heure de coucher du soleil avec un décalage positif ou négatif en quelques secondes. ⏰", + "brightness_mode": "Mode de luminosité à utiliser. Les valeurs possibles sont \"défaut\" , \"linear\" (linéaire) et \"tanh\" (tangente) utilise \"brightness_mode_time_dark\" et \"brightness_mode_time_light\". 📈", + "send_split_delay": "Délai (ms) entre \"separate_turn_on_commands\" pour les lumières qui ne supportent pas la commande de luminosité et le réglage de couleur en même temps. ⏲️", + "sleep_color_temp": "Température de couleur en mode nuit en Kelvin (utilisée lorsque \"sleep_rgb_or_color_temp\" est égaler à \"color_temp\") . 😴", "sunrise_offset": "Ajuster l'heure du lever de soleil avec un décalage positif ou négatif en secondes. ⏰", "transition": "Durée de la transition des changements lumineux, en secondes. 🕑", - "initial_transition": "Durée de la première transition des lampes passant de `off` à `on` en secondes. ⏲️", - "sleep_transition": "Durée de la transition quand le \"mode sommeil\" est déclenché en secondes. 😴", - "min_sunset_time": "Définir l'heure virtuelle de coucher du soleil la plus précoce (HH:MM:SS), permettant des couchers de soleil ultérieurs. 🌇", - "sleep_rgb_color": "Couleur RGB en mode sommeil (utilisée lorsque `sleep_rgb_or_color_temp` est `rgb_color`). 🌈", - "brightness_mode_time_light": "(Ignoré si `brightness_mode='default'`) La durée en secondes pour augmenter/diminuer progressivement la luminosité après/avant le lever/coucher du soleil. 📈📉.", - "sunset_time": "Définir une heure fixe (HH:MM:SS) pour le coucher du soleil. 🌅", + "initial_transition": "Durée de la première transition des lampes passent de \"off\" à \"on\" (en secondes). ⏲️", + "sleep_transition": "Durée de la transition quand le \"mode nuit\" est déclenché. (en secondes) 😴", + "min_sunset_time": "Définir l'heure virtuelle de coucher du soleil la plus précoce (HH:MM:SS), permettant des couchers de soleil tardifs. 🌇", + "sleep_rgb_color": "Couleur RGB en mode nuit (utilisée lorsque \"sleep_rgb_or_color_temp\" est \"rgb_color\"). 🌈", + "brightness_mode_time_light": "(Ignoré si \"brightness_mode='default'\") La durée en secondes pour augmenter/diminuer progressivement la luminosité après/avant le lever/coucher du soleil. 📈📉.", + "sunset_time": "Définir une heure fixe (HH:MM:SS) pour le coucher du soleil. 🌇", "sunrise_time": "Définir une heure fixe (HH:MM:SS) pour le lever du soleil. 🌅", - "brightness_mode_time_dark": "(Ignoré si brightness_mode='default') La durée en secondes pour augmenter/diminuer progressivement la luminosité après/avant le lever/coucher du soleil. 📈📉.", - "sleep_rgb_or_color_temp": "Utilisez soit `\"rgb_color\"` soit `\"color_temp\"` en mode sommeil. 🌙", - "min_sunrise_time": "Définir l'heure virtuelle de lever du soleil la plus précoce (HH:MM:SS), permettant des levers de soleil ultérieurs. 🌅", + "brightness_mode_time_dark": "(Ignoré si \"brightness_mode='default'\") La durée en secondes pour augmenter/diminuer progressivement la luminosité après/avant le lever/coucher du soleil. 📈📉.", + "sleep_rgb_or_color_temp": "Utilisez soit \"rgb_color\" soit \"color_temp\" en mode nuit. 🌙", + "min_sunrise_time": "Définir l'heure virtuelle de lever du soleil la plus précoce (HH:MM:SS), permettant des levers de soleil tardifs. 🌅", "adapt_delay": "Temps d'attente (en secondes) entre l'allumage de la lumière et l'application des changements par l'Éclairage Adaptatif. Peut aider à éviter les scintillements. ⏲️", "max_sunset_time": "Définir l'heure virtuelle de coucher du soleil la plus tardive (HH:MM:SS), permettant des couchers de soleil plus précoces. 🌇", - "max_sunrise_time": "Définir l'heure virtuelle de lever du soleil la plus tardive (HH:MM:SS), permettant des levers de soleil plus précoces. 🌅" + "max_sunrise_time": "Définir l'heure virtuelle de lever du soleil la plus tardive (HH:MM:SS), permettant des levers de soleil plus tôt. 🌅" } } }, "error": { "option_error": "Option invalide", - "entity_missing": "Une lumière sélectionnée n’a pas été trouvée" + "entity_missing": "Une ou plusieurs entités lumières sélectionnées sont manquantes de Home Assistant" } }, "services": { "change_switch_settings": { "fields": { "sleep_brightness": { - "description": "Pourcentage de luminosité des lumières en mode sommeil. 😴" + "description": "Pourcentage de luminosité des lumières en mode nuit. 😴" }, "only_once": { "description": "Adapter les lumières seulement quand elles sont allumées (\"vrai\") ou quel que soit leur état (\"faux\")." @@ -91,34 +91,34 @@ "description": "Ajustez l'heure de lever de soleil avec un décalage positif ou négatif en secondes. ⏰" }, "max_color_temp": { - "description": "Température de couleur la plus froide en Kelvin. assemblage" + "description": "Température de couleur la plus froide en Kelvin. ❄️" }, "send_split_delay": { - "description": "Délai (ms) entre \"separate_turn_on_commands\" pour les lumières qui ne supportent pas la commande de luminosité et le réglage de couleur en même temps. ⏲" + "description": "Délai (ms) entre \"separate_turn_on_commands\" pour les lumières qui ne supportent pas la commande de luminosité et le réglage de couleur en même temps. ⏲️" }, "detect_non_ha_changes": { - "description": "Détecte et arrête les adaptations pour un changement d'état autre que \"light.turn_on\". Nécessite \"take_over_control\" activé. 🕵️ Attention: ⚠️Certaines lumières pourraient faussement indiquer un état \"on\", ce qui pourrait donner lieu à des allumages inattendus. Désactivez cette fonctionnalité si vous rencontrez ce problème." + "description": "detect_non_ha_changes : Détecter et arrête les changement d'états autre que \"light.turn_on\". Nécessite que \"take_over_control\" soit activé. 🕵️ Attention : ⚠️ Certaines lumière peuvent faussement indiqué un état \"on\", ce qui pourrait occasionner des lumières s'allumant de façon inattendu. Désactivez cette fonctionnalité si vous rencontrez de tels problème." }, "autoreset_control_seconds": { - "description": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲∫" + "description": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲️" }, "sunset_offset": { "description": "Ajustez l'heure de coucher de soleil avec un décalage positif ou négatif en secondes. ⏰" }, "sleep_color_temp": { - "description": "Température de couleur en mode sommeil (utilisé lorsque \"sleep_rgb_or_color_temp\" est défini sur \"color_temp\") en Kelvin. 😴" + "description": "Température de couleur en mode nuit en Kelvin (utilisé lorsque \"sleep_rgb_or_color_temp\" est défini sur \"color_temp\") . 😴" }, "entity_id": { "description": "ID de l'Entité de l'interrupteur. 📝" }, "initial_transition": { - "description": "Durée de la première transition des lampes passant de `off` à `on` en secondes. ⏲️" + "description": "Durée de la première transition des lampes passant de \"off\" à \"on\" (en secondes). ⏲️" }, "transition": { "description": "Durée de la transition des changements lumineux, en secondes. 🕑" }, "sleep_transition": { - "description": "Durée de la transition quand le \"mode sommeil\" est déclenché en secondes. 😴" + "description": "Durée de la transition quand le \"mode nuit\" est déclenché en secondes. 😴" }, "min_brightness": { "description": "Pourcentage de luminosité minimum. 💡" @@ -130,52 +130,52 @@ "description": "Pourcentage de luminosité maximale. 💡" }, "take_over_control": { - "description": "Désactiver l'Éclairage Adaptatif si une autre source appelle `light.turn_on` lorsque les lumières sont allumées et en cours d'adaptation. Notez que cela appelle `homeassistant.update_entity` à chaque `intervalles` ! 🔒" + "description": "Désactiver l'Éclairage Adaptatif si une autre source appelle \"light.turn_on\" lorsque les lumières sont allumées et en cours d'adaptation. Notez que cela appelle \"homeassistant.update_entity\" à chaque \"intervalles\" ! 🔒" }, "use_defaults": { - "description": "Définit les valeurs par défaut non spécifiées dans cet appel de service. Options : \"current\" (par défaut, conserve les valeurs actuelles), \"factory\" (réinitialise aux valeurs par défaut documentées) ou \"configuration\" (revient aux valeurs par défaut de la configuration de l'interrupteur). ⚙️" + "description": "Définit les valeurs par défaut non spécifiées dans cet appel de service. Options : \"current\" (par défaut, conserve les valeurs actuelles), \"factory\" (réinitialise aux valeurs par défaut documentées) ou \"configuration\" (revient aux valeurs par défaut de la configuration de l'interrupteur). ⚙️" }, "sunset_time": { - "description": "Définir une heure fixe (HH:MM:SS) pour le coucher du soleil. 🌅" + "description": "Définir une heure fixe (HH:MM:SS) pour le coucher du soleil. 🌇" }, "min_sunset_time": { - "description": "Définir l'heure virtuelle de coucher du soleil la plus précoce (HH:MM:SS), permettant des couchers de soleil ultérieurs. 🌇" + "description": "Définir l'heure virtuelle de coucher du soleil la plus précoce (HH:MM:SS), permettant des couchers de soleil tardifs. 🌇" }, "max_sunrise_time": { - "description": "Définir l'heure virtuelle du lever du soleil la plus tardive (HH:MM:SS), permettant des levers de soleil plus précoces. 🌅" + "description": "Définir l'heure virtuelle du lever du soleil la plus tardive (HH:MM:SS), permettant des levers de soleil plus tôt. 🌅" }, "min_color_temp": { "description": "Température de couleur la plus chaude en Kelvin. 🔥" }, "sleep_rgb_or_color_temp": { - "description": "Utilisez soit `\"rgb_color\"` soit `\"color_temp\"` en mode sommeil. 🌙" + "description": "Utilisez soit \"rgb_color\" soit \"color_temp\" en mode nuit. 🌙" }, "turn_on_lights": { "description": "Indique s'il faut allumer les lumières qui sont actuellement éteintes. 🔆" }, "include_config_in_attributes": { - "description": "Afficher toutes les options en tant qu'attributs sur l'interrupteur dans Home Assistant lorsqu'il est défini sur `true`. 📝" + "description": "Afficher toutes les options en tant qu'attributs sur l'interrupteur dans Home Assistant lorsqu'il est défini sur \"true\". 📝" }, "sleep_rgb_color": { - "description": "Couleur RGB en mode sommeil (utilisée lorsque `sleep_rgb_or_color_temp` est `rgb_color`). 🌈" + "description": "Couleur RGB en mode nuit (utilisée lorsque `sleep_rgb_or_color_temp` est `rgb_color`). 🌈" }, "adapt_delay": { "description": "Temps d'attente (en secondes) entre l'allumage de la lumière et l'application des changements par l'Éclairage Adaptatif. Peut aider à éviter les scintillements. ⏲️" }, "separate_turn_on_commands": { - "description": "Utilisez des appels distincts à `light.turn_on` pour la couleur et la luminosité, nécessaire pour certains types de lumières. 🔀" + "description": "Utilisez des appels distincts à \"light.turn_on\" pour la couleur et la luminosité, nécessaire pour certains types de lumières. 🔀" }, "prefer_rgb_color": { "description": "Indique s'il faut privilégier l'ajustement de la couleur RGB plutôt que la température de couleur de la lumière lorsque c'est possible. 🌈" } }, - "description": "Changez les réglages que vous souhaitez dans le commutateur. Toutes les options ici sont les mêmes que dans le flux de configuration." + "description": "Changez les réglages que vous souhaitez dans le commutateur. Toutes les options ici sont les mêmes que dans le flow de configuration." }, "apply": { "description": "Applique les réglages d'éclairage adaptatif actuels aux lumières.", "fields": { "lights": { - "description": "Une lumière (ou une liste de lumières) pour appliquer les réglages. personnalisation" + "description": "Une lumière (ou une liste de lumières) à laquelle appliquer les réglages." }, "transition": { "description": "Durée de la transition des changements lumineux, en secondes. 🕑" @@ -200,16 +200,16 @@ "set_manual_control": { "fields": { "lights": { - "description": "entity_id(s) des lumières, si non spécifié, toutes les lumières dans le commutateur sont sélectionnées. 💡" + "description": "entity_id(s) des lumières, si non spécifié, toutes les lumières de l'interrupteur sont sélectionnées. 💡" }, "manual_control": { "description": "Indique s'il faut ajouter (\"true\") ou retirer (\"false\") la lumière de la liste \"manual_control\". 🔒" }, "entity_id": { - "description": "L'`entity_id` de l'interrupteur dans lequel (dé)marquer la lumière comme étant `manuellement contrôlée`. 📝" + "description": "L'\"entity_id\" de l'interrupteur dans lequel (dé)marquer la lumière comme étant \"manuellement contrôlée\". 📝" } }, - "description": "Indiquer si une lumière est 'manuellement contrôlée'." + "description": "Indiquer si une lumière est \"contrôlée manuellement\"." } } } diff --git a/custom_components/adaptive_lighting/translations/gl.json b/custom_components/adaptive_lighting/translations/gl.json new file mode 100644 index 00000000..2752f95c --- /dev/null +++ b/custom_components/adaptive_lighting/translations/gl.json @@ -0,0 +1,32 @@ +{ + "options": { + "step": { + "init": { + "data_description": { + "sleep_brightness": "Porcentaxe de brillo das luces en modo durmir. 😴" + }, + "title": "Configuración de Iluminación Adaptativa" + } + } + }, + "title": "Iluminación Adaptativa", + "services": { + "change_switch_settings": { + "fields": { + "sleep_brightness": { + "description": "Porcentaxe de brillo das luces en modo durmir. 😴" + }, + "only_once": { + "description": "Adaptar luces só cando estean acesas (`true`) ou mantelas adaptándose (`false`). 🔄" + } + } + } + }, + "config": { + "step": { + "user": { + "title": "Escolle un nome para a instancia de Iluminación Dinámica" + } + } + } +} diff --git a/custom_components/adaptive_lighting/translations/id.json b/custom_components/adaptive_lighting/translations/id.json index 2474796b..00876915 100644 --- a/custom_components/adaptive_lighting/translations/id.json +++ b/custom_components/adaptive_lighting/translations/id.json @@ -163,7 +163,7 @@ "data": { "detect_non_ha_changes": "detect_non_ha_changes: Mendeteksi dan menghentikan adaptasi untuk perubahan status non-`light.turn_on`. Perlu mengaktifkan `take_over_control`. 🕵️ Perhatian: ⚠️ Beberapa lampu mungkin salah menunjukkan status 'hidup' yang dapat mengakibatkan lampu menyala secara tidak terduga. Nonaktifkan fitur ini jika Anda mengalami masalah seperti itu.", "multi_light_intercept": "multi_light_intercept: Cegat dan sesuaikan panggilan `light.turn_on` yang menargetkan banyak lampu. ➗⚠️ Hal ini dapat mengakibatkan satu panggilan `light.turn_on` terpecah menjadi beberapa panggilan, misalnya saat lampu berada di sakelar yang berbeda. Membutuhkan `intercept` untuk diaktifkan.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Saat menyalakan lampu pada awalnya. Jika disetel ke `true`, Pencahayaan Adaptif hanya beradaptasi jika `light.turn_on` dipanggil tanpa menentukan warna atau kecerahan. ❌🌈 Misalnya mencegah adaptasi saat mengaktifkan scene. Jika `false`, Pencahayaan Adaptif beradaptasi terlepas dari keberadaan warna atau kecerahan di `service_data` awal. Perlu mengaktifkan `take_over_control`. 🕵️ ", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Saat menyalakan lampu pada awalnya. Jika diatur ke `true`, AL hanya beradaptasi jika `light.turn_on` dipanggil tanpa menentukan warna atau kecerahan. ❌🌈 Misalnya, mencegah adaptasi ketika mengaktifkan scene. Jika `false`, AL akan beradaptasi tanpa menghiraukan keberadaan warna atau kecerahan dalam `service_data` awal. Perlu `take_over_control` diaktifkan. 🕵️", "skip_redundant_commands": "skip_redundant_commands: Lewati pengiriman perintah adaptasi yang status targetnya sudah sama dengan status cahaya yang diketahui. Meminimalkan lalu lintas jaringan dan meningkatkan respons adaptasi dalam beberapa situasi. 📉Nonaktifkan jika status cahaya fisik tidak sinkron dengan status rekaman HA.", "separate_turn_on_commands": "separate_turn_on_commands: Gunakan panggilan `light.turn_on` terpisah untuk warna dan kecerahan, diperlukan untuk beberapa jenis lampu. 🔀", "max_color_temp": "max_color_temp: Suhu warna terdingin dalam Kelvin. ❄️", diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json index 49fd1019..d287ef0c 100644 --- a/custom_components/adaptive_lighting/translations/nl.json +++ b/custom_components/adaptive_lighting/translations/nl.json @@ -46,7 +46,7 @@ "detect_non_ha_changes": "detect_non_ha_changes: Detecteert en stopt aanpassingen voor`light.turn_on` statuswijzigingen. Vereist dat`take_over_control` is ingeschakeld. 🕵️ Voorzichtig: ⚠️ Sommige lampen kunnen een 'aan' status vals aangeven, wat kan leiden tot onverwacht inschakelen van lampen. Schakel deze functie uit als je dergelijke problemen tegenkomt.", "transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)", "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Bij het initieel inschakelen van de lampen. Als dit op `true` is ingesteld, past Av alleen aan als `light.turn_on` wordt aangeroepen zonder een kleur of helderheid te specificeren. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Als het `false` is, past Av aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. `take_over_control` moet ingeschakeld zijn. 🕵️ ", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Bij het initieel inschakelen van de lampen. Als dit op `true` is ingesteld, past Av alleen aan als `light.turn_on` wordt aangeroepen zonder een kleur of helderheid te specificeren. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Als het `false` is, past Av aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. `take_over_control` moet ingeschakeld zijn. 🕵️", "transition_until_sleep": "transition_until_sleep: Wanneer ingeschakeld, zal Adaptieve verlichting de slaapinstellingen behandelen als het minimum, overgaand naar deze waarden na zonsondergang. 🌙", "skip_redundant_commands": "skip_redundant_commands: Sla het verzenden van aanpassingscommando's over waarvan de doelstatus al gelijk is aan de bekende status van de lamp. Minimaliseert netwerkverkeer en verbetert de responsiviteit van de aanpassing in sommige situaties. 📉Schakel uit als de fysieke lichtstatus niet meer synchroon loopt met de door HA geregistreerde status.", "intercept": "intercept: Onderschep en pas `light.turn_on` oproepen aan om directe kleur- en helderheidsaanpassing mogelijk te maken. 🏎️ Schakel uit voor lampen die `light.turn_on` niet ondersteunen met kleur en helderheid.", diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json index 0ec6f6c4..bebb4b18 100644 --- a/custom_components/adaptive_lighting/translations/pl.json +++ b/custom_components/adaptive_lighting/translations/pl.json @@ -41,7 +41,7 @@ "detect_non_ha_changes": "detect_non_ha_changes: Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan \"on\", co może powodować nieoczekiwane włączanie się świateł. Wyłącz to ustawienie, jeżeli doświadczasz takich objawów.", "transition": "Transition time when applying a change to the lights (sekund)", "transition_until_sleep": "transition_until_sleep: Gdy włączone, Adaptacyjne oświetlenie będzie traktowało ustawienia spania jako minimalne i przejdzie do nich po zachodzie słońca. 🌙", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Gdy włączone (`true`) to adaptacyjne oświetlenie zastosuje adaptacje tylko jeżeli `light.turn_on` jest wywołane bez konkretnego koloru lub jasności. ❌🌈 To ustawienie zapobiega między innymi adaptacji, gdy aktywowana jest scena. Gdy wyłączone (`false`), adaptacyjne oświetlenie zastosuje adaptacje niezależnie czy `service_data` zawiera kolor lub jasność. Potrzebuje włączonej opcji `take_over_control`. 🕵️ ", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Gdy włączone (`true`) to adaptacyjne oświetlenie zastosuje adaptacje tylko jeżeli `light.turn_on` jest wywołane bez konkretnego koloru lub jasności. ❌🌈 To ustawienie zapobiega między innymi adaptacji, gdy aktywowana jest scena. Gdy wyłączone (`false`), adaptacyjne oświetlenie zastosuje adaptacje niezależnie czy `service_data` zawiera kolor lub jasność. Potrzebuje włączonej opcji `take_over_control`. 🕵️", "skip_redundant_commands": "skip_redundant_commands: Pomiń wysyłanie polecenia adaptacji, jeżeli stan światła jest taki sam jak docelowy stan adaptacji. Minimalizuje to ruch sieciowy oraz w niektórych przypadkach poprawia szybkość działania. 📉 Wyłącz, jeżeli faktyczny stan światła się nie pokrywa z tym który widnieje w Home Assistant.", "include_config_in_attributes": "include_config_in_attributes: Gdy włączone (`true`) pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant. 📝", "intercept": "intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`, aby błyskawicznie dostosować kolor i jasność. 🏎️ Wyłącz dla świateł, które nie akceptują wywołania `light.turn_on` zawierającego kolor i jasność.", diff --git a/custom_components/adaptive_lighting/translations/pt.json b/custom_components/adaptive_lighting/translations/pt.json index d8b72689..a491cc34 100644 --- a/custom_components/adaptive_lighting/translations/pt.json +++ b/custom_components/adaptive_lighting/translations/pt.json @@ -72,7 +72,17 @@ "brightness_mode": "Brilho que irá ser usado. Possíveis valores são `default`, `linear` e `tanh`(usa `brightness_mode_time_dark` e `brightness_mode_time_light`). 📈" }, "title": "Opções da Iluminação Adaptativa", - "description": "Configure um componente da Iluminação Adaptativa. O nome das opções são as mesmas que as do YML. Se você já definiu essa configuração no YAML, nenhuma opção vai aparecer aqui. Para acessar um gráfico que demonstra o efeito dos parâmetros, acesse [esse app](https://basnijholt.github.io/adaptive-lighting). Para mais detalhes, veja a [documentação oficial](https://github.com/basnijholt/adaptive-lighting#readme)." + "description": "Configure um componente da Iluminação Adaptativa. O nome das opções são as mesmas que as do YML. Se você já definiu essa configuração no YAML, nenhuma opção vai aparecer aqui. Para acessar um gráfico que demonstra o efeito dos parâmetros, acesse [esse app](https://basnijholt.github.io/adaptive-lighting). Para mais detalhes, veja a [documentação oficial](https://github.com/basnijholt/adaptive-lighting#readme).", + "data": { + "lights": "lights: Lista das entity_ids das luzes para serem controladas (pode ser vazia). 🌟", + "min_brightness": "min_brightness: Percentagem minima de brilho. 💡", + "max_brightness": "max_brightness: Percentagem máxima de brilho. 💡", + "min_color_temp": "min_color_temp: Cor mais quente em Kelvin. 🔥", + "max_color_temp": "max_color_temp: Cor mais fria em Kelvin. ❄️", + "prefer_rgb_color": "prefer_rgb_color: Quando possível escolher ajuste em RGB em vez de temperatura da cor. 🌈", + "transition_until_sleep": "transition_until_sleep: Quando ativado, Adaptive Lighting usará as definições do modo noturno como os mínimos, passando para esses valores no por do sol. 🌙", + "take_over_control": "take_over_control: Desativa Adaptive Lighting se alguma fonte chamar`light.turn_on` enquanto as luzes estiverem ligadas e a serem controladas. Tomar nota que esta opção chama o serviço `homeassistant.update_entity` a cada `interval`! 🔒" + } } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/ro.json b/custom_components/adaptive_lighting/translations/ro.json index e0f7ec02..2daa6c6c 100644 --- a/custom_components/adaptive_lighting/translations/ro.json +++ b/custom_components/adaptive_lighting/translations/ro.json @@ -2,8 +2,12 @@ "config": { "step": { "user": { - "description": "Fiecare instanţă poate conţine mai multe lumini!" + "description": "Fiecare instanţă poate conţine mai multe lumini!", + "title": "Alege un nume pentru instanța de Iluminare Adaptivă" } + }, + "abort": { + "already_configured": "Acest dispozitiv este deja configurat" } }, "options": { diff --git a/custom_components/adaptive_lighting/translations/sv.json b/custom_components/adaptive_lighting/translations/sv.json index df81271e..29ce0971 100644 --- a/custom_components/adaptive_lighting/translations/sv.json +++ b/custom_components/adaptive_lighting/translations/sv.json @@ -40,7 +40,7 @@ "detect_non_ha_changes": "detect_non_ha_changes, Upptäcker alla ändringar större än 5% gjorda på ljuskällorna som inte kommer från HA. Kräver att 'take_over_control' är påslaget.(Kallar på 'homeassistant.update_entity' vid varje 'interval'!)", "transition": "transition, i sekunder", "multi_light_intercept": "multi_light_intercept: Fånga upp och anpassa \"light.turn_on\"-anrop som riktar sig mot flera lampor. ➗⚠️ Detta kan resultera i att ett enda `light.turn_on`-anrop delas upp i flera anrop, t.ex. när lamporna är kopplade till olika strömbrytare. Kräver att \"intercept\" är aktiverat.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: När lampor först tänds. Om satt till \"true\", anpassar AL endast om \"light.turn_on\" anropas utan att ange färg eller ljusstyrka. ❌🌈 Detta förhindrar t.ex. anpassning när en scen aktiveras. Om \"false\" anpassas AL oavsett förekomsten av färg eller ljusstyrka i den initiala \"service_data\". \"takeover_control\" måste vara aktiverat. 🕵️ ", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: När lampor först tänds. Om satt till \"true\", anpassar AL endast om \"light.turn_on\" anropas utan att ange färg eller ljusstyrka. ❌🌈 Detta förhindrar t.ex. anpassning när en scen aktiveras. Om \"false\" anpassas AL oavsett förekomsten av färg eller ljusstyrka i den initiala \"service_data\". \"takeover_control\" måste vara aktiverat. 🕵️", "skip_redundant_commands": "skip_redundant_commands: Hoppa över att skicka anpassningskommandon vars måltillstånd redan är lika med lampans kända tillstånd. Minimerar nätverkstrafik och förbättrar anpassningsförmågan i vissa situationer. 📉 Inaktivera om lampans tillstånd blir osynkroniserade med HA:s registrerade tillstånd.", "intercept": "intercept: Fånga upp och anpassa `light.turn_on`-anrop för att möjliggöra omedelbar anpassning av färg och ljusstyrka. 🏎️ Inaktivera för lampor som inte stöder `light.turn_on` med färg och ljusstyrka.", "transition_until_sleep": "transition_until_sleep: När aktiverat kommer Adaptive Lighting att behandla sömninställningarna som ett minimum och övergå till dessa värden efter solnedgången. 🌙", diff --git a/custom_components/adaptive_lighting/translations/ta.json b/custom_components/adaptive_lighting/translations/ta.json index 66115d64..92c552e5 100644 --- a/custom_components/adaptive_lighting/translations/ta.json +++ b/custom_components/adaptive_lighting/translations/ta.json @@ -161,7 +161,7 @@ "take_over_control": "Take_over_control: விளக்குகள் இயக்கத்தில் இருக்கும்போது மற்றொரு சான்று `லைட்.டர்ன்_ஓஎன்` என்று அழைத்தால் தகவமைப்பு விளக்குகளை முடக்கு. இது `ஓமாசிச்டன்ட்.பிடேட்_என்டிட்டி` ஒவ்வொரு` இடைவெளியையும் 'என்று அழைக்கிறது என்பதை நினைவில் கொள்க! .", "detect_non_ha_changes": "கண்டறிதல்_நான்_ஆ_சேஞ்ச்ச்: `விளக்கு அல்லாத. டர்ன்_ஓஎன்` மாநில மாற்றங்களுக்கான தழுவல்களைக் கண்டறிந்து நிறுத்துகிறது. `Take_over_control` இயக்கப்பட்டது. 🕵œ எச்சரிக்கை: ⚠œ சில விளக்குகள் ஒரு 'ஆன்' நிலையை பொய்யாகக் குறிக்கக்கூடும், இதனால் விளக்குகள் எதிர்பாராத விதமாக இயக்கப்படலாம். இதுபோன்ற சிக்கல்களை நீங்கள் சந்தித்தால் இந்த அம்சத்தை முடக்கு.", "only_once": "மட்டும்_இன்: விளக்குகள் இயக்கப்படும்போது மட்டுமே (`உண்மை`) மாற்றியமைக்கும்போது அல்லது அவற்றைத் தழுவிக்கொள்ளுங்கள் (` தவறு`). .", - "adapt_only_on_bare_turn_on": "Sadve_only_on_bare_turn_on: ஆரம்பத்தில் விளக்குகளை இயக்கும்போது. `உண்மை` என அமைக்கப்பட்டால், வண்ணம் அல்லது பிரகாசத்தைக் குறிப்பிடாமல்` லைட்.டர்ன்_ஓஎன்` செயல்படுத்தப்பட்டால் மட்டுமே அல் மாற்றியமைக்கிறது. ❌🌈 இது எ.கா., ஒரு காட்சியை செயல்படுத்தும்போது தழுவலைத் தடுக்கிறது. `தவறு` என்றால், ஆரம்ப` சேவை_டா` இல் நிறம் அல்லது ஒளி இருப்பதைப் பொருட்படுத்தாமல் AL மாற்றியமைக்கிறது. `Take_over_control` இயக்கப்பட்டது. . ", + "adapt_only_on_bare_turn_on": "சரிசெய்_only_on_bare_turn_on: ஆரம்பத்தில் விளக்குகளை இயக்கும்போது. `உண்மை` என அமைக்கப்பட்டால், வண்ணம் அல்லது பிரகாசத்தைக் குறிப்பிடாமல்` லைட்.டர்ன்_ஓஎன்` செயல்படுத்தப்பட்டால் மட்டுமே அல் மாற்றியமைக்கிறது. ❌🌈 இது எ.கா., ஒரு காட்சியைச் செயல்படுத்தும்போது தழுவலைத் தடுக்கிறது. `தவறு` என்றால், ஆரம்ப` சேவை_டா` இல் நிறம் அல்லது ஒளி இருப்பதைப் பொருட்படுத்தாமல் AL மாற்றியமைக்கிறது. `Take_over_control` இயக்கப்பட்டது. . 🕵️", "separate_turn_on_commands": "தனித்தனி_டர்ன்_ஆன்_காமண்ட்ச்: சில ஒளி வகைகளுக்கு தேவைப்படும் வண்ணம் மற்றும் பிரகாசத்திற்கான தனித்தனி `லைட்.டர்ன்_ஓஎன்` அழைப்புகளைப் பயன்படுத்தவும். .", "skip_redundant_commands": "Skip_redundant_commands: தழுவல் கட்டளைகளை அனுப்புவதைத் தவிர்க்கவும், அதன் இலக்கு நிலை ஏற்கனவே ஒளியின் அறியப்பட்ட நிலைக்கு சமம். பிணையம் போக்குவரத்தை குறைக்கிறது மற்றும் சில சூழ்நிலைகளில் தழுவல் மறுமொழியை மேம்படுத்துகிறது. ஆ இன் பதிவு செய்யப்பட்ட நிலையுடன் இயற்பியல் ஒளி நிலைகள் ஒத்திசைவிலிருந்து வெளியேறினால் அது காணக்கூடியது.", "intercept": "இடைமறிப்பு: உடனடி வண்ணம் மற்றும் பிரகாசமான தழுவலை செயல்படுத்த `ஒளி. Color வண்ணம் மற்றும் பிரகாசத்துடன் `ஒளி.", diff --git a/custom_components/adaptive_lighting/translations/uk.json b/custom_components/adaptive_lighting/translations/uk.json index 866cee2c..ee12b0e9 100644 --- a/custom_components/adaptive_lighting/translations/uk.json +++ b/custom_components/adaptive_lighting/translations/uk.json @@ -39,8 +39,25 @@ "take_over_control": "take_over_control: Якщо що-небудь, окрім Адаптивного освітлення, викликає 'light.turn_on', коли світло вже увімкнено, чи адаптувати освітлення допоки світло (або перемикач) перемкнеться (off -> on).", "detect_non_ha_changes": "detect_non_ha_changes: виявляти всі зміни >10% до освітлення (включаючи ті, що зроблені поза HA), вимагає, щоб 'take_over_control' був включений (виклик 'homeassistant.update_entity' кожного оновлення 'interval'!)", "transition": "Час переходу, який застосовується до освітлення (секунди)", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Коли спочатку вмикається світло. Якщо `true`, Адаптивне Освітлення адаптується лише якщо `light.turn_on` було викликано без вказування кольору чи яскравості. ❌🌈 Це в тому числі запобігає адаптації, коли активується сцена. Якщо `false`, Адаптивне Освітлення адаптується не залежно від присутності кольору чи яскравості в першочерговому `service_data`. Потребує активації `take_over_control`. 🕵️ ", - "transition_until_sleep": "transition_until_sleep: Коли активовано, адаптивне освітлення буде ставитись до налаштування сну як мінімум, переходячи до цих значень після заходу сонця. 🌙" + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: На початку вмикання світла. Якщо `true`, освітлення адаптується лише якщо `light.turn_on` викликано без вказання кольору чи яскравості. ❌🌈 Це, наприклад, запобігає адаптації, коли сцена активується. Якщо `false`, освітлення адаптується незалежно від наявності кольору чи яскравості у початковому `service_data`. Потребує ввімкнення `take_over_control`. 🕵️", + "transition_until_sleep": "transition_until_sleep: Коли активовано, адаптивне освітлення буде ставитись до налаштування сну як мінімум, переходячи до цих значень після заходу сонця. 🌙", + "intercept": "intercept: Перехоплювати та адаптувати виклики увімкнення світла (`light.turn_on`), щоб увімкнути миттєву адаптацію кольору та яскравості. 🏎️ Вимкніть для світла, що не підтримує увімкнення світла (`light.turn_on`) з кольором та яскравістю.", + "include_config_in_attributes": "Включити конфігурацію в атрибути (`include_config_in_attributes`): Показувати всі опції як атрибути на перемикачі в Home Assistant, якщо встановлено значення `true`. 📝" + }, + "data_description": { + "sunrise_offset": "Змінити час сходу сонця на +/- секунд. ⏰", + "sunset_offset": "Змінити час заходу сонця на +/- секунд. ⏰", + "autoreset_control_seconds": "Самочинно скидати ручне керування після кількох секунд. Встановіть 0, щоб вимкнути.", + "initial_transition": "Тривалість першого переходу, коли світло перемикається зі стану вимкнено `off` на увімкнено `on`, у секундах. ⏲️", + "brightness_mode": "Режим яскравості для використання. Можливі значення: default (стандартний) , linear (лінійний) та tanh (гіперболічний тангенс) (використовує значення brightness_mode_time_dark та brightness_mode_time_light).", + "send_split_delay": "Затримка (мс) між `separate_turn_on_commands` (окремі команди увімкнення) для світла, що не підтримує одночасне налаштування яскравості та кольору. ⏲️", + "brightness_mode_time_dark": "(Ігнорується, якщо `brightness_mode='default'`) Тривалість у секундах для збільшення/зменшення яскравості до/після сходу/заходу сонця. 📈📉", + "brightness_mode_time_light": "(Ігнорується, якщо brightness_mode='default') Тривалість у секундах для збільшення/зменшення яскравості після/до сходу/заходу сонця. 📈📉.", + "transition": "Тривалість переходу, коли світло змінюється, у секундах. 🕑", + "interval": "Частота адаптації освітлення, у секундах. 🔄", + "sleep_brightness": "Відсоток яскравості світла в режимі сну. 😴", + "sleep_color_temp": "Колірна температура в режимі сну (використовується, коли `sleep_rgb_or_color_temp` має значення `color_temp`) у Кельвінах. 😴", + "sleep_transition": "Тривалість переходу, коли режим сну \"sleep mode\" увімкнено, у секундах. 😴" } } }, @@ -48,5 +65,79 @@ "option_error": "Хибна опція", "entity_missing": "Вибраного світла в домашньому помічнику не знайшли" } + }, + "services": { + "apply": { + "description": "Застосовує поточні налаштування Адаптивного освітлення до світильників.", + "fields": { + "lights": { + "description": "Світильник (або список світильників), до яких буде застосовано налаштування. 💡" + }, + "transition": { + "description": "Тривалість переходу, коли світло змінюється, у секундах. 🕑" + } + } + }, + "change_switch_settings": { + "fields": { + "sunrise_offset": { + "description": "Змінити час сходу сонця на +/- секунд. ⏰" + }, + "sunset_offset": { + "description": "Змінити час заходу сонця на +/- секунд. ⏰" + }, + "autoreset_control_seconds": { + "description": "Самочинно скидати ручне керування після кількох секунд. Встановіть 0, щоб вимкнути." + }, + "only_once": { + "description": "Приладжувати освітлення тільки тоді, коли воно ввімкнено (`true`) чи продовжувати завжди (`false`). 🔄" + }, + "sleep_brightness": { + "description": "Відсоток яскравості світла в режимі сну. 😴" + }, + "take_over_control": { + "description": "Вимкнути Адаптивне освітлення, якщо інше джерело викликає `light.turn_on`, коли світло увімкнене та адаптується. Зауважте, що це викликає `homeassistant.update_entity` щокожного заданого інтервалу `interval`! 🔒" + }, + "entity_id": { + "description": "Ідентифікатор (ID) перемикача. 📝" + }, + "initial_transition": { + "description": "Тривалість першого переходу, коли світло перемикається зі стану вимкнено `off` на увімкнено `on`, у секундах. ⏲️" + }, + "sleep_transition": { + "description": "Тривалість переходу, коли режим сну \"sleep mode\" увімкнено, у секундах. 😴" + }, + "max_color_temp": { + "description": "Найхолодніша колірна температура в Кельвінах. ❄️" + }, + "max_brightness": { + "description": "Максимальний відсоток яскравості. 💡" + }, + "min_brightness": { + "description": "Мінімальний відсоток яскравості. 💡" + }, + "send_split_delay": { + "description": "Затримка (мс) між `separate_turn_on_commands` (окремі команди увімкнення) для світла, що не підтримує одночасне налаштування яскравості та кольору. ⏲️" + }, + "sleep_color_temp": { + "description": "Колірна температура в режимі сну (використовується, коли `sleep_rgb_or_color_temp` має значення `color_temp`) в Кельвінах. 😴" + }, + "detect_non_ha_changes": { + "description": "Виявляє та припиняє адаптації для змін стану, що не є `light.turn_on`. Потребує увімкнення `take_over_control`. 🕵️ Обережно: ⚠️ Деякі лампи можуть хибно вказувати на стан \"увімкнено\", що може призвести до неочікуваного ввімкнення ламп. Вимкніть цю функцію, якщо ви зіткнетеся з такими проблемами." + }, + "transition": { + "description": "Тривалість переходу, коли світло змінюється, у секундах. 🕑" + } + }, + "description": "Змініть будь-які налаштування, які ви бажаєте, у цьому перемикачі. Усі опції тут такі ж, як і в поточному конфігураційному файлі." + }, + "set_manual_control": { + "description": "Позначте, чи світло \"керується вручну\".", + "fields": { + "lights": { + "description": "Ідентифікатор(и) світла (entity_id(s) of lights). Якщо не вказано, вибираються всі лампи у перемикачі. 💡" + } + } + } } } diff --git a/custom_components/adaptive_lighting/translations/zh-Hans.json b/custom_components/adaptive_lighting/translations/zh-Hans.json index 87d236d1..0d501731 100644 --- a/custom_components/adaptive_lighting/translations/zh-Hans.json +++ b/custom_components/adaptive_lighting/translations/zh-Hans.json @@ -50,7 +50,7 @@ "detect_non_ha_changes": "detect_non_ha_changes: 检测非`light.turn_on`的状态更改,并停止自适应照明。需要启用`take_over_control`。🕵️ 注意:⚠️ 一些灯光可能错误地显示为“开启”状态,这可能会导致灯光意外打开。如果遇到此类问题,请禁用此功能。", "autoreset_control_seconds": "自动重置时间(autoreset_control_seconds)", "only_once": "only_once:仅在打开时调整灯光(`true`)或始终调整灯光(`false`)。🔄", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on:当首次打开灯光时。如果设置为`true`,仅在没有指定颜色或亮度的情况下,AL才进行适应。❌🌈 例如,这可以防止在激活场景时进行适应。如果为`false`,则不考虑初始`service_data`中是否存在颜色或亮度,AL都会适应。需要启用`take_over_control`。🕵️ ", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on:当首次打开灯光时。如果设置为`true`,仅在没有指定颜色或亮度的情况下,AL才进行适应。❌🌈 例如,这可以防止在激活场景时进行适应。如果为`false`,则不考虑初始`service_data`中是否存在颜色或亮度,AL都会适应。需要启用`take_over_control`。🕵️", "separate_turn_on_commands": "separate_turn_on_commands:为某些灯光类型需要使用单独的`light.turn_on`调用来设置颜色和亮度。🔀", "send_split_delay": "指令发送间隔延迟(send_split_delay)", "adapt_delay": "自适应照明延迟(adapt_delay)", From 1232bcc2d59dc7b75512600db2f1d0a5b84afcf9 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:39:06 -0700 Subject: [PATCH 0855/1077] docs: add xuars as a contributor for translation (#1213) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 43c49beb..dc9180eb 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -963,6 +963,15 @@ "contributions": [ "translation" ] + }, + { + "login": "xuars", + "name": "xuars", + "avatar_url": "https://avatars.githubusercontent.com/u/197080354?v=4", + "profile": "https://github.com/xuars", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 8d01acfc..4005575f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-105-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-106-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -602,6 +602,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + + + From afc399e61362695b1456a57ce5651641b1150768 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:39:48 -0700 Subject: [PATCH 0856/1077] docs: add tinutac as a contributor for translation (#1214) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index dc9180eb..971571e2 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -972,6 +972,15 @@ "contributions": [ "translation" ] + }, + { + "login": "tinutac", + "name": "tinutac", + "avatar_url": "https://avatars.githubusercontent.com/u/2151553?v=4", + "profile": "https://github.com/tinutac", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 4005575f..b8184b68 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-106-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-107-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -604,6 +604,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 3a4d2ff3d8d09b4b07fb62efe680224213cf4768 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:40:16 -0700 Subject: [PATCH 0857/1077] docs: add amelenty as a contributor for translation (#1215) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 971571e2..9611d2e1 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -981,6 +981,15 @@ "contributions": [ "translation" ] + }, + { + "login": "amelenty", + "name": "amelenty", + "avatar_url": "https://avatars.githubusercontent.com/u/29466876?v=4", + "profile": "https://github.com/amelenty", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index b8184b68..31991553 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-107-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-108-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -605,6 +605,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 1ddd34a2305b8c8e499dfbb6d8057f18acd61df9 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:41:09 -0700 Subject: [PATCH 0858/1077] docs: add yeaxi as a contributor for translation (#1217) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 9611d2e1..47768512 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -990,6 +990,15 @@ "contributions": [ "translation" ] + }, + { + "login": "yeaxi", + "name": "Rostyslav Dudka", + "avatar_url": "https://avatars.githubusercontent.com/u/15959384?v=4", + "profile": "https://ua.linkedin.com/in/rostyslav-dudka", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 31991553..82650031 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-108-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-109-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -606,6 +606,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From fd5cef7901791c601563fd9d7e0e8f58b2d145a2 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:41:50 -0700 Subject: [PATCH 0859/1077] docs: add helderfmf as a contributor for translation (#1218) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 47768512..94110353 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -999,6 +999,15 @@ "contributions": [ "translation" ] + }, + { + "login": "helderfmf", + "name": "Helder Ferreira", + "avatar_url": "https://avatars.githubusercontent.com/u/5622687?v=4", + "profile": "https://github.com/helderfmf", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 82650031..6154663a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-109-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-110-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -607,6 +607,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From f5ee0aa58d8da57692e297c0474e66c5536e03f1 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:42:08 -0700 Subject: [PATCH 0860/1077] docs: add mrpiotr-dev as a contributor for translation (#1219) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 94110353..7adf21e4 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1008,6 +1008,15 @@ "contributions": [ "translation" ] + }, + { + "login": "mrpiotr-dev", + "name": "Piotr Laszczkowski", + "avatar_url": "https://avatars.githubusercontent.com/u/11849621?v=4", + "profile": "http://mrpiotr.dev", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 6154663a..d44452ad 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-110-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-111-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -608,6 +608,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 14e898bc8eabc39445493598b3bc2c819fcc30e7 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:42:34 -0700 Subject: [PATCH 0861/1077] docs: add rezaalmanda as a contributor for translation (#1220) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 7adf21e4..3b2c3c28 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1017,6 +1017,15 @@ "contributions": [ "translation" ] + }, + { + "login": "rezaalmanda", + "name": "Reza", + "avatar_url": "https://avatars.githubusercontent.com/u/22217419?v=4", + "profile": "http://rezaalmanda.github.io", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index d44452ad..6cf59d17 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-111-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-112-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -609,6 +609,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 4510ebdca76034eddb50bab68377e50080d49f61 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:42:54 -0700 Subject: [PATCH 0862/1077] docs: add bittin as a contributor for translation (#1221) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 3b2c3c28..ced78cff 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1026,6 +1026,15 @@ "contributions": [ "translation" ] + }, + { + "login": "bittin", + "name": "Luna Jernberg", + "avatar_url": "https://avatars.githubusercontent.com/u/43197?v=4", + "profile": "https://github.com/bittin", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 6cf59d17..7bc50e6c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-112-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-113-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -611,6 +611,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + + + From ff84a46aedacfba19e30d0ef09dbe8d3ddd95741 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:46:11 -0700 Subject: [PATCH 0863/1077] docs: add defaultpage as a contributor for translation (#1216) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index ced78cff..6354e1db 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -982,6 +982,15 @@ "translation" ] }, + { + "login": "defaultpage", + "name": "Default User", + "avatar_url": "https://avatars.githubusercontent.com/u/22825202?v=4", + "profile": "https://github.com/defaultpage", + "contributions": [ + "translation" + ] + }, { "login": "amelenty", "name": "amelenty", diff --git a/README.md b/README.md index 7bc50e6c..5b8b3b4b 100644 --- a/README.md +++ b/README.md @@ -605,13 +605,14 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + - + From 6cb99e80b0a669ecb46ee382009cf482aa1eb282 Mon Sep 17 00:00:00 2001 From: Rasmus Lundsgaard Date: Mon, 16 Jun 2025 06:47:06 +0200 Subject: [PATCH 0864/1077] suggested fix for HA2025 deprecation warnings (#1168) * suggested fix for HA2025 deprecation warnings * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/switch.py | 49 ++++++------------- 1 file changed, 14 insertions(+), 35 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 29955d0a..037bd083 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -16,7 +16,6 @@ import ulid_transform import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_FLASH, @@ -24,17 +23,8 @@ from homeassistant.components.light import ( ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_XY_COLOR, - COLOR_MODE_BRIGHTNESS, - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_HS, - COLOR_MODE_RGB, - COLOR_MODE_RGBW, - COLOR_MODE_RGBWW, - COLOR_MODE_XY, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, - SUPPORT_TRANSITION, + ColorMode, + LightEntityFeature, is_on, preprocess_turn_on_alternatives, ) @@ -179,13 +169,6 @@ if TYPE_CHECKING: from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity_platform import AddEntitiesCallback -_SUPPORT_OPTS = { - "brightness": SUPPORT_BRIGHTNESS, - "color_temp": SUPPORT_COLOR_TEMP, - "color": SUPPORT_COLOR, - "transition": SUPPORT_TRANSITION, -} - _LOGGER = logging.getLogger(__name__) @@ -651,17 +634,19 @@ def _supported_features(hass: HomeAssistant, light: str) -> set[str]: assert state is not None supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) assert isinstance(supported_features, int) - supported = { - key for key, value in _SUPPORT_OPTS.items() if supported_features & value - } + + supported = set() + + if supported_features & LightEntityFeature.TRANSITION: + supported.add("transition") supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES, set()) color_modes = { - COLOR_MODE_RGB, - COLOR_MODE_RGBW, - COLOR_MODE_RGBWW, - COLOR_MODE_XY, - COLOR_MODE_HS, + ColorMode.RGB, + ColorMode.RGBW, + ColorMode.RGBWW, + ColorMode.XY, + ColorMode.HS, } # Adding brightness when color mode is supported, see @@ -672,10 +657,10 @@ def _supported_features(hass: HomeAssistant, light: str) -> set[str]: supported.update({"color", "brightness"}) break - if COLOR_MODE_COLOR_TEMP in supported_color_modes: + if ColorMode.COLOR_TEMP in supported_color_modes: supported.update({"color_temp", "brightness"}) - if COLOR_MODE_BRIGHTNESS in supported_color_modes: + if ColorMode.BRIGHTNESS in supported_color_modes: supported.add("brightness") return supported @@ -2009,12 +1994,6 @@ class AdaptiveLightingManager: context.id, ) service_data = {ATTR_ENTITY_ID: skipped, **service_data_copy[CONF_PARAMS]} - if ( - ATTR_COLOR_TEMP in service_data - and ATTR_COLOR_TEMP_KELVIN in service_data - ): - # ATTR_COLOR_TEMP and ATTR_COLOR_TEMP_KELVIN are mutually exclusive - del service_data[ATTR_COLOR_TEMP] await self.hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, From 2623aafa618e32fceb41fe81b07d756e211f5fbe Mon Sep 17 00:00:00 2001 From: Jeff Wilson Date: Mon, 16 Jun 2025 00:47:26 -0400 Subject: [PATCH 0865/1077] Remove deprecated `@bind_hass` (#1207) * remove @bind_hass decorator * attempt to pass hass * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/switch.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 037bd083..f00227dc 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -76,7 +76,6 @@ from homeassistant.helpers.event import ( from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.sun import get_astral_location from homeassistant.helpers.template import area_entities -from homeassistant.loader import bind_hass from homeassistant.util import slugify from homeassistant.util.color import ( color_temperature_to_rgb, @@ -226,7 +225,6 @@ def is_our_context(context: Context | None, which: str | None = None) -> bool: return is_our_context_id(context.id, which) -@bind_hass def _switches_with_lights( hass: HomeAssistant, lights: list[str], @@ -244,7 +242,7 @@ def _switches_with_lights( if entry is None: # entry might be disabled and therefore missing continue switch = data[config.entry_id][SWITCH_DOMAIN] - switch._expand_light_groups() + switch._expand_light_groups(hass=hass) # Check if any of the lights are in the switch's lights if set(switch.lights) & set(all_check_lights): switches.append(switch) @@ -255,7 +253,6 @@ class NoSwitchFoundError(ValueError): """No switches found for lights.""" -@bind_hass def _switch_with_lights( hass: HomeAssistant, lights: list[str], @@ -286,7 +283,6 @@ def _switch_with_lights( # For documentation on this function, see integration_entities() from HomeAssistant Core: # https://github.com/home-assistant/core/blob/dev/homeassistant/helpers/template.py#L1109 -@bind_hass def _switches_from_service_call( hass: HomeAssistant, service_call: ServiceCall, @@ -599,7 +595,6 @@ def _is_state_event(event: Event, from_or_to_state: Iterable[str]): ) -@bind_hass def _expand_light_groups( hass: HomeAssistant, lights: list[str], @@ -628,7 +623,6 @@ def _is_light_group(state: State) -> bool: ) -@bind_hass def _supported_features(hass: HomeAssistant, light: str) -> set[str]: state = hass.states.get(light) assert state is not None @@ -973,8 +967,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Remove the listeners upon removing the component.""" self._remove_listeners() - def _expand_light_groups(self) -> None: - all_lights = _expand_light_groups(self.hass, self.lights) + def _expand_light_groups(self, hass=None) -> None: + hass = hass or self.hass + all_lights = _expand_light_groups(hass, self.lights) self.manager.lights.update(all_lights) self.manager.set_auto_reset_manual_control_times( all_lights, From ab934b0d03053be09fa08be917784d799e37dd7c Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:48:03 -0700 Subject: [PATCH 0866/1077] docs: add jawilson as a contributor for code (#1222) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 6354e1db..a9f04022 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1044,6 +1044,15 @@ "contributions": [ "translation" ] + }, + { + "login": "jawilson", + "name": "Jeff Wilson", + "avatar_url": "https://avatars.githubusercontent.com/u/1368827?v=4", + "profile": "http://jeffalwilson.com", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 5b8b3b4b..306da8ea 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-113-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-115-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -614,6 +614,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 33c1d2eb6be74ff0955d3219d14cc9a542bc68cf Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:48:37 -0700 Subject: [PATCH 0867/1077] docs: add TermeHansen as a contributor for code (#1223) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index a9f04022..76a8cf13 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1053,6 +1053,15 @@ "contributions": [ "code" ] + }, + { + "login": "TermeHansen", + "name": "Rasmus Lundsgaard", + "avatar_url": "https://avatars.githubusercontent.com/u/6922018?v=4", + "profile": "https://github.com/TermeHansen", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 306da8ea..7af1feb8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-115-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-116-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -615,6 +615,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 106d4733fe4aaa516a8ba96e51ab0842ad2d3f7d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:49:31 -0700 Subject: [PATCH 0868/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Pin=20python=20t?= =?UTF-8?q?o=203.13.5=20(#1181)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/deploy-webapp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml index f0dedea3..e52963b0 100644 --- a/.github/workflows/deploy-webapp.yml +++ b/.github/workflows/deploy-webapp.yml @@ -35,7 +35,7 @@ jobs: - name: Set Up Python uses: actions/setup-python@v5 with: - python-version: 3.x + python-version: 3.13.5 - name: Install Dependencies run: | From e08a7525144005b1d4f399660d6b800e1c781482 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:49:44 -0700 Subject: [PATCH 0869/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/setup-python=20action=20to=20v5.6.0=20(#1171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/install_dependencies/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index 8f1732d6..8c4483f7 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -28,7 +28,7 @@ runs: ref: ${{ inputs.core-version }} - name: Set up Python ${{ inputs.python-version }} id: python - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@v5.6.0 with: python-version: ${{ inputs.python-version }} - name: Set up UV From 8035ea3ed104242e4698f96e815035a818791473 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:55:36 -0700 Subject: [PATCH 0870/1077] [pre-commit.ci] pre-commit autoupdate (#1163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.8.4 → v0.11.13](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.4...v0.11.13) - [github.com/psf/black: 24.10.0 → 25.1.0](https://github.com/psf/black/compare/24.10.0...25.1.0) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix pre-commit --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .pre-commit-config.yaml | 4 ++-- custom_components/adaptive_lighting/color_and_brightness.py | 4 ++-- webapp/color_and_brightness.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fcaebb75..1321a487 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,11 +8,11 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.4 + rev: v0.11.13 hooks: - id: ruff args: ["--fix"] - repo: https://github.com/psf/black - rev: 24.10.0 + rev: 25.1.0 hooks: - id: black diff --git a/custom_components/adaptive_lighting/color_and_brightness.py b/custom_components/adaptive_lighting/color_and_brightness.py index 215a9a70..6fdb7083 100644 --- a/custom_components/adaptive_lighting/color_and_brightness.py +++ b/custom_components/adaptive_lighting/color_and_brightness.py @@ -502,9 +502,9 @@ def lerp_color_hsv( ) # Convert back to RGB - rgb = tuple(int(round(x * 255)) for x in colorsys.hsv_to_rgb(*hsv)) + rgb = tuple(round(x * 255) for x in colorsys.hsv_to_rgb(*hsv)) assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}" - return cast(tuple[int, int, int], rgb) + return cast("tuple[int, int, int]", rgb) def lerp(x, x1, x2, y1, y2): diff --git a/webapp/color_and_brightness.py b/webapp/color_and_brightness.py index b632ba8e..849c2373 100644 --- a/webapp/color_and_brightness.py +++ b/webapp/color_and_brightness.py @@ -502,9 +502,9 @@ def lerp_color_hsv( ) # Convert back to RGB - rgb = tuple(int(round(x * 255)) for x in colorsys.hsv_to_rgb(*hsv)) + rgb = tuple(round(x * 255) for x in colorsys.hsv_to_rgb(*hsv)) assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}" - return cast(tuple[int, int, int], rgb) + return cast("tuple[int, int, int]", rgb) def lerp(x, x1, x2, y1, y2): From 5f02e9de75042b1297ed361291f17354027a4f38 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 15 Jun 2025 21:57:08 -0700 Subject: [PATCH 0871/1077] Bump min supported version to 2025.12 (#1224) --- hacs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hacs.json b/hacs.json index 9c9035ef..545d0ec5 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { "name": "Adaptive Lighting", "render_readme": true, - "homeassistant": "2023.7.0" + "homeassistant": "2024.12.0" } From cb67a4cb9c0f36bd244fe90b0e8147d600471c76 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 15 Jun 2025 22:12:48 -0700 Subject: [PATCH 0872/1077] Fix test_light_switch_in_specific_area (#1225) --- tests/test_switch.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_switch.py b/tests/test_switch.py index 607f17ef..cc03fa24 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1373,6 +1373,9 @@ def mock_area_registry( area_kwargs["floor_id"] = "test-floor" if dt >= datetime.date(2024, 11, 1): area_kwargs.pop("normalized_name") + if dt >= datetime.date(2025, 2, 1): + area_kwargs["humidity_entity_id"] = None + area_kwargs["temperature_entity_id"] = None # This mess... 🤯 if dt >= datetime.date(2024, 2, 1) and dt != datetime.date(2024, 4, 1): From b1aca6408d3d00d56f56f921d78ad1e18c7c4682 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 15 Jun 2025 22:16:31 -0700 Subject: [PATCH 0873/1077] Skip broken test (#1226) Also see https://github.com/basnijholt/adaptive-lighting/pull/1159 --- tests/test_config_flow.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index af01fc11..0b9609d3 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,5 +1,6 @@ """Test Adaptive Lighting config flow.""" +import pytest from homeassistant.components.adaptive_lighting.const import ( CONF_SUNRISE_TIME, CONF_SUNSET_TIME, @@ -117,8 +118,11 @@ async def test_import_twice(hass): # TODO: Fix, broken for all supported versions # But in ≤2024.5 it gives homeassistant.config_entries.UnknownEntry: cd69dbda65bd3f86e9a32d974cdfa23f # and ≥2024.6 it times out +# NOTE: Just skip this test for now, currently (2025-06-15) I cannot figure out +# what this test is even testing. async def test_changing_options_when_using_yaml(hass): """Test changing options when using YAML.""" + pytest.skip(reason="TODO: Fix, broken for all supported versions") entry = MockConfigEntry( domain=DOMAIN, title=DEFAULT_NAME, From a8e35ae216bb777b21084670aef601e41def0007 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 15 Jun 2025 22:20:53 -0700 Subject: [PATCH 0874/1077] Release v1.26.0 (#1227) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 4b0cac4f..92100afa 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.25.0" + "version": "1.26.0" } From ab29fb080c26366fae8881df7d3920f3af983ff0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 15 Jun 2025 22:25:46 -0700 Subject: [PATCH 0875/1077] Fix Docker build --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 87f791e5..d4076072 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ RUN ln -s /core /app/core && /app/scripts/setup-symlinks RUN mkdir -p /venv ENV UV_PROJECT_ENVIRONMENT=/venv UV_PYTHON=3.13 PATH="/venv/bin:$PATH" RUN uv venv -RUN /app/scripts/setup-dependencies +RUN source /venv/bin/activate && /app/scripts/setup-dependencies WORKDIR /app/core From 5d7599e33bcf437ed9c0d7a62fb2a62c32a1d967 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 15 Jun 2025 22:28:12 -0700 Subject: [PATCH 0876/1077] Revert "Fix Docker build" This reverts commit ab29fb080c26366fae8881df7d3920f3af983ff0. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d4076072..87f791e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ RUN ln -s /core /app/core && /app/scripts/setup-symlinks RUN mkdir -p /venv ENV UV_PROJECT_ENVIRONMENT=/venv UV_PYTHON=3.13 PATH="/venv/bin:$PATH" RUN uv venv -RUN source /venv/bin/activate && /app/scripts/setup-dependencies +RUN /app/scripts/setup-dependencies WORKDIR /app/core From 762900262fc102dd503c2bee720a05dfd50b898c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 15 Jun 2025 22:33:35 -0700 Subject: [PATCH 0877/1077] Fix Docker build --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 87f791e5..3bd36ddd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,8 +19,8 @@ COPY . /app/ RUN ln -s /core /app/core && /app/scripts/setup-symlinks # Install home-assistant/core dependencies -RUN mkdir -p /venv -ENV UV_PROJECT_ENVIRONMENT=/venv UV_PYTHON=3.13 PATH="/venv/bin:$PATH" +RUN mkdir -p /.venv +ENV UV_PROJECT_ENVIRONMENT=/.venv UV_PYTHON=3.13 PATH="/.venv/bin:$PATH" RUN uv venv RUN /app/scripts/setup-dependencies From 86460f69485bd3c4f688de1715c1bbb07f7349c5 Mon Sep 17 00:00:00 2001 From: Zachary McCord Date: Sun, 13 Jul 2025 16:45:30 -0500 Subject: [PATCH 0878/1077] webapp: add missing requirements and instructions for local run (#1235) --- webapp/README.md | 10 ++++ webapp/requirements.txt | 97 ++++++++++++++++++++++++++++++++++++++ webapp/requirements.txt.in | 3 ++ 3 files changed, 110 insertions(+) create mode 100644 webapp/README.md diff --git a/webapp/README.md b/webapp/README.md new file mode 100644 index 00000000..45ee754a --- /dev/null +++ b/webapp/README.md @@ -0,0 +1,10 @@ +To run this app locally, install the requirements and run `shiny run`. + +``` +$ cd webapp +$ pip install requirements.txt +$ shiny run +``` + +After the server starts, which should take only a moment, you can open +[http://127.0.0.1:8000](http://127.0.0.1:8000) to see the interface. diff --git a/webapp/requirements.txt b/webapp/requirements.txt index cbcadafc..04c991f2 100644 --- a/webapp/requirements.txt +++ b/webapp/requirements.txt @@ -1,6 +1,103 @@ # This file was autogenerated by uv via the following command: # uv pip compile --output-file=requirements.txt requirements.txt.in +anyio==4.9.0 + # via + # starlette + # watchfiles +appdirs==1.4.4 + # via shiny +asgiref==3.9.1 + # via shiny astral==2.2 # via -r requirements.txt.in +click==8.2.1 + # via + # shiny + # uvicorn +contourpy==1.3.2 + # via matplotlib +cycler==0.12.1 + # via matplotlib +fonttools==4.58.5 + # via matplotlib +h11==0.16.0 + # via uvicorn +htmltools==0.6.0 + # via + # shiny + # shinyswatch +idna==3.10 + # via anyio +kiwisolver==1.4.8 + # via matplotlib +linkify-it-py==2.0.3 + # via shiny +markdown-it-py==3.0.0 + # via + # mdit-py-plugins + # shiny +matplotlib==3.10.3 + # via -r requirements.txt.in +mdit-py-plugins==0.4.2 + # via shiny +mdurl==0.1.2 + # via markdown-it-py +narwhals==1.46.0 + # via shiny +numpy==2.3.1 + # via + # contourpy + # matplotlib +orjson==3.10.18 + # via shiny +packaging==25.0 + # via + # htmltools + # matplotlib + # shiny + # shinyswatch +pillow==11.3.0 + # via matplotlib +prompt-toolkit==3.0.51 + # via + # questionary + # shiny +pyparsing==3.2.3 + # via matplotlib +python-dateutil==2.9.0.post0 + # via matplotlib +python-multipart==0.0.20 + # via shiny pytz==2023.3.post1 # via astral +questionary==2.1.0 + # via shiny +setuptools==80.9.0 + # via shiny +shiny==1.4.0 + # via + # -r requirements.txt.in + # shinyswatch +shinyswatch==0.9.0 + # via -r requirements.txt.in +six==1.17.0 + # via python-dateutil +sniffio==1.3.1 + # via anyio +starlette==0.47.1 + # via shiny +typing-extensions==4.14.1 + # via + # htmltools + # shiny + # shinyswatch +uc-micro-py==1.0.3 + # via linkify-it-py +uvicorn==0.35.0 + # via shiny +watchfiles==1.1.0 + # via shiny +wcwidth==0.2.13 + # via prompt-toolkit +websockets==15.0.1 + # via shiny diff --git a/webapp/requirements.txt.in b/webapp/requirements.txt.in index 39c93a83..9703163f 100644 --- a/webapp/requirements.txt.in +++ b/webapp/requirements.txt.in @@ -1 +1,4 @@ astral==2.2 +matplotlib +shinyswatch +shiny From 8e75144e5d5c776ca65988bbba50361753b6ab54 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 21 Jul 2025 15:22:11 -0700 Subject: [PATCH 0879/1077] Fix webapp Pyodide compatibility: pin matplotlib and contourpy versions (#1242) * Fix webapp README: correct pip install command The README had incorrect syntax for pip install. Changed from `pip install requirements.txt` to `pip install -r requirements.txt`. * Fix webapp Pyodide compatibility: pin matplotlib and contourpy versions The webapp uses Shinylive which runs Python in the browser via Pyodide. Pyodide only supports specific versions of packages that have C extensions. Pin matplotlib to 3.8.4 and contourpy to 1.3.1 to match Pyodide's available versions. This fixes the "Can't find a pure Python 3 wheel for 'contourpy==1.3.2'" error when loading the webapp. --- webapp/README.md | 2 +- webapp/requirements.txt | 10 ++++++---- webapp/requirements.txt.in | 3 ++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/webapp/README.md b/webapp/README.md index 45ee754a..b12432eb 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -2,7 +2,7 @@ To run this app locally, install the requirements and run `shiny run`. ``` $ cd webapp -$ pip install requirements.txt +$ pip install -r requirements.txt $ shiny run ``` diff --git a/webapp/requirements.txt b/webapp/requirements.txt index 04c991f2..c09a3d7a 100644 --- a/webapp/requirements.txt +++ b/webapp/requirements.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --output-file=requirements.txt requirements.txt.in +# uv pip compile requirements.txt.in --output-file requirements.txt anyio==4.9.0 # via # starlette @@ -14,8 +14,10 @@ click==8.2.1 # via # shiny # uvicorn -contourpy==1.3.2 - # via matplotlib +contourpy==1.3.1 + # via + # -r requirements.txt.in + # matplotlib cycler==0.12.1 # via matplotlib fonttools==4.58.5 @@ -36,7 +38,7 @@ markdown-it-py==3.0.0 # via # mdit-py-plugins # shiny -matplotlib==3.10.3 +matplotlib==3.8.4 # via -r requirements.txt.in mdit-py-plugins==0.4.2 # via shiny diff --git a/webapp/requirements.txt.in b/webapp/requirements.txt.in index 9703163f..986a4976 100644 --- a/webapp/requirements.txt.in +++ b/webapp/requirements.txt.in @@ -1,4 +1,5 @@ astral==2.2 -matplotlib +matplotlib==3.8.4 +contourpy==1.3.1 shinyswatch shiny From 3c86a6e28baf6484754176e0e3cf2e60c5f01fa5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 21 Jul 2025 15:38:56 -0700 Subject: [PATCH 0880/1077] Remove app requirements --- webapp/requirements.txt | 29 ----------------------------- webapp/requirements.txt.in | 3 --- 2 files changed, 32 deletions(-) diff --git a/webapp/requirements.txt b/webapp/requirements.txt index c09a3d7a..f312c72b 100644 --- a/webapp/requirements.txt +++ b/webapp/requirements.txt @@ -8,20 +8,10 @@ appdirs==1.4.4 # via shiny asgiref==3.9.1 # via shiny -astral==2.2 - # via -r requirements.txt.in click==8.2.1 # via # shiny # uvicorn -contourpy==1.3.1 - # via - # -r requirements.txt.in - # matplotlib -cycler==0.12.1 - # via matplotlib -fonttools==4.58.5 - # via matplotlib h11==0.16.0 # via uvicorn htmltools==0.6.0 @@ -30,48 +20,31 @@ htmltools==0.6.0 # shinyswatch idna==3.10 # via anyio -kiwisolver==1.4.8 - # via matplotlib linkify-it-py==2.0.3 # via shiny markdown-it-py==3.0.0 # via # mdit-py-plugins # shiny -matplotlib==3.8.4 - # via -r requirements.txt.in mdit-py-plugins==0.4.2 # via shiny mdurl==0.1.2 # via markdown-it-py narwhals==1.46.0 # via shiny -numpy==2.3.1 - # via - # contourpy - # matplotlib orjson==3.10.18 # via shiny packaging==25.0 # via # htmltools - # matplotlib # shiny # shinyswatch -pillow==11.3.0 - # via matplotlib prompt-toolkit==3.0.51 # via # questionary # shiny -pyparsing==3.2.3 - # via matplotlib -python-dateutil==2.9.0.post0 - # via matplotlib python-multipart==0.0.20 # via shiny -pytz==2023.3.post1 - # via astral questionary==2.1.0 # via shiny setuptools==80.9.0 @@ -82,8 +55,6 @@ shiny==1.4.0 # shinyswatch shinyswatch==0.9.0 # via -r requirements.txt.in -six==1.17.0 - # via python-dateutil sniffio==1.3.1 # via anyio starlette==0.47.1 diff --git a/webapp/requirements.txt.in b/webapp/requirements.txt.in index 986a4976..74559226 100644 --- a/webapp/requirements.txt.in +++ b/webapp/requirements.txt.in @@ -1,5 +1,2 @@ -astral==2.2 -matplotlib==3.8.4 -contourpy==1.3.1 shinyswatch shiny From f45fa66390a2cecff588bcc29d5717ad0c8bc675 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 21 Jul 2025 15:44:37 -0700 Subject: [PATCH 0881/1077] Revert "Remove app requirements" This reverts commit 3c86a6e28baf6484754176e0e3cf2e60c5f01fa5. --- webapp/requirements.txt | 29 +++++++++++++++++++++++++++++ webapp/requirements.txt.in | 3 +++ 2 files changed, 32 insertions(+) diff --git a/webapp/requirements.txt b/webapp/requirements.txt index f312c72b..c09a3d7a 100644 --- a/webapp/requirements.txt +++ b/webapp/requirements.txt @@ -8,10 +8,20 @@ appdirs==1.4.4 # via shiny asgiref==3.9.1 # via shiny +astral==2.2 + # via -r requirements.txt.in click==8.2.1 # via # shiny # uvicorn +contourpy==1.3.1 + # via + # -r requirements.txt.in + # matplotlib +cycler==0.12.1 + # via matplotlib +fonttools==4.58.5 + # via matplotlib h11==0.16.0 # via uvicorn htmltools==0.6.0 @@ -20,31 +30,48 @@ htmltools==0.6.0 # shinyswatch idna==3.10 # via anyio +kiwisolver==1.4.8 + # via matplotlib linkify-it-py==2.0.3 # via shiny markdown-it-py==3.0.0 # via # mdit-py-plugins # shiny +matplotlib==3.8.4 + # via -r requirements.txt.in mdit-py-plugins==0.4.2 # via shiny mdurl==0.1.2 # via markdown-it-py narwhals==1.46.0 # via shiny +numpy==2.3.1 + # via + # contourpy + # matplotlib orjson==3.10.18 # via shiny packaging==25.0 # via # htmltools + # matplotlib # shiny # shinyswatch +pillow==11.3.0 + # via matplotlib prompt-toolkit==3.0.51 # via # questionary # shiny +pyparsing==3.2.3 + # via matplotlib +python-dateutil==2.9.0.post0 + # via matplotlib python-multipart==0.0.20 # via shiny +pytz==2023.3.post1 + # via astral questionary==2.1.0 # via shiny setuptools==80.9.0 @@ -55,6 +82,8 @@ shiny==1.4.0 # shinyswatch shinyswatch==0.9.0 # via -r requirements.txt.in +six==1.17.0 + # via python-dateutil sniffio==1.3.1 # via anyio starlette==0.47.1 diff --git a/webapp/requirements.txt.in b/webapp/requirements.txt.in index 74559226..986a4976 100644 --- a/webapp/requirements.txt.in +++ b/webapp/requirements.txt.in @@ -1,2 +1,5 @@ +astral==2.2 +matplotlib==3.8.4 +contourpy==1.3.1 shinyswatch shiny From 0a2d2e9fb5ae210130d137ce5c23b7a3f787efee Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 21 Jul 2025 15:48:10 -0700 Subject: [PATCH 0882/1077] Properly fix the webapp --- webapp/README.md | 2 +- webapp/requirements-dev.txt | 4 ++ webapp/requirements.txt | 99 ------------------------------------- webapp/requirements.txt.in | 4 -- 4 files changed, 5 insertions(+), 104 deletions(-) create mode 100644 webapp/requirements-dev.txt diff --git a/webapp/README.md b/webapp/README.md index b12432eb..e561b8f4 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -2,7 +2,7 @@ To run this app locally, install the requirements and run `shiny run`. ``` $ cd webapp -$ pip install -r requirements.txt +$ pip install -r requirements-dev.txt $ shiny run ``` diff --git a/webapp/requirements-dev.txt b/webapp/requirements-dev.txt new file mode 100644 index 00000000..9703163f --- /dev/null +++ b/webapp/requirements-dev.txt @@ -0,0 +1,4 @@ +astral==2.2 +matplotlib +shinyswatch +shiny diff --git a/webapp/requirements.txt b/webapp/requirements.txt index c09a3d7a..882162f1 100644 --- a/webapp/requirements.txt +++ b/webapp/requirements.txt @@ -1,105 +1,6 @@ # This file was autogenerated by uv via the following command: # uv pip compile requirements.txt.in --output-file requirements.txt -anyio==4.9.0 - # via - # starlette - # watchfiles -appdirs==1.4.4 - # via shiny -asgiref==3.9.1 - # via shiny astral==2.2 # via -r requirements.txt.in -click==8.2.1 - # via - # shiny - # uvicorn -contourpy==1.3.1 - # via - # -r requirements.txt.in - # matplotlib -cycler==0.12.1 - # via matplotlib -fonttools==4.58.5 - # via matplotlib -h11==0.16.0 - # via uvicorn -htmltools==0.6.0 - # via - # shiny - # shinyswatch -idna==3.10 - # via anyio -kiwisolver==1.4.8 - # via matplotlib -linkify-it-py==2.0.3 - # via shiny -markdown-it-py==3.0.0 - # via - # mdit-py-plugins - # shiny -matplotlib==3.8.4 - # via -r requirements.txt.in -mdit-py-plugins==0.4.2 - # via shiny -mdurl==0.1.2 - # via markdown-it-py -narwhals==1.46.0 - # via shiny -numpy==2.3.1 - # via - # contourpy - # matplotlib -orjson==3.10.18 - # via shiny -packaging==25.0 - # via - # htmltools - # matplotlib - # shiny - # shinyswatch -pillow==11.3.0 - # via matplotlib -prompt-toolkit==3.0.51 - # via - # questionary - # shiny -pyparsing==3.2.3 - # via matplotlib -python-dateutil==2.9.0.post0 - # via matplotlib -python-multipart==0.0.20 - # via shiny pytz==2023.3.post1 # via astral -questionary==2.1.0 - # via shiny -setuptools==80.9.0 - # via shiny -shiny==1.4.0 - # via - # -r requirements.txt.in - # shinyswatch -shinyswatch==0.9.0 - # via -r requirements.txt.in -six==1.17.0 - # via python-dateutil -sniffio==1.3.1 - # via anyio -starlette==0.47.1 - # via shiny -typing-extensions==4.14.1 - # via - # htmltools - # shiny - # shinyswatch -uc-micro-py==1.0.3 - # via linkify-it-py -uvicorn==0.35.0 - # via shiny -watchfiles==1.1.0 - # via shiny -wcwidth==0.2.13 - # via prompt-toolkit -websockets==15.0.1 - # via shiny diff --git a/webapp/requirements.txt.in b/webapp/requirements.txt.in index 986a4976..39c93a83 100644 --- a/webapp/requirements.txt.in +++ b/webapp/requirements.txt.in @@ -1,5 +1 @@ astral==2.2 -matplotlib==3.8.4 -contourpy==1.3.1 -shinyswatch -shiny From af5a7151aaa6bd0ce5116e97e1f66865ca1ef4e1 Mon Sep 17 00:00:00 2001 From: Tom Matheussen <13683094+Tommatheussen@users.noreply.github.com> Date: Thu, 27 Nov 2025 17:05:30 +0100 Subject: [PATCH 0883/1077] Fix HA 2025.12 breaking (#1291) --- .../adaptive_lighting/hass_utils.py | 20 +++++++++++++++++++ custom_components/adaptive_lighting/switch.py | 3 +-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py index c87d481f..21ea67b3 100644 --- a/custom_components/adaptive_lighting/hass_utils.py +++ b/custom_components/adaptive_lighting/hass_utils.py @@ -4,6 +4,7 @@ import logging from collections.abc import Awaitable, Callable from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import device_registry, entity_registry from homeassistant.util.read_only_dict import ReadOnlyDict from .adaptation_utils import ServiceData @@ -11,6 +12,25 @@ from .adaptation_utils import ServiceData _LOGGER = logging.getLogger(__name__) +def area_entities(hass: HomeAssistant, area_id: str): + """Get all entities linked to an area.""" + ent_reg = entity_registry.async_get(hass) + entity_ids = [ + entry.entity_id + for entry in entity_registry.async_entries_for_area(ent_reg, area_id) + ] + dev_reg = device_registry.async_get(hass) + entity_ids.extend( + [ + entity.entity_id + for device in device_registry.async_entries_for_area(dev_reg, area_id) + for entity in entity_registry.async_entries_for_device(ent_reg, device.id) + if entity.area_id is None + ], + ) + return entity_ids + + def setup_service_call_interceptor( hass: HomeAssistant, domain: str, diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index f00227dc..fdeac7dd 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -75,7 +75,6 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.sun import get_astral_location -from homeassistant.helpers.template import area_entities from homeassistant.util import slugify from homeassistant.util.color import ( color_temperature_to_rgb, @@ -153,7 +152,7 @@ from .const import ( apply_service_schema, replace_none_str, ) -from .hass_utils import setup_service_call_interceptor +from .hass_utils import area_entities, setup_service_call_interceptor from .helpers import ( clamp, color_difference_redmean, From e8af7a485e8bbf1958e343b2ceae93657f8d9c11 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Nov 2025 09:06:41 -0800 Subject: [PATCH 0884/1077] Bump to 1.27.0 for fixes in 2025.12 (#1293) --- .github/workflows/pytest.yaml | 12 ++++++++- Dockerfile | 6 +++++ .../adaptive_lighting/manifest.json | 2 +- scripts/setup-dependencies | 16 ++++++++++++ tests/conftest.py | 26 +++++++++++++++++++ tests/test_switch.py | 11 +++++++- 6 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 tests/conftest.py diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index fb25eff0..21d76466 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -26,7 +26,17 @@ jobs: python-version: "3.13" - core-version: "2025.5.3" python-version: "3.13" - - core-version: "2025.6.1" + - core-version: "2025.6.3" + python-version: "3.13" + - core-version: "2025.7.4" + python-version: "3.13" + - core-version: "2025.8.3" + python-version: "3.13" + - core-version: "2025.9.4" + python-version: "3.13" + - core-version: "2025.10.4" + python-version: "3.13" + - core-version: "2025.11.3" python-version: "3.13" - core-version: "dev" python-version: "3.13" diff --git a/Dockerfile b/Dockerfile index 3bd36ddd..c4d9c695 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,12 @@ FROM ghcr.io/astral-sh/uv:debian +# Install build dependencies for Python extensions +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3-dev \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + # Clone home-assistant/core RUN git clone --depth 1 --branch dev https://github.com/home-assistant/core.git /core diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 92100afa..856abfe8 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.26.0" + "version": "1.27.0" } diff --git a/scripts/setup-dependencies b/scripts/setup-dependencies index b9e2de1f..92a41411 100755 --- a/scripts/setup-dependencies +++ b/scripts/setup-dependencies @@ -6,6 +6,22 @@ if grep -q 'mypy-dev==1.14.0a3' core/requirements_test.txt; then # mypy-dev==1.14.0a3 seems to not be available anymore, HA 2024.12 is affected sed -i 's/mypy-dev==1.14.0a3/mypy-dev==1.14.0a7/' core/requirements_test.txt fi +if grep -q 'mypy-dev==1.16.0a1' core/requirements_test.txt; then + # mypy-dev==1.16.0a1 seems to not be available anymore, HA 2025.2 is affected + sed -i 's/mypy-dev==1.16.0a1/mypy-dev==1.16.0a9/' core/requirements_test.txt +fi +if grep -q 'mypy-dev==1.16.0a3' core/requirements_test.txt; then + # mypy-dev==1.16.0a3 seems to not be available anymore, HA 2025.3 is affected + sed -i 's/mypy-dev==1.16.0a3/mypy-dev==1.16.0a9/' core/requirements_test.txt +fi +if grep -q 'mypy-dev==1.16.0a7' core/requirements_test.txt; then + # mypy-dev==1.16.0a7 seems to not be available anymore, HA 2025.4 is affected + sed -i 's/mypy-dev==1.16.0a7/mypy-dev==1.16.0a9/' core/requirements_test.txt +fi +if grep -q 'mypy-dev==1.16.0a8' core/requirements_test.txt; then + # mypy-dev==1.16.0a8 seems to not be available anymore, HA 2025.5 and 2025.6 is affected + sed -i 's/mypy-dev==1.16.0a8/mypy-dev==1.16.0a9/' core/requirements_test.txt +fi uv pip install -r core/requirements.txt uv pip install -r core/requirements_test.txt diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..cce77d96 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,26 @@ +"""Pytest configuration for adaptive-lighting tests.""" + +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def mock_template_deprecation_issue(): + """Mock the template deprecation issue creation. + + The template component's legacy platform syntax creates deprecation + issues that require translations. Since adaptive-lighting tests use + template lights as test fixtures (not testing the template integration + itself), we mock the issue creation to avoid translation validation errors. + """ + # Patch the create_legacy_template_issue function in the template helpers + # to be a no-op when called for the deprecated_legacy_templates issue + try: + with patch( + "homeassistant.components.template.helpers.create_legacy_template_issue", + ): + yield + except (ImportError, ModuleNotFoundError, AttributeError): + # Older HA versions don't have this function + yield diff --git a/tests/test_switch.py b/tests/test_switch.py index cc03fa24..fc5f67cc 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -82,7 +82,16 @@ from homeassistant.components.light import ( ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.components.template.light import LightTemplate + +try: + # HA >= 2025.8 + from homeassistant.components.template.light import ( + StateLightEntity as LightTemplate, + ) +except ImportError: + # HA < 2025.8 + from homeassistant.components.template.light import LightTemplate + from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_AREA_ID, From 95a8f34000ab4848bdd724265afe64a2f6fa3aae Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 09:17:12 -0800 Subject: [PATCH 0885/1077] docs: add Tommatheussen as a contributor for code (#1294) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 76a8cf13..6f34f6c3 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1062,6 +1062,15 @@ "contributions": [ "code" ] + }, + { + "login": "Tommatheussen", + "name": "Tom Matheussen", + "avatar_url": "https://avatars.githubusercontent.com/u/13683094?v=4", + "profile": "https://github.com/Tommatheussen", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 7af1feb8..0b2d11cc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-116-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-117-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -616,6 +616,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 6f846c26cab2666e296e2e5f617b9805ce8a251d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Nov 2025 09:27:13 -0800 Subject: [PATCH 0886/1077] Fix race condition where timer.start_time is None while is_running() is True (#1295) When start() creates a task with asyncio.create_task(), the task is scheduled but not immediately executed. This means is_running() returns True (task exists and not done), but start_time is still None because _run() hasn't executed yet. This causes a TypeError when comparing event.time_fired > timer.start_time. Fix by setting start_time in start() before creating the task. Fixes #1272 --- custom_components/adaptive_lighting/switch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index fdeac7dd..6a22a41f 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2680,7 +2680,6 @@ class _AsyncSingleShotTimer: async def _run(self): """Run the timer. Don't call this directly, use start() instead.""" - self.start_time = dt_util.utcnow() await asyncio.sleep(self.delay) if self.callback: if asyncio.iscoroutinefunction(self.callback): @@ -2696,6 +2695,10 @@ class _AsyncSingleShotTimer: """Start the timer.""" if self.task is not None and not self.task.done(): self.task.cancel() + # Set start_time before creating task to avoid race condition + # where is_running() returns True but start_time is still None + # See: https://github.com/basnijholt/adaptive-lighting/issues/1272 + self.start_time = dt_util.utcnow() self.task = asyncio.create_task(self._run()) def cancel(self): From b34cc1ffaa40c802ce07d256b8ef7c9bd8f2858e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 09:27:27 -0800 Subject: [PATCH 0887/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/checkout=20action=20to=20v6=20(#1288)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/deploy-webapp.yml | 2 +- .github/workflows/hassfest.yaml | 2 +- .github/workflows/install_dependencies/action.yml | 4 ++-- .github/workflows/main-to-master-sync.yml | 2 +- .github/workflows/pre-commit.yaml | 2 +- .github/workflows/pytest.yaml | 2 +- .github/workflows/update-readme.yml | 2 +- .github/workflows/validate.yml | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml index e52963b0..29b8a7b4 100644 --- a/.github/workflows/deploy-webapp.yml +++ b/.github/workflows/deploy-webapp.yml @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set Up Python uses: actions/setup-python@v5 diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml index 4d141e56..69f93e15 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/hassfest.yaml @@ -11,5 +11,5 @@ jobs: validate_hassfest: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v4.2.2" + - uses: "actions/checkout@v6.0.0" - uses: home-assistant/actions/hassfest@master diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index 8c4483f7..8d561b02 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -14,14 +14,14 @@ runs: using: "composite" steps: - name: Check out code from GitHub - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ github.repository }} ref: ${{ github.ref }} persist-credentials: false fetch-depth: 0 - name: Check out code from GitHub - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: home-assistant/core path: core diff --git a/.github/workflows/main-to-master-sync.yml b/.github/workflows/main-to-master-sync.yml index d5c915c8..424f50b3 100644 --- a/.github/workflows/main-to-master-sync.yml +++ b/.github/workflows/main-to-master-sync.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: main fetch-depth: 0 diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 7f579afa..c6f2cfb3 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -9,6 +9,6 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-python@v5 - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 21d76466..b8a59b40 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -42,7 +42,7 @@ jobs: python-version: "3.13" steps: - name: Check out code from GitHub - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Home Assistant uses: ./.github/workflows/install_dependencies diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index a4422130..7cdb8e24 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code from GitHub - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Home Assistant uses: ./.github/workflows/install_dependencies diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 79c7fe00..3fa46b4e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -11,7 +11,7 @@ jobs: validate_hacs: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v4" + - uses: "actions/checkout@v6" - name: HACS validation uses: "hacs/action@main" with: From 1556de8c4f0962ddc90611ca64e37d114fd04e7c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 09:27:38 -0800 Subject: [PATCH 0888/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20mcr.mic?= =?UTF-8?q?rosoft.com/devcontainers/python=20Docker=20tag=20to=20v3=20(#12?= =?UTF-8?q?92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer.json b/.devcontainer.json index 0a09ed54..8d5e2bd9 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -1,6 +1,6 @@ { "name": "basnijholt/adaptive_lighting", - "image": "mcr.microsoft.com/devcontainers/python:1-3.13", + "image": "mcr.microsoft.com/devcontainers/python:3-3.13", "postCreateCommand": "./scripts/setup-devcontainer && . .venv/bin/activate", "forwardPorts": [ 8123 From 68e243c87e69c97030d222846c0c0771bccc94ae Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Nov 2025 09:30:01 -0800 Subject: [PATCH 0889/1077] Fix infinite loop when disabling SimpleSwitch entities (#1296) * Add regression tests for SimpleSwitch initial state bug Adds tests that verify SimpleSwitch._state is set immediately in __init__ rather than waiting for async_added_to_hass(). These tests currently FAIL because _state is None after __init__, which causes an infinite loop in _setup_listeners when the entity is disabled (since async_added_to_hass is never called for disabled entities). Regression tests for: https://github.com/basnijholt/adaptive-lighting/issues/1264 * Fix infinite loop when disabling SimpleSwitch entities The issue was that SimpleSwitch._state was initialized to None in __init__, but only set to a boolean value in async_added_to_hass(). When an entity is disabled, async_added_to_hass() is never called, so _state stayed None. The _setup_listeners() method has a while loop that waits for _state is not None for all SimpleSwitch children (sleep_mode_switch, adapt_brightness_switch, adapt_color_switch). With _state stuck at None, this created an infinite loop. The fix sets _state to initial_state directly in __init__ instead of waiting for async_added_to_hass() to set it. The async_added_to_hass() will still properly restore state from the last session or set based on initial_state as before. Fixes: https://github.com/basnijholt/adaptive-lighting/issues/1264 --- custom_components/adaptive_lighting/switch.py | 2 +- tests/test_switch.py | 74 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 6a22a41f..19e99d78 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1563,7 +1563,7 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): self.hass = hass data = validate(config_entry) self._icon = icon - self._state: bool | None = None + self._state: bool = initial_state self._which = which self._config_name = data[CONF_NAME] self._unique_id = f"{self._config_name}_{slugify(self._which)}" diff --git a/tests/test_switch.py b/tests/test_switch.py index fc5f67cc..1016b591 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -65,6 +65,7 @@ from homeassistant.components.adaptive_lighting.switch import ( CONF_INTERCEPT, AdaptiveLightingManager, AdaptiveSwitch, + SimpleSwitch, _attributes_have_changed, color_difference_redmean, create_context, @@ -2275,3 +2276,76 @@ async def test_brightness_mode(hass, brightness_mode, dark, light): # After sunrise the brightness should be light_brightness await patch_time_and_update(after_sunrise) assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], light_brightness) + + +async def test_simple_switch_initial_state_not_none(hass): + """Test that SimpleSwitch._state is not None after __init__. + + Regression test for https://github.com/basnijholt/adaptive-lighting/issues/1264 + + When an entity is disabled in Home Assistant, async_added_to_hass() is never + called. Previously, SimpleSwitch._state was initialized to None and only set + to True/False in async_added_to_hass(). This caused an infinite loop in + AdaptiveSwitch._setup_listeners() which waits for all SimpleSwitch._state + to be not None. + + The fix is to initialize _state to the initial_state value in __init__. + """ + entry = MockConfigEntry(domain=DOMAIN, data={CONF_NAME: DEFAULT_NAME}) + entry.add_to_hass(hass) + + # Create a SimpleSwitch without calling async_added_to_hass + # (simulating a disabled entity) + switch = SimpleSwitch( + which="Test", + initial_state=True, + hass=hass, + config_entry=entry, + icon="mdi:test", + ) + + # Before the fix: _state would be None, causing infinite loop + # After the fix: _state should be the initial_state value + assert switch._state is not None, ( + "SimpleSwitch._state should not be None after __init__. " + "This would cause an infinite loop in _setup_listeners when the entity is disabled." + ) + assert switch._state is True # Should be the initial_state value + + +async def test_simple_switch_state_after_async_added_to_hass(hass): + """Test that SimpleSwitch._state is properly set after async_added_to_hass. + + This ensures the fix for #1264 doesn't break normal entity initialization. + """ + entry = MockConfigEntry(domain=DOMAIN, data={CONF_NAME: DEFAULT_NAME}) + entry.add_to_hass(hass) + + # Create switches with different initial states + switch_true = SimpleSwitch( + which="Test True", + initial_state=True, + hass=hass, + config_entry=entry, + icon="mdi:test", + ) + switch_false = SimpleSwitch( + which="Test False", + initial_state=False, + hass=hass, + config_entry=entry, + icon="mdi:test", + ) + + # Verify initial state is set correctly + assert switch_true._state is True + assert switch_false._state is False + + # Call async_added_to_hass (simulating normal entity setup) + # Since there's no last state, it should use the initial_state + await switch_true.async_added_to_hass() + await switch_false.async_added_to_hass() + + # State should still be correct after async_added_to_hass + assert switch_true._state is True + assert switch_false._state is False From 9244ff39fe5d2cc0e5ee1e696f2d8f8bdac9dba7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 09:30:12 -0800 Subject: [PATCH 0890/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20astral-?= =?UTF-8?q?sh/setup-uv=20action=20to=20v7=20(#1271)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/install_dependencies/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index 8d561b02..9a9903f9 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -32,7 +32,7 @@ runs: with: python-version: ${{ inputs.python-version }} - name: Set up UV - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v7 - name: Install dependencies shell: bash run: | From f6321afeae237e0514b249e2adbcc3609143761b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 09:30:26 -0800 Subject: [PATCH 0891/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/upload-pages-artifact=20action=20to=20v4=20(#1259)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/deploy-webapp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml index 29b8a7b4..b6614d12 100644 --- a/.github/workflows/deploy-webapp.yml +++ b/.github/workflows/deploy-webapp.yml @@ -53,7 +53,7 @@ jobs: uses: actions/configure-pages@v5 - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v4 with: # Upload the 'site' directory, where your app has been built path: "site" From 561749ac060e4ccfe706b401094663ec4588904d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 09:34:46 -0800 Subject: [PATCH 0892/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/setup-python=20action=20to=20v6=20(#1261)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/deploy-webapp.yml | 2 +- .github/workflows/install_dependencies/action.yml | 2 +- .github/workflows/pre-commit.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml index b6614d12..698d8fec 100644 --- a/.github/workflows/deploy-webapp.yml +++ b/.github/workflows/deploy-webapp.yml @@ -33,7 +33,7 @@ jobs: uses: actions/checkout@v6 - name: Set Up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.13.5 diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index 9a9903f9..4ab58169 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -28,7 +28,7 @@ runs: ref: ${{ inputs.core-version }} - name: Set up Python ${{ inputs.python-version }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.1.0 with: python-version: ${{ inputs.python-version }} - name: Set up UV diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index c6f2cfb3..57263d00 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -10,5 +10,5 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 - uses: pre-commit/action@v3.0.1 From 32719eabaea9eaa58fd7d52d9202101369c80aa7 Mon Sep 17 00:00:00 2001 From: ams2990 Date: Thu, 27 Nov 2025 07:35:09 -1000 Subject: [PATCH 0893/1077] Fix some type hint issues (#1280) --- custom_components/adaptive_lighting/__init__.py | 4 ++-- .../adaptive_lighting/_docs_helpers.py | 7 ++----- .../adaptive_lighting/adaptation_utils.py | 6 +++--- .../adaptive_lighting/color_and_brightness.py | 16 ++++++++-------- .../adaptive_lighting/config_flow.py | 3 ++- 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 13c2d7d1..0c8bad80 100644 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -70,12 +70,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): return True -async def async_update_options(hass, config_entry: ConfigEntry): +async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry): """Update options.""" await hass.config_entries.async_reload(config_entry.entry_id) -async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_forward_entry_unload( config_entry, diff --git a/custom_components/adaptive_lighting/_docs_helpers.py b/custom_components/adaptive_lighting/_docs_helpers.py index 31225a6c..49899395 100644 --- a/custom_components/adaptive_lighting/_docs_helpers.py +++ b/custom_components/adaptive_lighting/_docs_helpers.py @@ -57,8 +57,6 @@ def _type_to_str(type_: Any) -> str: # noqa: PLR0911 def generate_config_markdown_table(): - import pandas as pd - rows = [] for k, default, type_ in VALIDATION_TUPLES: description = DOCS[k] @@ -84,12 +82,11 @@ def _schema_to_dict(schema: vol.Schema) -> dict[str, tuple[Any, Any]]: def _generate_service_markdown_table( - schema: dict[str, tuple[Any, Any]], + schema: vol.Schema, alternative_docs: dict[str, str] | None = None, ): - schema = _schema_to_dict(schema) rows = [] - for k, (default, type_) in schema.items(): + for k, (default, type_) in _schema_to_dict(schema).items(): if alternative_docs is not None and k in alternative_docs: description = alternative_docs[k] else: diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py index aea061d1..2c339223 100644 --- a/custom_components/adaptive_lighting/adaptation_utils.py +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -54,7 +54,7 @@ def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]: common_data = {k: service_data[k] for k in common_attrs if k in service_data} attributes_split_sequence = [BRIGHTNESS_ATTRS, COLOR_ATTRS] - service_datas = [] + service_datas: list[dict[str, Any]] = [] for attributes in attributes_split_sequence: split_data = { @@ -106,7 +106,7 @@ async def _create_service_call_data_iterator( hass: HomeAssistant, service_datas: list[ServiceData], filter_by_state: bool, -) -> AsyncGenerator[ServiceData, None]: +) -> AsyncGenerator[ServiceData]: """Enumerates and filters a list of service datas on the fly. If filtering is enabled, every service data is filtered by the current state of @@ -141,7 +141,7 @@ class AdaptationData: entity_id: str context: Context sleep_time: float - service_call_datas: AsyncGenerator[ServiceData, None] + service_call_datas: AsyncGenerator[ServiceData] force: bool max_length: int which: Literal["brightness", "color", "both"] diff --git a/custom_components/adaptive_lighting/color_and_brightness.py b/custom_components/adaptive_lighting/color_and_brightness.py index 6fdb7083..2fd93e67 100644 --- a/custom_components/adaptive_lighting/color_and_brightness.py +++ b/custom_components/adaptive_lighting/color_and_brightness.py @@ -8,8 +8,8 @@ import datetime import logging import math from dataclasses import dataclass -from datetime import timedelta -from functools import cached_property, partial +from datetime import UTC, timedelta +from functools import partial from typing import TYPE_CHECKING, Any, Literal, cast from homeassistant.util.color import ( @@ -17,9 +17,10 @@ from homeassistant.util.color import ( color_temperature_to_rgb, color_xy_to_hs, ) +from propcache.api import cached_property if TYPE_CHECKING: - import astral + import astral.location # Same as homeassistant.const.SUN_EVENT_SUNRISE and homeassistant.const.SUN_EVENT_SUNSET # We re-define them here to not depend on homeassistant in this file. @@ -32,7 +33,6 @@ SUN_EVENT_MIDNIGHT = "solar_midnight" _ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT) _ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))} -UTC = datetime.timezone.utc utcnow: partial[datetime.datetime] = partial(datetime.datetime.now, UTC) utcnow.__doc__ = "Get now in UTC time." @@ -44,7 +44,7 @@ class SunEvents: """Track the state of the sun and associated light settings.""" name: str - astral_location: astral.Location + astral_location: astral.location.Location sunrise_time: datetime.time | None min_sunrise_time: datetime.time | None max_sunrise_time: datetime.time | None @@ -198,7 +198,7 @@ class SunLightSettings: """Track the state of the sun and associated light settings.""" name: str - astral_location: astral.Location + astral_location: astral.location.Location adapt_until_sleep: bool max_brightness: int max_color_temp: int @@ -296,7 +296,7 @@ class SunLightSettings: ) return clamp(brightness, self.min_brightness, self.max_brightness) - def brightness_pct(self, dt: datetime.datetime, is_sleep: bool) -> float: + def brightness_pct(self, dt: datetime.datetime, is_sleep: bool) -> float | None: """Calculate the brightness in %.""" if is_sleep: return self.sleep_brightness @@ -331,7 +331,7 @@ class SunLightSettings: ) -> dict[str, Any]: """Calculate the brightness and color.""" sun_position = self.sun.sun_position(dt) - rgb_color: tuple[float, float, float] + rgb_color: tuple[int, int, int] # Variable `force_rgb_color` is needed for RGB color after sunset (if enabled) force_rgb_color = False brightness_pct = self.brightness_pct(dt, is_sleep) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 3922d000..00214fdb 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Adaptive Lighting integration.""" import logging +from typing import Any import homeassistant.helpers.config_validation as cv import voluptuous as vol @@ -40,7 +41,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, user_input=None): + async def async_step_import(self, user_input: dict[str, Any]): """Handle configuration by YAML file.""" await self.async_set_unique_id(user_input[CONF_NAME]) # Keep a list of switches that are configured via YAML From 3d3d24691881ed386384a64f2295492dac5745e2 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 09:36:04 -0800 Subject: [PATCH 0894/1077] docs: add ams2990 as a contributor for code (#1297) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 6f34f6c3..004381bb 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1071,6 +1071,15 @@ "contributions": [ "code" ] + }, + { + "login": "ams2990", + "name": "ams2990", + "avatar_url": "https://avatars.githubusercontent.com/u/488907?v=4", + "profile": "https://github.com/ams2990", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 0b2d11cc..9f4388c3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-117-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-118-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -617,6 +617,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From afd7b935d7ad399c8b0a19dcb1ff6d2e07a31ddf Mon Sep 17 00:00:00 2001 From: DataGhost <3911340+DataGhost@users.noreply.github.com> Date: Thu, 27 Nov 2025 18:54:14 +0100 Subject: [PATCH 0895/1077] Check for external light mode (temperature vs rgb) switch (#1282) --- custom_components/adaptive_lighting/switch.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 19e99d78..b423fc3c 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -766,6 +766,31 @@ def _attributes_have_changed( context.id, ) return True + + if adapt_color and ( + ( + old_attributes.get(ATTR_COLOR_TEMP_KELVIN) + and not new_attributes.get(ATTR_COLOR_TEMP_KELVIN) + ) + or ( + old_attributes.get(ATTR_RGB_COLOR) + and not new_attributes.get(ATTR_RGB_COLOR) + ) + ): + last_mode = ( + "color_temp" if old_attributes.get(ATTR_COLOR_TEMP_KELVIN) else "rgb" + ) + current_mode = ( + "color_temp" if new_attributes.get(ATTR_COLOR_TEMP_KELVIN) else "rgb" + ) + _LOGGER.debug( + "Light mode of %s changed from %s to %s with context.id='%s'", + light, + last_mode, + current_mode, + context.id, + ) + return True return False From 41e13bc944acb987a6c524cffa198d6361860758 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 09:57:41 -0800 Subject: [PATCH 0896/1077] docs: add DataGhost as a contributor for code (#1298) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 004381bb..c498e6ce 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1080,6 +1080,15 @@ "contributions": [ "code" ] + }, + { + "login": "DataGhost", + "name": "DataGhost", + "avatar_url": "https://avatars.githubusercontent.com/u/3911340?v=4", + "profile": "https://github.com/DataGhost", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 9f4388c3..590056ad 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-118-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-119-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -618,6 +618,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 4feaf2c291577f95957df8c48de3369dfa4d88eb Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Nov 2025 10:02:50 -0800 Subject: [PATCH 0897/1077] Add bidirectional color mode change detection (issue #1275) (#1299) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract _has_color_mode_changed() function that checks original attributes BEFORE conversion, enabling detection of all mode switches: - color_temp → RGB ✓ - color_temp → XY ✓ - RGB → color_temp ✓ - RGB → XY ✓ - XY → color_temp ✓ - XY → RGB ✓ This improves on PR #1282 by detecting mode changes in both directions. --- custom_components/adaptive_lighting/switch.py | 87 ++++++++---- tests/test_switch.py | 125 ++++++++++++++++-- 2 files changed, 180 insertions(+), 32 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index b423fc3c..59b98a0c 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -694,6 +694,58 @@ def _add_missing_attributes( return old_attributes, new_attributes +def _has_color_mode_changed( + light: str, + old_attributes: dict[str, Any], + new_attributes: dict[str, Any], + context: Context, +) -> bool: + """Check if the light's color mode changed (e.g., color_temp to RGB or vice versa). + + This must be called BEFORE _add_missing_attributes() to detect mode changes + using the original attributes. See issue #1275. + """ + old_has_color_temp = old_attributes.get(ATTR_COLOR_TEMP_KELVIN) is not None + old_has_rgb = old_attributes.get(ATTR_RGB_COLOR) is not None + old_has_xy = old_attributes.get(ATTR_XY_COLOR) is not None + + new_has_color_temp = new_attributes.get(ATTR_COLOR_TEMP_KELVIN) is not None + new_has_rgb = new_attributes.get(ATTR_RGB_COLOR) is not None + new_has_xy = new_attributes.get(ATTR_XY_COLOR) is not None + + # Determine old and new color modes + # Priority: color_temp > rgb > xy (matching typical light behavior) + if old_has_color_temp: + old_mode = "color_temp" + elif old_has_rgb: + old_mode = "rgb" + elif old_has_xy: + old_mode = "xy" + else: + old_mode = None + + if new_has_color_temp: + new_mode = "color_temp" + elif new_has_rgb: + new_mode = "rgb" + elif new_has_xy: + new_mode = "xy" + else: + new_mode = None + + # Check if mode changed + if old_mode is not None and new_mode is not None and old_mode != new_mode: + _LOGGER.debug( + "Light mode of %s changed from %s to %s with context.id='%s'", + light, + old_mode, + new_mode, + context.id, + ) + return True + return False + + def _attributes_have_changed( light: str, old_attributes: dict[str, Any], @@ -706,6 +758,17 @@ def _attributes_have_changed( # so we must protect for `None` here # see https://github.com/home-assistant/core/pull/101946 + # Check for color mode changes BEFORE attribute conversion + # This detects external changes like Hue scenes switching from color_temp to RGB + # See: https://github.com/basnijholt/adaptive-lighting/issues/1275 + if adapt_color and _has_color_mode_changed( + light, + old_attributes, + new_attributes, + context, + ): + return True + if adapt_color: old_attributes, new_attributes = _add_missing_attributes( old_attributes, @@ -767,30 +830,6 @@ def _attributes_have_changed( ) return True - if adapt_color and ( - ( - old_attributes.get(ATTR_COLOR_TEMP_KELVIN) - and not new_attributes.get(ATTR_COLOR_TEMP_KELVIN) - ) - or ( - old_attributes.get(ATTR_RGB_COLOR) - and not new_attributes.get(ATTR_RGB_COLOR) - ) - ): - last_mode = ( - "color_temp" if old_attributes.get(ATTR_COLOR_TEMP_KELVIN) else "rgb" - ) - current_mode = ( - "color_temp" if new_attributes.get(ATTR_COLOR_TEMP_KELVIN) else "rgb" - ) - _LOGGER.debug( - "Light mode of %s changed from %s to %s with context.id='%s'", - light, - last_mode, - current_mode, - context.id, - ) - return True return False diff --git a/tests/test_switch.py b/tests/test_switch.py index 1016b591..5011ebc6 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1010,26 +1010,36 @@ def test_attributes_have_changed(): new_attributes=attrs, **kwargs, ) - _LOGGER.debug("Test switch from color_temp to rgb_color") - assert not _attributes_have_changed( + # Test color mode switches - feature added to detect external changes + # (e.g., when Hue scenes change light from color_temp to RGB mode) + # See: https://github.com/basnijholt/adaptive-lighting/issues/1275 + # + # All mode switches are now detected bidirectionally by checking original + # attributes BEFORE conversion in _has_color_mode_changed(). + _LOGGER.debug( + "Test switch from color_temp to rgb_color - should detect mode change", + ) + assert _attributes_have_changed( old_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702}, new_attributes={ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (255, 166, 87)}, **kwargs, ) - _LOGGER.debug("Test switch from rgb_color to color_temp") - assert not _attributes_have_changed( + _LOGGER.debug( + "Test switch from rgb_color to color_temp - should detect mode change", + ) + assert _attributes_have_changed( old_attributes={ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (255, 166, 87)}, new_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702}, **kwargs, ) - _LOGGER.debug("Test switch from color_temp to color_xy") - assert not _attributes_have_changed( + _LOGGER.debug("Test switch from color_temp to color_xy - should detect mode change") + assert _attributes_have_changed( old_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702}, new_attributes={ATTR_BRIGHTNESS: 1, ATTR_XY_COLOR: (0.526, 0.387)}, **kwargs, ) - _LOGGER.debug("Test switch from color_xy to color_temp") - assert not _attributes_have_changed( + _LOGGER.debug("Test switch from color_xy to color_temp - should detect mode change") + assert _attributes_have_changed( old_attributes={ATTR_BRIGHTNESS: 1, ATTR_XY_COLOR: (0.526, 0.387)}, new_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702}, **kwargs, @@ -2349,3 +2359,102 @@ async def test_simple_switch_state_after_async_added_to_hass(hass): # State should still be correct after async_added_to_hass assert switch_true._state is True assert switch_false._state is False + + +def test_attributes_have_changed_light_mode_switch(): + """Test detection of external light mode changes (color_temp vs rgb vs xy). + + Regression test for https://github.com/basnijholt/adaptive-lighting/issues/1275 + + When a user activates a Hue Scene (or similar) via an external app, the light + may switch from color_temp mode to RGB/XY mode (or vice versa). This should be + detected as an external change so AL doesn't immediately override it. + + The _has_color_mode_changed() function checks the original attributes BEFORE + any conversion, enabling bidirectional mode change detection. + """ + context = Context() + base_kwargs = { + "light": "light.test", + "adapt_brightness": True, + "context": context, + } + + # Test 1: adapt_color=True - all mode changes should be detected + kwargs_adapt_color = {**base_kwargs, "adapt_color": True} + + # color_temp → RGB + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + **kwargs_adapt_color, + ), "Should detect color_temp → RGB mode switch" + + # color_temp → XY + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, + **kwargs_adapt_color, + ), "Should detect color_temp → XY mode switch" + + # RGB → color_temp + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, + **kwargs_adapt_color, + ), "Should detect RGB → color_temp mode switch" + + # RGB → XY + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, + **kwargs_adapt_color, + ), "Should detect RGB → XY mode switch" + + # XY → color_temp + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, + **kwargs_adapt_color, + ), "Should detect XY → color_temp mode switch" + + # XY → RGB + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + **kwargs_adapt_color, + ), "Should detect XY → RGB mode switch" + + # No mode change - same type with same values shouldn't be detected + assert not _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, + **kwargs_adapt_color, + ), "Same color_temp should not be detected as change" + + assert not _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + **kwargs_adapt_color, + ), "Same RGB should not be detected as change" + + assert not _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, + **kwargs_adapt_color, + ), "Same XY should not be detected as change" + + # Test 2: adapt_color=False - mode changes should NOT be detected + kwargs_no_adapt = {**base_kwargs, "adapt_color": False} + + assert not _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + **kwargs_no_adapt, + ), "Mode change should not be detected when adapt_color=False" + + assert not _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, + **kwargs_no_adapt, + ), "RGB → color_temp should not be detected when adapt_color=False" From edefdbf3b8fc4f52f33155234e401b02eb3ac253 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:11:31 -0800 Subject: [PATCH 0898/1077] docs: add Wijt as a contributor for translation (#1300) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index c498e6ce..b43bc4ef 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1089,6 +1089,15 @@ "contributions": [ "code" ] + }, + { + "login": "Wijt", + "name": "Furkan Kaya", + "avatar_url": "https://avatars.githubusercontent.com/u/23127261?v=4", + "profile": "https://iamfurkan.com", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 590056ad..51df41cc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-119-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-120-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -620,6 +620,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + + + From 5274a3fefab1997ba76d71d8729ac4baa5a245ff Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:11:53 -0800 Subject: [PATCH 0899/1077] docs: add Rafael4A as a contributor for translation (#1301) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index b43bc4ef..b91bdf28 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1098,6 +1098,15 @@ "contributions": [ "translation" ] + }, + { + "login": "Rafael4A", + "name": "Rafael do Amaral Porciuncula", + "avatar_url": "https://avatars.githubusercontent.com/u/32150173?v=4", + "profile": "https://github.com/Rafael4A", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 51df41cc..366f3f4c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-120-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-121-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -622,6 +622,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From c37e992be98d39113759969576cf9b1ca1003b86 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:12:38 -0800 Subject: [PATCH 0900/1077] docs: add hhjuhl as a contributor for translation (#1302) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index b91bdf28..44ac8c43 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1107,6 +1107,15 @@ "contributions": [ "translation" ] + }, + { + "login": "hhjuhl", + "name": "hhjuhl", + "avatar_url": "https://avatars.githubusercontent.com/u/84127693?v=4", + "profile": "https://github.com/hhjuhl", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 366f3f4c..4e515f2d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-121-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-122-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -623,6 +623,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From ce16be20b59d210d823512acd83cf4698c8f9f7d Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:13:01 -0800 Subject: [PATCH 0901/1077] docs: add Athishbalu as a contributor for translation (#1303) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 44ac8c43..ec6f3158 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1116,6 +1116,15 @@ "contributions": [ "translation" ] + }, + { + "login": "Athishbalu", + "name": "B.Athish", + "avatar_url": "https://avatars.githubusercontent.com/u/177029556?v=4", + "profile": "https://github.com/Athishbalu", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 4e515f2d..aa2c2338 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-122-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-123-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -624,6 +624,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 0ddb79b03fb54c493c4ac172ec3b7e18f50e0a76 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:13:26 -0800 Subject: [PATCH 0902/1077] docs: add maksim2005UKR as a contributor for translation (#1304) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index ec6f3158..a7ce44dd 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1125,6 +1125,15 @@ "contributions": [ "translation" ] + }, + { + "login": "maksim2005UKR", + "name": "Горпиніч Максим Олександрович", + "avatar_url": "https://avatars.githubusercontent.com/u/233082001?v=4", + "profile": "https://github.com/maksim2005UKR", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index aa2c2338..24e0d58c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-123-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-124-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -625,6 +625,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From d12394dc03b3ed3b409ec0e789e421aad97ba6a2 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:13:53 -0800 Subject: [PATCH 0903/1077] docs: add plageoj as a contributor for translation (#1305) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index a7ce44dd..c8f2f8dd 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1134,6 +1134,15 @@ "contributions": [ "translation" ] + }, + { + "login": "plageoj", + "name": "Masayuki Sugahara", + "avatar_url": "https://avatars.githubusercontent.com/u/10688301?v=4", + "profile": "https://plageoj.me", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 24e0d58c..2f1e85e4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-124-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-125-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -626,6 +626,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From d7aa3439d506f96f218e933cd8287b153b41752c Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:14:16 -0800 Subject: [PATCH 0904/1077] docs: add therealmate as a contributor for translation (#1306) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index c8f2f8dd..d999de5a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1143,6 +1143,15 @@ "contributions": [ "translation" ] + }, + { + "login": "therealmate", + "name": "therealmate", + "avatar_url": "https://avatars.githubusercontent.com/u/61843503?v=4", + "profile": "https://github.com/therealmate", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 2f1e85e4..faf8c51a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-125-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-126-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -627,6 +627,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 8ff3babae25c1bbc04404f2938347092dcda5482 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 27 Nov 2025 19:15:06 +0100 Subject: [PATCH 0905/1077] Translations update from Hosted Weblate (#1228) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Galician) Currently translated at 50.3% (77 of 153 strings) Translated using Weblate (Galician) Currently translated at 49.0% (75 of 153 strings) Translated using Weblate (Galician) Currently translated at 46.4% (71 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Yago Raña Gayoso Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/gl/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Turkish) Currently translated at 100.0% (153 of 153 strings) Added translation using Weblate (Turkish) Co-authored-by: Furkan Kaya Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/tr/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Portuguese (Brazil)) Currently translated at 56.8% (87 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Rafael do Amaral Porciuncula Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pt_BR/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Danish) Currently translated at 90.1% (138 of 153 strings) Translated using Weblate (Danish) Currently translated at 88.8% (136 of 153 strings) Translated using Weblate (Danish) Currently translated at 88.8% (136 of 153 strings) Translated using Weblate (Danish) Currently translated at 88.8% (136 of 153 strings) Co-authored-by: Emil Friis Osmann Co-authored-by: Hans Henrik Juhl Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/da/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Russian) Currently translated at 99.3% (152 of 153 strings) Co-authored-by: Athish Athish Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ru/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Ukrainian) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: Максим Горпиніч Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/uk/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Japanese) Currently translated at 48.3% (74 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: M.Sugahara Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ja/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Hungarian) Currently translated at 100.0% (153 of 153 strings) Co-authored-by: Hosted Weblate Co-authored-by: therealmate Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/hu/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Slovenian) Currently translated at 71.2% (109 of 153 strings) Added translation using Weblate (Slovenian) Co-authored-by: Hosted Weblate Co-authored-by: Tim Music Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/sl/ Translation: Adaptive Lighting/Adaptive Lighting --------- Co-authored-by: Yago Raña Gayoso Co-authored-by: Furkan Kaya Co-authored-by: Rafael do Amaral Porciuncula Co-authored-by: Emil Friis Osmann Co-authored-by: Hans Henrik Juhl Co-authored-by: Athish Athish Co-authored-by: Максим Горпиніч Co-authored-by: M.Sugahara Co-authored-by: therealmate Co-authored-by: Tim Music --- .../adaptive_lighting/translations/da.json | 23 +- .../adaptive_lighting/translations/gl.json | 15 +- .../adaptive_lighting/translations/hu.json | 2 +- .../adaptive_lighting/translations/ja.json | 6 +- .../adaptive_lighting/translations/pt-BR.json | 6 +- .../adaptive_lighting/translations/ru.json | 2 +- .../adaptive_lighting/translations/sl.json | 75 +++++++ .../adaptive_lighting/translations/tr.json | 202 ++++++++++++++++++ .../adaptive_lighting/translations/uk.json | 75 ++++++- 9 files changed, 394 insertions(+), 12 deletions(-) create mode 100644 custom_components/adaptive_lighting/translations/sl.json create mode 100644 custom_components/adaptive_lighting/translations/tr.json diff --git a/custom_components/adaptive_lighting/translations/da.json b/custom_components/adaptive_lighting/translations/da.json index f0ff8742..347c3f5c 100644 --- a/custom_components/adaptive_lighting/translations/da.json +++ b/custom_components/adaptive_lighting/translations/da.json @@ -40,7 +40,8 @@ "detect_non_ha_changes": "detect_non_ha_changes: Registrer alle ændringer på >10% på et lys (også udenfor HA), kræver at 'take_over_control' er slået til (kalder 'homeassistant.update_entity' hvert 'interval'!)", "transition": "Overgangsperiode når en ændring i lyset udføres (i sekunder)", "transition_until_sleep": "overgang_til_sove: Når aktiveret, vil adaptiv belysning behandle søvnindstillinger som minimum, og overgår til disse værdier efter solnedgang. 🌙", - "adapt_only_on_bare_turn_on": "tilpas_kun_ved_enkelt_tænd: Når du tænder lys for første gang. Hvis indstillet til 'true', tilpasser AL kun, hvis 'lys.tænd' er kaldt uden at angive farve eller lysstyrke. ❌🌈 Dette forhindrer f.eks. tilpasning, når du aktiverer en scene. Hvis indstillet til 'false' tilpasser AL sig uanset tilstanden af farve eller lysstyrke i den oprindelige 'service_data'. Har brug for at 'take_over_control' er aktiveret. 🕵️ " + "adapt_only_on_bare_turn_on": "tilpas_kun_ved_enkelt_tænd: Når du tænder lys for første gang. Hvis indstillet til 'true', tilpasser AL kun, hvis 'lys.tænd' er kaldt uden at angive farve eller lysstyrke. ❌🌈 Dette forhindrer f.eks. tilpasning, når du aktiverer en scene. Hvis indstillet til 'false' tilpasser AL sig uanset tilstanden af farve eller lysstyrke i den oprindelige 'service_data'. Har brug for at 'take_over_control' er aktiveret. 🕵️", + "include_config_in_attributes": "include_config_in_attributes: Vis alle indstillinger som attributter for kontakten når dette er sat til »true«. 📝" }, "data_description": { "interval": "Frekvens til at tilpasse lysene, i sekunder. 🔄", @@ -59,7 +60,9 @@ "sunrise_offset": "Juster solopgangstiden med en positiv eller negativ offset på få sekunder. ⏰", "max_sunset_time": "Indstil den seneste virtuelle solnedgangstid (HH:MM:SS), hvilket giver mulighed for tidligere solnedgange. 🌇", "sleep_color_temp": "Farvetemperatur i søvntilstand (bruges når `sleep_rgb_or_color_temp` er `color_temp`) i Kelvin. 😴", - "brightness_mode": "Lysstyrketilstand til brug. Mulige værdier er \"default\", \"linear\" og \"tanh\" (bruger \"brightness_mode_time_dark\" og \"brightness_mode_time_light\"). 📈" + "brightness_mode": "Lysstyrketilstand til brug. Mulige værdier er \"default\", \"linear\" og \"tanh\" (bruger \"brightness_mode_time_dark\" og \"brightness_mode_time_light\"). 📈", + "send_split_delay": "Forsinkelse (ms) mellem »separate_turn_on_commands« for lyskilder som ikke understøtter simultane styrke- og farveindstillinger. ⏲️", + "initial_transition": "Den første overgangs varighed når lysene ændres fra »off« til »on« i sekunder. ⏲️" } } }, @@ -153,12 +156,26 @@ }, "sleep_color_temp": { "description": "Farvetemperatur i søvntilstand (bruges når `sleep_rgb_or_color_temp` er `color_temp`) i Kelvin. 😴" + }, + "send_split_delay": { + "description": "Forsinkelse (ms) mellem »separate_turn_on_commands« for lyskilder som ikke understøtter simultane styrke- og farveindstillinger. ⏲️" + }, + "detect_non_ha_changes": { + "description": "Opdager og stopper tilpasningen ved tilstandsændringer, som ikke er udløst af »light.turn_on«. Indstillingen »take_over_control« skal være aktiveret. 🕵️ Advarsel: ⚠️ Nogle lyskilder kan rapportere en falsk tændt-tilstand, hvilket kan medføre af lyskilden tændes når det ikke er meningen. Slå denne funktion fra, hvis du oplever dette problem." + }, + "initial_transition": { + "description": "Den første overgangs varighed når lysene ændres fra »off« til »on« i sekunder. ⏲️" } }, "description": "Skift de indstillinger du ønsker i kontakten. Alle muligheder her er de samme som i konfigurationsflowet." }, "set_manual_control": { - "description": "Markér om et lys er 'manuelt kontrolleret'." + "description": "Markér om et lys er 'manuelt kontrolleret'.", + "fields": { + "lights": { + "description": "entity_id(er) af lys, hvis ikke specificeret, vil alle lys i kontakten være valgt. 💡" + } + } } } } diff --git a/custom_components/adaptive_lighting/translations/gl.json b/custom_components/adaptive_lighting/translations/gl.json index 2752f95c..5a1f211c 100644 --- a/custom_components/adaptive_lighting/translations/gl.json +++ b/custom_components/adaptive_lighting/translations/gl.json @@ -3,7 +3,11 @@ "step": { "init": { "data_description": { - "sleep_brightness": "Porcentaxe de brillo das luces en modo durmir. 😴" + "sleep_brightness": "Porcentaxe de brillo das luces en modo durmir. 😴", + "send_split_delay": "Retraso (ms) entre `separate_turn_on_commands`", + "sunrise_offset": "Axusta a hora do amencer cun desfasamento positivo ou negativo en segundos. ⏰", + "sunset_offset": "Axusta a hora da posta de sol cun desfasamento positivo ou negativo en segundos. ⏰", + "interval": "Frecuencia para adaptar as luces, en segundos. 🔄" }, "title": "Configuración de Iluminación Adaptativa" } @@ -18,6 +22,15 @@ }, "only_once": { "description": "Adaptar luces só cando estean acesas (`true`) ou mantelas adaptándose (`false`). 🔄" + }, + "sunrise_offset": { + "description": "Axusta a hora do amencer cun desfasamento positivo ou negativo en segundos. ⏰" + }, + "sunset_offset": { + "description": "Axusta a hora da posta de sol cun desfasamento positivo ou negativo en segundos. ⏰" + }, + "transition": { + "description": "Duración da transición cando as luces cambian, en segundos. 🕑" } } } diff --git a/custom_components/adaptive_lighting/translations/hu.json b/custom_components/adaptive_lighting/translations/hu.json index 3af17eff..d349699e 100644 --- a/custom_components/adaptive_lighting/translations/hu.json +++ b/custom_components/adaptive_lighting/translations/hu.json @@ -30,7 +30,7 @@ "max_brightness": "max_brightness: Maximális fényerő százalékban megadva. 💡", "detect_non_ha_changes": "detect_non_ha_changes: `Világítás: Bekapcsolás`- szolgáltatás meghívástól eltérő állapotváltozások esetén észleli és leállítja az illesztéseket. A `take_over_control` beállítás engedélyezése szükséges. 🕵️ Vigyázat: ⚠️ Egyes lights tévesen jelezhetik a \"bekapcsolt\" állapotot, ami váratlanul bekapcsolódó lámpákhoz vezethet. Ha ilyen problémákat tapasztal, tiltsa le ezt a funkciót.", "multi_light_intercept": "multi_light_intercept: `Világítás: Bekapcsolás` szolgáltatás hívások elfogása és adaptálása, amelyek több fényt céloznak meg. ➗⚠️ Ez azt eredményezheti, hogy egyetlen `Világítás: Bekapcsolás` szolgáltatás hívás több hívásra oszlik fel, pl. ha a lights különböző kapcsolókban vannak. Az `elfogás` engedélyezése szükséges.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Kizárólag a bekapcsoláskor érvényes. A beállítást \"igaz\"-ra állítva, az AL csak akkor végzi az illesztést, amennyiben a \"Világítás: Bekapcsolás\" szolgáltatás meghívása a szín és fényerő paraméterek megadása nélkül történik.❌🌈 Ez pl. alkalmas az illesztés felfüggesztésére egy jelenet aktiválásakor. \"Hamis\" beállítás esetén az AL elvégzi a kezdeti illesztést a szín és fényerő paraméterek meghívásától függetlenül. A használatához engedélyezve kell lennie a \"take_over_control\" beállításnak. 🕵️ ", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Kizárólag a bekapcsoláskor érvényes. A beállítást \"igaz\"-ra állítva, az AL csak akkor végzi az illesztést, amennyiben a \"Világítás: Bekapcsolás\" szolgáltatás meghívása a szín és fényerő paraméterek megadása nélkül történik.❌🌈 Ez pl. alkalmas az illesztés felfüggesztésére egy jelenet aktiválásakor. \"Hamis\" beállítás esetén az AL elvégzi a kezdeti illesztést a szín és fényerő paraméterek meghívásától függetlenül. A használatához engedélyezve kell lennie a \"take_over_control\" beállításnak. 🕵️", "skip_redundant_commands": "skip_redundant_commands: Az olyan adaptációs parancsok küldésének kihagyása, amelyek célállapota már megegyezik a fény ismert állapotával. Minimalizálja a hálózati forgalmat, és bizonyos helyzetekben javítja az adaptációs reakciókészséget. 📉Kapcsolja ki, ha a fizikai fényállapotok nem szinkronizálódnak a HA rögzített állapotával.", "separate_turn_on_commands": "separate_turn_on_commands: Elkülönített `Világítás: Bekapcsolás` hívásokat használ a szín és a fényerő beállításához, ami néhány világítás típusnál szükséges. 🔀", "max_color_temp": "max_color_temp: A leghidegebb színhőmérséklet kelvinben. ❄️", diff --git a/custom_components/adaptive_lighting/translations/ja.json b/custom_components/adaptive_lighting/translations/ja.json index 065d8b3b..38f13d81 100644 --- a/custom_components/adaptive_lighting/translations/ja.json +++ b/custom_components/adaptive_lighting/translations/ja.json @@ -1,5 +1,5 @@ { - "title": "適応型照明", + "title": "明るさの自動調整", "services": { "change_switch_settings": { "fields": { @@ -7,7 +7,7 @@ "description": "日の出時間を基準に秒単位で正値もしくは負値で調整する。⏰" }, "only_once": { - "description": "適応型照明を照明がオンになっているときのみ(`true`)それとも適応し続ける場合は(`false`)。" + "description": "一度だけ明るさを自動調整するには(`true`)、常に自動調整し続ける場合は(`false`)。" }, "sunset_offset": { "description": "日の入時間を基準に秒単位で正値もしくは負値で調整する。⏰" @@ -35,7 +35,7 @@ "sunrise_offset": "日の出時間を基準に秒単位で正値もしくは負値で調整する。⏰", "sunset_offset": "日の入時間を基準に秒単位で正値もしくは負値で調整する。⏰" }, - "title": "適応型照明オプション" + "title": "明るさの自動調整オプション" } } } diff --git a/custom_components/adaptive_lighting/translations/pt-BR.json b/custom_components/adaptive_lighting/translations/pt-BR.json index 43eacb01..c4fc796f 100644 --- a/custom_components/adaptive_lighting/translations/pt-BR.json +++ b/custom_components/adaptive_lighting/translations/pt-BR.json @@ -39,7 +39,11 @@ "sunset_time": "sunset_time: substituição manual do horário do pôr do sol, se 'Nenhum', ele usa o horário real do nascer do sol em sua localização (HH:MM:SS)", "take_over_control": "take_over_control: Se qualquer coisa, exceto Adaptive Lighting, chamar 'light.turn_on' quando uma luz já estiver acesa, pare de adaptar essa luz até que ela (ou o interruptor) desligue -> ligue.", "detect_non_ha_changes": "detect_non_ha_changes: detecta todas as alterações > 10% feitas nas luzes (também fora do HA), requer que 'take_over_control' seja ativado (chama 'homeassistant.update_entity' a cada 'intervalo'!)", - "transition": "Tempo de transição ao aplicar uma mudança nas luzes (segundos)" + "transition": "Tempo de transição ao aplicar uma mudança nas luzes (segundos)", + "skip_redundant_commands": "skip_redundant_commands: Deixar de enviar comandos de adaptação cujo estado alvo já seja igual ao estado atual da luz. Minimiza o tráfego de rede e melhora a responsividade da adaptação em algumas situações. 📉Desative se os estados físicos das luzes podem ficar diferentes do estado registrado no HA.", + "transition_until_sleep": "transition_until_sleep: Quando ativada, a Iluminação Adaptativa considerará as configurações de sono como o valor mínimo, transicionando para esses valores após o pôr do sol. 🌙", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Ao ligar as luzes inicialmente. Se definido como `true`, a Iluminação Adaptativa se adapta somente se o comando `light.turn_on` for chamado sem especificar cor ou brilho. ❌🌈 Isso, por exemplo, impede a adaptação ao ativar uma cena. Se false, a Iluminação Adaptativa se adapta independentemente da presença de cor ou brilho nos dados iniciais do `service_data`. Precisa de `take_over_control` ativado. 🕵️", + "intercept": "intercept: Interceptar e adaptar os chamados `light.turn_on` para ativar a adaptação instantânea de cor e brilho. 🏎️ Desative para luzes que não suportam `light.turn_on` com cor e brilho." } } }, diff --git a/custom_components/adaptive_lighting/translations/ru.json b/custom_components/adaptive_lighting/translations/ru.json index fd4a4b55..79f3cd02 100644 --- a/custom_components/adaptive_lighting/translations/ru.json +++ b/custom_components/adaptive_lighting/translations/ru.json @@ -42,7 +42,7 @@ "transition": "Время перехода при применении изменения к источникам света. (секунды)", "adapt_delay": "Время ожидания между включением света и применением адаптации. Может помочь избежать мерцания. (секунды)", "multi_light_intercept": "multi_light_intercept: перехватывает и адаптирует вызовы `light.turn_on`, нацеленные на несколько источников света. ➗⚠️ Это может привести к разделению одного вызова `light.turn_on` на несколько вызовов, например, когда освещение включено в разные выключатели. Требуется, чтобы `перехват` был включен.", - "adapt_only_on_bare_turn_on": "Adapt_only_on_bare_turn_on: При первоначальном включении света. Если установлено значение «true», AL адаптируется только в том случае, если «light.turn_on» вызывается без указания цвета или яркости. ❌🌈 Это, например, предотвращает адаптацию при активации сцены. Если false, AL адаптируется независимо от наличия цвета или яркости в исходных service_data. Требуется включить take_over_control. 🕵️ ", + "adapt_only_on_bare_turn_on": "Adapt_only_on_bare_turn_on: При первоначальном включении света. Если установлено значение «true», AL адаптируется только в том случае, если «light.turn_on» вызывается без указания цвета или яркости. ❌🌈 Это, например, предотвращает адаптацию при активации сцены. Если false, AL адаптируется независимо от наличия цвета или яркости в исходных service_data. Требуется включить take_over_control. 🕵️", "skip_redundant_commands": "Skip_redundant_commands: Пропустить отправку команд адаптации, целевое состояние которых уже равно известному состоянию источника света. Минимизирует сетевой трафик и улучшает скорость адаптации в некоторых ситуациях. 📉Отключите, если физические состояния освещения не синхронизируются с записанным состоянием HA.", "intercept": "intercept: перехватывать и адаптировать вызовы `light.turn_on` для обеспечения мгновенной адаптации цвета и яркости. 🏎️ Отключите источники света, которые не поддерживают `light.turn_on` с цветом и яркостью.", "include_config_in_attributes": "include_config_in_attributes: отображать все параметры в качестве атрибутов на переключателе в Home Assistant, если установлено значение `true`. 📝", diff --git a/custom_components/adaptive_lighting/translations/sl.json b/custom_components/adaptive_lighting/translations/sl.json new file mode 100644 index 00000000..dc78acaa --- /dev/null +++ b/custom_components/adaptive_lighting/translations/sl.json @@ -0,0 +1,75 @@ +{ + "options": { + "step": { + "init": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: Ali v primeru možnosti raje uporabiti prilagoditev RGB barve kot barvno temperaturo luči. 🌈", + "transition_until_sleep": "transition_until_sleep: Če je omogočeno, bo Adaptive Lighting obravnaval nastavitve spanja kot minimalne vrednosti in bo po zahodu sonca prehajal na te vrednosti. 🌙", + "take_over_control": "take_over_control: Onemogoči Adaptive Lighting, če drug vir pokliče \"light.turn_on\", ko so luči prižgane in se prilagajajo. Opozorilo: to ob vsakem intervalu kliče \"homeassistant.update_entity\"! 🔒", + "detect_non_ha_changes": "„detect_non_ha_changes: Zazna in ustavi prilagoditve za spremembe stanja, ki niso posledica \"light.turn_on\". Zahteva omogočeno \"take_over_control\". 🕵️ Pozor: ⚠️ Nekatere luči lahko nepravilno poročajo, da so prižgane, kar lahko povzroči nepričakovano vklapljanje. Onemogočite to funkcijo, če naletite na takšne težave.", + "lights": "lights: Seznam entity_id-jev luči za nadzor (lahko je prazen). 🌟", + "min_brightness": "min_brightness: Odstotek najmanjše svetlosti. 💡", + "max_brightness": "max_brightness: Odstotek največeje svetlosti. 💡", + "min_color_temp": "min_color_temp: Najtoplejša barvna temperatura v Kelvinih. 🔥", + "max_color_temp": "max_color_temp: Najhladnejša barvna temperatura v kelvinih. ❄️", + "separate_turn_on_commands": "separate_turn_on_commands: Uporabi ločene klice \"light.turn_on\" za barvo in jakost, kar je potrebno za nekatere tipe luči. 🔀", + "skip_redundant_commands": "skip_redundant_commands: Preskoči pošiljanje prilagoditvenih ukazov, če je ciljano stanje že enako poznanemu stanju luči. Zmanjšuje omrežni promet in izboljšuje odzivnost prilagajanja v določenih situacijah. 📉 Onemogočite, če se fizična stanja luči ne ujemajo z zabeleženim stanjem v HA.", + "intercept": "intercept: Prestreza in prilagaja klice \"light.turn_on\" za takojšnjo prilagoditev barve in jakosti. 🏎️ Onemogočite za luči, ki ne podpirajo \"light.turn_on\" z barvo in svetlostjo.", + "multi_light_intercept": "multi_light_intercept: Prestreza in prilagaja klice \"light.turn_on\", ki ciljajo več luči. ➗⚠️ To lahko privede do razdelitve enega klica \"light.turn_on\" v več klicev, npr. ko so luči na različnih stikalih. Zahteva omogočeno \"intercept\".", + "include_config_in_attributes": "include_config_in_attributes: Ko je nastavljeno na \"true\", prikaže vse možnosti kot atribute stikala v Home Assistantu. 📝", + "only_once": "only_once: Prilagodi luči samo ob vklopu (true) ali pa jih še naprej prilagajaj (false). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Ob začetnem vklopu luči. Če je nastavljeno na \"true\", AL prilagodi samo, če je \"light.turn_on\" klic brez podanih parametrov barve ali jakosti. ❌🌈 S tem se npr. prepreči prilagajanje pri aktivaciji scene. Če je \"false\", AL prilagodi ne glede na prisotnost barve ali jakosti v začetnih \"service_data\". Zahteva omogočeno \"take_over_control\". 🕵️" + }, + "data_description": { + "sunrise_offset": "Prilagodite čas sončnega vzhoda z pozitivnim ali negativnim zamikom v sekundah. ⏰", + "send_split_delay": "Zamik (ms) med \"separate_turn_on_commands\" za luči, ki ne podpirajo hkratne nastavitve jakosti in barve. ⏲️", + "transition": "Trajanje prehoda pri spreminjanju luči, v sekundah. 🕑", + "interval": "Pogostost prilagajanja luči, v sekundah. 🔄", + "sleep_brightness": "Odstotek svetlosti luči v načinu spanja. 😴", + "sleep_rgb_or_color_temp": "V načinu spanja uporabi \"rgb_color\" ali \"color_temp\". 🌙", + "sleep_color_temp": "Barvna temperatura v načinu spanja (uporabljena, ko je \"sleep_rgb_or_color_temp\" nastavljena na \"color_temp\") v Kelvinih. 😴", + "sleep_transition": "Trajanje prehoda, ko se preklopi način spanja, v sekundah. 😴", + "sunrise_time": "Nastavi fiksni čas (HH:MM:SS) sončnega vzhoda. 🌅", + "min_sunrise_time": "Nastavi najzgodnejši navidezni sončni vzhod (HH:MM:SS), dovoljuje kasnejše vzhode. 🌅", + "sunset_time": "Nastavite fiksni čas (HH:MM:SS) za sončni zahod. 🌇", + "min_sunset_time": "Nastavite najzgodnejši navidezni čas sončnega zahoda (HH:MM:SS), dovoljuje kasnejše sončne zahode. 🌇", + "sunset_offset": "Prilagodite čas sončnega zahoda s pozitivnim ali negativnim zamikom v sekundah. ⏰", + "brightness_mode_time_dark": "(Prezrto, če je \"brightness_mode='default'\") Trajanje v sekundah za postopno povečanje ali zmanjšanje svetlosti pred/po sončnem vzhodu/zahodu. 📈📉", + "brightness_mode_time_light": "(Prezrto, če je \"brightness_mode='default'\") Trajanje v sekundah za postopno povečanje ali zmanjšanje svetlosti po/pred sončnem vzhodu/zahodu. 📈📉", + "autoreset_control_seconds": "Samodejno ponastavi ročni nadzor po določenem številu sekund. Nastavite na 0, da onemogočite. ⏲️", + "max_sunrise_time": "Nastavi najkasnejši virtualni sončni vzhod (HH:MM:SS), dovoljuje zgodnejše vzhode. 🌅", + "max_sunset_time": "Nastavite najpoznejši navidezni čas sončnega zahoda (HH:MM:SS), dovoljuje zgodnejše sončne zahode. 🌇", + "brightness_mode": "Način upravljanja svetlosti. Možne vrednosti so \"default\", \"linear\" in \"tanh\" (uporablja \"brightness_mode_time_dark\" in \"brightness_mode_time_light\"). 📈", + "initial_transition": "Trajanje prvega prehoda, ko se luči prižgejo (iz \"off\" v \"on\"), v sekundah. ⏲️", + "sleep_rgb_color": "RGB barva v načinu spanja (uporabljena, ko je \"sleep_rgb_or_color_temp\" nastavljeno na \"rgb_color\"). 🌈" + }, + "title": "Nastavitve prilagodljive osvetlitve", + "description": "Konfigurirajte komponento Adaptive Lighting. Imena možnosti so usklajena z nastavitvami v YAML. Če ste ta vnos definirali v YAML, tukaj ne bodo prikazane nobene možnosti. Za interaktivne grafe, ki ponazarjajo učinke parametrov, obiščite to [spletno aplikacijo](https://basnijholt.github.io/adaptive-lighting). Za dodatne podrobnosti glejte [uradno dokumentacijo](https://github.com/basnijholt/adaptive-lighting#readme)." + } + } + }, + "config": { + "step": { + "user": { + "title": "Izberite ime za instanco Adaptive Lighting", + "description": "Vsaka instanca lahko vsebuje več luči!" + } + }, + "abort": { + "already_configured": "Naprava je že konfigurirana" + } + }, + "services": { + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Prilagajaj luči samo kadar so prižgane (\"true\") ali konstantno jih prilagajaj (\"false\")" + }, + "max_sunrise_time": { + "description": "Nastavite najpoznejši navidezni čas sončnega vzhoda (HH:MM:SS), dovoljuje zgodnejše sončne vzhode. 🌅" + } + } + } + }, + "title": "Prilagodljiva osvetlitev" +} diff --git a/custom_components/adaptive_lighting/translations/tr.json b/custom_components/adaptive_lighting/translations/tr.json new file mode 100644 index 00000000..3b5c2d6e --- /dev/null +++ b/custom_components/adaptive_lighting/translations/tr.json @@ -0,0 +1,202 @@ +{ + "title": "Akıllı Aydınlatma", + "options": { + "step": { + "init": { + "title": "Akıllı Aydınlatma seçenekleri", + "data": { + "adapt_only_on_bare_turn_on": "Işıklar açıldığında geçerlidir. `true` olarak ayarlanırsa, eklenti yalnızca `light.turn_on` işlemi renk veya parlaklık belirtilmeden çağrıldığında uyarlama yapar (örneğin sahne etkinleştirmelerinde uyarlama yapılmaz). ❌🌈\n`false` olarak ayarlanırsa, renk veya parlaklık belirtilmiş olsa bile uyarlama yapılır. Bu ayarın çalışması için `take_over_control` etkin olmalıdır. 🕵️", + "detect_non_ha_changes": "`detect_non_ha_changes`: `light.turn_on` dışındaki durum değişikliklerini algılar ve uyarlamayı durdurur. Bu ayarın çalışması için `take_over_control` etkin olmalıdır. 🕵️\nDikkat: ⚠️ Bazı ışıklar yanlışlıkla “açık” durumunu bildirebilir, bu da ışıkların beklenmedik şekilde açılmasına yol açabilir. Böyle bir durumla karşılaşırsanız bu özelliği devre dışı bırakın.", + "include_config_in_attributes": "`include_config_in_attributes`: `true` olarak ayarlandığında, tüm seçenekleri Home Assistant’ta anahtarın attribute’ları olarak gösterir. 📝", + "intercept": "`intercept`: `light.turn_on` çağrılarını yakalar ve renk ile parlaklığın anında uyarlanmasını sağlar. 🏎️ Renk ve parlaklığı desteklemeyen ışıklar için devre dışı bırakın.", + "lights": "`lights`: Kontrol edilecek ışıkların entity_id listesi (boş bırakılabilir). 🌟", + "max_brightness": "`max_brightness`: Maksimum parlaklık yüzdesi. 💡", + "max_color_temp": "`max_color_temp`: En düşük renk sıcaklığı (Kelvin cinsinden). ❄️", + "min_brightness": "min_brightness: Minimum parlaklık yüzdesi.💡", + "min_color_temp": "`min_color_temp`: En yüksek (sıcak) renk sıcaklığı (Kelvin cinsinden). 🔥", + "multi_light_intercept": "`multi_light_intercept`: Birden fazla ışığı hedefleyen `light.turn_on` çağrılarını yakalar ve uyarlama yapar. ➗⚠️ Bu, örneğin ışıklar farklı anahtarlardaysa tek bir `light.turn_on` çağrısının birden fazla çağrıya bölünmesine yol açabilir. `intercept` etkin olmalıdır.", + "only_once": "`only_once`: Işıkları yalnızca açıldıklarında mı uyarlasın (`true`), yoksa sürekli uyarlamaya devam mı etsin (`false`). 🔄", + "prefer_rgb_color": "`prefer_rgb_color`: Mümkünse ışık renk sıcaklığı yerine RGB renk ayarını tercih edip etmeyeceğini belirler. 🌈", + "separate_turn_on_commands": "`separate_turn_on_commands`: Renk ve parlaklık için ayrı `light.turn_on` çağrıları kullanır; bazı ışık türleri için gereklidir. 🔀", + "skip_redundant_commands": "`skip_redundant_commands`: Hedef durumu ışığın bilinen durumu ile aynı olan uyarlama komutlarını atlar. Ağ trafiğini azaltır ve bazı durumlarda uyarlamanın yanıt hızını artırır. 📉 \nFiziksel ışık durumları HA’daki kaydedilen durumla senkronize değilse devre dışı bırakın.", + "take_over_control": "`take_over_control`: Işıklar açıkken ve uyarlanırken başka bir kaynaktan `light.turn_on` çağrılırsa Adaptive Lighting’i devre dışı bırakır. Dikkat: Bu işlem her `interval` süresinde `homeassistant.update_entity` çağrısı yapar! 🔒", + "transition_until_sleep": "`transition_until_sleep`: Etkinleştirildiğinde, Adaptive Lighting uyku ayarlarını minimum değer olarak kabul eder ve gün batımından sonra bu değerlere geçiş yapar. 🌙" + }, + "data_description": { + "sunrise_offset": "Gün doğumu saatini, saniye cinsinden pozitif veya negatif bir kaydırma ile ayarlayın. ⏰", + "sunset_offset": "Gün batımı saatini, saniye cinsinden pozitif veya negatif bir kaydırma ile ayarlayın. ⏰", + "autoreset_control_seconds": "Manuel kontrolü belirtilen saniye sonunda otomatik olarak sıfırlar. Devre dışı bırakmak için 0 olarak ayarlayın. ⏲️", + "brightness_mode": "Kullanılacak parlaklık modunu belirtir. Olası değerler: `default`, `linear` ve `tanh` (`brightness_mode_time_dark` ve `brightness_mode_time_light` ayarlarını kullanır). 📈", + "sleep_brightness": "Uyku modundayken ışıkların parlaklık yüzdesi 😴", + "sleep_color_temp": "Uyku modunda renk sıcaklığı ( `sleep_rgb_or_color_temp` `color_temp` olarak ayarlandığında kullanılır) Kelvin cinsinden. 😴", + "send_split_delay": "Parlaklık ve renk ayarını aynı anda desteklemeyen ışıklar için `separate_turn_on_commands` arasındaki gecikme (ms). ⏲️", + "initial_transition": "Işıklar `off` durumundan `on` durumuna geçerken ilk geçişin süresi (saniye cinsinden). ⏲️", + "transition": "Işıklar değişirken geçiş süresi (saniye cinsinden). 🕑", + "sleep_transition": "“Uyku modu” açılıp kapatıldığında geçiş süresi (saniye cinsinden). 😴", + "interval": "Işıkların uyarlanma sıklığı (saniye cinsinden). 🔄", + "brightness_mode_time_light": "(`brightness_mode='default'` ise göz ardı edilir) Gün doğumu/gün batımı öncesi/sonrası parlaklığı kademeli olarak artırma/azaltma süresi (saniye cinsinden). 📈📉", + "brightness_mode_time_dark": "(`brightness_mode='default'` ise göz ardı edilir) Gün doğumu/gün batımı öncesi/sonrası parlaklığı kademeli olarak artırma/azaltma süresi (saniye cinsinden). 📈📉", + "sleep_rgb_color": "Uyku modunda RGB renk ( `sleep_rgb_or_color_temp` \"rgb_color\" olarak ayarlandığında kullanılır). 🌈", + "sunrise_time": "Gün doğumu için sabit bir saat (SS:DD:YY) belirleyin. 🌅", + "sunset_time": "Gün batımı için sabit bir saat (SS:DD:YY) belirleyin. 🌇", + "min_sunrise_time": "En erken sanal gün doğumu saatini (SS:DD:YY) belirleyin; daha geç gün doğumlarına izin verir. 🌅", + "min_sunset_time": "En erken sanal gün batımı saatini (SS:DD:YY) belirleyin; daha geç gün batımlarına izin verir. 🌇", + "max_sunrise_time": "En geç sanal gün doğumu saatini (SS:DD:YY) belirleyin; daha erken gün doğumlarına izin verir. 🌅", + "max_sunset_time": "En geç sanal gün batımı saatini (SS:DD:YY) belirleyin; daha erken gün batımlarına izin verir. 🌇", + "sleep_rgb_or_color_temp": "Uyku modunda `\"rgb_color\"` veya `\"color_temp\"` kullanın. 🌙", + "adapt_delay": "Işık açıldıktan sonra Adaptive Lighting’in değişiklikleri uygulamasına kadar bekleme süresi (saniye cinsinden). Titremeyi önlemeye yardımcı olabilir. ⏲️" + }, + "description": "Bir Adaptive Lighting bileşenini yapılandırın. Seçenek adları YAML ayarlarıyla uyumludur. Eğer bu girdiyi YAML’da tanımladıysanız, burada seçenekler görünmez. \nParametrelerin etkilerini gösteren etkileşimli grafikler için [bu web uygulamasını](https://basnijholt.github.io/adaptive-lighting) ziyaret edebilirsiniz. Daha fazla bilgi için [resmi dokümantasyona](https://github.com/basnijholt/adaptive-lighting#readme) bakın." + } + }, + "error": { + "option_error": "Geçersiz seçenek", + "entity_missing": "Seçilen bir veya birden fazla ışık entity’si Home Assistant’ta bulunamadı." + } + }, + "services": { + "change_switch_settings": { + "fields": { + "only_once": { + "description": "Işıkları yalnızca açıkken (`true`) uyarla, veya sürekli olarak uyarlamaya devam et (`false`).🔄" + }, + "sunrise_offset": { + "description": "Gün doğumu saatini, saniye cinsinden pozitif veya negatif bir kaydırma ile ayarlayın. ⏰" + }, + "sunset_offset": { + "description": "Gün batımı saatini, saniye cinsinden pozitif veya negatif bir kaydırma ile ayarlayın. ⏰" + }, + "autoreset_control_seconds": { + "description": "Manuel kontrolü belirtilen saniye sonunda otomatik olarak sıfırlar. Devre dışı bırakmak için 0 olarak ayarlayın. ⏲️" + }, + "sleep_brightness": { + "description": "Uyku modundayken ışıkların parlaklık yüzdesi 😴" + }, + "max_color_temp": { + "description": "Kelvin cinsinden en düşük renk sıcaklığı. ❄️" + }, + "sleep_color_temp": { + "description": "Uyku modunda renk sıcaklığı ( `sleep_rgb_or_color_temp` `color_temp` olarak ayarlandığında kullanılır) Kelvin cinsinden. 😴" + }, + "send_split_delay": { + "description": "Parlaklık ve renk ayarını aynı anda desteklemeyen ışıklar için `separate_turn_on_commands` arasındaki gecikme (ms). ⏲️" + }, + "detect_non_ha_changes": { + "description": "`light.turn_on` dışındaki durum değişikliklerini algılar ve uyarlamayı durdurur. Bu ayarın çalışması için `take_over_control` etkin olmalıdır. 🕵️ \nDikkat: ⚠️ Bazı ışıklar yanlışlıkla “açık” durumunu bildirebilir, bu da ışıkların beklenmedik şekilde açılmasına neden olabilir. Böyle bir durumda bu özelliği devre dışı bırakın." + }, + "take_over_control": { + "description": "Işıklar açıkken ve uyarlanırken başka bir kaynaktan `light.turn_on` çağrılırsa Adaptive Lighting’i devre dışı bırakır. Dikkat: Bu işlem her `interval` süresinde `homeassistant.update_entity` çağrısı yapar! 🔒" + }, + "initial_transition": { + "description": "Işıklar `off` durumundan `on` durumuna geçerken ilk geçişin süresi (saniye cinsinden). ⏲️" + }, + "transition": { + "description": "Işıklar değişirken geçiş süresi (saniye cinsinden). 🕑" + }, + "sleep_transition": { + "description": "“Uyku modu” açılıp kapatıldığında geçiş süresi (saniye cinsinden). 😴" + }, + "entity_id": { + "description": "Anahtarın Entity ID’si. 📝" + }, + "max_brightness": { + "description": "Maksimum parlaklık yüzdesi.💡" + }, + "min_brightness": { + "description": "Minimum parlaklık yüzdesi.💡" + }, + "sleep_rgb_color": { + "description": "Uyku modunda RGB renk ( `sleep_rgb_or_color_temp` \"rgb_color\" olarak ayarlandığında kullanılır). 🌈" + }, + "sunrise_time": { + "description": "Gün doğumu için sabit bir saat (SS:DD:YY) belirleyin. 🌅" + }, + "sunset_time": { + "description": "Gün batımı için sabit bir saat (SS:DD:YY) belirleyin. 🌇" + }, + "use_defaults": { + "description": "Bu servis çağrısında belirtilmeyen varsayılan değerleri ayarlar. Seçenekler: \n- `current` (varsayılan, mevcut değerleri korur) \n- `factory` (belgelendirilmiş varsayılanlara sıfırlar) \n- `configuration` (anahtar yapılandırma varsayılanlarına döner) ⚙️" + }, + "min_sunset_time": { + "description": "En erken sanal gün batımı saatini (SS:DD:YY) belirleyin; daha geç gün batımlarına izin verir. 🌇" + }, + "max_sunrise_time": { + "description": "En geç sanal gün doğumu saatini (SS:DD:YY) belirleyin; daha erken gün doğumlarına izin verir. 🌅" + }, + "include_config_in_attributes": { + "description": "`true` olarak ayarlandığında, tüm seçenekleri Home Assistant’ta anahtarın attribute’ları olarak gösterir. 📝" + }, + "sleep_rgb_or_color_temp": { + "description": "Uyku modunda `\"rgb_color\"` veya `\"color_temp\"` kullanın. 🌙" + }, + "separate_turn_on_commands": { + "description": "Renk ve parlaklık için ayrı `light.turn_on` çağrıları kullanın; bazı ışık türleri için gereklidir. 🔀" + }, + "min_color_temp": { + "description": "En yüksek (sıcak) renk sıcaklığı (Kelvin cinsinden). 🔥" + }, + "prefer_rgb_color": { + "description": "Mümkün olduğunda ışık renk sıcaklığı yerine RGB renk ayarını tercih edip etmeyeceğini belirler. 🌈" + }, + "turn_on_lights": { + "description": "Şu anda kapalı olan ışıkların açılıp açılmayacağını belirler. 🔆" + }, + "adapt_delay": { + "description": "Işık açıldıktan sonra Adaptive Lighting’in değişiklikleri uygulamasına kadar bekleme süresi (saniye cinsinden). Titremeyi önlemeye yardımcı olabilir. ⏲️" + } + }, + "description": "Anahtardaki tüm ayarları dilediğiniz gibi değiştirebilirsiniz. Buradaki seçeneklerin hepsi, yapılandırma akışındakilerle aynıdır." + }, + "apply": { + "fields": { + "lights": { + "description": "Ayarları bir veya birden fazla ışığa uygula.💡" + }, + "transition": { + "description": "Işıklar değişirken geçiş süresi (saniye cinsinden). 🕑" + }, + "entity_id": { + "description": "Uygulanacak ayarların bulunduğu anahtarın `entity_id`’si. 📝" + }, + "adapt_brightness": { + "description": "Işığın parlaklığının uyarlanıp uyarlanmayacağını belirler. 🌞" + }, + "adapt_color": { + "description": "Destekleyen ışıklarda rengin uyarlanıp uyarlanmayacağını belirler. 🌈" + }, + "prefer_rgb_color": { + "description": "Mümkün olduğunda ışık renk sıcaklığı yerine RGB renk ayarını tercih edip etmeyeceğini belirler. 🌈" + }, + "turn_on_lights": { + "description": "Şu anda kapalı olan ışıkların açılıp açılmayacağını belirler. 🔆" + } + }, + "description": "Şu anki Akıllı Işıklandırma ayarlarını ışıklara uygular." + }, + "set_manual_control": { + "fields": { + "lights": { + "description": "Işıkların entity_id(leri). Belirtilmezse, anahtardaki tüm ışıklar seçilir. 💡" + }, + "entity_id": { + "description": "Işığın “manuel olarak kontrol edildiğini” işaretlemek veya kaldırmak için kullanılacak anahtarın `entity_id`’si. 📝" + }, + "manual_control": { + "description": "Işığı “manuel kontrol” listesinden eklemek (`true`) veya çıkarmak (`false`) için kullanılır. 🔒" + } + }, + "description": "Bir ışığın 'manuel olarak kontrol' edilip edilmediğini işaretleyin." + } + }, + "config": { + "step": { + "user": { + "title": "Akıllı ışıklandırma örneği için bir ad seçin.", + "description": "Her örnek birden fazla ışık içerebilir!" + } + }, + "abort": { + "already_configured": "Bu cihaz zaten ayarlanmış." + } + } +} diff --git a/custom_components/adaptive_lighting/translations/uk.json b/custom_components/adaptive_lighting/translations/uk.json index ee12b0e9..749271d5 100644 --- a/custom_components/adaptive_lighting/translations/uk.json +++ b/custom_components/adaptive_lighting/translations/uk.json @@ -42,7 +42,9 @@ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: На початку вмикання світла. Якщо `true`, освітлення адаптується лише якщо `light.turn_on` викликано без вказання кольору чи яскравості. ❌🌈 Це, наприклад, запобігає адаптації, коли сцена активується. Якщо `false`, освітлення адаптується незалежно від наявності кольору чи яскравості у початковому `service_data`. Потребує ввімкнення `take_over_control`. 🕵️", "transition_until_sleep": "transition_until_sleep: Коли активовано, адаптивне освітлення буде ставитись до налаштування сну як мінімум, переходячи до цих значень після заходу сонця. 🌙", "intercept": "intercept: Перехоплювати та адаптувати виклики увімкнення світла (`light.turn_on`), щоб увімкнути миттєву адаптацію кольору та яскравості. 🏎️ Вимкніть для світла, що не підтримує увімкнення світла (`light.turn_on`) з кольором та яскравістю.", - "include_config_in_attributes": "Включити конфігурацію в атрибути (`include_config_in_attributes`): Показувати всі опції як атрибути на перемикачі в Home Assistant, якщо встановлено значення `true`. 📝" + "include_config_in_attributes": "Включити конфігурацію в атрибути (`include_config_in_attributes`): Показувати всі опції як атрибути на перемикачі в Home Assistant, якщо встановлено значення `true`. 📝", + "multi_light_intercept": "multi_light_intercept: Перехоплення та адаптація викликів `light.turn_on`, які спрямовані на кілька світильників. ➗⚠️ Це може призвести до розділення одного виклику `light.turn_on` на кілька викликів, наприклад, коли світильники підключені до різних вимикачів. Потрібно ввімкнути `intercept`.", + "skip_redundant_commands": "skip_redundant_commands: Пропускати надсилання команд адаптації, цільовий стан яких вже дорівнює відомому стану освітлення. Мінімізує мережевий трафік і покращує швидкість реагування адаптації в деяких ситуаціях. 📉Вимкнути, якщо фізичний стан освітлення не синхронізується із записаним станом HA." }, "data_description": { "sunrise_offset": "Змінити час сходу сонця на +/- секунд. ⏰", @@ -57,7 +59,16 @@ "interval": "Частота адаптації освітлення, у секундах. 🔄", "sleep_brightness": "Відсоток яскравості світла в режимі сну. 😴", "sleep_color_temp": "Колірна температура в режимі сну (використовується, коли `sleep_rgb_or_color_temp` має значення `color_temp`) у Кельвінах. 😴", - "sleep_transition": "Тривалість переходу, коли режим сну \"sleep mode\" увімкнено, у секундах. 😴" + "sleep_transition": "Тривалість переходу, коли режим сну \"sleep mode\" увімкнено, у секундах. 😴", + "sleep_rgb_color": "Колір RGB у режимі сну (використовується, коли `sleep_rgb_or_color_temp` має значення \"rgb_color\"). 🌈", + "sunrise_time": "Встановіть фіксований час (ГГ:ХХ:СС) для сходу сонця. 🌅", + "sunset_time": "Встановіть фіксований час (ГГ:ХХ:СС) для заходу сонця. 🌇", + "min_sunrise_time": "Встановіть найраніший час віртуального сходу сонця (ГГ:ХХ:СС), враховуючи пізніші сходи. 🌅", + "min_sunset_time": "Встановіть найраніший час віртуального заходу сонця (ГГ:ХХ:СС), враховуючи пізніші заходи сонця. 🌇", + "max_sunrise_time": "Встановіть найпізніший час віртуального сходу сонця (ГГ:ХХ:СС), враховуючи більш ранні сходи сонця. 🌅", + "max_sunset_time": "Встановіть найновіший час віртуального заходу сонця (ГГ:ХХ:СС), враховуючи більш ранні заходи сонця. 🌇", + "sleep_rgb_or_color_temp": "Використовуйте `\"rgb_color\"` або `\"color_temp\"` у режимі сну. 🌙", + "adapt_delay": "Час очікування (секунди) між увімкненням світла та застосуванням змін системою адаптивного освітлення. Може допомогти уникнути мерехтіння. ⏲️" } } }, @@ -75,6 +86,21 @@ }, "transition": { "description": "Тривалість переходу, коли світло змінюється, у секундах. 🕑" + }, + "entity_id": { + "description": "`entity_id` перемикача з налаштуваннями, які потрібно застосувати. 📝" + }, + "adapt_brightness": { + "description": "Чи потрібно адаптувати яскравість світла. 🌞" + }, + "adapt_color": { + "description": "Чи адаптувати колір допоміжних ламп. 🌈" + }, + "prefer_rgb_color": { + "description": "Чи надавати перевагу налаштуванню кольору RGB над температурою кольору світла, коли це можливо. 🌈" + }, + "turn_on_lights": { + "description": "Чи вмикати світло, яке наразі вимкнене. 🔆" } } }, @@ -127,6 +153,45 @@ }, "transition": { "description": "Тривалість переходу, коли світло змінюється, у секундах. 🕑" + }, + "sleep_rgb_color": { + "description": "Колір RGB у режимі сну (використовується, коли `sleep_rgb_or_color_temp` має значення \"rgb_color\"). 🌈" + }, + "sunrise_time": { + "description": "Встановіть фіксований час (ГГ:ХХ:СС) для сходу сонця. 🌅" + }, + "sunset_time": { + "description": "Встановіть фіксований час (ГГ:ХХ:СС) для заходу сонця. 🌇" + }, + "use_defaults": { + "description": "Встановлює значення за замовчуванням, не вказані в цьому виклику служби. Параметри: «current» (за замовчуванням, зберігає поточні значення), «factory» (скидає до задокументованих значень за замовчуванням) або «configuration» (повертає до значень за замовчуванням конфігурації комутатора). ⚙️" + }, + "min_sunset_time": { + "description": "Встановіть найраніший час віртуального заходу сонця (ГГ:ХХ:СС), враховуючи пізніші заходи сонця. 🌇" + }, + "max_sunrise_time": { + "description": "Встановіть найпізніший час віртуального сходу сонця (ГГ:ХХ:СС), враховуючи більш ранні сходи сонця. 🌅" + }, + "include_config_in_attributes": { + "description": "Показувати всі опції як атрибути перемикача в Домашньому помічнику, якщо встановлено значення `true`. 📝" + }, + "sleep_rgb_or_color_temp": { + "description": "Використовуйте `\"rgb_color\"` або `\"color_temp\"` у режимі сну. 🌙" + }, + "separate_turn_on_commands": { + "description": "Використовуйте окремі виклики `light.turn_on` для кольору та яскравості, що необхідно для деяких типів освітлення. 🔀" + }, + "adapt_delay": { + "description": "Час очікування (секунди) між увімкненням світла та застосуванням змін системою адаптивного освітлення. Може допомогти уникнути мерехтіння. ⏲️" + }, + "min_color_temp": { + "description": "Найтепліша колірна температура в Кельвінах. 🔥" + }, + "prefer_rgb_color": { + "description": "Чи надавати перевагу налаштуванню кольору RGB над температурою кольору світла, коли це можливо. 🌈" + }, + "turn_on_lights": { + "description": "Чи вмикати світло, яке наразі вимкнене. 🔆" } }, "description": "Змініть будь-які налаштування, які ви бажаєте, у цьому перемикачі. Усі опції тут такі ж, як і в поточному конфігураційному файлі." @@ -136,6 +201,12 @@ "fields": { "lights": { "description": "Ідентифікатор(и) світла (entity_id(s) of lights). Якщо не вказано, вибираються всі лампи у перемикачі. 💡" + }, + "entity_id": { + "description": "`entity_id` перемикача, в якому потрібно (зняти) позначку світла як `керованого вручну`. 📝" + }, + "manual_control": { + "description": "Додавати (\"true\") чи видаляти (\"false\") світло зі списку \"manual_control\". 🔒" } } } From d95f0d4bc6be05030e4eb271e6969f04b9633fc5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Nov 2025 10:52:11 -0800 Subject: [PATCH 0906/1077] Fix xfail and skipped tests to properly pass (#1307) --- tests/test_config_flow.py | 33 +++++++++++++++++++-------------- tests/test_switch.py | 14 +++++++++----- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 0b9609d3..02ed6d42 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,6 +1,5 @@ """Test Adaptive Lighting config flow.""" -import pytest from homeassistant.components.adaptive_lighting.const import ( CONF_SUNRISE_TIME, CONF_SUNSET_TIME, @@ -115,14 +114,13 @@ async def test_import_twice(hass): ) -# TODO: Fix, broken for all supported versions -# But in ≤2024.5 it gives homeassistant.config_entries.UnknownEntry: cd69dbda65bd3f86e9a32d974cdfa23f -# and ≥2024.6 it times out -# NOTE: Just skip this test for now, currently (2025-06-15) I cannot figure out -# what this test is even testing. -async def test_changing_options_when_using_yaml(hass): - """Test changing options when using YAML.""" - pytest.skip(reason="TODO: Fix, broken for all supported versions") +async def test_options_flow_for_yaml_import(hass): + """Test that options flow for YAML-imported entries shows empty form. + + When a config entry is imported from YAML (source=SOURCE_IMPORT), + the options flow should show an empty form since the user should + modify the YAML configuration directly, not through the UI. + """ entry = MockConfigEntry( domain=DOMAIN, title=DEFAULT_NAME, @@ -132,11 +130,18 @@ async def test_changing_options_when_using_yaml(hass): ) entry.add_to_hass(hass) - await hass.block_till_done() + # For YAML imports, the switch setup requires the unique_id to be in + # hass.data[DOMAIN]["__yaml__"], otherwise it deletes the entry. + # This simulates what async_step_import does. + hass.data.setdefault(DOMAIN, {}).setdefault("__yaml__", set()).add(entry.unique_id) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() result = await hass.config_entries.options.async_init(entry.entry_id) - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={}, - ) + + # For YAML imports, the options flow shows an empty form (data_schema=None) + # This is intentional - users should modify YAML, not UI + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + assert result.get("data_schema") is None diff --git a/tests/test_switch.py b/tests/test_switch.py index 5011ebc6..e85cecb9 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1296,13 +1296,17 @@ async def test_restore_off_state(hass, state): assert not _switch.is_on -@pytest.mark.xfail(reason="Offset is larger than half a day") async def test_offset_too_large(hass): - """Test that update fails when the offset is too large.""" + """Test that update fails when the sunrise offset is too large. + + A 12-hour offset causes sun events to be out of order (e.g., sunrise after sunset), + which makes the adaptive lighting algorithm fail with a ValueError. + """ _, switch = await setup_switch(hass, {CONF_SUNRISE_OFFSET: 3600 * 12}) - await switch._update_attrs_and_maybe_adapt_lights( - context=switch.create_context("test"), - ) + with pytest.raises(ValueError, match="sun events.*not in the expected order"): + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + ) await hass.async_block_till_done() From a982acb384d9df70be279a568d5bc4384455c67a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Nov 2025 12:00:47 -0800 Subject: [PATCH 0907/1077] Stop pinning mypy-dev versions since old releases get deleted from PyPI (#1308) --- scripts/setup-dependencies | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/scripts/setup-dependencies b/scripts/setup-dependencies index 92a41411..f462335d 100755 --- a/scripts/setup-dependencies +++ b/scripts/setup-dependencies @@ -2,29 +2,16 @@ set -ex cd "$(dirname "$0")/.." -if grep -q 'mypy-dev==1.14.0a3' core/requirements_test.txt; then - # mypy-dev==1.14.0a3 seems to not be available anymore, HA 2024.12 is affected - sed -i 's/mypy-dev==1.14.0a3/mypy-dev==1.14.0a7/' core/requirements_test.txt -fi -if grep -q 'mypy-dev==1.16.0a1' core/requirements_test.txt; then - # mypy-dev==1.16.0a1 seems to not be available anymore, HA 2025.2 is affected - sed -i 's/mypy-dev==1.16.0a1/mypy-dev==1.16.0a9/' core/requirements_test.txt -fi -if grep -q 'mypy-dev==1.16.0a3' core/requirements_test.txt; then - # mypy-dev==1.16.0a3 seems to not be available anymore, HA 2025.3 is affected - sed -i 's/mypy-dev==1.16.0a3/mypy-dev==1.16.0a9/' core/requirements_test.txt -fi -if grep -q 'mypy-dev==1.16.0a7' core/requirements_test.txt; then - # mypy-dev==1.16.0a7 seems to not be available anymore, HA 2025.4 is affected - sed -i 's/mypy-dev==1.16.0a7/mypy-dev==1.16.0a9/' core/requirements_test.txt -fi -if grep -q 'mypy-dev==1.16.0a8' core/requirements_test.txt; then - # mypy-dev==1.16.0a8 seems to not be available anymore, HA 2025.5 and 2025.6 is affected - sed -i 's/mypy-dev==1.16.0a8/mypy-dev==1.16.0a9/' core/requirements_test.txt -fi +# Remove mypy-dev from requirements_test.txt since the maintainer deletes old versions from PyPI. +# We'll install the latest version separately below. +# See: https://github.com/cdce8p/mypy-dev/issues/62 +sed -i '/^mypy-dev/d' core/requirements_test.txt uv pip install -r core/requirements.txt uv pip install -r core/requirements_test.txt uv pip install -e core/ uv pip install ulid-transform # this is in Adaptive-lighting's manifest.json uv pip install $(python test_dependencies.py) + +# Install the latest mypy-dev (not pinned since old versions get deleted from PyPI) +uv pip install --upgrade mypy-dev From 19d30430bbab26828511d48c42e9cc87b21dd902 Mon Sep 17 00:00:00 2001 From: Dobby Date: Thu, 27 Nov 2025 21:01:05 +0100 Subject: [PATCH 0908/1077] Friendly names (#1258) --- .../adaptive_lighting/__init__.py | 18 ++-- .../adaptive_lighting/_docs_helpers.py | 21 ++--- .../adaptive_lighting/adaptation_utils.py | 3 +- .../adaptive_lighting/color_and_brightness.py | 6 +- .../adaptive_lighting/config_flow.py | 35 +++++--- custom_components/adaptive_lighting/const.py | 33 +++---- .../adaptive_lighting/hass_utils.py | 16 +++- .../adaptive_lighting/helpers.py | 13 +++ custom_components/adaptive_lighting/switch.py | 85 ++++++++++--------- 9 files changed, 140 insertions(+), 90 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 0c8bad80..44151c84 100644 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -7,7 +7,7 @@ import homeassistant.helpers.config_validation as cv import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_SOURCE -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from .const import ( _DOMAIN_SCHEMA, @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["switch"] -def _all_unique_names(value): +def _all_unique_names(value: list[dict[str, Any]]) -> list[dict[str, Any]]: """Validate that all entities have a unique profile name.""" hosts = [device[CONF_NAME] for device in value] schema = vol.Schema(vol.Unique()) @@ -36,12 +36,16 @@ CONFIG_SCHEMA = vol.Schema( ) -async def reload_configuration_yaml(event: dict, hass: HomeAssistant): # noqa: ARG001 +async def reload_configuration_yaml(event: Event) -> None: """Reload configuration.yaml.""" - await hass.services.async_call("homeassistant", "check_config", {}) + hass: HomeAssistant | None = event.data.get("hass") + if hass is not None: + await hass.services.async_call("homeassistant", "check_config", {}) + else: + _LOGGER.error("HomeAssistant instance not found in event data.") -async def async_setup(hass: HomeAssistant, config: dict[str, Any]): +async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: """Import integration from config.""" if DOMAIN in config: for entry in config[DOMAIN]: @@ -55,7 +59,7 @@ async def async_setup(hass: HomeAssistant, config: dict[str, Any]): return True -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the component.""" data = hass.data.setdefault(DOMAIN, {}) @@ -70,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): return True -async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Update options.""" await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/custom_components/adaptive_lighting/_docs_helpers.py b/custom_components/adaptive_lighting/_docs_helpers.py index 49899395..afbb9d66 100644 --- a/custom_components/adaptive_lighting/_docs_helpers.py +++ b/custom_components/adaptive_lighting/_docs_helpers.py @@ -15,7 +15,7 @@ from .const import ( ) -def _format_voluptuous_instance(instance): +def _format_voluptuous_instance(instance: vol.All) -> str: coerce_type = None min_val = None max_val = None @@ -56,8 +56,8 @@ def _type_to_str(type_: Any) -> str: # noqa: PLR0911 raise ValueError(msg) -def generate_config_markdown_table(): - rows = [] +def generate_config_markdown_table() -> str: + rows: list[dict[str, str]] = [] for k, default, type_ in VALIDATION_TUPLES: description = DOCS[k] row = { @@ -73,7 +73,7 @@ def generate_config_markdown_table(): def _schema_to_dict(schema: vol.Schema) -> dict[str, tuple[Any, Any]]: - result = {} + result: dict[str, tuple[Any, Any]] = {} for key, value in schema.schema.items(): if isinstance(key, vol.Optional): default_value = key.default @@ -82,11 +82,12 @@ def _schema_to_dict(schema: vol.Schema) -> dict[str, tuple[Any, Any]]: def _generate_service_markdown_table( - schema: vol.Schema, + schema: dict[str, tuple[Any, Any]] | vol.Schema, alternative_docs: dict[str, str] | None = None, -): - rows = [] - for k, (default, type_) in _schema_to_dict(schema).items(): +) -> str: + schema_dict = _schema_to_dict(schema) if isinstance(schema, vol.Schema) else schema + rows: list[dict[str, str]] = [] + for k, (default, type_) in schema_dict.items(): if alternative_docs is not None and k in alternative_docs: description = alternative_docs[k] else: @@ -103,11 +104,11 @@ def _generate_service_markdown_table( return df.to_markdown(index=False) -def generate_apply_markdown_table(): +def generate_apply_markdown_table() -> str: return _generate_service_markdown_table(apply_service_schema(), DOCS_APPLY) -def generate_set_manual_control_markdown_table(): +def generate_set_manual_control_markdown_table() -> str: return _generate_service_markdown_table( SET_MANUAL_CONTROL_SCHEMA, DOCS_MANUAL_CONTROL, diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py index 2c339223..14c28ae8 100644 --- a/custom_components/adaptive_lighting/adaptation_utils.py +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -84,10 +84,11 @@ def _remove_redundant_attributes( Removes all attributes from service call data whose values are already present in the target entity's state. """ + attributes: dict[str, Any] = dict(state.attributes) return { k: v for k, v in service_data.items() - if k not in state.attributes or v != state.attributes[k] + if k not in attributes or v != attributes[k] } diff --git a/custom_components/adaptive_lighting/color_and_brightness.py b/custom_components/adaptive_lighting/color_and_brightness.py index 2fd93e67..34da23c6 100644 --- a/custom_components/adaptive_lighting/color_and_brightness.py +++ b/custom_components/adaptive_lighting/color_and_brightness.py @@ -375,8 +375,8 @@ class SunLightSettings: def get_settings( self, - is_sleep, - transition, + is_sleep: bool, + transition: float | None, ) -> dict[str, float | int | tuple[float, float] | tuple[float, float, float]]: """Get all light settings. @@ -507,7 +507,7 @@ def lerp_color_hsv( return cast("tuple[int, int, int]", rgb) -def lerp(x, x1, x2, y1, y2): +def lerp(x: float, x1: float, x2: float, y1: float, y2: float) -> float: """Linearly interpolate between two values.""" return y1 + (x - x1) * (y2 - y1) / (x2 - x1) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 00214fdb..d1799888 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -16,6 +16,7 @@ from .const import ( # pylint: disable=unused-import NONE_STR, VALIDATION_TUPLES, ) +from .helpers import get_friendly_name from .switch import _supported_features, validate _LOGGER = logging.getLogger(__name__) @@ -26,9 +27,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input: dict[str, Any] | None = None): """Handle the initial step.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: await self.async_set_unique_id(user_input[CONF_NAME]) @@ -41,8 +42,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, user_input: dict[str, Any]): + async def async_step_import(self, user_input: dict[str, Any] | None = None): """Handle configuration by YAML file.""" + if user_input is None: + return self.async_abort(reason="no_data") + await self.async_set_unique_id(user_input[CONF_NAME]) # Keep a list of switches that are configured via YAML data = self.hass.data.setdefault(DOMAIN, {}) @@ -57,7 +61,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> "OptionsFlowHandler": """Get the options flow for this handler.""" if (MAJOR_VERSION, MINOR_VERSION) >= (2024, 12): # https://github.com/home-assistant/core/pull/129651 @@ -65,7 +71,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return OptionsFlowHandler(config_entry) -def validate_options(user_input, errors): +def validate_options(user_input: dict[str, Any], errors: dict[str, str]) -> None: """Validate the options in the OptionsFlow. This is an extra validation step because the validators @@ -85,7 +91,7 @@ def validate_options(user_input, errors): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for Adaptive Lighting.""" - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize options flow.""" if (MAJOR_VERSION, MINOR_VERSION) >= (2024, 12): super().__init__(*args, **kwargs) @@ -93,7 +99,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): else: self.config_entry = args[0] - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input: dict[str, Any] | None = None): """Handle options flow.""" conf = self.config_entry data = validate(conf) @@ -105,11 +111,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if not errors: return self.async_create_entry(title="", data=user_input) - all_lights = [ - light + all_lights_with_names = { + light: get_friendly_name(self.hass, light) for light in self.hass.states.async_entity_ids("light") if _supported_features(self.hass, light) - ] + } + all_lights = list(all_lights_with_names.keys()) for configured_light in data[CONF_LIGHTS]: if configured_light not in all_lights: errors = {CONF_LIGHTS: "entity_missing"} @@ -119,7 +126,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow): configured_light, ) all_lights.append(configured_light) - to_replace = {CONF_LIGHTS: cv.multi_select(sorted(all_lights))} + all_lights_with_names[configured_light] = configured_light + + light_options = { + entity_id: f"{name} ({entity_id})" + for entity_id, name in all_lights_with_names.items() + } + to_replace = {CONF_LIGHTS: cv.multi_select(light_options)} options_schema = {} for name, default, validation in VALIDATION_TUPLES: diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 3fc9c5d4..f7c3620b 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,5 +1,8 @@ """Constants for the Adaptive Lighting integration.""" +from datetime import timedelta +from typing import Any + import homeassistant.helpers.config_validation as cv import voluptuous as vol from homeassistant.components.light import VALID_TRANSITION @@ -291,13 +294,13 @@ DOCS_APPLY = { } -def int_between(min_int, max_int): +def int_between(min_int: int, max_int: int) -> vol.All: """Return an integer between 'min_int' and 'max_int'.""" return vol.All(vol.Coerce(int), vol.Range(min=min_int, max=max_int)) -VALIDATION_TUPLES = [ - (CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), +VALIDATION_TUPLES: list[tuple[str, Any, Any]] = [ + (CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), # type: ignore[arg-type] (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), @@ -310,7 +313,7 @@ VALIDATION_TUPLES = [ ( CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP, - selector.SelectSelector( + selector.SelectSelector( # type: ignore[arg-type] selector.SelectSelectorConfig( options=["color_temp", "rgb_color"], multiple=False, @@ -322,7 +325,7 @@ VALIDATION_TUPLES = [ ( CONF_SLEEP_RGB_COLOR, DEFAULT_SLEEP_RGB_COLOR, - selector.ColorRGBSelector(selector.ColorRGBSelectorConfig()), + selector.ColorRGBSelector(selector.ColorRGBSelectorConfig()), # type: ignore[arg-type] ), (CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION, VALID_TRANSITION), (CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP, bool), @@ -337,7 +340,7 @@ VALIDATION_TUPLES = [ ( CONF_BRIGHTNESS_MODE, DEFAULT_BRIGHTNESS_MODE, - selector.SelectSelector( + selector.SelectSelector( # type: ignore[arg-type] selector.SelectSelectorConfig( options=["default", "linear", "tanh"], multiple=False, @@ -370,7 +373,7 @@ VALIDATION_TUPLES = [ ] -def timedelta_as_int(value): +def timedelta_as_int(value: timedelta) -> float: """Convert a `datetime.timedelta` object to an integer. This integer can be serialized to json but a timedelta cannot. @@ -380,7 +383,7 @@ def timedelta_as_int(value): # conf_option: (validator, coerce) tuples # these validators cannot be serialized but can be serialized when coerced by coerce. -EXTRA_VALIDATION = { +EXTRA_VALIDATION: dict[str, tuple[Any, Any]] = { CONF_INTERVAL: (cv.time_period, timedelta_as_int), CONF_SUNRISE_OFFSET: (cv.time_period, timedelta_as_int), CONF_SUNRISE_TIME: (cv.time, str), @@ -395,7 +398,7 @@ EXTRA_VALIDATION = { } -def maybe_coerce(key, validation): +def maybe_coerce(key: str, validation: Any) -> vol.All | Any: """Coerce the validation into a json serializable type.""" validation, coerce = EXTRA_VALIDATION.get(key, (validation, None)) if coerce is not None: @@ -403,7 +406,7 @@ def maybe_coerce(key, validation): return validation -def replace_none_str(value, replace_with=None): +def replace_none_str(value: Any, replace_with: Any | None = None) -> Any: """Replace "None" -> replace_with.""" return value if value != NONE_STR else replace_with @@ -421,12 +424,12 @@ _DOMAIN_SCHEMA = vol.Schema( ) -def apply_service_schema(initial_transition: int = 1): +def apply_service_schema(initial_transition: int = 1) -> vol.Schema: """Return the schema for the apply service.""" return vol.Schema( { - vol.Optional(CONF_ENTITY_ID): cv.entity_ids, - vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, + vol.Optional(CONF_ENTITY_ID): cv.entity_ids, # type: ignore[arg-type] + vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, # type: ignore[arg-type] vol.Optional( CONF_TRANSITION, default=initial_transition, @@ -441,8 +444,8 @@ def apply_service_schema(initial_transition: int = 1): SET_MANUAL_CONTROL_SCHEMA = vol.Schema( { - vol.Optional(CONF_ENTITY_ID): cv.entity_ids, - vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, + vol.Optional(CONF_ENTITY_ID): cv.entity_ids, # type: ignore[arg-type] + vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, # type: ignore[arg-type] vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean, }, ) diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py index 21ea67b3..550ae350 100644 --- a/custom_components/adaptive_lighting/hass_utils.py +++ b/custom_components/adaptive_lighting/hass_utils.py @@ -47,7 +47,7 @@ def setup_service_call_interceptor( # This is necessary to replace a registered service handler with our # proxy handler to intercept calls. registered_services = ( - hass.services._services # pylint: disable=protected-access + hass.services._services # pylint: disable=protected-access # type: ignore[attr-defined] ) except AttributeError as error: msg = ( @@ -68,7 +68,9 @@ def setup_service_call_interceptor( data = dict(call.data) # Call interceptor - await intercept_func(call, data) + result = intercept_func(call, data) + if result is not None: + await result # Convert data back to read-only call.data = ReadOnlyDict(data) @@ -79,7 +81,13 @@ def setup_service_call_interceptor( call.data, ) # Call original service handler with processed data - await existing_service.job.target(call) + import asyncio + + target = existing_service.job.target + if asyncio.iscoroutinefunction(target): + await target(call) + else: + target(call) hass.services.async_register( domain, @@ -88,7 +96,7 @@ def setup_service_call_interceptor( existing_service.schema, ) - def remove(): + def remove() -> None: # Remove the interceptor by reinstalling the original service handler hass.services.async_register( domain, diff --git a/custom_components/adaptive_lighting/helpers.py b/custom_components/adaptive_lighting/helpers.py index fa3af6ef..df2abb95 100644 --- a/custom_components/adaptive_lighting/helpers.py +++ b/custom_components/adaptive_lighting/helpers.py @@ -4,6 +4,10 @@ from __future__ import annotations import base64 import math +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant def clamp(value: float, minimum: float, maximum: float) -> float: @@ -83,3 +87,12 @@ def color_difference_redmean( green_term = 4 * delta_g**2 blue_term = (2 + (255 - r_hat) / 256) * delta_b**2 return math.sqrt(red_term + green_term + blue_term) + + +def get_friendly_name(hass: HomeAssistant, entity_id: str) -> str: + """Retrieve the friendly name of an entity.""" + state = hass.states.get(entity_id) + if state and hasattr(state, "attributes"): + attributes: dict[str, Any] = dict(getattr(state, "attributes", {})) + return attributes.get("friendly_name", entity_id) + return entity_id diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 59b98a0c..dbb72ea1 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -232,7 +232,7 @@ def _switches_with_lights( """Get all switches that control at least one of the lights passed.""" config_entries = hass.config_entries.async_entries(DOMAIN) data = hass.data[DOMAIN] - switches = [] + switches: list[AdaptiveSwitch] = [] all_check_lights = ( _expand_light_groups(hass, lights) if expand_light_groups else set(lights) ) @@ -369,7 +369,7 @@ def _fire_manual_control_event( switch: AdaptiveSwitch, light: str, context: Context, -): +) -> None: """Fire an event that 'light' is marked as manual_control.""" hass = switch.hass _LOGGER.debug( @@ -389,7 +389,7 @@ async def async_setup_entry( # noqa: PLR0915 hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, -): +) -> None: """Set up the AdaptiveLighting switch.""" assert hass is not None data = hass.data[DOMAIN] @@ -456,7 +456,7 @@ async def async_setup_entry( # noqa: PLR0915 ) @callback - async def handle_apply(service_call: ServiceCall): + async def handle_apply(service_call: ServiceCall) -> None: """Handle the entity service apply.""" data = service_call.data _LOGGER.debug( @@ -488,7 +488,7 @@ async def async_setup_entry( # noqa: PLR0915 ) @callback - async def handle_set_manual_control(service_call: ServiceCall): + async def handle_set_manual_control(service_call: ServiceCall) -> None: """Set or unset lights as 'manually controlled'.""" data = service_call.data _LOGGER.debug( @@ -583,7 +583,7 @@ def validate( return data -def _is_state_event(event: Event, from_or_to_state: Iterable[str]): +def _is_state_event(event: Event, from_or_to_state: Iterable[str]) -> bool: """Match state event when either 'from_state' or 'to_state' matches.""" return ( (old_state := event.data.get("old_state")) is not None @@ -598,7 +598,7 @@ def _expand_light_groups( hass: HomeAssistant, lights: list[str], ) -> list[str]: - all_lights = set() + all_lights: set[str] = set() manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER] for light in lights: state = hass.states.get(light) @@ -625,15 +625,15 @@ def _is_light_group(state: State) -> bool: def _supported_features(hass: HomeAssistant, light: str) -> set[str]: state = hass.states.get(light) assert state is not None - supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + supported_features = int(state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)) # type: ignore[arg-type] assert isinstance(supported_features, int) - supported = set() + supported: set[str] = set() if supported_features & LightEntityFeature.TRANSITION: supported.add("transition") - supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES, set()) + supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES, set()) # type: ignore[arg-type] color_modes = { ColorMode.RGB, ColorMode.RGBW, @@ -838,7 +838,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): def __init__( self, - hass, + hass: HomeAssistant, config_entry: ConfigEntry, manager: AdaptiveLightingManager, sleep_mode_switch: SimpleSwitch, @@ -892,7 +892,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self, data: dict[str, Any], defaults: dict[str, Any] | None = None, - ): + ) -> None: # Only pass settings users can change during runtime data = validate( config_entry=None, @@ -984,12 +984,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) @property - def name(self): + def name(self) -> str: """Return the name of the device if any.""" return f"Adaptive Lighting: {self._name}" @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of entity.""" return self._name @@ -1026,11 +1026,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._state = False assert not self.remove_listeners - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Remove the listeners upon removing the component.""" self._remove_listeners() - def _expand_light_groups(self, hass=None) -> None: + def _expand_light_groups(self, hass: HomeAssistant | None = None) -> None: hass = hass or self.hass all_lights = _expand_light_groups(hass, self.lights) self.manager.lights.update(all_lights) @@ -1187,7 +1187,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): force=True, ) - async def async_turn_off(self, **kwargs) -> None: # noqa: ARG002 + async def async_turn_off(self, **kwargs: Any) -> None: # noqa: ARG002 """Turn off adaptive lighting.""" if not self.is_on: return @@ -1195,7 +1195,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._remove_listeners() self.manager.reset(*self.lights) - async def _async_update_at_interval_action(self, now=None) -> None: # noqa: ARG002 + async def _async_update_at_interval_action( + self, + now: Any = None, # noqa: ARG002 + ) -> None: """Update the attributes and maybe adapt the lights.""" await self._update_attrs_and_maybe_adapt_lights( context=self.create_context("interval"), @@ -1328,7 +1331,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): await self.execute_cancellable_adaptation_calls(data) - async def _execute_adaptation_calls(self, data: AdaptationData): + async def _execute_adaptation_calls(self, data: AdaptationData) -> None: """Executes a sequence of adaptation service calls for the given service datas.""" for index in range(data.max_length): is_first_call = index == 0 @@ -1379,7 +1382,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def execute_cancellable_adaptation_calls( self, data: AdaptationData, - ): + ) -> None: """Executes a cancellable sequence of adaptation service calls for the given service datas. Wraps the sequence of service calls in a task that can be cancelled from elsewhere, e.g., @@ -1635,12 +1638,12 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): self._initial_state = initial_state @property - def name(self): + def name(self) -> str: """Return the name of the device if any.""" return self._name @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of entity.""" return self._unique_id @@ -1676,12 +1679,12 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): else: await self.async_turn_off() - async def async_turn_on(self, **kwargs) -> None: # noqa: ARG002 + async def async_turn_on(self, **kwargs: Any) -> None: # noqa: ARG002 """Turn on adaptive lighting sleep mode.""" _LOGGER.debug("%s: Turning on", self._name) self._state = True - async def async_turn_off(self, **kwargs) -> None: # noqa: ARG002 + async def async_turn_off(self, **kwargs: Any) -> None: # noqa: ARG002 """Turn off adaptive lighting sleep mode.""" _LOGGER.debug("%s: Turning off", self._name) self._state = False @@ -1769,7 +1772,7 @@ class AdaptiveLightingManager: exc_info=True, ) - def disable(self): + def disable(self) -> None: """Disable the listener by removing all subscribed handlers.""" for remove in self.listener_removers: remove() @@ -1991,7 +1994,7 @@ class AdaptiveLightingManager: skipped, ) - def modify_service_data(service_data, entity_ids): + def modify_service_data(service_data, entity_ids) -> dict[str, Any]: """Modify the service data to contain the entity IDs.""" service_data.pop(ATTR_ENTITY_ID, None) service_data.pop(ATTR_AREA_ID, None) @@ -2169,7 +2172,7 @@ class AdaptiveLightingManager: light, ) - async def reset(): + async def reset() -> None: # Called when the timer expires, doesn't need to do anything _LOGGER.debug( "Transition finished for light %s", @@ -2178,7 +2181,11 @@ class AdaptiveLightingManager: self._handle_timer(light, self.transition_timers, last_transition, reset) - def set_auto_reset_manual_control_times(self, lights: list[str], time: float): + def set_auto_reset_manual_control_times( + self, + lights: list[str], + time: float, + ) -> None: """Set the time after which the lights are automatically reset.""" if time == 0: return @@ -2201,7 +2208,7 @@ class AdaptiveLightingManager: self.manual_control[light] = True delay = self.auto_reset_manual_control_times.get(light) - async def reset(): + async def reset() -> None: _LOGGER.debug( "Auto resetting 'manual_control' status of '%s' because" " it was not manually controlled for %s seconds.", @@ -2227,7 +2234,7 @@ class AdaptiveLightingManager: self, light_id: str, which: Literal["color", "brightness", "both"] = "both", - ): + ) -> None: """Cancel ongoing adaptation service calls for a specific light entity.""" brightness_task = self.adaptation_tasks_brightness.get(light_id) color_task = self.adaptation_tasks_color.get(light_id) @@ -2256,7 +2263,7 @@ class AdaptiveLightingManager: # color_task might be the same as brightness_task color_task.cancel() - def reset(self, *lights, reset_manual_control: bool = True) -> None: + def reset(self, *lights: str, reset_manual_control: bool = True) -> None: """Reset the 'manual_control' status of the lights.""" for light in lights: if reset_manual_control: @@ -2306,11 +2313,11 @@ class AdaptiveLightingManager: if not any(eid in self.lights for eid in entity_ids): return - def off(eid: str, event: Event): + def off(eid: str, event: Event) -> None: self.turn_off_event[eid] = event self.reset(eid) - def on(eid: str, event: Event): + def on(eid: str, event: Event) -> None: task = self.sleep_tasks.get(eid) if task is not None: task.cancel() @@ -2735,14 +2742,14 @@ class AdaptiveLightingManager: class _AsyncSingleShotTimer: - def __init__(self, delay, callback) -> None: + def __init__(self, delay: float, callback: Callable[[], None | Any]) -> None: """Initialize the timer.""" self.delay = delay self.callback = callback self.task = None self.start_time: datetime.datetime | None = None - async def _run(self): + async def _run(self) -> None: """Run the timer. Don't call this directly, use start() instead.""" await asyncio.sleep(self.delay) if self.callback: @@ -2751,11 +2758,11 @@ class _AsyncSingleShotTimer: else: self.callback() - def is_running(self): + def is_running(self) -> bool: """Return whether the timer is running.""" return self.task is not None and not self.task.done() - def start(self): + def start(self) -> None: """Start the timer.""" if self.task is not None and not self.task.done(): self.task.cancel() @@ -2765,13 +2772,13 @@ class _AsyncSingleShotTimer: self.start_time = dt_util.utcnow() self.task = asyncio.create_task(self._run()) - def cancel(self): + def cancel(self) -> None: """Cancel the timer.""" if self.task: self.task.cancel() self.callback = None - def remaining_time(self): + def remaining_time(self) -> float: """Return the remaining time before the timer expires.""" if self.start_time is not None: elapsed_time = (dt_util.utcnow() - self.start_time).total_seconds() From bba7bb8350952446e0bbe2d9c474818004404b2d Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:01:33 +0000 Subject: [PATCH 0909/1077] docs: update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index faf8c51a..27d79686 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-126-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-127-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -629,6 +629,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + + + From 2391d995393c90ba0079d87679c5910f65ceedd0 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:01:34 +0000 Subject: [PATCH 0910/1077] docs: update .all-contributorsrc --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index d999de5a..21c19107 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1152,6 +1152,15 @@ "contributions": [ "translation" ] + }, + { + "login": "dobby5", + "name": "Dobby", + "avatar_url": "https://avatars.githubusercontent.com/u/1346316?v=4", + "profile": "https://github.com/dobby5", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, From 1968e1a9a697a1713022dabf28f65d347d4d5417 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Nov 2025 12:42:20 -0800 Subject: [PATCH 0911/1077] Bump to v1.28.0 (#1310) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 856abfe8..1d1e431a 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.27.0" + "version": "1.28.0" } From 80fb93b9f6e457b56d2cee548f2061956212e041 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 28 Nov 2025 16:43:52 -0800 Subject: [PATCH 0912/1077] Fix propcache dependency (revert to functools) (#1314) --- .../adaptive_lighting/color_and_brightness.py | 3 +-- webapp/color_and_brightness.py | 19 +++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/custom_components/adaptive_lighting/color_and_brightness.py b/custom_components/adaptive_lighting/color_and_brightness.py index 34da23c6..caee67c2 100644 --- a/custom_components/adaptive_lighting/color_and_brightness.py +++ b/custom_components/adaptive_lighting/color_and_brightness.py @@ -9,7 +9,7 @@ import logging import math from dataclasses import dataclass from datetime import UTC, timedelta -from functools import partial +from functools import cached_property, partial from typing import TYPE_CHECKING, Any, Literal, cast from homeassistant.util.color import ( @@ -17,7 +17,6 @@ from homeassistant.util.color import ( color_temperature_to_rgb, color_xy_to_hs, ) -from propcache.api import cached_property if TYPE_CHECKING: import astral.location diff --git a/webapp/color_and_brightness.py b/webapp/color_and_brightness.py index 849c2373..d636c674 100644 --- a/webapp/color_and_brightness.py +++ b/webapp/color_and_brightness.py @@ -8,7 +8,7 @@ import datetime import logging import math from dataclasses import dataclass -from datetime import timedelta +from datetime import UTC, timedelta from functools import cached_property, partial from typing import TYPE_CHECKING, Any, Literal, cast @@ -19,7 +19,7 @@ from homeassistant_util_color import ( ) if TYPE_CHECKING: - import astral + import astral.location # Same as homeassistant.const.SUN_EVENT_SUNRISE and homeassistant.const.SUN_EVENT_SUNSET # We re-define them here to not depend on homeassistant in this file. @@ -32,7 +32,6 @@ SUN_EVENT_MIDNIGHT = "solar_midnight" _ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT) _ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))} -UTC = datetime.timezone.utc utcnow: partial[datetime.datetime] = partial(datetime.datetime.now, UTC) utcnow.__doc__ = "Get now in UTC time." @@ -44,7 +43,7 @@ class SunEvents: """Track the state of the sun and associated light settings.""" name: str - astral_location: astral.Location + astral_location: astral.location.Location sunrise_time: datetime.time | None min_sunrise_time: datetime.time | None max_sunrise_time: datetime.time | None @@ -198,7 +197,7 @@ class SunLightSettings: """Track the state of the sun and associated light settings.""" name: str - astral_location: astral.Location + astral_location: astral.location.Location adapt_until_sleep: bool max_brightness: int max_color_temp: int @@ -296,7 +295,7 @@ class SunLightSettings: ) return clamp(brightness, self.min_brightness, self.max_brightness) - def brightness_pct(self, dt: datetime.datetime, is_sleep: bool) -> float: + def brightness_pct(self, dt: datetime.datetime, is_sleep: bool) -> float | None: """Calculate the brightness in %.""" if is_sleep: return self.sleep_brightness @@ -331,7 +330,7 @@ class SunLightSettings: ) -> dict[str, Any]: """Calculate the brightness and color.""" sun_position = self.sun.sun_position(dt) - rgb_color: tuple[float, float, float] + rgb_color: tuple[int, int, int] # Variable `force_rgb_color` is needed for RGB color after sunset (if enabled) force_rgb_color = False brightness_pct = self.brightness_pct(dt, is_sleep) @@ -375,8 +374,8 @@ class SunLightSettings: def get_settings( self, - is_sleep, - transition, + is_sleep: bool, + transition: float | None, ) -> dict[str, float | int | tuple[float, float] | tuple[float, float, float]]: """Get all light settings. @@ -507,7 +506,7 @@ def lerp_color_hsv( return cast("tuple[int, int, int]", rgb) -def lerp(x, x1, x2, y1, y2): +def lerp(x: float, x1: float, x2: float, y1: float, y2: float) -> float: """Linearly interpolate between two values.""" return y1 + (x - x1) * (y2 - y1) / (x2 - x1) From 894d0025cdc83b61e0798dd87ca3d208a14781e2 Mon Sep 17 00:00:00 2001 From: lenucksi Date: Sun, 30 Nov 2025 17:06:21 +0100 Subject: [PATCH 0913/1077] fix(ci): add branch filter to TOC generator workflow (#1320) --- .github/workflows/toc.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/toc.yaml b/.github/workflows/toc.yaml index 28dac912..a2665757 100644 --- a/.github/workflows/toc.yaml +++ b/.github/workflows/toc.yaml @@ -1,4 +1,6 @@ -on: push +on: + push: + branches: [main] name: TOC Generator jobs: generateTOC: From cc25a504011c09aed81e278fbc2200baba8149af Mon Sep 17 00:00:00 2001 From: Lenucksi Date: Sun, 30 Nov 2025 10:49:12 +0100 Subject: [PATCH 0914/1077] ci: add automated weekly test matrix updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add automation to keep the pytest test matrix up-to-date with latest Home Assistant Core releases: 1. **Weekly Automation**: New workflow updates test matrix automatically 2. **Version Fetcher**: Python script queries GitHub API for latest HA Core releases 3. **PR Creation**: Automatically creates PRs when new versions are available ## Benefits - No more manual updates when new HA Core versions are released - Automatic tracking of new HA Core releases - Reduces maintenance burden ## Background This builds on commit a982acb which fixed mypy-dev pinning. While upstream now installs latest mypy-dev automatically, the test matrix versions still need manual updates. This automation solves that remaining manual task. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/update-test-matrix.yaml | 59 +++++++++ scripts/update-test-matrix.py | 147 ++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 .github/workflows/update-test-matrix.yaml create mode 100755 scripts/update-test-matrix.py diff --git a/.github/workflows/update-test-matrix.yaml b/.github/workflows/update-test-matrix.yaml new file mode 100644 index 00000000..658a5c4e --- /dev/null +++ b/.github/workflows/update-test-matrix.yaml @@ -0,0 +1,59 @@ +name: Update Test Matrix + +on: + schedule: + # Run weekly on Monday at 9:00 UTC + - cron: "0 9 * * 1" + workflow_dispatch: # Allow manual trigger + +permissions: + contents: write + pull-requests: write + +jobs: + update-matrix: + name: Update HA Core versions in test matrix + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Update test matrix + run: python scripts/update-test-matrix.py + + - name: Check for changes + id: changes + run: | + if git diff --quiet .github/workflows/pytest.yaml; then + echo "changed=false" >> $GITHUB_OUTPUT + else + echo "changed=true" >> $GITHUB_OUTPUT + echo "Detected changes:" + git diff .github/workflows/pytest.yaml + fi + + - name: Create Pull Request + if: steps.changes.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "ci: update HA Core test matrix versions" + title: "ci: Update Home Assistant Core test matrix" + body: | + This PR automatically updates the pytest workflow to test against the latest Home Assistant Core versions. + + ## Changes + - Updated HA Core versions in the test matrix to include latest patch releases + + --- + 🤖 Generated automatically by the update-test-matrix workflow + branch: update-test-matrix + delete-branch: true + labels: | + automation + ci diff --git a/scripts/update-test-matrix.py b/scripts/update-test-matrix.py new file mode 100755 index 00000000..1d06d992 --- /dev/null +++ b/scripts/update-test-matrix.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""Update the pytest workflow matrix with latest HA Core versions. + +This script fetches the latest Home Assistant Core release versions from GitHub +and updates the pytest workflow matrix to test against them. + +Usage: + python scripts/update-test-matrix.py +""" + +from __future__ import annotations + +import json +import re +import urllib.request +from pathlib import Path + +# Minimum HA Core version to include in the test matrix +# This should be the oldest version we want to support +MIN_VERSION = (2024, 12) + + +def get_ha_core_versions() -> list[str]: + """Fetch latest stable HA Core versions from GitHub API.""" + all_tags = [] + page = 1 + + # Paginate through all tags to ensure we get older versions too + while True: + url = f"https://api.github.com/repos/home-assistant/core/tags?per_page=100&page={page}" + with urllib.request.urlopen(url) as response: # noqa: S310 + tags = json.loads(response.read().decode()) + + if not tags: + break + + all_tags.extend(tags) + + # Check if we've gone far enough back + # Stop if we've found versions older than our minimum + oldest_in_page = None + for t in tags: + if re.match(r"^\d+\.\d+\.\d+$", t["name"]): + parts = t["name"].split(".") + year, month = int(parts[0]), int(parts[1]) + if oldest_in_page is None or (year, month) < oldest_in_page: + oldest_in_page = (year, month) + + if oldest_in_page and oldest_in_page < MIN_VERSION: + break + + page += 1 + if page > 10: # Safety limit + break + + # Filter to stable releases only (no beta/rc) + stable_pattern = re.compile(r"^\d+\.\d+\.\d+$") + versions = [t["name"] for t in all_tags if stable_pattern.match(t["name"])] + + # Group by minor version and get latest patch for each + latest: dict[str, str] = {} + for version in versions: + parts = version.split(".") + year, month = int(parts[0]), int(parts[1]) + # Only include versions >= MIN_VERSION + if (year, month) >= MIN_VERSION: + minor_key = f"{parts[0]}.{parts[1]}" + # Keep the one with highest patch number + if minor_key not in latest: + latest[minor_key] = version + else: + current_patch = int(latest[minor_key].split(".")[2]) + new_patch = int(parts[2]) + if new_patch > current_patch: + latest[minor_key] = version + + # Sort by version + return sorted(latest.values(), key=lambda v: [int(x) for x in v.split(".")]) + + +def get_python_version(ha_version: str) -> str: + """Determine Python version based on HA Core version.""" + parts = ha_version.split(".") + year, month = int(parts[0]), int(parts[1]) + # 2024.x and 2025.1 use Python 3.12, 2025.2+ use Python 3.13 + if year == 2024 or (year == 2025 and month == 1): + return "3.12" + return "3.13" + + +def generate_matrix_yaml(versions: list[str]) -> str: + """Generate the YAML matrix include block.""" + lines = [] + for version in versions: + python_ver = get_python_version(version) + lines.append(f' - core-version: "{version}"') + lines.append(f' python-version: "{python_ver}"') + # Add dev version + lines.append(' - core-version: "dev"') + lines.append(' python-version: "3.13"') + return "\n".join(lines) + + +def update_workflow_file(workflow_path: Path, new_matrix: str) -> bool: + """Update the workflow file with new matrix. Returns True if changed.""" + content = workflow_path.read_text() + + # Pattern to match the matrix include block + # Matches from "include:" to just before " steps:" + pattern = re.compile( + r"( include:\n)(.*?)( steps:)", + re.DOTALL, + ) + + def replacer(match: re.Match) -> str: + return f"{match.group(1)}{new_matrix}\n{match.group(3)}" + + new_content = pattern.sub(replacer, content) + + if new_content == content: + return False + + workflow_path.write_text(new_content) + return True + + +def main() -> None: + """Main entry point.""" + print("Fetching latest HA Core versions...") # noqa: T201 + versions = get_ha_core_versions() + print(f"Found {len(versions)} versions: {', '.join(versions)}") # noqa: T201 + + print("\nGenerating matrix...") # noqa: T201 + matrix = generate_matrix_yaml(versions) + print(matrix) # noqa: T201 + + workflow_path = Path(__file__).parent.parent / ".github/workflows/pytest.yaml" + print(f"\nUpdating {workflow_path}...") # noqa: T201 + + if update_workflow_file(workflow_path, matrix): + print("Workflow updated successfully!") # noqa: T201 + else: + print("No changes needed.") # noqa: T201 + + +if __name__ == "__main__": + main() From 7a5ad22c627176a8687e1ed2f4da50d89452868b Mon Sep 17 00:00:00 2001 From: lenucksi Date: Sun, 30 Nov 2025 17:14:27 +0100 Subject: [PATCH 0915/1077] fix(devcontainer): install uv and add venv cleanup (#1317) --- scripts/setup-devcontainer | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/setup-devcontainer b/scripts/setup-devcontainer index f16c2cbd..fb5bf472 100755 --- a/scripts/setup-devcontainer +++ b/scripts/setup-devcontainer @@ -10,9 +10,12 @@ fi pip install \ colorlog \ pip \ - ruff + ruff \ + uv -uv venv --python 3.13 +pip cache purge + +uv venv --clear --python 3.13 ./scripts/setup-dependencies ./scripts/setup-symlinks uv run pre-commit install-hooks From fb6e96c87892fe5553b4da6afce3424a92637558 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 30 Nov 2025 08:16:05 -0800 Subject: [PATCH 0916/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/checkout=20action=20to=20v6=20(#1324)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/update-test-matrix.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-test-matrix.yaml b/.github/workflows/update-test-matrix.yaml index 658a5c4e..26c63469 100644 --- a/.github/workflows/update-test-matrix.yaml +++ b/.github/workflows/update-test-matrix.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v5 From 1be701994b9994fd86e4cff170001ea7922d83c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 30 Nov 2025 08:16:24 -0800 Subject: [PATCH 0917/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Pin=20python=20t?= =?UTF-8?q?o=203.12.12=20(#1323)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/update-test-matrix.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-test-matrix.yaml b/.github/workflows/update-test-matrix.yaml index 26c63469..9d154c27 100644 --- a/.github/workflows/update-test-matrix.yaml +++ b/.github/workflows/update-test-matrix.yaml @@ -21,7 +21,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: "3.12.12" - name: Update test matrix run: python scripts/update-test-matrix.py From f04c91b54b043f725d3340d2ab00bbaa495a16a9 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 30 Nov 2025 08:18:36 -0800 Subject: [PATCH 0918/1077] docs: add lenucksi as a contributor for code (#1325) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 21c19107..00513fec 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1161,6 +1161,15 @@ "contributions": [ "code" ] + }, + { + "login": "lenucksi", + "name": "lenucksi", + "avatar_url": "https://avatars.githubusercontent.com/u/2451899?v=4", + "profile": "https://github.com/lenucksi", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 27d79686..f01db63a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-127-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-128-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -631,6 +631,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 077e086424d9e189e3bbbc7d70813caa8c29e997 Mon Sep 17 00:00:00 2001 From: lenucksi Date: Sun, 30 Nov 2025 17:22:57 +0100 Subject: [PATCH 0919/1077] fix(ci): correct branch name and path in update-readme workflow (#1321) --- .github/workflows/update-readme.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index 7cdb8e24..fb8fa6f4 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -3,11 +3,11 @@ name: Update README.md, strings.json, and services.yaml on: push: branches: - - master + - main paths: - "README.md" - "custom_components/adaptive_lighting/const.py" - - "github/workflows/update-readme.yml" + - ".github/workflows/update-readme.yml" pull_request: jobs: From 2f03e00dc685dce73426f001af213cab693b925f Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Mon, 1 Dec 2025 07:53:52 +0100 Subject: [PATCH 0920/1077] Translated using Weblate (Danish) (#1326) Currently translated at 94.1% (144 of 153 strings) Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/da/ Translation: Adaptive Lighting/Adaptive Lighting Co-authored-by: Hans Henrik Juhl --- .../adaptive_lighting/translations/da.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/da.json b/custom_components/adaptive_lighting/translations/da.json index 347c3f5c..f89bd614 100644 --- a/custom_components/adaptive_lighting/translations/da.json +++ b/custom_components/adaptive_lighting/translations/da.json @@ -41,7 +41,10 @@ "transition": "Overgangsperiode når en ændring i lyset udføres (i sekunder)", "transition_until_sleep": "overgang_til_sove: Når aktiveret, vil adaptiv belysning behandle søvnindstillinger som minimum, og overgår til disse værdier efter solnedgang. 🌙", "adapt_only_on_bare_turn_on": "tilpas_kun_ved_enkelt_tænd: Når du tænder lys for første gang. Hvis indstillet til 'true', tilpasser AL kun, hvis 'lys.tænd' er kaldt uden at angive farve eller lysstyrke. ❌🌈 Dette forhindrer f.eks. tilpasning, når du aktiverer en scene. Hvis indstillet til 'false' tilpasser AL sig uanset tilstanden af farve eller lysstyrke i den oprindelige 'service_data'. Har brug for at 'take_over_control' er aktiveret. 🕵️", - "include_config_in_attributes": "include_config_in_attributes: Vis alle indstillinger som attributter for kontakten når dette er sat til »true«. 📝" + "include_config_in_attributes": "include_config_in_attributes: Vis alle indstillinger som attributter for kontakten når dette er sat til »true«. 📝", + "skip_redundant_commands": "skip_redundant_commands: Undlad at sende tilpasningskommando, hvis lampens kendte tilstand allerede er lig den ønskede tilstand. Mindsker mængden af netværkstrafik og forbedrer tilpasningens responsivitet i visse situationer. 📉 Slå fra, hvis lampens faktiske tilstand kommer ud af takt med den tilstand, som HA rapporterer.", + "intercept": "intercept: Indfang og tilpas »light.turn_on«-kald for at muliggøre øjeblikkelig farve- og lysstyrketilpasning. 🏎️ Slå fra for lyskilder, som ikke understøtter »light.turn_on« med farve og lysstyrke.", + "multi_light_intercept": "multi_light_intercept: Indfang og tilpas »light.turn_on«-kald til mere end en enkelt lyskilde. ➗⚠️ Dette kan bevirke at et enkelt »light.turn_on«-kald deles op i flere, f.eks. hvis lyskilderne er forbundet til forskellige kontakter. Forudsætter at »intercept« er slået til." }, "data_description": { "interval": "Frekvens til at tilpasse lysene, i sekunder. 🔄", @@ -62,7 +65,10 @@ "sleep_color_temp": "Farvetemperatur i søvntilstand (bruges når `sleep_rgb_or_color_temp` er `color_temp`) i Kelvin. 😴", "brightness_mode": "Lysstyrketilstand til brug. Mulige værdier er \"default\", \"linear\" og \"tanh\" (bruger \"brightness_mode_time_dark\" og \"brightness_mode_time_light\"). 📈", "send_split_delay": "Forsinkelse (ms) mellem »separate_turn_on_commands« for lyskilder som ikke understøtter simultane styrke- og farveindstillinger. ⏲️", - "initial_transition": "Den første overgangs varighed når lysene ændres fra »off« til »on« i sekunder. ⏲️" + "initial_transition": "Den første overgangs varighed når lysene ændres fra »off« til »on« i sekunder. ⏲️", + "sleep_rgb_color": "RGB-farve i søvntilstand (anvendes når »sleep_rgb_or_color_temp« er sat til »rgb_color«). 🌈", + "brightness_mode_time_dark": "(Ignoreres hvis »brightness_mode='default'«) Varigheden i sekunder for tilpasningen af lysstyrken ved solopgang eller -nedgang. 📈📉", + "brightness_mode_time_light": "(Ignoreres hvis »brightness_mode='default'«) Varigheden i sekunder for tilpasningen af lysstyrken ved solopgang eller -nedgang. 📈📉" } } }, From 886c77bcc23fd955bf4cda6ddb010259fda58a41 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 30 Nov 2025 22:54:08 -0800 Subject: [PATCH 0921/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/setup-python=20action=20to=20v6=20(#1327)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/update-test-matrix.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-test-matrix.yaml b/.github/workflows/update-test-matrix.yaml index 9d154c27..8a0e88e9 100644 --- a/.github/workflows/update-test-matrix.yaml +++ b/.github/workflows/update-test-matrix.yaml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.12.12" From 4aeb50bfe346f53a444f86de24304dcf79ffecc9 Mon Sep 17 00:00:00 2001 From: edgimar Date: Fri, 5 Dec 2025 07:15:24 -0500 Subject: [PATCH 0922/1077] feat: add option to duplicate existing lighting instance (#1329) * feat: add option to duplicate existing lighting instance A menu step in the config flow was added that allows a user to create a new instance or duplicate the options of an existing one. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add type annotation, tests, and translations for duplicate feature - Add type annotation for source_options class attribute - Add menu step translations to en.json - Add tests for menu display, new instance creation, and duplication * Simplify source_options access with class-level default --------- Co-authored-by: Bas Nijholt --- .../adaptive_lighting/config_flow.py | 37 ++++++- .../adaptive_lighting/strings.json | 7 ++ .../adaptive_lighting/translations/en.json | 7 ++ tests/test_config_flow.py | 102 ++++++++++++++++++ 4 files changed, 152 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index d1799888..12f1f65e 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -27,14 +27,49 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + source_options: dict[str, Any] | None = None + async def async_step_user(self, user_input: dict[str, Any] | None = None): """Handle the initial step.""" + if user_input is None and self._async_current_entries(): + return await self.async_step_menu() + return await self.async_step_wait_for_name(user_input) + + async def async_step_menu(self, user_input: dict[str, Any] | None = None): + """Handle the menu step.""" + if user_input is not None: + if user_input["action"] != "new": + entry_id = user_input["action"] + entry = self.hass.config_entries.async_get_entry(entry_id) + if entry: + self.source_options = dict(entry.options) + return await self.async_step_wait_for_name() + + entries = self._async_current_entries() + options = {"new": "Create new instance"} + for entry in entries: + options[entry.entry_id] = f"Duplicate '{entry.title}'" + + return self.async_show_form( + step_id="menu", + data_schema=vol.Schema( + {vol.Required("action", default="new"): vol.In(options)}, + ), + ) + + async def async_step_wait_for_name(self, user_input: dict[str, Any] | None = None): + """Handle the name step.""" errors: dict[str, str] = {} if user_input is not None: await self.async_set_unique_id(user_input[CONF_NAME]) self._abort_if_unique_id_configured() - return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) + options = self.source_options + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + options=options, + ) return self.async_show_form( step_id="user", diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index a2724f6b..1a9ba06c 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -7,6 +7,13 @@ "data": { "name": "Name" } + }, + "menu": { + "title": "Create or Duplicate", + "description": "Do you want to create a new instance or duplicate an existing one?", + "data": { + "action": "Action" + } } }, "abort": { diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 9991d232..fcb2ed8b 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -8,6 +8,13 @@ "data": { "name": "Name" } + }, + "menu": { + "title": "Create or Duplicate", + "description": "Do you want to create a new instance or duplicate an existing one?", + "data": { + "action": "Action" + } } }, "abort": { diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 02ed6d42..8a09265e 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -145,3 +145,105 @@ async def test_options_flow_for_yaml_import(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result.get("data_schema") is None + + +async def test_menu_shown_when_entries_exist(hass): + """Test that menu step is shown when existing entries exist.""" + # Create an existing entry + entry = MockConfigEntry( + domain=DOMAIN, + title="existing", + data={CONF_NAME: "existing"}, + options={"min_brightness": 10}, + ) + entry.add_to_hass(hass) + + # Start a new config flow - should show menu + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "user"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "menu" + + +async def test_menu_create_new_instance(hass): + """Test creating a new instance through the menu.""" + # Create an existing entry + entry = MockConfigEntry( + domain=DOMAIN, + title="existing", + data={CONF_NAME: "existing"}, + options={"min_brightness": 10}, + ) + entry.add_to_hass(hass) + + # Start config flow - shows menu + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "user"}, + ) + assert result["step_id"] == "menu" + + # Choose to create new instance + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"action": "new"}, + ) + + # Should show name form + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + # Enter name + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_NAME: "new instance"}, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "new instance" + # New instance should have no options (not duplicated) + assert result["options"] == {} + + +async def test_menu_duplicate_instance(hass): + """Test duplicating an existing instance through the menu.""" + # Create an existing entry with custom options + source_options = {"min_brightness": 20, "max_brightness": 80} + entry = MockConfigEntry( + domain=DOMAIN, + title="source", + data={CONF_NAME: "source"}, + options=source_options, + ) + entry.add_to_hass(hass) + + # Start config flow - shows menu + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "user"}, + ) + assert result["step_id"] == "menu" + + # Choose to duplicate existing entry + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"action": entry.entry_id}, + ) + + # Should show name form + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + # Enter name for duplicated instance + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_NAME: "duplicated"}, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "duplicated" + # Duplicated instance should have copied options + assert result["options"] == source_options From ea623ce6040e0de068d8bb91da375aa60c85cd11 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 04:15:38 -0800 Subject: [PATCH 0923/1077] docs: add edgimar as a contributor for code (#1330) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 00513fec..9a04d160 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1170,6 +1170,15 @@ "contributions": [ "code" ] + }, + { + "login": "edgimar", + "name": "edgimar", + "avatar_url": "https://avatars.githubusercontent.com/u/393850?v=4", + "profile": "https://gitlab.com/edgimar", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index f01db63a..6102c85c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-128-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-129-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -632,6 +632,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From ef15d5cd48a263afd0d3bf4aac46748f4489d6cb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 23:31:05 -0800 Subject: [PATCH 0924/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20peter-e?= =?UTF-8?q?vans/create-pull-request=20action=20to=20v8=20(#1334)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/update-test-matrix.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-test-matrix.yaml b/.github/workflows/update-test-matrix.yaml index 8a0e88e9..af1c5dce 100644 --- a/.github/workflows/update-test-matrix.yaml +++ b/.github/workflows/update-test-matrix.yaml @@ -39,7 +39,7 @@ jobs: - name: Create Pull Request if: steps.changes.outputs.changed == 'true' - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@v8 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: "ci: update HA Core test matrix versions" From 97ecbb17a0ff896d84ed0576e3319a3083d49ac0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 23:31:17 -0800 Subject: [PATCH 0925/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/checkout=20action=20to=20v6.0.1=20(#1328)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/hassfest.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml index 69f93e15..58a152e4 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/hassfest.yaml @@ -11,5 +11,5 @@ jobs: validate_hassfest: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v6.0.0" + - uses: "actions/checkout@v6.0.1" - uses: home-assistant/actions/hassfest@master From 37ef65586d2c6bcaa176c40d5f45ddc6c88a4dc6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 23:31:41 -0800 Subject: [PATCH 0926/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20python?= =?UTF-8?q?=20to=20v3.14.2=20(#1251)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/deploy-webapp.yml | 2 +- .github/workflows/update-test-matrix.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml index 698d8fec..0a76e3ac 100644 --- a/.github/workflows/deploy-webapp.yml +++ b/.github/workflows/deploy-webapp.yml @@ -35,7 +35,7 @@ jobs: - name: Set Up Python uses: actions/setup-python@v6 with: - python-version: 3.13.5 + python-version: 3.14.2 - name: Install Dependencies run: | diff --git a/.github/workflows/update-test-matrix.yaml b/.github/workflows/update-test-matrix.yaml index af1c5dce..5f948a67 100644 --- a/.github/workflows/update-test-matrix.yaml +++ b/.github/workflows/update-test-matrix.yaml @@ -21,7 +21,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.12.12" + python-version: "3.14.2" - name: Update test matrix run: python scripts/update-test-matrix.py From 0180fa4a937cede4c86eef344358ede83f81f337 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 9 Dec 2025 23:51:17 -0800 Subject: [PATCH 0927/1077] feat: use EntitySelector for light entity selection with search support (#1335) Replace cv.multi_select with HA's EntitySelector for the lights field in the options flow. This provides: - Built-in search/typing to find entities faster - Better UX with HA's native entity picker - Improved handling of renamed entities Closes #1208 --- .../adaptive_lighting/config_flow.py | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 12f1f65e..5e21e67b 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -3,11 +3,11 @@ import logging from typing import Any -import homeassistant.helpers.config_validation as cv import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_NAME, MAJOR_VERSION, MINOR_VERSION from homeassistant.core import callback +from homeassistant.helpers.selector import EntitySelector, EntitySelectorConfig from .const import ( # pylint: disable=unused-import CONF_LIGHTS, @@ -16,8 +16,7 @@ from .const import ( # pylint: disable=unused-import NONE_STR, VALIDATION_TUPLES, ) -from .helpers import get_friendly_name -from .switch import _supported_features, validate +from .switch import validate _LOGGER = logging.getLogger(__name__) @@ -146,12 +145,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if not errors: return self.async_create_entry(title="", data=user_input) - all_lights_with_names = { - light: get_friendly_name(self.hass, light) - for light in self.hass.states.async_entity_ids("light") - if _supported_features(self.hass, light) - } - all_lights = list(all_lights_with_names.keys()) + # Validate that all configured lights still exist + all_lights = set(self.hass.states.async_entity_ids("light")) for configured_light in data[CONF_LIGHTS]: if configured_light not in all_lights: errors = {CONF_LIGHTS: "entity_missing"} @@ -160,14 +155,15 @@ class OptionsFlowHandler(config_entries.OptionsFlow): data[CONF_NAME], configured_light, ) - all_lights.append(configured_light) - all_lights_with_names[configured_light] = configured_light - light_options = { - entity_id: f"{name} ({entity_id})" - for entity_id, name in all_lights_with_names.items() + to_replace = { + CONF_LIGHTS: EntitySelector( + EntitySelectorConfig( + domain="light", + multiple=True, + ), + ), } - to_replace = {CONF_LIGHTS: cv.multi_select(light_options)} options_schema = {} for name, default, validation in VALIDATION_TUPLES: From dc61bd191cf6be25361b3d24902ea721481fb17f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 9 Dec 2025 23:58:19 -0800 Subject: [PATCH 0928/1077] Bump to v1.29.0 --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 1d1e431a..97d79950 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.28.0" + "version": "1.29.0" } From 94b91a0ddca39989383c486691d0d971451403ca Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 12 Dec 2025 13:05:01 -0800 Subject: [PATCH 0929/1077] fix: use correct entity and context in service interceptor (#1349) --- custom_components/adaptive_lighting/switch.py | 19 ++- tests/test_switch.py | 142 ++++++++++++++++++ 2 files changed, 157 insertions(+), 4 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index dbb72ea1..2d667d97 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1746,6 +1746,7 @@ class AdaptiveLightingManager: ] self._proactively_adapting_contexts: dict[str, str] = {} + self._context_cnt: int = 0 try: self.listener_removers.append( @@ -1809,6 +1810,16 @@ class AdaptiveLightingManager: for key in keys: self._proactively_adapting_contexts.pop(key) + def create_context( + self, + which: str = "default", + parent: Context | None = None, + ) -> Context: + """Create a context that identifies this integration.""" + context = create_context("manager", which, self._context_cnt, parent=parent) + self._context_cnt += 1 + return context + def _separate_entity_ids( self, entity_ids: list[str], @@ -2047,7 +2058,7 @@ class AdaptiveLightingManager: assert set(skipped) == set(entity_ids) return # The call will be intercepted with the original data # Call light turn_on service for skipped entities - context = switch.create_context("skipped") + context = self.create_context("skipped") _LOGGER.debug( "(5) _service_interceptor_turn_on_handler: calling `light.turn_on` with skipped='%s', service_data: '%s', context='%s'", skipped, @@ -2081,11 +2092,11 @@ class AdaptiveLightingManager: # `state_changed_event_listener`, however, this function is called # before that one. self.reset(*entity_ids, reset_manual_control=False) - for entity_id in entity_ids: - self.clear_proactively_adapting(entity_id) + for eid in entity_ids: + self.clear_proactively_adapting(eid) adaptation_data = await switch.prepare_adaptation_data( - entity_id, + entity_ids[0], transition, ) if adaptation_data is None: diff --git a/tests/test_switch.py b/tests/test_switch.py index e85cecb9..372fdd66 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -71,6 +71,7 @@ from homeassistant.components.adaptive_lighting.switch import ( create_context, is_our_context, is_our_context_id, + short_hash, ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -2462,3 +2463,144 @@ def test_attributes_have_changed_light_mode_switch(): new_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, **kwargs_no_adapt, ), "RGB → color_temp should not be detected when adapt_color=False" + + +# Regression tests for bugs found in PR #1348 by @protyposis +# See: https://github.com/basnijholt/adaptive-lighting/pull/1348 + + +async def test_multi_light_intercept_prepares_adaptation_for_first_entity(hass): + """Test that adaptation data is prepared for the first entity, not the last. + + Regression test for a bug where `entity_id` from a for-loop was used after + the loop ended, causing `prepare_adaptation_data` to be called with only + the last entity's ID instead of the first. + + In `_service_interceptor_turn_on_single_light_handler`: + ```python + for entity_id in entity_ids: + self.clear_proactively_adapting(entity_id) + + adaptation_data = await switch.prepare_adaptation_data( + entity_id, # BUG: This uses the last entity_id from the loop! + transition, + ) + ``` + + The adaptation data should be prepared for the first entity in the list since + that's the one whose service call is being intercepted and modified. + + See: https://github.com/basnijholt/adaptive-lighting/pull/1348 + """ + switch, _ = await setup_lights_and_switch(hass, {CONF_INTERCEPT: True}, True) + + # Turn off all lights first + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: [ENTITY_LIGHT_1, ENTITY_LIGHT_2, ENTITY_LIGHT_3]}, + blocking=True, + ) + await hass.async_block_till_done() + + # Mock prepare_adaptation_data to track which entity_id it's called with + original_prepare = switch.prepare_adaptation_data + called_with_entities = [] + + async def mock_prepare_adaptation_data(light, *args, **kwargs): + called_with_entities.append(light) + return await original_prepare(light, *args, **kwargs) + + switch.prepare_adaptation_data = mock_prepare_adaptation_data + + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 67, + ATTR_COLOR_TEMP_KELVIN: 3448, + "force_rgb_color": False, + }, + ) + + # Turn on multiple lights at once - this triggers the interceptor + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [ENTITY_LIGHT_1, ENTITY_LIGHT_2]}, + blocking=True, + ) + await hass.async_block_till_done() + + # The bug causes prepare_adaptation_data to be called with the LAST entity + # (ENTITY_LIGHT_2) instead of the FIRST entity (ENTITY_LIGHT_1) + assert len(called_with_entities) >= 1, "prepare_adaptation_data should be called" + + # The first call should be for ENTITY_LIGHT_1 (the first entity in the list) + # since the intercepted service call will apply to all entities in entity_ids + # BUG: Currently this fails because entity_id is ENTITY_LIGHT_2 (the last one) + assert called_with_entities[0] == ENTITY_LIGHT_1, ( + f"prepare_adaptation_data should be called with the first entity " + f"({ENTITY_LIGHT_1}), but was called with {called_with_entities[0]}. " + f"This indicates the bug where the last entity from the for-loop is used." + ) + + +async def test_skipped_lights_context_not_from_arbitrary_switch(hass): + """Test that context for skipped lights uses manager, not an arbitrary switch. + + Regression test for a bug where the context for skipped lights was created + using `switch.create_context("skipped")` where `switch` was from the last + iteration of a for-loop, which had no relationship to the skipped lights. + + The fix uses `self.create_context("skipped")` on the AdaptiveLightingManager + instead, which uses "manager" as the context name. + + See: https://github.com/basnijholt/adaptive-lighting/pull/1348 + """ + # Setup two switches with different lights + lights, switch1, switch2 = await setup_proactive_multiple_lights_two_switches(hass) + + # Turn on all three lights at once: + # - ENTITY_LIGHT_1 is in switch1 + # - ENTITY_LIGHT_2 is in switch2 + # - ENTITY_LIGHT_3 is not in any switch (will be skipped) + events = await _turn_on_and_track_event_contexts( + hass, + "test_skipped_context", + lights, + return_full_events=True, + ) + + # Find the skipped event (contains ":skpp:" in context) + skipped_events = [e for e in events if ":skpp:" in e.context.id] + assert ( + len(skipped_events) == 1 + ), f"Expected 1 skipped event, got {len(skipped_events)}" + + skipped_event = skipped_events[0] + skipped_context_id = skipped_event.context.id + + # Extract the name_hash from the context + # Context format: {timestamp}:{al}:{name_hash}:{which_short}:{index} + context_parts = skipped_context_id.split(":") + assert len(context_parts) >= 4, f"Unexpected context format: {skipped_context_id}" + + # The context should still be recognized as ours + assert is_our_context_id(skipped_context_id), "Skipped context should be recognized" + assert is_our_context_id( + skipped_context_id, + "skipped", + ), "Skipped context should have 'skipped' marker" + + # Verify the skipped lights are the ones not in any switch + assert skipped_event.data["service_data"][ATTR_ENTITY_ID] == [ENTITY_LIGHT_3] + + # After the fix, the context should use "manager" as the name, not a switch name. + # The name_hash is the 3rd segment (index 2) in the context ID. + name_hash_in_context = context_parts[2] + expected_manager_hash = short_hash("manager") + assert name_hash_in_context == expected_manager_hash, ( + f"Skipped context should use 'manager' hash ({expected_manager_hash}), " + f"but got {name_hash_in_context}. This indicates the context is still " + f"being created from an arbitrary switch instead of the manager." + ) From ce7dadebddf4ef8f2d7b82aed82c12acfe3374c9 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 12 Dec 2025 13:29:46 -0800 Subject: [PATCH 0930/1077] ci: workaround aiodns/pycares compatibility issue (#1351) Upgrade aiodns after installing dependencies to fix compatibility issue with pycares. See: https://github.com/aio-libs/aiodns/issues/214 --- scripts/setup-dependencies | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/setup-dependencies b/scripts/setup-dependencies index f462335d..7722c58d 100755 --- a/scripts/setup-dependencies +++ b/scripts/setup-dependencies @@ -15,3 +15,7 @@ uv pip install $(python test_dependencies.py) # Install the latest mypy-dev (not pinned since old versions get deleted from PyPI) uv pip install --upgrade mypy-dev + +# Workaround for aiodns/pycares compatibility issue +# See: https://github.com/aio-libs/aiodns/issues/214 +uv pip install --upgrade aiodns From 5886eee04cecb30a0951ced38e53ce8edaaa50eb Mon Sep 17 00:00:00 2001 From: Mario Guggenberger Date: Fri, 12 Dec 2025 22:37:42 +0100 Subject: [PATCH 0931/1077] Code cleanup (#1348) --- .../adaptive_lighting/__init__.py | 2 +- .../adaptive_lighting/color_and_brightness.py | 76 ++++--- .../adaptive_lighting/config_flow.py | 21 +- custom_components/adaptive_lighting/switch.py | 192 +++++++++--------- tests/test_color_and_brightness.py | 11 +- 5 files changed, 155 insertions(+), 147 deletions(-) diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 44151c84..8e61e93b 100644 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_SOURCE from homeassistant.core import Event, HomeAssistant from .const import ( - _DOMAIN_SCHEMA, + _DOMAIN_SCHEMA, # pyright: ignore[reportPrivateUsage] ATTR_ADAPTIVE_LIGHTING_MANAGER, CONF_NAME, DOMAIN, diff --git a/custom_components/adaptive_lighting/color_and_brightness.py b/custom_components/adaptive_lighting/color_and_brightness.py index caee67c2..f3ae3efe 100644 --- a/custom_components/adaptive_lighting/color_and_brightness.py +++ b/custom_components/adaptive_lighting/color_and_brightness.py @@ -9,6 +9,7 @@ import logging import math from dataclasses import dataclass from datetime import UTC, timedelta +from enum import Enum from functools import cached_property, partial from typing import TYPE_CHECKING, Any, Literal, cast @@ -21,15 +22,19 @@ from homeassistant.util.color import ( if TYPE_CHECKING: import astral.location -# Same as homeassistant.const.SUN_EVENT_SUNRISE and homeassistant.const.SUN_EVENT_SUNSET -# We re-define them here to not depend on homeassistant in this file. -SUN_EVENT_SUNRISE = "sunrise" -SUN_EVENT_SUNSET = "sunset" -SUN_EVENT_NOON = "solar_noon" -SUN_EVENT_MIDNIGHT = "solar_midnight" +class SunEvent(str, Enum): + """A set of sun events that happen during a day.""" -_ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT) + # Same as homeassistant.const.SUN_EVENT_SUNRISE and homeassistant.const.SUN_EVENT_SUNSET + # We re-define them here to not depend on homeassistant in this file. + SUNRISE = "sunrise" + SUNSET = "sunset" + NOON = "solar_noon" + MIDNIGHT = "solar_midnight" + + +_ORDER = (SunEvent.SUNRISE, SunEvent.NOON, SunEvent.SUNSET, SunEvent.MIDNIGHT) _ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))} utcnow: partial[datetime.datetime] = partial(datetime.datetime.now, UTC) @@ -126,21 +131,21 @@ class SunEvents: noon = midnight + timedelta(hours=12) * (1 if midnight.hour < 12 else -1) return noon, midnight - def sun_events(self, dt: datetime.datetime) -> list[tuple[str, float]]: + def sun_events(self, dt: datetime.datetime) -> list[tuple[SunEvent, float]]: """Get the four sun event's timestamps at 'dt'.""" sunrise = self.sunrise(dt) sunset = self.sunset(dt) solar_noon, solar_midnight = self.noon_and_midnight(dt, sunset, sunrise) - events = [ - (SUN_EVENT_SUNRISE, sunrise.timestamp()), - (SUN_EVENT_SUNSET, sunset.timestamp()), - (SUN_EVENT_NOON, solar_noon.timestamp()), - (SUN_EVENT_MIDNIGHT, solar_midnight.timestamp()), + events: list[tuple[SunEvent, float]] = [ + (SunEvent.SUNRISE, sunrise.timestamp()), + (SunEvent.SUNSET, sunset.timestamp()), + (SunEvent.NOON, solar_noon.timestamp()), + (SunEvent.MIDNIGHT, solar_midnight.timestamp()), ] self._validate_sun_event_order(events) return events - def _validate_sun_event_order(self, events: list[tuple[str, float]]) -> None: + def _validate_sun_event_order(self, events: list[tuple[SunEvent, float]]) -> None: """Check if the sun events are in the expected order.""" events = sorted(events, key=lambda x: x[1]) events_names, _ = zip(*events, strict=True) @@ -154,7 +159,10 @@ class SunEvents: _LOGGER.error(msg) raise ValueError(msg) - def prev_and_next_events(self, dt: datetime.datetime) -> list[tuple[str, float]]: + def prev_and_next_events( + self, + dt: datetime.datetime, + ) -> list[tuple[SunEvent, float]]: """Get the previous and next sun event.""" events = [ event @@ -171,23 +179,26 @@ class SunEvents: (_, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt) h, x = ( (prev_ts, next_ts) - if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE) + if next_event in (SunEvent.SUNSET, SunEvent.SUNRISE) else (next_ts, prev_ts) ) # k = -1 between sunset and sunrise (sun below horizon) # k = 1 between sunrise and sunset (sun above horizon) - k = 1 if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_NOON) else -1 + k = 1 if next_event in (SunEvent.SUNSET, SunEvent.NOON) else -1 return k * (1 - ((target_ts - h) / (h - x)) ** 2) - def closest_event(self, dt: datetime.datetime) -> tuple[str, float]: + def closest_event( + self, + dt: datetime.datetime, + ) -> tuple[Literal[SunEvent.SUNRISE, SunEvent.SUNSET], float]: """Get the closest sunset or sunrise event.""" (prev_event, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt) - if SUN_EVENT_SUNRISE in (prev_event, next_event): - ts_event = prev_ts if prev_event == SUN_EVENT_SUNRISE else next_ts - return SUN_EVENT_SUNRISE, ts_event - if SUN_EVENT_SUNSET in (prev_event, next_event): - ts_event = prev_ts if prev_event == SUN_EVENT_SUNSET else next_ts - return SUN_EVENT_SUNSET, ts_event + if SunEvent.SUNRISE in (prev_event, next_event): + ts_event = prev_ts if prev_event == SunEvent.SUNRISE else next_ts + return SunEvent.SUNRISE, ts_event + if SunEvent.SUNSET in (prev_event, next_event): + ts_event = prev_ts if prev_event == SunEvent.SUNSET else next_ts + return SunEvent.SUNSET, ts_event msg = "No sunrise or sunset event found." raise ValueError(msg) @@ -249,7 +260,7 @@ class SunLightSettings: event, ts_event = self.sun.closest_event(dt) dark = self.brightness_mode_time_dark.total_seconds() light = self.brightness_mode_time_light.total_seconds() - if event == SUN_EVENT_SUNRISE: + if event == SunEvent.SUNRISE: brightness = scaled_tanh( dt.timestamp() - ts_event, x1=-dark, @@ -259,7 +270,7 @@ class SunLightSettings: y_min=self.min_brightness, y_max=self.max_brightness, ) - elif event == SUN_EVENT_SUNSET: + elif event == SunEvent.SUNSET: brightness = scaled_tanh( dt.timestamp() - ts_event, x1=-light, # shifted timestamp for the start of sunset @@ -269,6 +280,9 @@ class SunLightSettings: y_min=self.min_brightness, y_max=self.max_brightness, ) + else: + msg = "Unsupported sun event" + raise ValueError(msg) return clamp(brightness, self.min_brightness, self.max_brightness) def _brightness_pct_linear(self, dt: datetime.datetime) -> float: @@ -277,7 +291,7 @@ class SunLightSettings: # at ts_event + dt_end, brightness == end_brightness dark = self.brightness_mode_time_dark.total_seconds() light = self.brightness_mode_time_light.total_seconds() - if event == SUN_EVENT_SUNRISE: + if event == SunEvent.SUNRISE: brightness = lerp( dt.timestamp() - ts_event, x1=-dark, @@ -285,7 +299,7 @@ class SunLightSettings: y1=self.min_brightness, y2=self.max_brightness, ) - elif event == SUN_EVENT_SUNSET: + elif event == SunEvent.SUNSET: brightness = lerp( dt.timestamp() - ts_event, x1=-light, @@ -293,6 +307,9 @@ class SunLightSettings: y1=self.max_brightness, y2=self.min_brightness, ) + else: + msg = "Unsupported sun event" + raise ValueError(msg) return clamp(brightness, self.min_brightness, self.max_brightness) def brightness_pct(self, dt: datetime.datetime, is_sleep: bool) -> float | None: @@ -356,7 +373,8 @@ class SunLightSettings: force_rgb_color = True else: color_temp_kelvin = self.color_temp_kelvin(sun_position) - rgb_color = color_temperature_to_rgb(color_temp_kelvin) + r, g, b = color_temperature_to_rgb(color_temp_kelvin) + rgb_color = (round(r), round(g), round(b)) # backwards compatibility for versions < 1.3.1 - see #403 color_temp_mired: float = math.floor(1000000 / color_temp_kelvin) xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color) diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 5e21e67b..5b0dd13c 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -5,7 +5,7 @@ from typing import Any import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_NAME, MAJOR_VERSION, MINOR_VERSION +from homeassistant.const import CONF_NAME from homeassistant.core import callback from homeassistant.helpers.selector import EntitySelector, EntitySelectorConfig @@ -96,13 +96,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: config_entries.ConfigEntry, # noqa: ARG004 ) -> "OptionsFlowHandler": """Get the options flow for this handler.""" - if (MAJOR_VERSION, MINOR_VERSION) >= (2024, 12): - # https://github.com/home-assistant/core/pull/129651 - return OptionsFlowHandler() - return OptionsFlowHandler(config_entry) + return OptionsFlowHandler() def validate_options(user_input: dict[str, Any], errors: dict[str, str]) -> None: @@ -125,21 +122,13 @@ def validate_options(user_input: dict[str, Any], errors: dict[str, str]) -> None class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for Adaptive Lighting.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize options flow.""" - if (MAJOR_VERSION, MINOR_VERSION) >= (2024, 12): - super().__init__(*args, **kwargs) - # https://github.com/home-assistant/core/pull/129651 - else: - self.config_entry = args[0] - async def async_step_init(self, user_input: dict[str, Any] | None = None): """Handle options flow.""" conf = self.config_entry data = validate(conf) if conf.source == config_entries.SOURCE_IMPORT: return self.async_show_form(step_id="init", data_schema=None) - errors = {} + errors: dict[str, str] = {} if user_input is not None: validate_options(user_input, errors) if not errors: @@ -156,7 +145,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): configured_light, ) - to_replace = { + to_replace: dict[str, Any] = { CONF_LIGHTS: EntitySelector( EntitySelectorConfig( domain="light", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 2d667d97..9217e8d9 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -44,8 +44,6 @@ from homeassistant.const import ( EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_STARTED, EVENT_STATE_CHANGED, - MAJOR_VERSION, - MINOR_VERSION, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -62,14 +60,10 @@ from homeassistant.core import ( callback, ) from homeassistant.helpers import entity_platform, entity_registry +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_component import async_update_entity - -if [MAJOR_VERSION, MINOR_VERSION] < [2023, 9]: - from homeassistant.helpers.entity import DeviceInfo -else: - from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.event import ( + EventStateChangedData, async_track_state_change_event, async_track_time_interval, ) @@ -166,6 +160,7 @@ if TYPE_CHECKING: from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity_platform import AddEntitiesCallback + from homeassistant.helpers.typing import NoEventData, VolDictType _LOGGER = logging.getLogger(__name__) @@ -228,11 +223,11 @@ def _switches_with_lights( hass: HomeAssistant, lights: list[str], expand_light_groups: bool = True, -) -> list[AdaptiveSwitch]: +) -> AdaptiveSwitches: """Get all switches that control at least one of the lights passed.""" config_entries = hass.config_entries.async_entries(DOMAIN) data = hass.data[DOMAIN] - switches: list[AdaptiveSwitch] = [] + switches: AdaptiveSwitches = [] all_check_lights = ( _expand_light_groups(hass, lights) if expand_light_groups else set(lights) ) @@ -285,7 +280,7 @@ def _switch_with_lights( def _switches_from_service_call( hass: HomeAssistant, service_call: ServiceCall, -) -> list[AdaptiveSwitch]: +) -> AdaptiveSwitches: data = service_call.data lights = data[CONF_LIGHTS] switch_entity_ids: list[str] | None = data.get("entity_id") @@ -307,7 +302,7 @@ def _switches_from_service_call( f" Invalid service data received: {service_call.data}" ) raise ValueError(msg) - switches = [] + switches: AdaptiveSwitches = [] ent_reg = entity_registry.async_get(hass) for entity_id in switch_entity_ids: ent_entry = ent_reg.async_get(entity_id) @@ -536,7 +531,7 @@ async def async_setup_entry( # noqa: PLR0915 schema=SET_MANUAL_CONTROL_SCHEMA, ) - args = {vol.Optional(CONF_USE_DEFAULTS, default="current"): cv.string} + args: VolDictType = {vol.Optional(CONF_USE_DEFAULTS, default="current"): cv.string} # Modifying these after init isn't possible skip = (CONF_INTERVAL, CONF_NAME, CONF_LIGHTS) for k, _, valid in VALIDATION_TUPLES: @@ -583,7 +578,10 @@ def validate( return data -def _is_state_event(event: Event, from_or_to_state: Iterable[str]) -> bool: +def _is_state_event( + event: Event[EventStateChangedData], + from_or_to_state: Iterable[str], +) -> bool: """Match state event when either 'from_state' or 'to_state' matches.""" return ( (old_state := event.data.get("old_state")) is not None @@ -625,7 +623,9 @@ def _is_light_group(state: State) -> bool: def _supported_features(hass: HomeAssistant, light: str) -> set[str]: state = hass.states.get(light) assert state is not None - supported_features = int(state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)) # type: ignore[arg-type] + supported_features = int( + state.attributes.get(ATTR_SUPPORTED_FEATURES, 0), + ) # type: ignore[arg-type] assert isinstance(supported_features, int) supported: set[str] = set() @@ -633,7 +633,10 @@ def _supported_features(hass: HomeAssistant, light: str) -> set[str]: if supported_features & LightEntityFeature.TRANSITION: supported.add("transition") - supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES, set()) # type: ignore[arg-type] + supported_color_modes = state.attributes.get( + ATTR_SUPPORTED_COLOR_MODES, + set(), + ) # type: ignore[arg-type] color_modes = { ColorMode.RGB, ColorMode.RGBW, @@ -920,7 +923,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._only_once = data[CONF_ONLY_ONCE] self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR] self._separate_turn_on_commands = data[CONF_SEPARATE_TURN_ON_COMMANDS] - self._transition = data[CONF_TRANSITION] + self._transition: int = data[CONF_TRANSITION] self._adapt_delay = data[CONF_ADAPT_DELAY] self._send_split_delay = data[CONF_SEND_SPLIT_DELAY] self._take_over_control = data[CONF_TAKE_OVER_CONTROL] @@ -1040,29 +1043,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) self.lights = list(all_lights) - async def _setup_listeners(self, _=None) -> None: + async def _setup_listeners(self, _: Event[NoEventData] | None = None) -> None: _LOGGER.debug("%s: Called '_setup_listeners'", self._name) if not self.is_on or not self.hass.is_running: _LOGGER.debug("%s: Cancelled '_setup_listeners'", self._name) return - while not all( - sw._state is not None - for sw in [ - self.sleep_mode_switch, - self.adapt_brightness_switch, - self.adapt_color_switch, - ] - ): - # Waits until `async_added_to_hass` is done, which in SimpleSwitch - # is when `_state` is set to `True` or `False`. - # Fixes first issue in https://github.com/basnijholt/adaptive-lighting/issues/682 - _LOGGER.debug( - "%s: Waiting for simple switches to be initialized", - self._name, - ) - await asyncio.sleep(0.1) - assert not self.remove_listeners self._update_time_interval_listener() @@ -1449,7 +1435,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if force: filtered_lights = on_lights else: - filtered_lights = [] + filtered_lights: list[str] = [] for light in on_lights: # Don't adapt lights that haven't finished prior transitions. timer = self.manager.transition_timers.get(light) @@ -1486,7 +1472,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): adapt_color = self.adapt_color_switch.is_on assert isinstance(adapt_brightness, bool) assert isinstance(adapt_color, bool) - tasks = [] + tasks: list[asyncio.Task[None]] = [] for light in filtered_lights: manually_controlled = ( self._take_over_control @@ -1541,7 +1527,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if tasks: await asyncio.gather(*tasks) - async def _respond_to_off_to_on_event(self, entity_id: str, event: Event) -> None: + async def _respond_to_off_to_on_event( + self, + entity_id: str, + event: Event[EventStateChangedData], + ) -> None: assert not self.manager.is_proactively_adapting(event.context.id) from_turn_on = self.manager._off_to_on_state_event_is_from_turn_on( entity_id, @@ -1597,7 +1587,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): force=True, ) - async def _sleep_mode_switch_state_event_action(self, event: Event) -> None: + async def _sleep_mode_switch_state_event_action( + self, + event: Event[EventStateChangedData], + ) -> None: if not _is_state_event(event, (STATE_ON, STATE_OFF)): _LOGGER.debug("%s: Ignoring sleep event %s", self._name, event) return @@ -1690,6 +1683,10 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): self._state = False +type AdaptiveSwitches = list[AdaptiveSwitch] +type AdaptiveSwitchMap = dict[AdaptiveSwitch, list[str]] + + class AdaptiveLightingManager: """Track 'light.turn_off' and 'light.turn_on' service calls.""" @@ -1706,11 +1703,11 @@ class AdaptiveLightingManager: # Tracks 'light.toggle' service calls self.toggle_event: dict[str, Event] = {} # Tracks 'on' → 'off' state changes - self.on_to_off_event: dict[str, Event] = {} + self.on_to_off_event: dict[str, Event[EventStateChangedData]] = {} # Tracks 'off' → 'on' state changes - self.off_to_on_event: dict[str, Event] = {} + self.off_to_on_event: dict[str, Event[EventStateChangedData]] = {} # Keep 'asyncio.sleep' tasks that can be cancelled by 'light.turn_on' events - self.sleep_tasks: dict[str, asyncio.Task] = {} + self.sleep_tasks: dict[str, asyncio.Task[None]] = {} # Locks that prevent light adjusting when waiting for a light to 'turn_off' self.turn_off_locks: dict[str, asyncio.Lock] = {} # Tracks which lights are manually controlled @@ -1720,8 +1717,8 @@ class AdaptiveLightingManager: # Track last 'service_data' to 'light.turn_on' resulting from this integration self.last_service_data: dict[str, dict[str, Any]] = {} # Track ongoing split adaptations to be able to cancel them - self.adaptation_tasks_brightness: dict[str, asyncio.Task] = {} - self.adaptation_tasks_color: dict[str, asyncio.Task] = {} + self.adaptation_tasks_brightness: dict[str, asyncio.Task[None]] = {} + self.adaptation_tasks_color: dict[str, asyncio.Task[None]] = {} # Track auto reset of manual_control self.auto_reset_manual_control_timers: dict[str, _AsyncSingleShotTimer] = {} @@ -1731,7 +1728,7 @@ class AdaptiveLightingManager: self.transition_timers: dict[str, _AsyncSingleShotTimer] = {} # Track _execute_cancellable_adaptation_calls tasks - self.adaptation_tasks = set() + self.adaptation_tasks: set[asyncio.Task[None]] = set() # Setup listeners and its callbacks to remove them later self.listener_removers = [ @@ -1823,15 +1820,11 @@ class AdaptiveLightingManager: def _separate_entity_ids( self, entity_ids: list[str], - data, - ) -> tuple[list[str], list[str]]: + data: ServiceData, + ) -> tuple[AdaptiveSwitchMap, list[str]]: # Create a mapping from switch to entity IDs - # AdaptiveSwitch.name → entity_ids mapping - switch_to_eids: dict[str, list[str]] = {} - # AdaptiveSwitch.name → AdaptiveSwitch mapping - switch_name_mapping: dict[str, AdaptiveSwitch] = {} - # Note: In HA≥2023.5, AdaptiveSwitch is hashable, so we can - # use dict[AdaptiveSwitch, list[str]] + # AdaptiveSwitch → entity_ids mapping + switch_to_eids: AdaptiveSwitchMap = {} skipped: list[str] = [] for entity_id in entity_ids: try: @@ -1855,7 +1848,7 @@ class AdaptiveLightingManager: not switch.is_on or not switch._intercept # Never adapt on light groups, because HA will make a separate light.turn_on - or _is_light_group(self.hass.states.get(entity_id)) + or ((e := self.hass.states.get(entity_id)) and _is_light_group(e)) # Prevent adaptation of TURN_ON calls when light is already on, # and of TOGGLE calls when toggling off. or self.hass.states.is_state(entity_id, STATE_ON) @@ -1881,19 +1874,17 @@ class AdaptiveLightingManager: ) skipped.append(entity_id) else: - switch_to_eids.setdefault(switch.name, []).append(entity_id) - switch_name_mapping[switch.name] = switch - return switch_to_eids, switch_name_mapping, skipped + switch_to_eids.setdefault(switch, []).append(entity_id) + return switch_to_eids, skipped def _correct_for_multi_light_intercept( self, - entity_ids, - switch_to_eids, - switch_name_mapping, - skipped, + entity_ids: list[str], + switch_to_eids: AdaptiveSwitchMap, + skipped: list[str], ): # Check for `multi_light_intercept: true/false` - mli = [sw._multi_light_intercept for sw in switch_name_mapping.values()] + mli = [sw._multi_light_intercept for sw in switch_to_eids] more_than_one_switch = len(switch_to_eids) > 1 single_switch_with_multiple_lights = ( len(switch_to_eids) == 1 and len(next(iter(switch_to_eids.values()))) > 1 @@ -1919,7 +1910,7 @@ class AdaptiveLightingManager: ) skipped = entity_ids switch_to_eids = {} - return switch_to_eids, switch_name_mapping, skipped + return switch_to_eids, skipped async def _service_interceptor_turn_on_handler( self, @@ -1984,19 +1975,17 @@ class AdaptiveLightingManager: # we skip them and rely on the followup call that HA will make # with the expanded entity IDs. - switch_to_eids, switch_name_mapping, skipped = self._separate_entity_ids( + switch_to_eids, skipped = self._separate_entity_ids( entity_ids, service_data, ) ( switch_to_eids, - switch_name_mapping, skipped, ) = self._correct_for_multi_light_intercept( entity_ids, switch_to_eids, - switch_name_mapping, skipped, ) _LOGGER.debug( @@ -2005,7 +1994,10 @@ class AdaptiveLightingManager: skipped, ) - def modify_service_data(service_data, entity_ids) -> dict[str, Any]: + def modify_service_data( + service_data: ServiceData, + entity_ids: list[str], + ) -> dict[str, Any]: """Modify the service data to contain the entity IDs.""" service_data.pop(ATTR_ENTITY_ID, None) service_data.pop(ATTR_AREA_ID, None) @@ -2014,8 +2006,7 @@ class AdaptiveLightingManager: # Intercept the call for first switch and call _adapt_light for the rest has_intercepted = False # Can only intercept a turn_on call once - for adaptive_switch_name, _entity_ids in switch_to_eids.items(): - switch = switch_name_mapping[adaptive_switch_name] + for switch, _entity_ids in switch_to_eids.items(): transition = service_data[CONF_PARAMS].get( ATTR_TRANSITION, switch.initial_transition, @@ -2289,8 +2280,8 @@ class AdaptiveLightingManager: if ATTR_ENTITY_ID in service_data: return cv.ensure_list_csv(service_data[ATTR_ENTITY_ID]) if ATTR_AREA_ID in service_data: - entity_ids = [] - area_ids = cv.ensure_list_csv(service_data[ATTR_AREA_ID]) + entity_ids: list[str] = [] + area_ids: list[str] = cv.ensure_list_csv(service_data[ATTR_AREA_ID]) for area_id in area_ids: area_entity_ids = area_entities(self.hass, area_id) eids = [ @@ -2369,14 +2360,18 @@ class AdaptiveLightingManager: event.context.id, ) for eid in entity_ids: - state = self.hass.states.get(eid).state + state = self.hass.states.get(eid) + assert state self.toggle_event[eid] = event - if state == STATE_ON: # is turning off + if state.state == STATE_ON: # is turning off off(eid, event) - elif state == STATE_OFF: # is turning on + elif state.state == STATE_OFF: # is turning on on(eid, event) - async def state_changed_event_listener(self, event: Event) -> None: + async def state_changed_event_listener( + self, + event: Event[EventStateChangedData], + ) -> None: """Track 'state_changed' events.""" entity_id = event.data.get(ATTR_ENTITY_ID, "") if entity_id not in self.lights: @@ -2385,17 +2380,29 @@ class AdaptiveLightingManager: old_state = event.data.get("old_state") new_state = event.data.get("new_state") - new_on = new_state is not None and new_state.state == STATE_ON - new_off = new_state is not None and new_state.state == STATE_OFF - old_on = old_state is not None and old_state.state == STATE_ON - old_off = old_state is not None and old_state.state == STATE_OFF + new_on = ( + new_state if new_state is not None and new_state.state == STATE_ON else None + ) + new_off = ( + new_state + if new_state is not None and new_state.state == STATE_OFF + else None + ) + old_on = ( + old_state if old_state is not None and old_state.state == STATE_ON else None + ) + old_off = ( + old_state + if old_state is not None and old_state.state == STATE_OFF + else None + ) if new_on: _LOGGER.debug( "Detected a '%s' 'state_changed' event: '%s' with context.id='%s'", entity_id, - new_state.attributes, - new_state.context.id, + new_on.attributes, + new_on.context.id, ) # It is possible to have multiple state change events with the same context. # This can happen because a `turn_on.light(brightness_pct=100, transition=30)` @@ -2411,29 +2418,29 @@ class AdaptiveLightingManager: last_state: list[State] | None = self.our_last_state_on_change.get( entity_id, ) - if is_our_context(new_state.context): + if is_our_context(new_on.context): if ( last_state is not None - and last_state[0].context.id == new_state.context.id + and last_state[0].context.id == new_on.context.id ): _LOGGER.debug( "AdaptiveLightingManager: State change event of '%s' is already" " in 'self.our_last_state_on_change' (%s)" " adding this state also", entity_id, - new_state.context.id, + new_on.context.id, ) - self.our_last_state_on_change[entity_id].append(new_state) + self.our_last_state_on_change[entity_id].append(new_on) else: _LOGGER.debug( "AdaptiveLightingManager: New adapt '%s' found for %s", - new_state, + new_on, entity_id, ) - self.our_last_state_on_change[entity_id] = [new_state] + self.our_last_state_on_change[entity_id] = [new_on] self.start_transition_timer(entity_id) elif last_state is not None: - self.our_last_state_on_change[entity_id].append(new_state) + self.our_last_state_on_change[entity_id].append(new_on) if old_on and new_off: # Tracks 'on' → 'off' state changes @@ -2583,7 +2590,7 @@ class AdaptiveLightingManager: def _off_to_on_state_event_is_from_turn_on( self, entity_id: str, - off_to_on_event: Event, + off_to_on_event: Event[EventStateChangedData], ) -> bool: # Adaptive Lighting should never turn on lights itself if is_our_context(off_to_on_event.context) and not is_our_context( @@ -2601,11 +2608,7 @@ class AdaptiveLightingManager: ) turn_on_event: Event | None = self.turn_on_event.get(entity_id) id_off_to_on = off_to_on_event.context.id - return ( - turn_on_event is not None - and id_off_to_on is not None - and id_off_to_on == turn_on_event.context.id - ) + return turn_on_event is not None and id_off_to_on == turn_on_event.context.id async def just_turned_off( # noqa: PLR0911 self, @@ -2662,7 +2665,6 @@ class AdaptiveLightingManager: if ( turn_off_event is not None and id_on_to_off == turn_off_event.context.id - and id_on_to_off is not None and transition is not None # 'turn_off' is called with transition=... ): # State change 'on' → 'off' and 'light.turn_off(..., transition=...)' come diff --git a/tests/test_color_and_brightness.py b/tests/test_color_and_brightness.py index 6c76175f..cc3e8c4d 100644 --- a/tests/test_color_and_brightness.py +++ b/tests/test_color_and_brightness.py @@ -5,8 +5,7 @@ import pytest from astral import LocationInfo from astral.location import Location from homeassistant.components.adaptive_lighting.color_and_brightness import ( - SUN_EVENT_NOON, - SUN_EVENT_SUNRISE, + SunEvent, SunEvents, ) @@ -167,7 +166,7 @@ def test_sun_events(tzinfo_and_location): date = dt.datetime(2022, 1, 1) events = sun_events.sun_events(date) assert len(events) == 4 - assert (SUN_EVENT_SUNRISE, location.sunrise(date).timestamp()) in events + assert (SunEvent.SUNRISE, location.sunrise(date).timestamp()) in events def test_prev_and_next_events(tzinfo_and_location): @@ -186,8 +185,8 @@ def test_prev_and_next_events(tzinfo_and_location): datetime = dt.datetime(2022, 1, 1, 10, 0) after_sunrise = sun_events.sunrise(datetime.date()) + dt.timedelta(hours=1) prev_event, next_event = sun_events.prev_and_next_events(after_sunrise) - assert prev_event[0] == SUN_EVENT_SUNRISE - assert next_event[0] == SUN_EVENT_NOON + assert prev_event[0] == SunEvent.SUNRISE + assert next_event[0] == SunEvent.NOON def test_closest_event(tzinfo_and_location): @@ -206,5 +205,5 @@ def test_closest_event(tzinfo_and_location): datetime = dt.datetime(2022, 1, 1, 6, 0) sunrise = sun_events.sunrise(datetime.date()) event_name, ts = sun_events.closest_event(sunrise) - assert event_name == SUN_EVENT_SUNRISE + assert event_name == SunEvent.SUNRISE assert ts == location.sunrise(sunrise.date()).timestamp() From 79973fb71d4e23c19014561e2502d2f4ff8e87f3 Mon Sep 17 00:00:00 2001 From: Mario Guggenberger Date: Fri, 12 Dec 2025 22:38:35 +0100 Subject: [PATCH 0932/1077] chore(devcontainer): fix Pylance resolution of HA core modules (#1343) --- .vscode/settings.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index e59dc709..5cb46e1a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,5 +16,8 @@ "-p", "no:sugar", "core/tests/components/adaptive_lighting" + ], + "python.analysis.extraPaths": [ + "${workspaceFolder}/core" ] } From 9c7a95f696a6fe61290e2f7f7a28f2e3479f1117 Mon Sep 17 00:00:00 2001 From: lenucksi Date: Fri, 12 Dec 2025 22:49:45 +0100 Subject: [PATCH 0933/1077] fix(ci): fix broken Docker workflow and modernize (#1318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(ci): fix broken Docker workflow and modernize ## Critical Bug Fixes 1. **Fix broken push condition** (CRITICAL): - Old: `push: ${{ github.ref == 'refs/heads/master' }}` - Problem: Branch renamed to `main`, so images NEVER pushed - New: `push: ${{ github.event_name != 'pull_request' }}` - Result: Docker images will actually be published again 2. **Add missing checkout step**: - Build was failing because source code wasn't checked out - Required for Docker build context ## Modernization Improvements 3. **Migrate to GitHub Container Registry (GHCR)**: - Old: DockerHub with `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` secrets - New: GHCR with built-in `GITHUB_TOKEN` - Benefits: No external account required, better integration 4. **Add semantic versioning**: - Automatically tags releases: `v1.2.3`, `v1.2`, `v1`, `latest` - Supports version tags (v*), branches, and PRs - Uses docker/metadata-action for automatic tagging 5. **Add GitHub Actions caching**: - Uses `type=gha` cache for faster builds - Reduces build times and GitHub Actions minutes 6. **Security: Digest pinning**: - All actions pinned to commit SHAs - Prevents supply chain attacks via tag manipulation - Follows security best practices 7. **Add explicit permissions**: - Minimal required permissions (contents: read, packages: write) - Follows principle of least privilege 8. **Add workflow triggers**: - Tags (v*) for releases - Pull requests for testing - Manual dispatch for on-demand builds ## Testing - Workflow syntax validated - Push logic tested with different event types - Compatible with existing Docker build process 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * refactor(ci): simplify docker workflow - Use version tags instead of SHA pins for readability - Remove verbose step names (action names are self-documenting) - Compact YAML formatting - Fix actions/checkout to v4 (v6 doesn't exist) --------- Co-authored-by: Bas Nijholt --- .github/workflows/docker-build.yml | 58 +++++++++++++++++++----------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 3e088e96..7c1909f8 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -1,32 +1,50 @@ -name: docker +name: Docker on: push: - branches: - - "main" + branches: [main] + tags: ['v*'] + pull_request: + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} jobs: - docker: + build: runs-on: ubuntu-latest + permissions: + contents: read + packages: write strategy: matrix: - platform: - - linux/amd64 - - linux/arm64 + platform: [linux/amd64, linux/arm64] steps: - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub - uses: docker/login-action@v3 + - uses: actions/checkout@v4 + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push - uses: docker/build-push-action@v6 + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - id: meta + uses: docker/metadata-action@v5 with: - # Only push on the master branch - push: ${{ github.ref == 'refs/heads/master' }} + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + - uses: docker/build-push-action@v6 + with: + context: . platforms: ${{ matrix.platform }} - tags: ${{ secrets.DOCKERHUB_USERNAME }}/adaptive-lighting:latest + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max From 828f73d4815f60401d2652dd3960323c27f408f3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:59:40 -0800 Subject: [PATCH 0934/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/checkout=20action=20to=20v6=20(#1352)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 7c1909f8..edea4bb5 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -21,7 +21,7 @@ jobs: matrix: platform: [linux/amd64, linux/arm64] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 From 2f599d6e1b4ef932780bb958713c89c36eff7af7 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Fri, 12 Dec 2025 23:00:30 +0100 Subject: [PATCH 0935/1077] Translations update from Hosted Weblate (#1331) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (German) Currently translated at 100.0% (156 of 156 strings) Co-authored-by: Anton Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/de/ Translation: Adaptive Lighting/Adaptive Lighting * Translated using Weblate (Catalan) Currently translated at 100.0% (156 of 156 strings) Co-authored-by: Enric Pagès i Gassull Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ca/ Translation: Adaptive Lighting/Adaptive Lighting --------- Co-authored-by: Anton Co-authored-by: Enric Pagès i Gassull --- custom_components/adaptive_lighting/translations/ca.json | 7 +++++++ custom_components/adaptive_lighting/translations/de.json | 9 ++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/ca.json b/custom_components/adaptive_lighting/translations/ca.json index 3b3a9b36..48f409d2 100644 --- a/custom_components/adaptive_lighting/translations/ca.json +++ b/custom_components/adaptive_lighting/translations/ca.json @@ -193,6 +193,13 @@ "user": { "title": "Tria un nom per a la instància d'Adaptive Lighting", "description": "Cada instància pot contenir múltiples llums!" + }, + "menu": { + "title": "Crear o Duplicar", + "description": "Vols crear una nova instància, o duplicar-ne una d'existent?", + "data": { + "action": "Acció" + } } }, "abort": { diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json index acd2de22..c5a0f0e2 100644 --- a/custom_components/adaptive_lighting/translations/de.json +++ b/custom_components/adaptive_lighting/translations/de.json @@ -8,6 +8,13 @@ "data": { "name": "Name" } + }, + "menu": { + "title": "Erstellen oder Duplizieren", + "description": "Möchtest du eine neue Instanz erstellen oder eine existierende duplizieren?", + "data": { + "action": "Aktion" + } } }, "abort": { @@ -47,7 +54,7 @@ "transition": "transition, Wechselzeit in Sekunden", "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.", "skip_redundant_commands": "skip_redundant_commands: Überspringt das Senden von Anpassungsbefehlen, deren Zielzustand bereits mit dem bekannten Zustand der Leuchte übereinstimmt. Minimiert den Netzwerkverkehr und verbessert die Anpassungsreaktion in einigen Situationen. 📉 Deaktivieren, falls der physikalische Zustand der Lichter nicht mehr mit dem Zustand in HA übereinstimmt.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Beim ersten Einschalten des Lichts. Wenn auf `true` gesetzt, passt AL das Licht nur an, wenn `light.turn_on` ohne eine Angabe von Farbe oder Helligkeit aufgerufen wird. ❌🌈 Dies verhindert z.B. die Anpassung durch AL beim Aktivieren einer Szene. Wenn auf \"false\" gesetzt, passt AL das licht unabhängig von der Angabe von Farbe oder Helligkeit in den ursprünglichen `service_data` an. Benötigt das `take_over_control` aktiviert ist. 🕵️ ", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Beim ersten Einschalten des Lichts. Wenn auf `true` gesetzt, passt AL das Licht nur an, wenn `light.turn_on` ohne eine Angabe von Farbe oder Helligkeit aufgerufen wird. ❌🌈 Dies verhindert z.B. die Anpassung durch AL beim Aktivieren einer Szene. Wenn auf \"false\" gesetzt, passt AL das licht unabhängig von der Angabe von Farbe oder Helligkeit in den ursprünglichen `service_data` an. Benötigt das `take_over_control` aktiviert ist. 🕵️", "include_config_in_attributes": "include_config_in_attributes: Alle Optionen als Attribute auf dem Schalter im Home Assistant anzeigen, wenn auf `true` gesetzt. 📝", "multi_light_intercept": "multi_light_intercept: Abfangen und Anpassen von `light.turn_on`-Aufrufen, die auf mehrere Lichter aufrufen. ➗⚠️ Dies kann dazu führen, dass ein einzelner `light.turn_on`-Aufruf in mehrere Aufrufe aufgeteilt wird, z.B. wenn Lichter in verschiedenen Schaltern sind. Erfordert, dass `intercept` aktiviert ist.", "transition_until_sleep": "transition_until_sleep: Wenn diese Option aktiviert ist, behandelt die adaptive Beleuchtung die Schlafeinstellungen als Minimum und geht nach Sonnenuntergang zu diesen Werten über. 🌙", From f84ee445b70112c19da08c3dcfafa9ff4da75c06 Mon Sep 17 00:00:00 2001 From: Mario Guggenberger Date: Tue, 23 Dec 2025 07:55:16 +0100 Subject: [PATCH 0936/1077] Individual manual control of brightness and color (#1356) * refactor: introduce light control parameter enum * refactor: replace manual control flag with parameter enum * test: update deprecated color temp attribute * build: set execution bits on task scripts * feat: individual manual control of brightness and color * test: add tests for individual manual control evaluation * fix: sequential manual changes not always detected If multiple attributes of a light were changed within an interval, only the last change was detected because the check in the interval only used the latest event. For example, if there was a brightness change and a following color change, only the color attribute was detected as manually controlled. To fix this, the manual control attribute flags are now set directly from the event handler so that all events are processed. * fix: invalid service description * docs: fix missing space in config description * refactor: pluralize multivalued bitmask enum name --- README.md | 91 ++-- .../adaptive_lighting/_docs_helpers.py | 2 + .../adaptive_lighting/adaptation_utils.py | 122 ++++- custom_components/adaptive_lighting/const.py | 50 +- .../adaptive_lighting/services.yaml | 15 +- .../adaptive_lighting/strings.json | 16 +- custom_components/adaptive_lighting/switch.py | 453 +++++++++++------- .../adaptive_lighting/translations/en.json | 16 +- scripts/develop | 0 scripts/lint | 0 tests/test_adaptation_utils.py | 142 ++++++ tests/test_switch.py | 232 +++++++-- 12 files changed, 846 insertions(+), 293 deletions(-) mode change 100644 => 100755 scripts/develop mode change 100644 => 100755 scripts/lint diff --git a/README.md b/README.md index 6102c85c..6f5a3715 100644 --- a/README.md +++ b/README.md @@ -105,46 +105,47 @@ The YAML and frontend configuration methods support all of the options listed be -| Variable name | Description | Default | Type | -|:-------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:---------------------------------------| -| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | -| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | -| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | -| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | -| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | -| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | -| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | -| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | -| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | -| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | -| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | -| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | -| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | -| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | -| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | -| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | -| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | -| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | -| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | -| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | -| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | -| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | -| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | -| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | -| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | -| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` | -| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | -| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | -| `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | -| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | -| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | -| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | -| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | -| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | -| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | -| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| Variable name | Description | Default | Type | +|:-------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:----------------------------------------| +| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | +| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | +| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | +| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | +| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | +| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | +| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | +| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | +| `take_over_control` | Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒 | `True` | `bool` | +| `take_over_control_mode` | The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed. | `pause_all` | one of `['pause_all', 'pause_changed']` | +| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues. | `False` | `bool` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | +| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | +| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | +| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | +| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | @@ -210,11 +211,11 @@ adaptive_lighting: -| Service data attribute | Description | Required | Type | -|:-------------------------|:-----------------------------------------------------------------------------------------------|:-----------|:---------------------| -| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s | -| `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s | -| `manual_control` | Whether to add ("true") or remove ("false") the light from the "manual_control" list. 🔒 | ❌ | bool | +| Service data attribute | Description | Required | Type | +|:-------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:-----------------------------------------| +| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s | +| `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s | +| `manual_control` | Whether to add ("true") or remove ("false") all adapted attributes of the light from the "manual_control" list, or the name of an attribute for selective addition. 🔒 | ❌ | bool or one of `['brightness', 'color']` | #### `adaptive_lighting.change_switch_settings` diff --git a/custom_components/adaptive_lighting/_docs_helpers.py b/custom_components/adaptive_lighting/_docs_helpers.py index afbb9d66..0c4ed45d 100644 --- a/custom_components/adaptive_lighting/_docs_helpers.py +++ b/custom_components/adaptive_lighting/_docs_helpers.py @@ -46,6 +46,8 @@ def _type_to_str(type_: Any) -> str: # noqa: PLR0911 return "bool" if isinstance(type_, vol.All): return _format_voluptuous_instance(type_) + if isinstance(type_, vol.Any): + return " or ".join(_type_to_str(t) for t in type_.validators) if isinstance(type_, vol.In): return f"one of `{type_.container}`" if isinstance(type_, selector.SelectSelector): diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py index 14c28ae8..26acd92b 100644 --- a/custom_components/adaptive_lighting/adaptation_utils.py +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -3,7 +3,8 @@ import logging from collections.abc import AsyncGenerator from dataclasses import dataclass -from typing import Any, Literal +from enum import IntFlag, auto +from typing import Any from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -12,6 +13,8 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS_STEP_PCT, ATTR_COLOR_NAME, ATTR_COLOR_TEMP_KELVIN, + ATTR_EFFECT, + ATTR_FLASH, ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, @@ -45,6 +48,41 @@ BRIGHTNESS_ATTRS = { ServiceData = dict[str, Any] +class LightControlAttributes(IntFlag): + """Attributes of lights that the adaptation engine can control.""" + + NONE = 0 + BRIGHTNESS = auto() + COLOR = auto() + + ALL = BRIGHTNESS | COLOR + + def __str__(self) -> str: + """Return a string representation of the attributes.""" + if self == LightControlAttributes.NONE: + return "NONE" + + return "|".join( + member.name + for member in type(self) + if member is not LightControlAttributes.NONE + and member in self + and member.name is not None + ) + + def has_any(self) -> bool: + """Determine whether any attribute is selected.""" + return self != LightControlAttributes.NONE + + def has_none(self) -> bool: + """Determine whether no attribute is selected.""" + return self == LightControlAttributes.NONE + + def has_all(self) -> bool: + """Determine whether all attributes are selected.""" + return (self & LightControlAttributes.ALL) == LightControlAttributes.ALL + + def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]: """Splits the service data by the adapted attributes. @@ -145,7 +183,7 @@ class AdaptationData: service_call_datas: AsyncGenerator[ServiceData] force: bool max_length: int - which: Literal["brightness", "color", "both"] + attributes: LightControlAttributes initial_sleep: bool = False async def next_service_call_data(self) -> ServiceData | None: @@ -161,7 +199,7 @@ class AdaptationData: f"sleep_time={self.sleep_time}, " f"force={self.force}, " f"max_length={self.max_length}, " - f"which={self.which}, " + f"attributes={self.attributes}, " f"initial_sleep={self.initial_sleep}" ")" ) @@ -171,20 +209,25 @@ class NoColorOrBrightnessInServiceDataError(Exception): """Exception raised when no color or brightness attributes are found in service data.""" -def _identify_lighting_type( +def _identify_light_control_attributes( service_data: ServiceData, -) -> Literal["brightness", "color", "both"]: +) -> LightControlAttributes: """Extract the 'which' attribute from the service data.""" has_brightness = ATTR_BRIGHTNESS in service_data has_color = any(attr in service_data for attr in COLOR_ATTRS) - if has_brightness and has_color: - return "both" + + parameters = LightControlAttributes.NONE + if has_brightness: - return "brightness" + parameters |= LightControlAttributes.BRIGHTNESS if has_color: - return "color" - msg = f"Invalid service_data, no brightness or color attributes found: {service_data=}" - raise NoColorOrBrightnessInServiceDataError(msg) + parameters |= LightControlAttributes.COLOR + + if parameters == LightControlAttributes.NONE: + msg = f"Invalid service_data, no brightness or color attributes found: {service_data=}" + raise NoColorOrBrightnessInServiceDataError(msg) + + return parameters def prepare_adaptation_data( @@ -220,7 +263,7 @@ def prepare_adaptation_data( filter_by_state, ) - lighting_type = _identify_lighting_type(service_data) + attributes = _identify_light_control_attributes(service_data) return AdaptationData( entity_id=entity_id, @@ -229,5 +272,58 @@ def prepare_adaptation_data( service_call_datas=service_data_iterator, force=force, max_length=service_datas_length, - which=lighting_type, + attributes=attributes, ) + + +def manual_control_event_attribute_to_flags( + manual_control_attribute: bool | str, +) -> LightControlAttributes: + """Convert manual control event data to light control attributes.""" + if isinstance(manual_control_attribute, bool) and manual_control_attribute: + return LightControlAttributes.ALL + if manual_control_attribute == "brightness": + return LightControlAttributes.BRIGHTNESS + if manual_control_attribute == "color": + return LightControlAttributes.COLOR + return LightControlAttributes.NONE + + +def has_brightness_attribute( + service_data: ServiceData, +) -> bool: + """Determine whether the service data contains brightness attributes.""" + return any(attr in BRIGHTNESS_ATTRS for attr in service_data) + + +def has_color_attribute( + service_data: ServiceData, +) -> bool: + """Determine whether the service data contains color attributes.""" + return any(attr in COLOR_ATTRS for attr in service_data) + + +def has_effect_attribute( + service_data: ServiceData, +) -> bool: + """Determine whether the service data contains effect attributes.""" + return ATTR_FLASH in service_data or ATTR_EFFECT in service_data + + +def get_light_control_attributes( + service_data: ServiceData, +) -> LightControlAttributes: + """Get the light control attributes affected by the service call data.""" + parameters = LightControlAttributes.NONE + + if has_brightness_attribute(service_data): + parameters |= LightControlAttributes.BRIGHTNESS + + if has_color_attribute(service_data): + parameters |= LightControlAttributes.COLOR + + if has_effect_attribute(service_data): + parameters |= LightControlAttributes.BRIGHTNESS + parameters |= LightControlAttributes.COLOR + + return parameters diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index f7c3620b..37a9ba8b 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,6 +1,7 @@ """Constants for the Adaptive Lighting integration.""" from datetime import timedelta +from enum import Enum from typing import Any import homeassistant.helpers.config_validation as cv @@ -16,6 +17,14 @@ ICON_SLEEP = "mdi:sleep" DOMAIN = "adaptive_lighting" + +class TakeOverControlMode(Enum): + """Modes for pausing adaptation when control of a light is taken over externally.""" + + PAUSE_ALL = "pause_all" + PAUSE_CHANGED = "pause_changed" + + DOCS = {CONF_ENTITY_ID: "Entity ID of the switch. 📝"} @@ -34,6 +43,7 @@ DOCS[CONF_DETECT_NON_HA_CHANGES] = ( "Needs `take_over_control` enabled. 🕵️ " "Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result " "in lights turning on unexpectedly. " + "Note that this calls `homeassistant.update_entity` every `interval`! " "Disable this feature if you encounter such issues." ) @@ -188,9 +198,19 @@ DOCS[CONF_BRIGHTNESS_MODE_TIME_LIGHT] = ( CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True DOCS[CONF_TAKE_OVER_CONTROL] = ( - "Disable Adaptive Lighting if another source calls `light.turn_on` while lights " - "are on and being adapted. Note that this calls `homeassistant.update_entity` " - "every `interval`! 🔒" + "Pause adaptation of individual lights and hand over (manual) control to other sources that " + "issue `light.turn_on` calls for lights that are on. 🔒" +) + +CONF_TAKE_OVER_CONTROL_MODE, DEFAULT_TAKE_OVER_CONTROL_MODE = ( + "take_over_control_mode", + TakeOverControlMode.PAUSE_ALL.value, +) +DOCS[CONF_TAKE_OVER_CONTROL_MODE] = ( + "The adaptation pausing mode when other sources change brightness and/or color of lights. " + "`pause_all` always pauses both brightness and color adaptation. " + "`pause_changed` pauses the adaptation of only the changed attributes and continues adapting " + "unchanged attributes, e.g., continues color adaptation when only brightness was changed." ) CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 45 @@ -284,8 +304,9 @@ DOCS_MANUAL_CONTROL = { "light as being `manually controlled`. 📝", CONF_LIGHTS: "entity_id(s) of lights, if not specified, all lights in the " "switch are selected. 💡", - CONF_MANUAL_CONTROL: 'Whether to add ("true") or remove ("false") the ' - 'light from the "manual_control" list. 🔒', + CONF_MANUAL_CONTROL: 'Whether to add ("true") or remove ("false") all ' + 'adapted attributes of the light from the "manual_control" list, or the ' + "name of an attribute for selective addition. 🔒", } DOCS_APPLY = { @@ -351,6 +372,20 @@ VALIDATION_TUPLES: list[tuple[str, Any, Any]] = [ (CONF_BRIGHTNESS_MODE_TIME_DARK, DEFAULT_BRIGHTNESS_MODE_TIME_DARK, int), (CONF_BRIGHTNESS_MODE_TIME_LIGHT, DEFAULT_BRIGHTNESS_MODE_TIME_LIGHT, int), (CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool), + ( + CONF_TAKE_OVER_CONTROL_MODE, + DEFAULT_TAKE_OVER_CONTROL_MODE, + selector.SelectSelector( # type: ignore[arg-type] + selector.SelectSelectorConfig( + options=[ + TakeOverControlMode.PAUSE_ALL.value, + TakeOverControlMode.PAUSE_CHANGED.value, + ], + multiple=False, + mode=selector.SelectSelectorMode.DROPDOWN, + ), + ), + ), (CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool), ( CONF_AUTORESET_CONTROL, @@ -446,6 +481,9 @@ SET_MANUAL_CONTROL_SCHEMA = vol.Schema( { vol.Optional(CONF_ENTITY_ID): cv.entity_ids, # type: ignore[arg-type] vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, # type: ignore[arg-type] - vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean, + vol.Optional(CONF_MANUAL_CONTROL, default=True): vol.Any( + cv.boolean, + vol.In(["brightness", "color"]), + ), }, ) diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 4b79f88c..09979bac 100644 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -57,7 +57,7 @@ set_manual_control: domain: light multiple: true manual_control: - description: Whether to add ("true") or remove ("false") the light from the "manual_control" list. 🔒 + description: Whether to add ("true") or remove ("false") all adapted attributes of the light from the "manual_control" list, or the name of an attribute for selective addition. 🔒 example: true default: true selector: @@ -220,13 +220,22 @@ change_switch_settings: selector: time: null take_over_control: - description: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 + description: Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒 required: false example: true selector: boolean: null + take_over_control_mode: + description: The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed. + required: false + example: pause_changed + selector: + select: + options: + - pause_all + - pause_changed detect_non_ha_changes: - description: 'Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an ''on'' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.' + description: 'Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an ''on'' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.' required: false example: false selector: diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 1a9ba06c..6985a94e 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -52,8 +52,9 @@ "brightness_mode": "brightness_mode", "brightness_mode_time_dark": "brightness_mode_time_dark", "brightness_mode_time_light": "brightness_mode_time_light", - "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", - "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", + "take_over_control": "take_over_control: Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒", + "take_over_control_mode": "take_over_control_mode", + "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", @@ -85,6 +86,7 @@ "brightness_mode": "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", "brightness_mode_time_dark": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", "brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.", + "take_over_control_mode": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.", "autoreset_control_seconds": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", "send_split_delay": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", "adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️" @@ -144,7 +146,7 @@ "name": "lights" }, "manual_control": { - "description": "Whether to add (\"true\") or remove (\"false\") the light from the \"manual_control\" list. 🔒", + "description": "Whether to add (\"true\") or remove (\"false\") all adapted attributes of the light from the \"manual_control\" list, or the name of an attribute for selective addition. 🔒", "name": "manual_control" } } @@ -250,11 +252,15 @@ "name": "min_sunset_time" }, "take_over_control": { - "description": "Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", + "description": "Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒", "name": "take_over_control" }, + "take_over_control_mode": { + "description": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.", + "name": "take_over_control_mode" + }, "detect_non_ha_changes": { - "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", + "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", "name": "detect_non_ha_changes" }, "transition": { diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 9217e8d9..3ded833e 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -8,7 +8,7 @@ import logging import zoneinfo from copy import deepcopy from datetime import timedelta -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Any import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -17,8 +17,6 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP_KELVIN, - ATTR_EFFECT, - ATTR_FLASH, ATTR_RGB_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, @@ -76,10 +74,12 @@ from homeassistant.util.color import ( ) from .adaptation_utils import ( - BRIGHTNESS_ATTRS, - COLOR_ATTRS, AdaptationData, + LightControlAttributes, ServiceData, + get_light_control_attributes, + has_effect_attribute, + manual_control_event_attribute_to_flags, prepare_adaptation_data, ) from .color_and_brightness import SunLightSettings @@ -127,6 +127,7 @@ from .const import ( CONF_SUNSET_OFFSET, CONF_SUNSET_TIME, CONF_TAKE_OVER_CONTROL, + CONF_TAKE_OVER_CONTROL_MODE, CONF_TRANSITION, CONF_TURN_ON_LIGHTS, CONF_USE_DEFAULTS, @@ -143,6 +144,7 @@ from .const import ( SLEEP_MODE_SWITCH, TURNING_OFF_DELAY, VALIDATION_TUPLES, + TakeOverControlMode, apply_service_schema, replace_none_str, ) @@ -359,27 +361,6 @@ async def handle_change_switch_settings( ) -@callback -def _fire_manual_control_event( - switch: AdaptiveSwitch, - light: str, - context: Context, -) -> None: - """Fire an event that 'light' is marked as manual_control.""" - hass = switch.hass - _LOGGER.debug( - "'adaptive_lighting.manual_control' event fired for %s for light %s", - switch.entity_id, - light, - ) - switch.manager.mark_as_manual_control(light) - hass.bus.async_fire( - f"{DOMAIN}.manual_control", - {ATTR_ENTITY_ID: light, SWITCH_DOMAIN: switch.entity_id}, - context=context, - ) - - async def async_setup_entry( # noqa: PLR0915 hass: HomeAssistant, config_entry: ConfigEntry, @@ -497,9 +478,21 @@ async def async_setup_entry( # noqa: PLR0915 all_lights = switch.lights else: all_lights = _expand_light_groups(hass, lights) - if service_call.data[CONF_MANUAL_CONTROL]: + + manual_attributes = manual_control_event_attribute_to_flags( + service_call.data[CONF_MANUAL_CONTROL], + ) + + if manual_attributes: for light in all_lights: - _fire_manual_control_event(switch, light, service_call.context) + switch.manager.set_manual_control_attributes( + light, + manual_attributes, + ) + switch.fire_manual_control_event( + light, + service_call.context, + ) else: switch.manager.reset(*all_lights) if switch.is_on: @@ -753,36 +746,32 @@ def _attributes_have_changed( light: str, old_attributes: dict[str, Any], new_attributes: dict[str, Any], - adapt_brightness: bool, - adapt_color: bool, context: Context, -) -> bool: +) -> LightControlAttributes: # 2023-11-19: HA core no longer removes light domain attributes when off # so we must protect for `None` here # see https://github.com/home-assistant/core/pull/101946 + changed_attributes = LightControlAttributes.NONE + # Check for color mode changes BEFORE attribute conversion # This detects external changes like Hue scenes switching from color_temp to RGB # See: https://github.com/basnijholt/adaptive-lighting/issues/1275 - if adapt_color and _has_color_mode_changed( + if _has_color_mode_changed( light, old_attributes, new_attributes, context, ): - return True + changed_attributes |= LightControlAttributes.COLOR - if adapt_color: + if LightControlAttributes.COLOR not in changed_attributes: old_attributes, new_attributes = _add_missing_attributes( old_attributes, new_attributes, ) - if ( - adapt_brightness - and old_attributes.get(ATTR_BRIGHTNESS) - and new_attributes.get(ATTR_BRIGHTNESS) - ): + if old_attributes.get(ATTR_BRIGHTNESS) and new_attributes.get(ATTR_BRIGHTNESS): last_brightness = old_attributes[ATTR_BRIGHTNESS] current_brightness = new_attributes[ATTR_BRIGHTNESS] if abs(current_brightness - last_brightness) > BRIGHTNESS_CHANGE: @@ -794,10 +783,10 @@ def _attributes_have_changed( current_brightness, context.id, ) - return True + changed_attributes |= LightControlAttributes.BRIGHTNESS if ( - adapt_color + LightControlAttributes.COLOR not in changed_attributes and old_attributes.get(ATTR_COLOR_TEMP_KELVIN) and new_attributes.get(ATTR_COLOR_TEMP_KELVIN) ): @@ -812,10 +801,10 @@ def _attributes_have_changed( current_color_temp, context.id, ) - return True + changed_attributes |= LightControlAttributes.COLOR if ( - adapt_color + LightControlAttributes.COLOR not in changed_attributes and old_attributes.get(ATTR_RGB_COLOR) and new_attributes.get(ATTR_RGB_COLOR) ): @@ -831,9 +820,9 @@ def _attributes_have_changed( current_rgb_color, context.id, ) - return True + changed_attributes |= LightControlAttributes.COLOR - return False + return changed_attributes class AdaptiveSwitch(SwitchEntity, RestoreEntity): @@ -937,6 +926,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._name, ) self._take_over_control = True + self._take_over_control_mode = TakeOverControlMode( + data[CONF_TAKE_OVER_CONTROL_MODE], + ) self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] self._adapt_only_on_bare_turn_on = data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL] @@ -1203,12 +1195,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context: Context | None = None, ) -> AdaptationData | None: """Prepare `AdaptationData` for adapting a light.""" + adaptation_attributes = self.manager.get_adaption_control_attributes( + self, + light, + ) + if transition is None: transition = self._transition if adapt_brightness is None: - adapt_brightness = self.adapt_brightness_switch.is_on + adapt_brightness = ( + LightControlAttributes.BRIGHTNESS in adaptation_attributes + ) if adapt_color is None: - adapt_color = self.adapt_color_switch.is_on + adapt_color = LightControlAttributes.COLOR in adaptation_attributes if prefer_rgb_color is None: prefer_rgb_color = self._prefer_rgb_color @@ -1375,7 +1374,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): to cancel an ongoing adaptation when a light is turned off. """ # Prevent overlap of multiple adaptation sequences - self.manager.cancel_ongoing_adaptation_calls(data.entity_id, which=data.which) + self.manager.cancel_ongoing_adaptation_calls(data.entity_id) _LOGGER.debug( "%s: execute_cancellable_adaptation_calls with data: %s", self._name, @@ -1384,9 +1383,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Execute adaptation calls within a task try: task = asyncio.ensure_future(self._execute_adaptation_calls(data)) - if data.which in ("both", "brightness"): + if LightControlAttributes.BRIGHTNESS in data.attributes: self.manager.adaptation_tasks_brightness[data.entity_id] = task - if data.which in ("both", "color"): + if LightControlAttributes.COLOR in data.attributes: self.manager.adaptation_tasks_color[data.entity_id] = task await task except asyncio.CancelledError: @@ -1397,7 +1396,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): data, ) - async def _update_attrs_and_maybe_adapt_lights( # noqa: PLR0912 + async def _update_attrs_and_maybe_adapt_lights( self, *, context: Context, @@ -1468,49 +1467,26 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if not filtered_lights: return - adapt_brightness = self.adapt_brightness_switch.is_on - adapt_color = self.adapt_color_switch.is_on - assert isinstance(adapt_brightness, bool) - assert isinstance(adapt_color, bool) tasks: list[asyncio.Task[None]] = [] for light in filtered_lights: - manually_controlled = ( - self._take_over_control - and self.manager.is_manually_controlled( - self, - light, - force, - adapt_brightness, - adapt_color, - ) + await self.manager.update_manually_controlled_from_untracked_change( + self, + light, + force, + context, ) - if manually_controlled: + + # Performance optimization: Skip adaptation task if all attributes are + # manually controlled and the task wouldn't actually do anything. + if self.manager.get_adaption_control_attributes(self, light).has_none(): _LOGGER.debug( - "%s: '%s' is being manually controlled, stop adapting, context.id=%s.", + "%s: '%s' is being manually controlled, skip adaptation, context.id=%s.", self._name, light, context.id, ) continue - significant_change = ( - self._take_over_control - and self._detect_non_ha_changes - and not force - # Note: This call updates the state of the light - # so it might suddenly be off. - and await self.manager.significant_change( - self, - light, - adapt_brightness, - adapt_color, - context, - ) - ) - if significant_change: - _fire_manual_control_event(self, light, context) - continue - _LOGGER.debug( "%s: Calling _adapt_light from _update_attrs_and_maybe_adapt_lights:" " '%s' with transition %s and context.id=%s", @@ -1553,7 +1529,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): entity_id, event.context.id, ) - self.manager.mark_as_manual_control(entity_id) + self.manager.set_manual_control_attributes(entity_id) return if ( @@ -1607,6 +1583,28 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): force=True, ) + def fire_manual_control_event( + self, + light: str, + context: Context, + ) -> None: + """Fire an event that 'light' is marked as manual_control.""" + _LOGGER.debug( + "'adaptive_lighting.manual_control' event fired for %s for light %s", + self.entity_id, + light, + ) + manual_attributes = self.manager.get_manual_control_attributes(light) + self.hass.bus.async_fire( + f"{DOMAIN}.manual_control", + { + ATTR_ENTITY_ID: light, + SWITCH_DOMAIN: self.entity_id, + CONF_MANUAL_CONTROL: manual_attributes, + }, + context=context, + ) + class SimpleSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" @@ -1711,7 +1709,7 @@ class AdaptiveLightingManager: # Locks that prevent light adjusting when waiting for a light to 'turn_off' self.turn_off_locks: dict[str, asyncio.Lock] = {} # Tracks which lights are manually controlled - self.manual_control: dict[str, bool] = {} + self.manual_control: dict[str, LightControlAttributes] = {} # Track 'state_changed' events of self.lights resulting from this integration self.our_last_state_on_change: dict[str, list[State]] = {} # Track last 'service_data' to 'light.turn_on' resulting from this integration @@ -1954,10 +1952,7 @@ class AdaptiveLightingManager: # were skipped by us return - if ( - ATTR_EFFECT in service_data[CONF_PARAMS] - or ATTR_FLASH in service_data[CONF_PARAMS] - ): + if has_effect_attribute(service_data[CONF_PARAMS]): return _LOGGER.debug( @@ -2204,10 +2199,26 @@ class AdaptiveLightingManager: ) self.auto_reset_manual_control_times[light] = time - def mark_as_manual_control(self, light: str) -> None: - """Mark a light as manually controlled.""" - _LOGGER.debug("Marking '%s' as manually controlled.", light) - self.manual_control[light] = True + def get_manual_control_attributes( + self, + light: str, + ) -> LightControlAttributes: + """Get the attributes for a light that are manually controlled.""" + return self.manual_control.get(light, LightControlAttributes.NONE) + + def set_manual_control_attributes( + self, + light: str, + attributes: LightControlAttributes = LightControlAttributes.ALL, + ) -> None: + """Mark attributes of a light as manually controlled.""" + _LOGGER.debug( + "Light %s: Setting manual control attributes to %s (from %s).", + light, + attributes, + self.manual_control[light], + ) + self.manual_control[light] = attributes delay = self.auto_reset_manual_control_times.get(light) async def reset() -> None: @@ -2228,35 +2239,87 @@ class AdaptiveLightingManager: transition=switch.initial_transition, force=True, ) - assert not self.manual_control[light] + assert self.manual_control[light] == LightControlAttributes.NONE self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset) + def add_manual_control_attributes( + self, + light: str, + attributes: LightControlAttributes, + ) -> None: + """Add attributes to the manual control status of a light.""" + current = self.get_manual_control_attributes(light) + _LOGGER.debug( + "Light %s: Adding manual control attributes %s (current: %s).", + light, + attributes, + current, + ) + new = current | attributes + self.set_manual_control_attributes(light, new) + + def get_adaption_control_attributes( + self, + switch: AdaptiveSwitch, + light: str, + ) -> LightControlAttributes: + """Get the attributes that should be adapted for a light. + + Determines the attributes that should actually be adapted from the attributes + marked as manually controlled, the state of adaptation switches, and the adaptation + configuration. + + Example 1: When no attributes are marked as manually controlled and all adaptation + switches are on, all attributes are returned. + + Example 2: When no attributes are marked as manually controlled and the brightness + adaptation switch is off, only the color attribute is returned. + + Example 3: When only brightness is marked as manually controlled, but the configuration + specifies to pause all adaptations on manual change, no attributes are returned so that + color is also not adapted. + """ + denied_adaptation_attributes = self.get_manual_control_attributes(light) + + if ( + denied_adaptation_attributes.has_any() + and switch._take_over_control_mode == TakeOverControlMode.PAUSE_ALL + ): + # Extend to pausing all only if there is at least one manually controlled attribute + denied_adaptation_attributes = LightControlAttributes.ALL + + enabled_adaptation_attributes = ( + LightControlAttributes.BRIGHTNESS + if switch.adapt_brightness_switch.is_on + else LightControlAttributes.NONE + ) | ( + LightControlAttributes.COLOR + if switch.adapt_color_switch.is_on + else LightControlAttributes.NONE + ) + + return ( + LightControlAttributes.ALL + & ~denied_adaptation_attributes + & enabled_adaptation_attributes + ) + def cancel_ongoing_adaptation_calls( self, light_id: str, - which: Literal["color", "brightness", "both"] = "both", ) -> None: """Cancel ongoing adaptation service calls for a specific light entity.""" brightness_task = self.adaptation_tasks_brightness.get(light_id) color_task = self.adaptation_tasks_color.get(light_id) - if ( - which in ("both", "brightness") - and brightness_task is not None - and not brightness_task.done() - ): + if brightness_task is not None and not brightness_task.done(): _LOGGER.debug( "Cancelled ongoing brightness adaptation calls (%s) for '%s'", brightness_task, light_id, ) brightness_task.cancel() - if ( - which in ("both", "color") - and color_task is not None - and color_task is not brightness_task - and not color_task.done() - ): + if color_task is not None and not color_task.done(): _LOGGER.debug( "Cancelled ongoing color adaptation calls (%s) for '%s'", color_task, @@ -2269,7 +2332,11 @@ class AdaptiveLightingManager: """Reset the 'manual_control' status of the lights.""" for light in lights: if reset_manual_control: - self.manual_control[light] = False + _LOGGER.debug( + "Light %s: Clearing manual control attributes.", + light, + ) + self.manual_control[light] = LightControlAttributes.NONE if timer := self.auto_reset_manual_control_timers.pop(light, None): timer.cancel() self.our_last_state_on_change.pop(light, None) @@ -2319,11 +2386,29 @@ class AdaptiveLightingManager: self.turn_off_event[eid] = event self.reset(eid) - def on(eid: str, event: Event) -> None: + async def on(eid: str, event: Event) -> None: task = self.sleep_tasks.get(eid) if task is not None: task.cancel() self.turn_on_event[eid] = event + + try: + switch = _switch_with_lights( + self.hass, + [eid], + expand_light_groups=False, + ) + await self.update_manually_controlled_from_event( + switch, + eid, + force=False, + ) + except NoSwitchFoundError: + _LOGGER.debug( + "No switch found for entity_id='%s' in 'on' event listener", + eid, + ) + timer = self.auto_reset_manual_control_timers.get(eid) if ( timer is not None @@ -2351,7 +2436,7 @@ class AdaptiveLightingManager: event.context.id, ) for eid in entity_ids: - on(eid, event) + await on(eid, event) elif service == SERVICE_TOGGLE: _LOGGER.debug( @@ -2366,7 +2451,7 @@ class AdaptiveLightingManager: if state.state == STATE_ON: # is turning off off(eid, event) elif state.state == STATE_OFF: # is turning on - on(eid, event) + await on(eid, event) async def state_changed_event_listener( self, @@ -2489,68 +2574,95 @@ class AdaptiveLightingManager: event, ) - def is_manually_controlled( + async def update_manually_controlled_from_event( self, switch: AdaptiveSwitch, light: str, force: bool, - adapt_brightness: bool, - adapt_color: bool, - ) -> bool: - """Check if the light has been 'on' and is now manually controlled.""" - manual_control = self.manual_control.setdefault(light, False) - if manual_control: - # Manually controlled until light is turned on and off - return True + ) -> None: + """Check if the light has been manually controlled by the latest turn on event.""" + if not switch._take_over_control: + return turn_on_event = self.turn_on_event.get(light) + if ( - turn_on_event is not None - and not self.is_proactively_adapting(turn_on_event.context.id) - and not is_our_context(turn_on_event.context) - and not force + turn_on_event is None + or self.is_proactively_adapting(turn_on_event.context.id) + or is_our_context(turn_on_event.context) + or force ): - keys = turn_on_event.data[ATTR_SERVICE_DATA].keys() - if ( - (adapt_color and COLOR_ATTRS.intersection(keys)) - or (adapt_brightness and BRIGHTNESS_ATTRS.intersection(keys)) - or (ATTR_FLASH in keys) - or (ATTR_EFFECT in keys) - ): - # Light was already on and 'light.turn_on' was not called by - # the adaptive_lighting integration. - manual_control = True - _fire_manual_control_event(switch, light, turn_on_event.context) - _LOGGER.debug( - "'%s' was already on and 'light.turn_on' was not called by the" - " adaptive_lighting integration (context.id='%s'), the Adaptive" - " Lighting will stop adapting the light until the switch or the" - " light turns off and then on again.", - light, - turn_on_event.context.id, - ) - return manual_control + return + + turn_on_attributes = get_light_control_attributes( + turn_on_event.data[ATTR_SERVICE_DATA], + ) + + if not turn_on_attributes: + return + + # Light was already on and 'light.turn_on' was not called by + # the adaptive_lighting integration. + self.add_manual_control_attributes(light, turn_on_attributes) + switch.fire_manual_control_event(light, turn_on_event.context) + _LOGGER.debug( + "'%s' was already on and 'light.turn_on' was not called by the" + " adaptive_lighting integration (context.id='%s'), the Adaptive" + " Lighting will stop adapting %s of the light until the switch or the" + " light turns off and then on again.", + light, + turn_on_event.context.id, + turn_on_attributes, + ) + + async def update_manually_controlled_from_untracked_change( + self, + switch: AdaptiveSwitch, + light: str, + force: bool, + context: Context, + ) -> None: + """Check if the light has been manually controlled from an untracked change. + + An untracked change is a change that has been made outsideof HA and is + therefore not visible through events. + """ + if not switch._take_over_control or not switch._detect_non_ha_changes or force: + return + + # Note: This call updates the state of the light + # so it might suddenly be off. + significantly_changed_attributes = await self.significant_change( + switch, + light, + context, + ) + + if not significantly_changed_attributes: + return + + self.add_manual_control_attributes( + light, + significantly_changed_attributes, + ) + switch.fire_manual_control_event(light, context) async def significant_change( self, switch: AdaptiveSwitch, light: str, - adapt_brightness: bool, - adapt_color: bool, context: Context, # just for logging - ) -> bool: + ) -> LightControlAttributes: """Has the light made a significant change since last update. This method will detect changes that were made to the light without - calling 'light.turn_on', so outside of Home Assistant. If a change is - detected, we mark the light as 'manually controlled' until the light - or switch is turned 'off' and 'on' again. + calling 'light.turn_on', so outside of Home Assistant. """ assert switch._detect_non_ha_changes last_service_data = self.last_service_data.get(light) if last_service_data is None: - return False + return LightControlAttributes.NONE # Update state and check for a manual change not done in HA. # Ensure HASS is correctly updating your light's state with # light.turn_on calls if any problems arise. This @@ -2559,33 +2671,32 @@ class AdaptiveLightingManager: refreshed_state = self.hass.states.get(light) assert refreshed_state is not None - changed = _attributes_have_changed( + changed_attributes = _attributes_have_changed( old_attributes=last_service_data, new_attributes=refreshed_state.attributes, light=light, - adapt_brightness=adapt_brightness, - adapt_color=adapt_color, context=context, ) - if changed: + if changed_attributes: _LOGGER.debug( - "%s: State attributes of '%s' changed (%s) wrt 'last_service_data' (%s) (context.id=%s)", + "%s: State attributes %s of '%s' changed (%s) wrt 'last_service_data' (%s) (context.id=%s)", + switch._name, + changed_attributes, + light, + refreshed_state.attributes, + last_service_data, + context.id, + ) + else: + _LOGGER.debug( + "%s: State attributes of '%s' did not change (%s) wrt 'last_service_data' (%s) (context.id=%s)", switch._name, light, refreshed_state.attributes, last_service_data, context.id, ) - return True - _LOGGER.debug( - "%s: State attributes of '%s' did not change (%s) wrt 'last_service_data' (%s) (context.id=%s)", - switch._name, - light, - refreshed_state.attributes, - last_service_data, - context.id, - ) - return False + return changed_attributes def _off_to_on_state_event_is_from_turn_on( self, @@ -2745,12 +2856,12 @@ class AdaptiveLightingManager: entity_id, service_data, ) - if any( - attr in service_data - for attr in COLOR_ATTRS | BRIGHTNESS_ATTRS | {ATTR_EFFECT} - ): - self.mark_as_manual_control(entity_id) + manual_control_attributes = get_light_control_attributes(service_data) + + if manual_control_attributes: + self.set_manual_control_attributes(entity_id, manual_control_attributes) return True + return False diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index fcb2ed8b..1e5a1fb9 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -53,8 +53,9 @@ "brightness_mode": "brightness_mode", "brightness_mode_time_dark": "brightness_mode_time_dark", "brightness_mode_time_light": "brightness_mode_time_light", - "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", - "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", + "take_over_control": "take_over_control: Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒", + "take_over_control_mode": "take_over_control_mode", + "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", @@ -86,6 +87,7 @@ "brightness_mode": "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", "brightness_mode_time_dark": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", "brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.", + "take_over_control_mode": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.", "autoreset_control_seconds": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", "send_split_delay": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", "adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️" @@ -145,7 +147,7 @@ "name": "lights" }, "manual_control": { - "description": "Whether to add (\"true\") or remove (\"false\") the light from the \"manual_control\" list. 🔒", + "description": "Whether to add (\"true\") or remove (\"false\") all adapted attributes of the light from the \"manual_control\" list, or the name of an attribute for selective addition. 🔒", "name": "manual_control" } } @@ -251,11 +253,15 @@ "name": "min_sunset_time" }, "take_over_control": { - "description": "Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", + "description": "Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒", "name": "take_over_control" }, + "take_over_control_mode": { + "description": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.", + "name": "take_over_control_mode" + }, "detect_non_ha_changes": { - "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.", + "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", "name": "detect_non_ha_changes" }, "transition": { diff --git a/scripts/develop b/scripts/develop old mode 100644 new mode 100755 diff --git a/scripts/lint b/scripts/lint old mode 100644 new mode 100755 diff --git a/tests/test_adaptation_utils.py b/tests/test_adaptation_utils.py index 10774fa2..6caf335c 100644 --- a/tests/test_adaptation_utils.py +++ b/tests/test_adaptation_utils.py @@ -4,16 +4,26 @@ from unittest.mock import Mock import pytest from homeassistant.components.adaptive_lighting.adaptation_utils import ( + LightControlAttributes, ServiceData, _create_service_call_data_iterator, _has_relevant_service_data_attributes, _remove_redundant_attributes, _split_service_call_data, + get_light_control_attributes, + has_brightness_attribute, + has_color_attribute, + has_effect_attribute, + manual_control_event_attribute_to_flags, prepare_adaptation_data, ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP_KELVIN, + ATTR_EFFECT, + ATTR_FLASH, + ATTR_HS_COLOR, ATTR_TRANSITION, ) from homeassistant.const import ATTR_ENTITY_ID, STATE_ON @@ -338,3 +348,135 @@ def fixture_hass_states_mock(): hass = Mock() hass.states.get.return_value = Mock(attributes={ATTR_BRIGHTNESS: 10}) return hass + + +@pytest.mark.parametrize( + ("attribute", "expected_str", "has_any", "has_none", "has_all"), + [ + (LightControlAttributes.NONE, "NONE", False, True, False), + (LightControlAttributes.BRIGHTNESS, "BRIGHTNESS", True, False, False), + (LightControlAttributes.COLOR, "COLOR", True, False, False), + ( + LightControlAttributes.BRIGHTNESS | LightControlAttributes.COLOR, + "BRIGHTNESS|COLOR", + True, + False, + True, + ), + ( + LightControlAttributes.ALL, + "BRIGHTNESS|COLOR", + True, + False, + True, + ), + ], +) +def test_light_control_attribute_flags( + attribute: LightControlAttributes, + expected_str: str, + has_any: bool, + has_none: bool, + has_all: bool, +): + """Test helper methods and string conversion for the light attribute flag.""" + assert str(attribute) == expected_str + assert attribute.has_any() is has_any + assert attribute.has_none() is has_none + assert attribute.has_all() is has_all + + +@pytest.mark.parametrize( + ("manual_control_attribute", "expected_flag"), + [ + (True, LightControlAttributes.ALL), + (False, LightControlAttributes.NONE), + ("brightness", LightControlAttributes.BRIGHTNESS), + ("color", LightControlAttributes.COLOR), + ("unsupported", LightControlAttributes.NONE), + ], +) +def test_manual_control_event_attribute_to_flags( + manual_control_attribute: bool | str, + expected_flag: LightControlAttributes, +): + """Test mapping of manual control events to attribute flags.""" + assert ( + manual_control_event_attribute_to_flags(manual_control_attribute) + == expected_flag + ) + + +@pytest.mark.parametrize( + ("service_data", "expected"), + [ + ({ATTR_BRIGHTNESS: 125}, True), + ({ATTR_BRIGHTNESS_PCT: 50}, True), + ({ATTR_BRIGHTNESS_PCT: 50, ATTR_COLOR_TEMP_KELVIN: 3500}, True), + ({ATTR_BRIGHTNESS_PCT: 50, "unknown": "foo"}, True), + ({ATTR_COLOR_TEMP_KELVIN: 3500}, False), + ({}, False), + ], +) +def test_has_brightness_attribute(service_data: ServiceData, expected: bool): + """Test detection of brightness attributes in service data.""" + assert has_brightness_attribute(service_data) is expected + + +@pytest.mark.parametrize( + ("service_data", "expected"), + [ + ({ATTR_HS_COLOR: (10, 20)}, True), + ({ATTR_COLOR_TEMP_KELVIN: 5000}, True), + ({ATTR_COLOR_TEMP_KELVIN: 5000, ATTR_BRIGHTNESS: 125}, True), + ({ATTR_COLOR_TEMP_KELVIN: 5000, "unknown": "foo"}, True), + ({ATTR_BRIGHTNESS: 125}, False), + ({}, False), + ], +) +def test_has_color_attribute(service_data: ServiceData, expected: bool): + """Test detection of color attributes in service data.""" + assert has_color_attribute(service_data) is expected + + +@pytest.mark.parametrize( + ("service_data", "expected"), + [ + ({ATTR_EFFECT: "colorloop"}, True), + ({ATTR_FLASH: "short"}, True), + ({ATTR_EFFECT: "colorloop", ATTR_FLASH: "short"}, True), + ({ATTR_EFFECT: "colorloop", "unknown": "foo"}, True), + ({}, False), + ], +) +def test_has_effect_attribute(service_data: ServiceData, expected: bool): + """Test detection of effect attributes in service data.""" + assert has_effect_attribute(service_data) is expected + + +@pytest.mark.parametrize( + ("service_data", "expected_flags"), + [ + ({ATTR_BRIGHTNESS: 1}, LightControlAttributes.BRIGHTNESS), + ({ATTR_HS_COLOR: (1, 2)}, LightControlAttributes.COLOR), + ( + {ATTR_BRIGHTNESS: 1, ATTR_HS_COLOR: (1, 2)}, + LightControlAttributes.BRIGHTNESS | LightControlAttributes.COLOR, + ), + ( + {ATTR_EFFECT: "colorloop"}, + LightControlAttributes.BRIGHTNESS | LightControlAttributes.COLOR, + ), + ( + {ATTR_FLASH: "short"}, + LightControlAttributes.BRIGHTNESS | LightControlAttributes.COLOR, + ), + ({}, LightControlAttributes.NONE), + ], +) +def test_get_light_control_attributes( + service_data: ServiceData, + expected_flags: LightControlAttributes, +): + """Test determination of light control attributes.""" + assert get_light_control_attributes(service_data) == expected_flags diff --git a/tests/test_switch.py b/tests/test_switch.py index 372fdd66..3810bdbe 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -18,6 +18,7 @@ import voluptuous.error from flaky import flaky from homeassistant.components.adaptive_lighting.adaptation_utils import ( AdaptationData, + LightControlAttributes, _create_service_call_data_iterator, ) from homeassistant.components.adaptive_lighting.color_and_brightness import ( @@ -60,6 +61,7 @@ from homeassistant.components.adaptive_lighting.const import ( SERVICE_SET_MANUAL_CONTROL, SLEEP_MODE_SWITCH, UNDO_UPDATE_LISTENER, + TakeOverControlMode, ) from homeassistant.components.adaptive_lighting.switch import ( CONF_INTERCEPT, @@ -690,7 +692,7 @@ async def test_manual_control( # Call light.turn_on for ENTITY_LIGHT_1 await turn_light(True, brightness=increased_brightness()) # Check that ENTITY_LIGHT_1 is manually controlled - assert manual_control[ENTITY_LIGHT_1] + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS # Test adaptive_lighting.set_manual_control await change_manual_control(False) # Check that ENTITY_LIGHT_1 is not manually controlled @@ -703,11 +705,9 @@ async def test_manual_control( assert not manual_control[ENTITY_LIGHT_1], manual_control await turn_light(True, brightness=increased_brightness()) assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON - if adapt_only_on_bare_turn_on: - # Marks as manually controlled beacuse we turned it on with brightness - assert manual_control[ENTITY_LIGHT_1], manual_control - else: - assert not manual_control[ENTITY_LIGHT_1], manual_control + assert ( + manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS + ), manual_control # Check that toggling (sleep mode) switch resets manual control for entity_id in [ENTITY_SWITCH, ENTITY_SLEEP_MODE_SWITCH]: @@ -722,7 +722,7 @@ async def test_manual_control( await turn_light(False) await change_manual_control(True) await turn_light(True) - assert manual_control[ENTITY_LIGHT_1] + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL # Check that when 'adapt_brightness' is off, changing the brightness # doesn't mark it as manually controlled but changing color_temp @@ -732,7 +732,7 @@ async def test_manual_control( assert not manual_control[ENTITY_LIGHT_1] await switch.adapt_brightness_switch.async_turn_off() await turn_light(True, brightness=increased_brightness()) - assert not manual_control[ENTITY_LIGHT_1] + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS mired_range = (light.min_color_temp_kelvin, light.max_color_temp_kelvin) kelvin_range = ( color_temperature_mired_to_kelvin(mired_range[1]), @@ -743,7 +743,7 @@ async def test_manual_control( True, color_temp_kelvin=(light._attr_color_temp + 100) % ptp_kelvin, ) - assert manual_control[ENTITY_LIGHT_1] + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL await switch.adapt_brightness_switch.async_turn_on() # turn on again # Check that when 'adapt_color' is off, changing the color @@ -753,10 +753,10 @@ async def test_manual_control( await turn_light(True) assert not manual_control[ENTITY_LIGHT_1] await switch.adapt_color_switch.async_turn_off() - await turn_light(True, color_temp=increased_color_temp()) - assert not manual_control[ENTITY_LIGHT_1] + await turn_light(True, color_temp_kelvin=increased_color_temp()) + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.COLOR await turn_light(True, brightness=increased_brightness()) - assert manual_control[ENTITY_LIGHT_1] + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL # Check that when 'adapt_color' adapt_brightness are both off # nothing marks it as manually controlled @@ -765,14 +765,14 @@ async def test_manual_control( await switch.adapt_color_switch.async_turn_off() await switch.adapt_brightness_switch.async_turn_off() assert not manual_control[ENTITY_LIGHT_1] - await turn_light(True, color_temp=increased_color_temp()) + await turn_light(True, color_temp_kelvin=increased_color_temp()) await turn_light(True, brightness=increased_brightness()) await turn_light( True, - color_temp=increased_color_temp(), + color_temp_kelvin=increased_color_temp(), brightness=increased_brightness(), ) - assert not manual_control[ENTITY_LIGHT_1] + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL # Turn switches on again await switch.adapt_color_switch.async_turn_on() await switch.adapt_brightness_switch.async_turn_on() @@ -800,6 +800,22 @@ async def test_manual_control( assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON assert not manual_control[ENTITY_LIGHT_1] + # Check that manual control `True` sets all attributes + await change_manual_control(False) + assert not manual_control[ENTITY_LIGHT_1] + await change_manual_control(True) + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL + + # Check that manual control `False` unsets all attributes + await change_manual_control(False) + assert not manual_control[ENTITY_LIGHT_1] + + # Check that manual control attributes can be selectively set + await change_manual_control("brightness") + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS + await change_manual_control("color") + assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.COLOR + @flaky(max_runs=3, min_passes=1) async def test_auto_reset_manual_control(hass): @@ -833,7 +849,7 @@ async def test_auto_reset_manual_control(hass): _LOGGER.debug("Start test auto reset manual control") await turn_light(True, brightness=1) await turn_light(True, brightness=10) - assert manual_control[light.entity_id] + assert manual_control[light.entity_id] == LightControlAttributes.BRIGHTNESS assert ( switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] > 0 ) @@ -856,6 +872,143 @@ async def test_auto_reset_manual_control(hass): assert not manual_control[light.entity_id] +async def test_adaptation_attribute_selection(hass): + """Test the 'manual control' tracking.""" + switch, (light, *_) = await setup_lights_and_switch(hass) + + # Assert default settings + assert switch._take_over_control + assert switch._take_over_control_mode == TakeOverControlMode.PAUSE_ALL + + # Check that PAUSE_ALL leads to adaptation of all attributes when none are manually controlled + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.NONE + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.ALL + ) + + # Check that PAUSE_ALL leads to no adaptation when a single attribute is manually controlled + switch.manager.add_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.BRIGHTNESS, + ) + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.BRIGHTNESS + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.NONE + ) + + # Check that PAUSE_ALL leads to no adaptation when all attributes are manually controlled + switch.manager.add_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.COLOR, + ) + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.ALL + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.NONE + ) + + switch._take_over_control_mode = TakeOverControlMode.PAUSE_CHANGED + switch.manager.set_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.NONE, + ) + + # Check that PAUSE_CHANGED leads to adaptation of all attributes when none are manually controlled + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.NONE + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.ALL + ) + + # Check that PAUSE_CHANGED leads to adaptation of the remaining non-manual attributes + switch.manager.add_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.BRIGHTNESS, + ) + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.BRIGHTNESS + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.COLOR + ) + + # Check that PAUSE_CHANGED leads to no adaptation when all attributes are manually controlled + switch.manager.add_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.COLOR, + ) + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.ALL + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.NONE + ) + + await switch.adapt_brightness_switch.async_turn_off() + + # Check that with adapt_brightness off and PAUSE_CHANGED, only color is adapted when none are manually controlled + switch._take_over_control_mode = TakeOverControlMode.PAUSE_CHANGED + switch.manager.set_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.NONE, + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.COLOR + ) + + # Check that with adapt_brightness off and PAUSE_CHANGED, nothing is adapted when color is manually controlled + switch._take_over_control_mode = TakeOverControlMode.PAUSE_CHANGED + switch.manager.set_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.COLOR, + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.NONE + ) + + # Check that with adapt_brightness off and PAUSE_ALL, only color is adapted when none are manually controlled + switch._take_over_control_mode = TakeOverControlMode.PAUSE_ALL + switch.manager.set_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.NONE, + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.COLOR + ) + + # Check that with adapt_brightness off and PAUSE_ALL, nothing is adapted when color is manually controlled + switch._take_over_control_mode = TakeOverControlMode.PAUSE_ALL + switch.manager.set_manual_control_attributes( + ENTITY_LIGHT_1, + LightControlAttributes.COLOR, + ) + assert ( + switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1) + == LightControlAttributes.NONE + ) + + async def test_apply_service(hass): """Test adaptive_lighting.apply service.""" switch, (_, _, light) = await setup_lights_and_switch(hass) @@ -994,8 +1147,6 @@ def test_attributes_have_changed(): } kwargs = { "light": "light.test", - "adapt_brightness": True, - "adapt_color": True, "context": Context(), } assert not _attributes_have_changed( @@ -1229,9 +1380,15 @@ async def test_state_change_handlers(hass): await turn_light(True, brightness=40) await turn_light(True, brightness=20) await update(force=False) - assert switch.manager.manual_control[ENTITY_LIGHT_1] + assert ( + switch.manager.manual_control[ENTITY_LIGHT_1] + == LightControlAttributes.BRIGHTNESS + ) await update(force=True) - assert switch.manager.manual_control[ENTITY_LIGHT_1] + assert ( + switch.manager.manual_control[ENTITY_LIGHT_1] + == LightControlAttributes.BRIGHTNESS + ) # turn light off then on should reset manual control. await turn_light(False) @@ -1245,7 +1402,10 @@ async def test_state_change_handlers(hass): await update(force=False) assert switch.manager.last_service_data.get(ENTITY_LIGHT_1) is not None assert switch.manager.our_last_state_on_change.get(ENTITY_LIGHT_1) is not None - assert switch.manager.manual_control[ENTITY_LIGHT_1] + assert ( + switch.manager.manual_control[ENTITY_LIGHT_1] + == LightControlAttributes.BRIGHTNESS + ) def test_is_our_context(): @@ -1345,7 +1505,7 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands): # check whether the brightness and color_temp change. context = switch.create_context("test") # needs to be passed to update method brightness = light.brightness - color_temp = light.color_temp + color_temp = light.color_temp_kelvin await switch.sleep_mode_switch.async_turn_on() await switch._update_attrs_and_maybe_adapt_lights(context=context) await hass.async_block_till_done() @@ -1353,7 +1513,7 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands): # TODO: figure out why `light.brightness` is not updating attrs = hass.states.get(light.entity_id).attributes sleep_brightness = attrs["brightness"] - sleep_color_temp = attrs["color_temp"] + sleep_color_temp = attrs["color_temp_kelvin"] assert sleep_brightness != brightness assert sleep_color_temp != color_temp @@ -1364,7 +1524,7 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands): attrs = hass.states.get(light.entity_id).attributes brightness = attrs["brightness"] - color_temp = attrs["color_temp"] + color_temp = attrs["color_temp_kelvin"] assert sleep_brightness != brightness assert sleep_color_temp != color_temp @@ -1538,7 +1698,7 @@ async def test_cancellable_service_calls_task(hass): _create_service_call_data_iterator(hass, [service_data], False), force=False, max_length=1, - which="both", + attributes=LightControlAttributes.ALL, ) await switch.execute_cancellable_adaptation_calls(adaptation_data) @@ -1948,7 +2108,7 @@ async def test_two_switches_for_single_light(hass): assert light1.is_on await turn_light(True, brightness=increased_brightness()) - await turn_light(True, color_temp=increased_color_temp()) + await turn_light(True, color_temp_kelvin=increased_color_temp()) attrs = hass.states.get(light1.entity_id).attributes before_brightness = attrs[ATTR_BRIGHTNESS] @@ -2381,12 +2541,9 @@ def test_attributes_have_changed_light_mode_switch(): context = Context() base_kwargs = { "light": "light.test", - "adapt_brightness": True, "context": context, } - - # Test 1: adapt_color=True - all mode changes should be detected - kwargs_adapt_color = {**base_kwargs, "adapt_color": True} + kwargs_adapt_color = base_kwargs # color_temp → RGB assert _attributes_have_changed( @@ -2449,21 +2606,6 @@ def test_attributes_have_changed_light_mode_switch(): **kwargs_adapt_color, ), "Same XY should not be detected as change" - # Test 2: adapt_color=False - mode changes should NOT be detected - kwargs_no_adapt = {**base_kwargs, "adapt_color": False} - - assert not _attributes_have_changed( - old_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, - new_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, - **kwargs_no_adapt, - ), "Mode change should not be detected when adapt_color=False" - - assert not _attributes_have_changed( - old_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, - new_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, - **kwargs_no_adapt, - ), "RGB → color_temp should not be detected when adapt_color=False" - # Regression tests for bugs found in PR #1348 by @protyposis # See: https://github.com/basnijholt/adaptive-lighting/pull/1348 From f95093e1dc5d8ae91551b2ba7451cd5df147eeb6 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 5 Jan 2026 12:43:07 -0800 Subject: [PATCH 0937/1077] Bump to v1.30.0 --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 97d79950..27762b19 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.29.0" + "version": "1.30.0" } From 49f9da14fe29cd0d8f5c0a812e7460e7baa91519 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 11 Jan 2026 21:57:52 +0100 Subject: [PATCH 0938/1077] docs: clarify Docker test setup requirements (#1383) --- Dockerfile | 6 ++++-- tests/README.md | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index c4d9c695..353a0748 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,10 @@ # See tests/README.md for instructions on how to run the tests. # tl;dr: -# Run the following command in the adaptive-lighting repo folder to run the tests: -# docker run -v $(pwd):/app basnijholt/adaptive-lighting:latest +# 1. Clone HA core into ./core: git clone --depth 1 https://github.com/home-assistant/core.git core +# 2. Setup symlinks: ./scripts/setup-symlinks +# 3. Run tests (mount entire repo, not individual dirs, or symlinks break): +# docker run -v $(pwd):/app basnijholt/adaptive-lighting:latest # Optionally build the image yourself with: # docker build -t basnijholt/adaptive-lighting:latest . diff --git a/tests/README.md b/tests/README.md index a5b60839..3ccec21c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -3,7 +3,23 @@ To run the tests, check out the [CI configuration](../.github/workflows/pytest.yml) to see how they are executed in the CI pipeline. Alternatively, you can use the provided Docker image to run the tests locally or run them with VS Code directly in the dev container. -To run the tests using the Docker image, navigate to the `adaptive-lighting` repo folder and execute the following command: +## Prerequisites + +Before running tests with Docker, you need a local Home Assistant core checkout with symlinks: + +```bash +# Clone HA core (one-time setup) +git clone --depth 1 https://github.com/home-assistant/core.git core + +# Setup symlinks (one-time setup) +./scripts/setup-symlinks +``` + +## Running tests with Docker + +Navigate to the `adaptive-lighting` repo folder and execute the following command. + +**Important:** Mount the entire repo (`-v $(pwd):/app`), not individual directories, or the symlinks will break. Linux / MacOS / Windows PowerShell: ```bash From e61a0186164aee57bf40baa7ed0e0d21a713f1d1 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 12 Jan 2026 13:40:55 +0100 Subject: [PATCH 0939/1077] Fix regression: lights not adapting when turned on by automation (#1380) ## Summary - Fixes regression in v1.30.0 where lights turned on by automations were incorrectly marked as "manually controlled" - Makes `adapt_only_on_bare_turn_on` respect individual attribute tracking from #1356 ## Root Cause PR #1356 added a call to `update_manually_controlled_from_event()` in the `turn_on_off_event_listener.on()` handler for ALL `light.turn_on` events, including when turning a light on from OFF state. When an automation turns on a light with brightness/color attributes, this incorrectly marked the light as "manually controlled", preventing Adaptive Lighting from adapting it. ## Fix 1. Only call `update_manually_controlled_from_event()` when the light was **already ON** before the turn_on event. Turning on from OFF is handled by `_respond_to_off_to_on_event()`. 2. Make `adapt_only_on_bare_turn_on` respect `take_over_control_mode`: - With `PAUSE_CHANGED`: Only pause adaptation of specified attributes, continue adapting unspecified ones - With `PAUSE_ALL`: Pause all adaptation (existing behavior) ## Expected Behavior After Fix | Scenario | `adapt_only_on_bare_turn_on` | `take_over_control_mode` | Result | |----------|------------------------------|--------------------------|--------| | Turn on from OFF with brightness | `false` | Either | NOT manually controlled | | Turn on from OFF with brightness | `true` | `PAUSE_ALL` | All adaptation paused | | Turn on from OFF with brightness | `true` | `PAUSE_CHANGED` | Only brightness paused, color adapts | | Turn on from OFF without attributes | Either | Either | NOT manually controlled | | Change brightness while ON | Either | Either | Brightness manually controlled | ## Test plan - [x] Turn on light via automation with brightness/color (`adapt_only_on_bare_turn_on=false`) - should adapt - [x] Turn on light via scene (`adapt_only_on_bare_turn_on=true`, `PAUSE_ALL`) - should pause all adaptation - [x] Turn on light with brightness only (`adapt_only_on_bare_turn_on=true`, `PAUSE_CHANGED`) - should adapt color - [x] Both intercept=True and intercept=False paths tested for consistency - [x] CI tests pass Fixes #1378 Co-authored-by: Mario Guggenberger --- README.md | 82 ++++----- custom_components/adaptive_lighting/const.py | 2 +- .../adaptive_lighting/strings.json | 2 +- custom_components/adaptive_lighting/switch.py | 52 +++--- .../adaptive_lighting/translations/en.json | 2 +- tests/test_switch.py | 168 ++++++++++++++++++ 6 files changed, 244 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 6f5a3715..7a7ee9d9 100644 --- a/README.md +++ b/README.md @@ -105,47 +105,47 @@ The YAML and frontend configuration methods support all of the options listed be -| Variable name | Description | Default | Type | -|:-------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:----------------------------------------| -| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | -| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | -| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | -| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | -| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | -| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | -| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | -| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | -| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | -| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | -| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | -| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | -| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | -| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | -| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | -| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | -| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | -| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | -| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | -| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | -| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | -| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | -| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | -| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | -| `take_over_control` | Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒 | `True` | `bool` | -| `take_over_control_mode` | The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed. | `pause_all` | one of `['pause_all', 'pause_changed']` | -| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues. | `False` | `bool` | -| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | -| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | -| `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | -| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | -| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | -| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | -| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | -| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | -| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | -| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| Variable name | Description | Default | Type | +|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:----------------------------------------| +| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | +| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | +| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | +| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | +| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | +| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | +| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | +| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | +| `take_over_control` | Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒 | `True` | `bool` | +| `take_over_control_mode` | The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed. | `pause_all` | one of `['pause_all', 'pause_changed']` | +| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues. | `False` | `bool` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | +| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | +| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | +| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | +| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 37a9ba8b..502318f0 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -95,7 +95,7 @@ CONF_ADAPT_ONLY_ON_BARE_TURN_ON, DEFAULT_ADAPT_ONLY_ON_BARE_TURN_ON = ( DOCS[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] = ( "When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is " "invoked without specifying color or brightness. ❌🌈 " - "This e.g., prevents adaptation when activating a scene. " + "This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. " "If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. " "Needs `take_over_control` enabled. 🕵️" ) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 6985a94e..4b18ea06 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -57,7 +57,7 @@ "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay", "adapt_delay": "adapt_delay", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 3ded833e..3923e056 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1545,13 +1545,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): service_data, ): _LOGGER.debug( - "Skipping responding to 'off' → 'on' event for '%s' with context.id='%s' because" - " we only adapt on bare `light.turn_on` events and not on service_data: '%s'", + "Marked attributes from service_data as manually controlled for '%s' " + "with context.id='%s'. Continuing to adapt remaining attributes. " + "service_data: '%s'", entity_id, event.context.id, service_data, ) - return if self._adapt_delay > 0: await asyncio.sleep(self._adapt_delay) @@ -2216,7 +2216,7 @@ class AdaptiveLightingManager: "Light %s: Setting manual control attributes to %s (from %s).", light, attributes, - self.manual_control[light], + self.get_manual_control_attributes(light), ) self.manual_control[light] = attributes delay = self.auto_reset_manual_control_times.get(light) @@ -2392,22 +2392,28 @@ class AdaptiveLightingManager: task.cancel() self.turn_on_event[eid] = event - try: - switch = _switch_with_lights( - self.hass, - [eid], - expand_light_groups=False, - ) - await self.update_manually_controlled_from_event( - switch, - eid, - force=False, - ) - except NoSwitchFoundError: - _LOGGER.debug( - "No switch found for entity_id='%s' in 'on' event listener", - eid, - ) + # Only check for manual control via this path if the light was already ON. + # Turning on from OFF is handled separately in _respond_to_off_to_on_event, + # where adapt_only_on_bare_turn_on can mark lights as manually controlled. + # Fix for https://github.com/basnijholt/adaptive-lighting/issues/1378 + state = self.hass.states.get(eid) + if state is not None and state.state == STATE_ON: + try: + switch = _switch_with_lights( + self.hass, + [eid], + expand_light_groups=False, + ) + await self.update_manually_controlled_from_event( + switch, + eid, + force=False, + ) + except NoSwitchFoundError: + _LOGGER.debug( + "No switch found for entity_id='%s' in 'on' event listener", + eid, + ) timer = self.auto_reset_manual_control_timers.get(eid) if ( @@ -2851,6 +2857,12 @@ class AdaptiveLightingManager: entity_id: str, service_data: ServiceData, ) -> bool: + """Mark light as manually controlled if turn_on call has brightness/color attributes. + + This is used by adapt_only_on_bare_turn_on to mark lights as manually controlled + when they are turned on with specific attributes (e.g., from a scene). + This ensures scenes persist and AL doesn't override them. + """ _LOGGER.debug( "_mark_manual_control_if_non_bare_turn_on: entity_id='%s', service_data='%s'", entity_id, diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 1e5a1fb9..e39898fd 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -58,7 +58,7 @@ "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay", "adapt_delay": "adapt_delay", diff --git a/tests/test_switch.py b/tests/test_switch.py index 3810bdbe..5ba3cb60 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -47,6 +47,7 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_SUNRISE_TIME, CONF_SUNSET_TIME, CONF_TAKE_OVER_CONTROL, + CONF_TAKE_OVER_CONTROL_MODE, CONF_TRANSITION, CONF_TURN_ON_LIGHTS, CONF_USE_DEFAULTS, @@ -705,6 +706,21 @@ async def test_manual_control( assert not manual_control[ENTITY_LIGHT_1], manual_control await turn_light(True, brightness=increased_brightness()) assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + # Turning on from OFF with brightness: + # - With adapt_only_on_bare_turn_on=True: SHOULD mark as manually controlled (to preserve scenes) + # - With adapt_only_on_bare_turn_on=False: should NOT mark (fix for issue #1378) + if adapt_only_on_bare_turn_on: + assert ( + manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS + ), manual_control + else: + assert not manual_control[ENTITY_LIGHT_1], manual_control + # Reset for next test + await turn_light(False) + await turn_light(True) + assert not manual_control[ENTITY_LIGHT_1], manual_control + # Now change brightness while ON - this should always be manual control + await turn_light(True, brightness=increased_brightness()) assert ( manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS ), manual_control @@ -2746,3 +2762,155 @@ async def test_skipped_lights_context_not_from_arbitrary_switch(hass): f"but got {name_hash_in_context}. This indicates the context is still " f"being created from an arbitrary switch instead of the manager." ) + + +async def test_automation_turn_on_from_off_not_marked_as_manual_control(hass): + """Test that turning on a light from OFF via automation is not marked as manual control. + + Regression test for https://github.com/basnijholt/adaptive-lighting/issues/1378 + + When an automation turns on a light from OFF state with brightness/color attributes, + the light should NOT be marked as manually controlled. Adaptive Lighting should + adapt the light normally. + + The bug in v1.30.0 was that `update_manually_controlled_from_event` was called for + ALL `light.turn_on` events, not just when the light was already ON. This caused + lights turned on by automations to be incorrectly marked as "manually controlled". + """ + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_TAKE_OVER_CONTROL: True, + CONF_DETECT_NON_HA_CHANGES: False, + }, + ) + + # Ensure light is OFF + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_OFF + + # Verify light is not manually controlled + assert not switch.manager.manual_control.get( + ENTITY_LIGHT_1, + ), "Light should not be manually controlled before test" + + # Simulate an automation turning on the light with brightness + # This is an external call (not from AL) with brightness attribute + external_context = Context(id="automation_context_12345") + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT_1, + ATTR_BRIGHTNESS: 255, + }, + blocking=True, + context=external_context, + ) + await hass.async_block_till_done() + + # The light should be ON + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + + # CRITICAL: The light should NOT be marked as manually controlled! + # The bug in v1.30.0 would incorrectly mark this as manual control because + # the turn_on had a brightness attribute. + manual_control_attrs = switch.manager.manual_control.get(ENTITY_LIGHT_1) + assert not manual_control_attrs, ( + f"Bug confirmed: Light was incorrectly marked as manually controlled " + f"(attributes: {manual_control_attrs}) when turned on from OFF state. " + f"Lights turned on from OFF by automations should NOT be marked as " + f"manually controlled - only lights that were already ON and then had " + f"their brightness/color changed externally should be marked as such." + ) + + +@pytest.mark.parametrize("intercept", [True, False]) +async def test_adapt_only_on_bare_turn_on_respects_pause_changed_mode(hass, intercept): + """Test that adapt_only_on_bare_turn_on respects take_over_control_mode=PAUSE_CHANGED. + + When adapt_only_on_bare_turn_on=True and take_over_control_mode=PAUSE_CHANGED, + turning on a light from OFF with only brightness should: + 1. Mark ONLY brightness as manually controlled (not all attributes) + 2. Continue adapting color (since only brightness was specified) + + This test verifies the integration of #1356 (individual attribute tracking) + with adapt_only_on_bare_turn_on. Prior to the fix, the code would return early + after marking attributes as manually controlled, skipping all adaptation + including unspecified attributes like color. + + The test is parameterized with intercept=True/False to verify consistency + between the intercept path and the reactive (event-based) path. + """ + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_TAKE_OVER_CONTROL: True, + CONF_TAKE_OVER_CONTROL_MODE: TakeOverControlMode.PAUSE_CHANGED.value, + CONF_ADAPT_ONLY_ON_BARE_TURN_ON: True, + CONF_DETECT_NON_HA_CHANGES: False, + CONF_INTERCEPT: intercept, + }, + ) + + # Verify settings + assert switch._take_over_control + assert switch._take_over_control_mode == TakeOverControlMode.PAUSE_CHANGED + assert switch._adapt_only_on_bare_turn_on + + # Ensure light is OFF + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_OFF + + # Clear any prior service data + switch.manager.last_service_data.pop(ENTITY_LIGHT_1, None) + + # Turn on light from OFF with only brightness (simulating a scene or automation) + external_context = Context(id="scene_turn_on_with_brightness") + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT_1, + ATTR_BRIGHTNESS: 200, # Only brightness specified + }, + blocking=True, + context=external_context, + ) + await hass.async_block_till_done() + + # Light should be ON + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + + # 1. Verify that ONLY brightness is marked as manually controlled + manual_control_attrs = switch.manager.manual_control.get(ENTITY_LIGHT_1) + assert manual_control_attrs == LightControlAttributes.BRIGHTNESS, ( + f"Expected only BRIGHTNESS to be marked as manually controlled, " + f"but got: {manual_control_attrs}. With adapt_only_on_bare_turn_on=True, " + f"only the attributes specified in the turn_on call should be marked." + ) + + # 2. Verify that color WAS adapted (last_service_data should have color_temp) + last_service_data = switch.manager.last_service_data.get(ENTITY_LIGHT_1) + assert last_service_data is not None, ( + "Bug: last_service_data is None, meaning adaptation was skipped entirely. " + "With PAUSE_CHANGED mode, color should still be adapted since only brightness " + "was marked as manually controlled." + ) + assert ATTR_COLOR_TEMP_KELVIN in last_service_data, ( + f"Bug: Color was not adapted. last_service_data={last_service_data}. " + f"With take_over_control_mode=PAUSE_CHANGED and only brightness marked " + f"as manually controlled, color_temp should still be adapted." + ) From e0f812d406ffa8ec9309ef2d1bfd27529c67c4ec Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 12 Jan 2026 13:46:19 +0100 Subject: [PATCH 0940/1077] Bump to v1.30.1 (#1384) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 27762b19..fc854221 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.30.0" + "version": "1.30.1" } From cbbcabdd1f2ad27dfd9cb6a3007f51306faa60c4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 12 Jan 2026 23:39:57 +0100 Subject: [PATCH 0941/1077] Add documentation site with Zensical framework (#1385) * Add documentation site with Zensical framework Create comprehensive documentation site for adaptive-lighting.nijho.lt: - Add zensical.toml configuration with Material theme (amber/orange) - Create docs_gen.py module for extracting README sections via markers - Add section markers to README.md for content reuse - Create documentation pages: - index.md: Home with features overview - getting-started.md: Installation and quick setup - configuration.md: Auto-generated config options table - services.md: Auto-generated service documentation - automation-examples.md: Real-world automation recipes - troubleshooting.md: Common issues and solutions - see-also.md: External resources and links - advanced/brightness-modes.md: Brightness mode deep dive - advanced/manual-control.md: Manual control system docs - advanced/sleep-mode.md: Sleep mode configuration - Add GitHub Actions workflow for building and deploying to Pages - Add custom CSS with sun-themed styling - Add CNAME for custom domain Uses markdown-code-runner to auto-generate content from code schemas and extract README sections for single-source documentation. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Remove duplicated content from docs, make pages thin wrappers - troubleshooting.md: Remove manually written "Additional Tips" section - automation-examples.md: Remove duplicate "Additional Examples" section - configuration.md: Remove duplicate "Option Categories" tables - sleep-mode.md: Simplify to reference main config, remove duplicate examples - docs_gen.py: Remove unused get_troubleshooting() and get_sleep_mode_intro() This reduces duplication risk by keeping README as single source of truth. Docs pages now primarily pull content via markdown-code-runner. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Integrate webapp (simulator) into docs workflow - Merge deploy-webapp.yml into docs.yml workflow - Build simulator and place at /simulator/ subdirectory - Update docs links to use relative paths to simulator - Remove separate deploy-webapp.yml to avoid conflicts The combined workflow now: 1. Builds docs with zensical 2. Builds webapp with shinylive 3. Copies webapp to site/simulator/ 4. Deploys everything to GitHub Pages Simulator will be at adaptive-lighting.nijho.lt/simulator/ * Fix pre-commit and CI issues - Add site_name to zensical.toml (required by MkDocs) - Fix RET504 in docs_gen.py (unnecessary assignment before return) - Remove docs/run_markdown_code_runner.py (lint issues, not needed for CI) * Fix _docs_helpers.py import error in CI - Add try/except for relative vs absolute imports in _docs_helpers.py - Remove silent error handling in docs workflow (fail on error) The relative import fails when markdown-code-runner executes the code directly via sys.path.insert. The fallback to absolute import fixes this. * Temporarily enable deployment from feature branch * Add tabulate dependency for pandas to_markdown() * Fix theme configuration for proper light/dark mode - Restructure zensical.toml to match working agent-cli config - Add three-way palette toggle (system/light/dark) - Use proper [project.theme] structure - Simplify extra.css to not override theme colors - Add Inter font for text, JetBrains Mono for code * Add Plausible analytics and fix homepage navigation - Add custom analytics override for plausible.nijho.lt tracking - Remove hide:navigation from index.md to show menu on homepage * Remove temporary feature branch deployment settings Revert to main-only deployment for docs workflow before merging. * Revert "Remove temporary feature branch deployment settings" This reverts commit c56869483760051599234b820b3cf44de128901f. * Add markdown-gfm-admonition for GitHub-style admonitions The zensical build was failing silently because gfm_admonition extension was not installed. Add the dependency to pyproject.toml and docs workflow. * Use uv sync for documentation dependencies Switch from manual pip installs to uv sync with pyproject.toml for cleaner dependency management and reproducible builds. * Fix markdown rendering inside details blocks Enable md_in_html extension and add markdown="1" attribute to
tags so markdown content inside them is properly rendered. * Remove emojis from manually written documentation Keep emojis in auto-generated content from README, but remove from manually maintained docs in favor of clean text and Material icons. * Enable attr_list extension for button styling * Improve pyproject.toml and use GitHub-style admonitions - Add accurate project metadata (version, authors, classifiers, URLs) - Organize dependency groups: docs, dev, test - Add tool configs for ruff, mypy, pytest - Convert MkDocs-style admonitions (!!! tip) to GitHub-style (> [!TIP]) - Use docs group in CI workflow * Add homeassistant and ulid-transform as runtime dependencies Remove speculative test dependencies since tests run inside HA core. * Simplify docs_gen.py - remove wrapper functions Use readme_section() directly in docs instead of 10 one-liner wrappers. Changed default strip_heading to True since that's the common case. * Populate empty OUTPUT sections with markdown-code-runner * Add markdown-code-runner workflow for auto-updating docs - Add docs/run_markdown_code_runner.py script to process all docs - Add GitHub workflow to run on push/PR and auto-commit changes * Exclude README.md from markdown-code-runner workflow README.md contains code blocks that import from homeassistant.components.adaptive_lighting, which only exists when running inside Home Assistant core, not in a regular venv. * Update auto-generated docs * Use editable install for markdown-code-runner workflow - Add setuptools.packages.find config pointing to custom_components - Remove sys.path.insert manipulation from all docs files - Update imports to use adaptive_lighting.* package paths - Install package with `uv pip install -e .` in workflow - Remove deprecated license classifier (PEP 639) * Consolidate markdown-code-runner into single workflow - Remove separate markdown-code-runner.yml workflow - Update update-readme.yml to handle all markdown files (docs + README) - Rename workflow to "Update auto-generated content" - Update README imports to use adaptive_lighting package path * Rename workflow to markdown-code-runner * Remove accidentally committed files * Remove redundant markdown-code-runner from docs workflow * Fix: use uv pip install instead of uv add in CI * Add webapp deps (astral, shinylive) to docs group * Remove unused install_dependencies action * Update to latest action versions (uv@v5, upload-pages-artifact@v4) * Remove try/except import fallback in _docs_helpers.py * Restore install_dependencies action (used by pytest) * Simplify docs workflow: run on all pushes/PRs * Simplify mcr workflow paths; revert install_dependencies to main * Remove redundant cp+sed for webapp (file already in repo) * Fix mcr push: pull --rebase before push * Fix mcr: checkout PR branch instead of detached HEAD * Update auto-generated content * Switch from setuptools to hatch build system Replace [tool.setuptools.packages.find] with [tool.hatch.build.targets.wheel] for hatchling compatibility. * Update auto-generated content * Move homeassistant deps to docs group This is a HA custom component, not a pip package. The homeassistant dependency is only needed for docs building, not as a project dependency. * Update auto-generated content * Remove PyPI-only metadata from pyproject.toml * Remove arbitrary version constraints from dependency groups * Update auto-generated content * Remove unused troubleshooting section markers from README * Remove temporary feature branch settings from docs workflow * Use GitHub admonition syntax for warning in change_switch_settings section * Update auto-generated content --- .github/workflows/deploy-webapp.yml | 63 - .github/workflows/docs.yml | 64 + ...te-readme.yml => markdown-code-runner.yml} | 40 +- README.md | 146 +- .../adaptive_lighting/docs_gen.py | 84 + docs/CNAME | 1 + docs/advanced/brightness-modes.md | 124 + docs/advanced/manual-control.md | 162 + docs/advanced/sleep-mode.md | 43 + docs/assets/logo.png | Bin 0 -> 17642 bytes docs/assets/stylesheets/extra.css | 46 + docs/automation-examples.md | 115 + docs/configuration.md | 150 + docs/getting-started.md | 117 + docs/index.md | 85 + .../integrations/analytics/custom.html | 6 + docs/run_markdown_code_runner.py | 71 + docs/see-also.md | 73 + docs/services.md | 203 + docs/troubleshooting.md | 106 + pyproject.toml | 66 + uv.lock | 6079 +++++++++++++++++ zensical.toml | 117 + 23 files changed, 7816 insertions(+), 145 deletions(-) delete mode 100644 .github/workflows/deploy-webapp.yml create mode 100644 .github/workflows/docs.yml rename .github/workflows/{update-readme.yml => markdown-code-runner.yml} (50%) create mode 100644 custom_components/adaptive_lighting/docs_gen.py create mode 100644 docs/CNAME create mode 100644 docs/advanced/brightness-modes.md create mode 100644 docs/advanced/manual-control.md create mode 100644 docs/advanced/sleep-mode.md create mode 100644 docs/assets/logo.png create mode 100644 docs/assets/stylesheets/extra.css create mode 100644 docs/automation-examples.md create mode 100644 docs/configuration.md create mode 100644 docs/getting-started.md create mode 100644 docs/index.md create mode 100644 docs/overrides/partials/integrations/analytics/custom.html create mode 100755 docs/run_markdown_code_runner.py create mode 100644 docs/see-also.md create mode 100644 docs/services.md create mode 100644 docs/troubleshooting.md create mode 100644 pyproject.toml create mode 100644 uv.lock create mode 100644 zensical.toml diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml deleted file mode 100644 index 0a76e3ac..00000000 --- a/.github/workflows/deploy-webapp.yml +++ /dev/null @@ -1,63 +0,0 @@ -# Simple workflow for deploying WebAssembly app to GitHub Pages -name: Deploy WebAssembly app to Pages - -on: - # Runs on pushes targeting the default branch - push: - branches: ["main"] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - # Single deploy job since we're just deploying - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Set Up Python - uses: actions/setup-python@v6 - with: - python-version: 3.14.2 - - - name: Install Dependencies - run: | - pip install -r webapp/requirements.txt - pip install shinylive - - - name: Build the WebAssembly app - run: | - set -ex - cp custom_components/adaptive_lighting/color_and_brightness.py webapp/color_and_brightness.py - sed -i 's/homeassistant.util.color/homeassistant_util_color/g' "webapp/color_and_brightness.py" - shinylive export webapp site - - - name: Setup Pages - uses: actions/configure-pages@v5 - - - name: Upload artifact - uses: actions/upload-pages-artifact@v4 - with: - # Upload the 'site' directory, where your app has been built - path: "site" - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..7b62b15c --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,64 @@ +name: Documentation + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Install dependencies + run: uv sync --group docs + + - name: Build documentation + run: uv run zensical build + + - name: Build webapp (simulator) + run: uv run shinylive export webapp webapp-site + + - name: Integrate webapp into docs + run: | + # Copy webapp into docs site at /simulator/ + mkdir -p site/simulator + cp -r webapp-site/* site/simulator/ + echo "Webapp integrated at site/simulator/" + + - name: Upload artifact + uses: actions/upload-pages-artifact@v4 + with: + path: ./site + + deploy: + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/update-readme.yml b/.github/workflows/markdown-code-runner.yml similarity index 50% rename from .github/workflows/update-readme.yml rename to .github/workflows/markdown-code-runner.yml index fb8fa6f4..884cfa71 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/markdown-code-runner.yml @@ -1,33 +1,34 @@ -name: Update README.md, strings.json, and services.yaml +name: markdown-code-runner on: push: branches: - main - paths: - - "README.md" - - "custom_components/adaptive_lighting/const.py" - - ".github/workflows/update-readme.yml" pull_request: jobs: - update_readme: + markdown-code-runner: runs-on: ubuntu-latest steps: - name: Check out code from GitHub - uses: actions/checkout@v6 + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 - - name: Install Home Assistant - uses: ./.github/workflows/install_dependencies + - name: Set up Python + uses: actions/setup-python@v5 with: python-version: "3.13" - - name: Install markdown-code-runner and README code dependencies - run: | - uv pip install markdown-code-runner==2.1.0 pandas tabulate + - name: Install uv + uses: astral-sh/setup-uv@v5 - name: Run markdown-code-runner - run: uv run markdown-code-runner --verbose README.md + run: | + uv sync --group docs + uv pip install -e . + uv run python docs/run_markdown_code_runner.py - name: Run update services.yaml run: uv run python .github/update-services.py @@ -35,23 +36,22 @@ jobs: - name: Run update strings.json run: uv run python .github/update-strings.py - - name: Commit updated README.md, strings.json, and services.yaml + - name: Commit updated files id: commit run: | git add -u . git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" if git diff --quiet && git diff --staged --quiet; then - echo "No changes in README.md, strings.json, and services.yaml, skipping commit." + echo "No changes, skipping commit." echo "commit_status=skipped" >> $GITHUB_ENV else - git commit -m "Update README.md, strings.json, and services.yaml" + git commit -m "Update auto-generated content" echo "commit_status=committed" >> $GITHUB_ENV fi - name: Push changes if: env.commit_status == 'committed' - uses: ad-m/github-push-action@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - branch: ${{ github.head_ref }} + run: | + git pull --rebase + git push diff --git a/README.md b/README.md index 7a7ee9d9..23bb93f1 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ https://github.com/basnijholt/adaptive-lighting/assets/6897215/68908f7d-fbf1-499 [[ToC](#books-table-of-contents)] + ## :bulb: Features When initially turning on a light that is controlled by Adaptive Lighting, the `light.turn_on` service call is intercepted, and the light's brightness and color are automatically adjusted based on the sun's position. @@ -35,7 +36,9 @@ Adaptive Lighting provides four switches (using "living_room" as an example comp - `switch.adaptive_lighting_sleep_mode_living_room`: Activate "sleep mode" 😴 and set custom sleep_brightness and sleep_color_temp. - `switch.adaptive_lighting_adapt_brightness_living_room`: Enable or disable brightness adaptation 🔆 for supported lights. - `switch.adaptive_lighting_adapt_color_living_room`: Enable or disable color adaptation 🌈 for supported lights. + + ### :control_knobs: Regain Manual Control Adaptive Lighting is designed to automatically detect when you or another source (e.g., automation) manually changes light settings 🕹️. @@ -46,6 +49,7 @@ Additionally, enabling `detect_non_ha_changes` allows Adaptive Lighting to detec The `adaptive_lighting.manual_control` event is fired when a light is marked as "manually controlled," allowing for integration with automations 🤖. > ⚠️ **_Caution: Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable `detect_non_ha_changes` if you encounter such issues._** + ## :books: Table of Contents @@ -99,56 +103,57 @@ All of the configuration options are listed below, along with their default valu The YAML and frontend configuration methods support all of the options listed below. - - + + -| Variable name | Description | Default | Type | -|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:----------------------------------------| -| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | -| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | -| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | -| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | -| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | -| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | -| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | -| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | -| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | -| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | -| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | -| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | -| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | -| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | -| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | -| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | -| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | -| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | -| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | -| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | -| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | -| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | +| Variable name | Description | Default | Type | +|:-------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:----------------------------------------| +| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | +| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | +| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | +| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | +| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | +| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | | `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | | `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | -| `take_over_control` | Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒 | `True` | `bool` | -| `take_over_control_mode` | The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed. | `pause_all` | one of `['pause_all', 'pause_changed']` | -| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues. | `False` | `bool` | -| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | -| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `take_over_control` | Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒 | `True` | `bool` | +| `take_over_control_mode` | The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed. | `pause_all` | one of `['pause_all', 'pause_changed']` | +| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues. | `False` | `bool` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | | `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | -| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | -| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | -| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | -| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | -| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | -| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | -| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | +| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | +| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | +| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | +| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | + Full example: ```yaml @@ -175,6 +180,7 @@ adaptive_lighting: only_once: false ``` + ### :hammer_and_wrench: Services @@ -183,21 +189,21 @@ adaptive_lighting: `adaptive_lighting.apply` applies Adaptive Lighting settings to lights on demand. - - + + -| Service data attribute | Description | Required | Type | -|:-------------------------|:-------------------------------------------------------------------------------------|:-----------|:---------------------| -| `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ✅ | list of `entity_id`s | -| `lights` | A light (or list of lights) to apply the settings to. 💡 | ❌ | list of `entity_id`s | -| `transition` | Duration of transition when lights change, in seconds. 🕑 | ❌ | `float` 0-6553 | -| `adapt_brightness` | Whether to adapt the brightness of the light. 🌞 | ❌ | bool | -| `adapt_color` | Whether to adapt the color on supporting lights. 🌈 | ❌ | bool | -| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | ❌ | bool | -| `turn_on_lights` | Whether to turn on lights that are currently off. 🔆 | ❌ | bool | +| Service data attribute | Description | Required | Type | +|:-------------------------|:--------------------------------------------------------------------------------------|:-----------|:---------------------| +| `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ✅ | list of `entity_id`s | +| `lights` | A light (or list of lights) to apply the settings to. 💡 | ❌ | list of `entity_id`s | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | ❌ | `float` 0-6553 | +| `adapt_brightness` | Whether to adapt the brightness of the light. 🌞 | ❌ | bool | +| `adapt_color` | Whether to adapt the color on supporting lights. 🌈 | ❌ | bool | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | ❌ | bool | +| `turn_on_lights` | Whether to turn on lights that are currently off. 🔆 | ❌ | bool | #### `adaptive_lighting.set_manual_control` @@ -205,24 +211,27 @@ adaptive_lighting: `adaptive_lighting.set_manual_control` can mark (or unmark) whether a light is "manually controlled", meaning that when a light has `manual_control`, the light is not adapted. - - + + -| Service data attribute | Description | Required | Type | -|:-------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:-----------------------------------------| -| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s | -| `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s | -| `manual_control` | Whether to add ("true") or remove ("false") all adapted attributes of the light from the "manual_control" list, or the name of an attribute for selective addition. 🔒 | ❌ | bool or one of `['brightness', 'color']` | +| Service data attribute | Description | Required | Type | +|:-------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:-----------------------------------------| +| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s | +| `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s | +| `manual_control` | Whether to add ("true") or remove ("false") all adapted attributes of the light from the "manual_control" list, or the name of an attribute for selective addition. 🔒 | ❌ | bool or one of `['brightness', 'color']` | + + #### `adaptive_lighting.change_switch_settings` `adaptive_lighting.change_switch_settings` (new in 1.7.0) Change any of the above configuration options of Adaptive Lighting (such as `sunrise_time` or `prefer_rgb_color`) with a service call directly from your script/automation. -> ⚠️ **_Note: These settings will **not** be written to your config and will be reset on restart of Home Assistant! You can see the current settings in the `switch.adaptive_lighting_XXX` attributes if `include_config_in_attributes` is enabled._** +> [!WARNING] +> These settings will **not** be written to your config and will be reset on restart of Home Assistant! You can see the current settings in the `switch.adaptive_lighting_XXX` attributes if `include_config_in_attributes` is enabled. | Service data attribute | Required | Description | | --------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | @@ -237,10 +246,12 @@ The following keys are disallowed: | `lights` | You may call `adaptive_lighting.apply` with your lights or create a new config instead. | | `name` | You can rename your switch's display name in Home Assistant's UI. | | `interval` | The interval is used only once when the config loads. A config change and restart are required. | + + ## :robot: Automation examples -
+
Reset the manual_control status of a light after an hour. ```yaml @@ -265,7 +276,7 @@ The following keys are disallowed:
-
+
Toggle multiple Adaptive Lighting switches to "sleep mode" using an input_boolean.sleep_mode. ```yaml @@ -336,6 +347,7 @@ iphone_carly_wakeup: ```
+ ## Additional Information @@ -345,6 +357,7 @@ Adaptive Lighting was initially inspired by @claytonjn's [hass-circadian\_lighti ## :sos: Troubleshooting + Encountering issues? Enable debug logging in your `configuration.yaml`: ```yaml @@ -355,7 +368,9 @@ logger: ``` After the issue occurs, create a new issue report with the log (`/config/home-assistant.log`). + + ### :exclamation: Common Problems & Solutions #### :bulb: Lights Not Responding or Turning On by Themselves @@ -413,7 +428,9 @@ These lights are known to exhibit disadvantageous behaviour due to firmware bugs - Ikea Tradfri bulbs/drivers (and related Ikea smart light products) - Unsupported simultaneous transition of brightness and color: When receiving such a command, they switch the brightness instantly and only transition the color. To get smooth transitions of both brightness and color, enable `separate_turn_on_commands`. - Unresponsiveness during color transitions: No other commands are processed during an ongoing color transition, e.g., turn-off commands are ignored and lights stay on despite being reported as off to Home Assistant. The default config with long transitions thus results in long periods of unresponsiveness. To work around this, disable transitions by setting `transition` to `0`, and increase the adaptation frequency by setting `interval` to a short time, e.g., `15` seconds, to retain the impression of smooth continuous adaptations. Keeping the `initial_transition` is recommended for a smooth fade-in (lights are usually not turned off momentarily after being turned on, in which case a short period of unresponsiveness is tolerable). + + ## :bar_chart: Graphs! These graphs were generated using the values calculated by the Adaptive Lighting sensor/switch(es). @@ -428,10 +445,12 @@ These graphs were generated using the values calculated by the Adaptive Lighting ### While using `transition_until_sleep: true` ![image](https://user-images.githubusercontent.com/2219836/228949675-f9699624-8abc-466c-bb04-250ce0f495b8.png) + + ### Custom brightness ramps using `brightness_mode` with `"linear"` and `"tanh"` -
+
Enhance your control over brightness transitions during sunrise and sunset with brightness_mode (click here to learn more 🧠). With Adaptive Lighting, you can set a `brightness_mode` to specify how the brightness changes during sunrise and sunset. The `brightness_mode` can be set to `"default"` ([as illustrated in other graphs above](#high_brightness-brightness)), `"linear"`, or `"tanh"`. If you choose to deviate from the `"default"` mode, you can adjust `brightness_mode_time_dark` and `brightness_mode_time_light` to further customize the lighting transitions. @@ -454,12 +473,15 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark ![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/3dcbdc42-63c4-49df-8651-d2fae53dd08d) > Check out the interactive webapp on https://basnijholt.github.io/adaptive-lighting/ to play with the parameters and see how the brightness changes! + + ## :eyes: See also - [*Sleep better with Adaptive Lighting in Home Assistant*](https://wartner.io/sleep-better-with-adaptive-lightning-in-home-assistant/) by Florian Wartner on 2023-02-23 (blog post 📜) - [*Automatic smart light brightness and color based on the sun*](https://www.youtube.com/watch?v=Rg3zI1Oyk3c) by Home Automation Guy on 2022-08-31 (YouTube video 📺) - [*Adaptive Lighting Blew My Mind in Home Assistant - How to set it up*](https://www.youtube.com/watch?v=c1cnccmgl3k) by Smart Home Junkie on 2022-06-26 (YouTube video 📺) + ## :busts_in_silhouette: Contributors diff --git a/custom_components/adaptive_lighting/docs_gen.py b/custom_components/adaptive_lighting/docs_gen.py new file mode 100644 index 00000000..8681da4b --- /dev/null +++ b/custom_components/adaptive_lighting/docs_gen.py @@ -0,0 +1,84 @@ +"""Documentation generation utilities for Adaptive Lighting. + +Provides functions to extract sections from README.md and transform +content for the documentation site. Used by markdown-code-runner +to generate documentation pages from README content. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +# Path to README relative to this module +_MODULE_DIR = Path(__file__).parent +README_PATH = _MODULE_DIR.parent.parent / "README.md" + + +def readme_section(section_name: str, *, strip_heading: bool = True) -> str: + """Extract a marked section from README.md. + + Sections are marked with HTML comments: + + content + + + Args: + section_name: The name of the section to extract + strip_heading: If True, remove the first heading from the section + + Returns: + The content between the section markers + + Raises: + ValueError: If the section is not found in README.md + + """ + content = README_PATH.read_text() + + start_marker = f"" + end_marker = f"" + + start_idx = content.find(start_marker) + if start_idx == -1: + msg = f"Section '{section_name}' not found in README.md" + raise ValueError(msg) + + end_idx = content.find(end_marker, start_idx) + if end_idx == -1: + msg = f"End marker for section '{section_name}' not found" + raise ValueError(msg) + + section = content[start_idx + len(start_marker) : end_idx].strip() + + if strip_heading: + # Remove first heading (# or ## or ###) + section = re.sub(r"^#{1,3}\s+[^\n]+\n+", "", section, count=1) + + return _transform_readme_links(section) + + +def _transform_readme_links(content: str) -> str: + """Transform README internal links to docs site links.""" + # Map README anchors to doc pages + link_map = { + "#gear-configuration": "configuration.md", + "#memo-options": "configuration.md#all-options", + "#hammer_and_wrench-services": "services.md", + "#adaptive_lightingapply": "services.md#adaptive_lightingapply", + "#adaptive_lightingset_manual_control": "services.md#adaptive_lightingset_manual_control", + "#adaptive_lightingchange_switch_settings": "services.md#adaptive_lightingchange_switch_settings", + "#robot-automation-examples": "automation-examples.md", + "#sos-troubleshooting": "troubleshooting.md", + "#exclamation-common-problems--solutions": "troubleshooting.md#common-problems-solutions", + "#bar_chart-graphs": "advanced/brightness-modes.md#graphs", + "#bulb-features": "index.md#features", + "#control_knobs-regain-manual-control": "advanced/manual-control.md", + "#eyes-see-also": "see-also.md", + } + + for old_link, new_link in link_map.items(): + content = content.replace(f"]({old_link})", f"]({new_link})") + + # Remove ToC link pattern [[ToC](#...)] + return re.sub(r"\[\[ToC\]\([^)]+\)\]", "", content) diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 00000000..31586af9 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +adaptive-lighting.nijho.lt diff --git a/docs/advanced/brightness-modes.md b/docs/advanced/brightness-modes.md new file mode 100644 index 00000000..b735a583 --- /dev/null +++ b/docs/advanced/brightness-modes.md @@ -0,0 +1,124 @@ +--- +icon: lucide/trending-up +--- + +# Brightness Modes + +Enhance your control over brightness transitions during sunrise and sunset with the `brightness_mode` option. + +## Available Modes + +Adaptive Lighting supports three brightness modes: + +| Mode | Description | +|------|-------------| +| `default` | Standard behavior based on sun position | +| `linear` | Linear ramp between min/max brightness | +| `tanh` | Smooth S-curve using hyperbolic tangent | + +## Detailed Explanation + + + + + + + +
+Enhance your control over brightness transitions during sunrise and sunset with brightness_mode (click here to learn more 🧠). + +With Adaptive Lighting, you can set a `brightness_mode` to specify how the brightness changes during sunrise and sunset. The `brightness_mode` can be set to `"default"` ([as illustrated in other graphs above](#high_brightness-brightness)), `"linear"`, or `"tanh"`. If you choose to deviate from the `"default"` mode, you can adjust `brightness_mode_time_dark` and `brightness_mode_time_light` to further customize the lighting transitions. + +When `brightness_mode` is set to `"linear"`: + +- During **_sunset_**, the brightness begins to gradually decrease from `max_brightness` starting at `time=sunset_time - brightness_mode_time_light`, until it reaches `min_brightness` at `time=sunset_time + brightness_mode_time_dark`. +- During **_sunrise_**, the brightness begins to gradually increase from `min_brightness` starting at `time=sunrise_time - brightness_mode_time_dark`, until it reaches `max_brightness` at `time=sunrise_time + brightness_mode_time_light`. + +When `brightness_mode` is set to `"tanh"`, it uses the smooth transition of a [hyperbolic tangent function](https://mathworld.wolfram.com/HyperbolicTangent.html): + +- During **_sunset_**, the brightness starts to decrease from 95% of `max_brightness` starting at `time=sunset_time - brightness_mode_time_light`, until it reaches 5% of `min_brightness` at `time=sunset_time + brightness_mode_time_dark`. +- During **_sunrise_**, the brightness starts to increase from 5% of `min_brightness` starting at `time=sunrise_time - brightness_mode_time_dark`, until it reaches 95% of `max_brightness` at `time=sunrise_time + brightness_mode_time_light`. +
+ +Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark` in the text box. +![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/15143580-13cd-4ab2-a603-89f2b7830afd) +![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/f61fdac9-6d47-48c9-84ed-cbb451d5de5d) +![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/e5fc5d27-3c37-4e3d-93d1-6e7cf4b48e7c) +![image](https://github.com/basnijholt/adaptive-lighting/assets/6897215/3dcbdc42-63c4-49df-8651-d2fae53dd08d) + +> Check out the interactive webapp on https://basnijholt.github.io/adaptive-lighting/ to play with the parameters and see how the brightness changes! + + + +## Configuration Parameters + +When using `linear` or `tanh` modes, you can fine-tune the transition with these parameters: + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `brightness_mode_time_dark` | 900 (15 min) | Duration to ramp brightness before/after sunrise/sunset | +| `brightness_mode_time_light` | 3600 (1 hour) | Duration to ramp brightness after/before sunrise/sunset | + +## Example Configurations + +### Quick Transition (Linear) + +```yaml +adaptive_lighting: + - name: "Quick transitions" + lights: + - light.living_room + brightness_mode: linear + brightness_mode_time_dark: 600 # 10 minutes + brightness_mode_time_light: 1800 # 30 minutes +``` + +### Smooth Transition (Tanh) + +```yaml +adaptive_lighting: + - name: "Smooth transitions" + lights: + - light.bedroom + brightness_mode: tanh + brightness_mode_time_dark: 1200 # 20 minutes + brightness_mode_time_light: 3600 # 1 hour +``` + +## Graphs + +These graphs show how brightness changes throughout the day based on calculated values: + + + + + + + +These graphs were generated using the values calculated by the Adaptive Lighting sensor/switch(es). + +### :sunny: Sun Position +![cl_percent|690x131](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/6/5/657ff98beb65a94598edeb4bdfd939095db1a22c.PNG) + +### :thermometer: Color Temperature +![cl_color_temp|690x129](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/9/59e84263cbecd8e428cb08777a0413672c48dfcd.PNG) + +### :high_brightness: Brightness +![cl_brightness|690x130](https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/8/58ebd994b62a8b1abfb3497a5288d923ff4e2330.PNG) + +### While using `transition_until_sleep: true` +![image](https://user-images.githubusercontent.com/2219836/228949675-f9699624-8abc-466c-bb04-250ce0f495b8.png) + + + +## Interactive Simulator + +The best way to understand brightness modes is to experiment with the interactive simulator: + + + +Adjust the `brightness_mode`, `brightness_mode_time_dark`, and `brightness_mode_time_light` parameters to see how they affect the brightness curve in real-time. diff --git a/docs/advanced/manual-control.md b/docs/advanced/manual-control.md new file mode 100644 index 00000000..157e081e --- /dev/null +++ b/docs/advanced/manual-control.md @@ -0,0 +1,162 @@ +--- +icon: lucide/hand +--- + +# Manual Control + +Adaptive Lighting is designed to work seamlessly with manual adjustments, detecting when you or another source changes light settings and pausing adaptation accordingly. + +## How It Works + + + + + + + +Adaptive Lighting is designed to automatically detect when you or another source (e.g., automation) manually changes light settings 🕹️. +When this occurs, the affected light is marked as "manually controlled," and Adaptive Lighting will not make further adjustments until the light is turned off and back on or reset using the `adaptive_lighting.set_manual_control` service call. +This feature is available when `take_over_control` is enabled. + +Additionally, enabling `detect_non_ha_changes` allows Adaptive Lighting to detect all state changes, including those made outside of Home Assistant, by comparing the light's state to its previously used settings. +The `adaptive_lighting.manual_control` event is fired when a light is marked as "manually controlled," allowing for integration with automations 🤖. + +> ⚠️ **_Caution: Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable `detect_non_ha_changes` if you encounter such issues._** + + + +## Configuration Options + +### take_over_control + +When enabled (default: `true`), Adaptive Lighting detects `light.turn_on` service calls that specify brightness or color values. If such a call is detected for a light that's already on, that light is marked as "manually controlled". + +```yaml +adaptive_lighting: + - name: "With manual control detection" + lights: + - light.living_room + take_over_control: true # default +``` + +### take_over_control_mode + +Controls how adaptation pauses when manual changes are detected: + +| Mode | Behavior | +|------|----------| +| `pause_all` | Pause both brightness and color adaptation (default) | +| `pause_changed` | Only pause adaptation of the changed attribute | + +```yaml +adaptive_lighting: + - name: "Selective pause" + lights: + - light.living_room + take_over_control: true + take_over_control_mode: pause_changed # Only pause changed attributes +``` + +### detect_non_ha_changes + +When enabled, Adaptive Lighting detects state changes made outside of Home Assistant by comparing the light's current state to its previously applied settings. + +> [!WARNING] +> **Use with caution.** Some lights may falsely report an "on" state, which could result in lights turning on unexpectedly. Disable this option if you encounter such issues. + +```yaml +adaptive_lighting: + - name: "Detect external changes" + lights: + - light.living_room + take_over_control: true + detect_non_ha_changes: true +``` + +### autoreset_control_seconds + +Automatically resets the manual control flag after a specified number of seconds. Set to `0` to disable (default). + +```yaml +adaptive_lighting: + - name: "Auto-reset after 2 hours" + lights: + - light.living_room + take_over_control: true + autoreset_control_seconds: 7200 # 2 hours +``` + +### adapt_only_on_bare_turn_on + +When enabled, Adaptive Lighting only adapts lights when `light.turn_on` is called without specifying brightness or color. This is useful when you want scenes to work without interference. + +```yaml +adaptive_lighting: + - name: "Respect scenes" + lights: + - light.living_room + take_over_control: true + adapt_only_on_bare_turn_on: true +``` + +## Checking Manual Control Status + +You can see which lights are marked as manually controlled by checking the switch attributes: + +1. Go to **Developer Tools** → **States** +2. Find your Adaptive Lighting switch (e.g., `switch.adaptive_lighting_living_room`) +3. Look at the `manual_control` attribute - it lists all manually controlled lights + +## Resetting Manual Control + +### Via Service Call + +```yaml +service: adaptive_lighting.set_manual_control +data: + entity_id: switch.adaptive_lighting_living_room + lights: + - light.floor_lamp + manual_control: false # Resume adaptation +``` + +### By Turning Light Off and On + +Simply turning a light off and then back on will reset its manual control status. + +### Via Automation + +See [Automation Examples](../automation-examples.md) for automation recipes that automatically reset manual control. + +## Events + +When a light is marked as manually controlled, Adaptive Lighting fires an event: + +**Event type:** `adaptive_lighting.manual_control` + +**Event data:** +```yaml +entity_id: light.living_room +switch: switch.adaptive_lighting_living_room +``` + +You can use this event to trigger automations: + +```yaml +automation: + - alias: "Notify on manual control" + trigger: + platform: event + event_type: adaptive_lighting.manual_control + action: + - service: notify.mobile_app + data: + message: "{{ trigger.event.data.entity_id }} was manually adjusted" +``` + +## Best Practices + +1. **Use Zigbee groups** when controlling multiple bulbs together - this ensures consistent manual control detection +2. **Set reasonable autoreset times** if you want lights to eventually resume adaptation +3. **Use `pause_changed` mode** if you only adjust brightness or color individually +4. **Disable `detect_non_ha_changes`** if you experience unexpected light turn-ons diff --git a/docs/advanced/sleep-mode.md b/docs/advanced/sleep-mode.md new file mode 100644 index 00000000..40736340 --- /dev/null +++ b/docs/advanced/sleep-mode.md @@ -0,0 +1,43 @@ +--- +icon: lucide/moon +--- + +# Sleep Mode + +Sleep mode is a special operating mode that sets your lights to minimal brightness and very warm color, perfect for winding down at night without disrupting your circadian rhythm. + +## Activating Sleep Mode + +Each Adaptive Lighting configuration creates a sleep mode switch: + +``` +switch.adaptive_lighting_sleep_mode_ +``` + +Turn it on to activate sleep mode: + +```yaml +service: switch.turn_on +target: + entity_id: switch.adaptive_lighting_sleep_mode_living_room +``` + +## Configuration Options + +Sleep mode is configured through the main Adaptive Lighting configuration. See the [Configuration](../configuration.md) page for the full options table. The sleep-related options are: + +| Option | Default | Description | +|--------|---------|-------------| +| `sleep_brightness` | 1 | Brightness percentage in sleep mode | +| `sleep_rgb_or_color_temp` | `color_temp` | Use `rgb_color` or `color_temp` in sleep mode | +| `sleep_color_temp` | 1000 | Color temperature in Kelvin for sleep mode | +| `sleep_rgb_color` | `[255, 56, 0]` | RGB color for sleep mode | +| `sleep_transition` | 1 | Transition duration in seconds | +| `transition_until_sleep` | false | Gradually transition to sleep settings after sunset | + +## Automation Examples + +See [Automation Examples](../automation-examples.md) for sleep mode automation recipes, including: + +- Toggle sleep mode using an `input_boolean` +- Set sunrise/sunset based on alarm time diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d055aca3da7d8586f9872b7abcf331631629c9fc GIT binary patch literal 17642 zcmd^nc|6qH|NomT+0soY+bE>6Rb)vFNgA@;HYtqiW=SE6tTR!$rL0M^%`H+XlqBnr z%9cGT+mK-_F&WDkGv@t0Z_-t{pZ|Z4pB_=X-mmvLuk%`-ujROAWnm)3FToE%kPv3? z?)?zN3I53mE$0DWCN6%M0bh7dn3?Q`VB`;>I6Ve}WFgFM!-E$SM~Z!4*v!tBgI zi#;0zAPJyL)Z0vsC`_%@18hE<8Ep@9!_|H}cIts95FWOitH|k28h2b$Lf0 ze|al=B=YdJja-*w;tk}lZ;a+!X1Xd`{p(~gj+{uA|6K0#bn>(7C+4J|3`)nJNT(EA ze{w3;e^Trf&IMNW|L=!Gh|&xnWT1U4uS?UHmRu5JN2;uPY@%3Gq3Az&P{-#v+46Qr zl?D4RHJ>6Mw*vX9VQtLpNx5Y8DL6*noL+}%(#MqZx=&f6SU9cqE?M%v)G1%els46; z#wBmOZ6t{_l9)X4j3nJU<4up4=`^Z{7){n=KK{p+Eepx2D?^aN2@j8K_yb0_Zb-5v z-@b8B|GH&NMBOBfvSZLz&F5w6!%ivGx2Jj=;P+nP)p{eTCdJM7yM`MpT?!*|RWcbS z5#w#!T*s8?+(KOV@N!Pb3ewf=*c$joSh&*l9J%B5r$P$muH2x;_CEsn*J{?S+VarR+gd` z_hO$#oImx!<(*;-#Zk%F7iCqX-r~YzC2(O*obA|O4G^Tw10LcMT(M%ReyIIi{y5td zV{Lj=V~kzIEwM9WhUOj+_u|hex44eHpzQ&#htkaA!nuUFA@Gp**N%ys@p5vFN^ZGGSD-POM1?GIzDxeizii4FOauH09b3oes)U1e??zRQ*E5I2bA(y z5&h@X@}f^ce$rylfX>S-;wgh?U0;(9@7BwwL5Z~(;wz|04jQDX>G=e2EaUqr!G2r*^?Tl%samvx`>Fc?M%Q!YK7l6)7Te>mc zjma@PB;_ciFKMTmKJlR#?UNTWSP`{P&NandwVQZr9A-a#-ipJ(7-4^$vW&Dd&pwPa zOvW8+I@g;gha;!Rx60?Vnx%xLM${i;-Amz$<+vp!20h%_&f{+P<=zZap1ubYC_*)2 zSE4bhvLgQZ?Y3m}He65GVP4T?w^jpE`7SJK#MZwo;{>IvH#Khids)C%Hg0{+9#41+ z9NO;gp>+5|Nb+)nYhV$kW79*D=~L$MEKRjy5+*01FA~?|xi2N-Doc^}H=RwjxcCwd zbF%aP%qA#uTX7zN2&>L?1@xv;r0&o(DXbK;9mR*l7;&cIRh}&|@b2UrNzC@aiin^@ z>@mSZcy7q_CW>_s&UEgVJefb-LJpmhiZWrJianWEgF7*LPI2}u{6Y`b@Si$lWGKwD z5vbxu1!Vn4a6Q9`bT}nW7d^1GlF09QW*@y0w;mnS9O^S7Gs~j&)tH3vaFBuEZ(Tn( zR>LHoZ%W*)=s3Tlvr8J!_duushETr3-6>ue@lZ26oT$PNE zBHE#7TDRby8*wsEs))FAbPaQtgqK<~N9WwR)cy4h=~Rp21uXM(h>uCd%Gm}^fW zJ*5M^ZL82qf!MMAUD~9Szp<9mJX5^?egq2|MsGtf|wl`_Nl+j&%hN z^q|-*m@>{0sih1>Dz&#h4R|)*NHPl=YoxFH&OI##zMPe7(Zm!}IXYELP~>!g1R(P3 z=HbK18mR;KY3!ua-wKS_C)bj(#l1BfVYyM-Ok4>jkeY&qzjPW}^2;t$h8oU!KgnlE z=mhR>T6=TUAHUX)GKO1J_68e~X^n21;wQ6L)uj9gQ zcBog^aKioxte9E}nPMo+wRi8q2@(8gxj)rAvzVcBj#TMLj$qYHQy+Y##mQ67=X#BW zNNPR_%AzNGZNF143T-*nS^%rJGfV3I2CpC1A){{_vRiaZq{t+E;n3tjlDPfFs*i-@ z5J&c*;I+D~U98)m=ueH=l_-Yfq;$XWpSA9vvU^JCJVIQ{@k&r+e0uBCoi1+*E#Eb{ zzpPpReW9X?upCJu^6(Vhgi3bjH#iJVLPYmtX27Eu*eUB_k0MJk`1?^?uVPp_q{noX zI5(!{@rrV209D@q?85Nb7|Tyy-67FTBemA4r}7Kyq*cM+I_*>LXpRh`?;=r*upiF{ zRO7_Ur;Hia-3}qof(F**zgZ(QkvJ`&`Ho(-gLz`AMSq>N(kWdNuzcmJXRe*qc`b>@ zLRcboyO6D)uEa+34B68&N#{ukA<4jV+H&A!U%C~arH%Pth0|kj5g#sztf%D-8?lS! z;rop9xRGgCbIX=3QWKX6>EO6tvQErCb^6fTn1v}gxQl(waUDD)jlIyPhVg9b-usk# z!HoAdTY2D`Vf|}PNab^Gf4{h2x}*Zy><8`JKJ+?zDt+{(nHaHK*H9Rre7PPnIN+UW z!%(HNd?InzZRYGkj~17DAZeU*lsLwV1KxH3^NJIy$Ua#v-F$y0CuO|J9Wf%2_j4N? zjfNWo^7U<*Ng)2{OKNcWfvBY&tjO!W;2^w~;unKGia!;TD8iHp%{W(K1+ zc*`Gv?#p5X3tvm|*vAC@LL#X$@)v`Jv1>XwxZYZc$Fx01nEceR!~K?Y4_s6olM zlm3@k&9!r{?usVLC-oFF*>`K{*tHsK3S`8fhRh2}r2f=)g(U4DNt)#r0mKabEZMz2 z($o{31&+PTsQjDpnOC_9p<`WzNv?d%OA278z_nJhc^Ee{Q^km_hZC1aVpVJxI3lGBA0eu<|god*1S0U;;6>V1x!}r zMcsw>N8);KG>;h7-zXY=UZdKMn&2Y1I&chZ^*;0JWI+bnKP)wFUy(FJ@y-=ihEG5x z(XQWL?!Zv1?KWn#Q*iR~^W4gi-kUi`@U?R!n)uh^75o!l)>txK9Y(L&Rm%Ywl1xH{#YZ4 zcBJEVVJ5wY=w}Pu9UHbhT&)b*y*m)1mAvK=d@NdQu~12w)PWm5UZ;m(dP{;ee6+#p zp&+1s6E5Vl5A|pgcc3Zy8u0X7Igz-@aA6SXx7RBVWm%StjH>vcP#RL34L>Pyo7VSc zxF_!LHHFXG`~4)_k(uCaqsf?aBe7xPYF*+7xzesGL3X7dpXQk-Yoy^67OffU@lH)L zd`Qc!tH^i1)!JJUV)*O5#XgE(*UsFSeyhK52zkS1iDTA!lBj8?pjeN|;~WpQEi>VF zq4LGPUw&AW5!*X(&}p{6J7gGE-YA%MMJV(2*P~wJ?Zx9MDHnej0FV@R%BD+~J{6YY z(}U*KTWQKGU|{^Y#H}w(E=yNl+D~o85A`Eu*1LWDd%R$X5q3L3*BQFSFNWuT9yn4f zu%6*BpQQ1xT>-W!vrnAXqp&6C8=K4xLkJq#(7_vs?W=^}^H*4G)9Y}BB^I@we#%p` zakkGrdg=(5toWx_I$ZhmtS5Z%-0IHF4!%bU(EbY*`jrRfRnEA`uI%vvkmHkxSe+ywfAx9XW!XDe2fI{NLjyTf z&g|(r0ZhYmJf#4KXeIv98j41Uw3Q(R2g=<9(ny-ti<$*gHP6zf8s5^{ndjTv9&**d zq(=oG?CUw(O8L<1{4{m&sb?ilIb_cBk}}omZIYs3ZU2)fyX-SPuTi{0m%~d%@th!7 z_zy)DywsdGN%HY|T$Z9sLxFuEp=6HK zmyEXfT4aCm^Xn@aA<0~S0oSP0<6iWvKVX8si(RXZppJQ29nxo*?I)*MUZjrgxA?}J zBvM$HFrgcb4VW1D`H=cYsEAYl ziYwzi4gJ2Zjl9WAHC~SyPf+A}1mwEn#y*)6`%2=uIk1j6$>f~J>u{L6I)2@yDVMp{ zNrG6mrSM}wFY6P7SHWC{DD*Se9W+$EQ0x5xU$vvz4z3L$vZBZX&sZb@1}Xu8yx&b= zs0tgsq2dqVG*MB9g=IZ4|w-9qQUBl=*v^GYtGW>&gu{8swD>Z5n{ks*fkyF$hF;wVX zi!BLYI9qXWm5D+py^Ym_cz>2ff>IGjB$xg?gD3+?}L8T z_ZSqQRE~3Ok@XxUY847Ewd5-`aQDb|(nO*_&*j==Jw;$2<>V1!G`1)|u$Jx>1D+E~p<{KITTNkCm4|{4Wi3N%q$?naqm-aPF0V_6( z$81+hV<}Ym(@Q|KLtQD!31H22trbNpP5jSkP!smS8D;1e(~O$_VO zVb@m@U;nnl8hbXcJKfKzT$iA797pru(rpbm`RCUchP&APKd*O(JbdoNTX9v?nja7qkfZ9LediP`MlP(@xiqxqtfBYYl zWYmPbpOhFLN(>8s#llWw6lxY>!d<`}@To_=X$SQR=tUWGSdLMi!d7FYMxXitSa&)h zntXn37teA$H$kXfOVw_}px`5}-?y4ppLl^1a!ggoZ@-?PfProi?&iVZ7}MK}K-d+4 z8@Bx=kI8MzC=V_cK!}LXzb!@c&Z};?n&2TU^2&5_1DfQ~9`Ptt*KwmOwH(lVO@UXKrA5p^9XdbyBM}q zp{fn@@guo3qS(nf$mgs3auQ=GXYpd$kj8OP+$Ujb5<0OS}{`nM?3Eoe$Cl% z*(Oj|mELsi=BU!h?>r4g-{>8PAZj~XA+%f+N$ifzOOQFM-tIk%-vC}+<|{qU#<>97 z_<{Kk#>c7Zvg#G|4Cj+)1o4O= zxF-^$G5qkz*UsmJV3`nZ!q%G9RVjpjAqk@Prz+*|whSl?z2R)YXQLT1Cx$<^Z?J5) zhSTV7rh8>TfO$D*>J()p%&d!XK0N-3-5E5UD!Oc0x#1N(PHQOqL|a!>S|v$S=FzWyqwAa|;KxIOYQ_D-0B(Nt*MpZ9(Atu|)YX!_t!u9Cu24UER>uO^<3p z6|$0ec0LR*>^XKf@UwT$RoqsgaOqu>xy0?jsY(;q03q%79q(sE{L$hX7M#2{qLg#X zAL43f&a2Z**k`?dI2kpg4UJE3A(coefl{s`(1w+RpTv2Wpb-Q#I<4Q(G(S->Jl{o^ zo+vCR(vvp)you_~I1DXW2@-MOSaY)Cs_p}`9X>2q;MLpb!sdeV-?I^;q3`IzAIr`* z2fx;X1WMQ3zdw0Jf;n#R`-V>`;xLcR@3OzL2)q-JGSvIwi}5O&*2u3%7K!svz0Q#B z2Fc9H=_3MBAc`f-C#HxKTD$F<;{^kDw7jDJa%~=gQf*Z~`@-ck;rG9W^-8pPIJbkK z7~W|5vkKLieH>`fPoZ3ZkzTDc$Rconb>mZO6WkN!2VWqWLn#RMgTn?7T#y4xd!Ksu z3hMx+=zM>7w*#($pc=|;(8O1MASGi1POK8WCG;1-v<`lfU(#8TvkU|I!HV&;j?HSsOE;msRxnCRwf>*^mR3^KS&Ys}piC22ogtSC}a zkKqilGXvtYIE_eIbmx5j^Ml5?kJ?9WPh|RE0!J1Qrm~V@{q3jF2sU>cUUN2!?X9av zg!Y#7gh9WPw8RnLaF%isc;YTa;K@$w~p3X1fvkZ1W2p_TRi z;w0i&3YielyRXK0meR$4@z|WJ9E~Bq5e23C102(vTUI1L7WPN zo-o35HILt%RQtr*zk;32Uwx&f-L}U0bBN)2)Pny9!}tE|a3w@fzUa_Y{>PhJx|iWH z-?kHrIR)^#Mcjv{xWj+-WVi(VkbU2t^ruWw_zfB@2_CGnJZLkM)8v;R3miy_;f9Nc zhlxWKoK_Pe8zmHl4*fU3m$-W3B$k89?H9zcd{*9h(7t;K|Ds;WRnI@e;STEaK)+5H z6Hmu&>J*Ur1&1I$jVHd_rU%_Q8E2Sgw-4xR9kcW4sM>$iRt3HAuK||a04&*uEpAR# zYX48Ze7LMIdM)rHy8z>;FbTIyY5xCp=1OnXE_m5;IBxotZApJ%iO6qlZS#4URw zKEU86XxhTY|IKt8@J%=0ToA+GOEGA?k+=q}P?cAWK%Q+2FmOTqduVa1Q?^cgIIynF z?Eys`|2Zd1BAQhPqUlo9-n6*5_LB31O;H37@#b-XQp1juH{C$*lQ>tZm=C#UX9TyE z<(%nLt3r#d9`!r|$?IgLNP!S(!d98XJ>NqWVnwG^ahNV?uR5yn5oiVN(;GD;^^IRh zeE~9<(_;(Je~bWp#PA`n%Hu+oq*(!~RO{bHaO-8)9OsB6S9EbANanX??bryyhUhtN zgZ)@w7lIe_E82pzKymOFR2hkTP;wV_oMWXmB#+_UFiT61{&INjZ)j^5+lIL({i`;U zVrWzVW+O1mmML9TAU^#(TS+wk88u#E3C(oW$Eo?;Hw*!!_^pW@5fYu@takFxaTml` zHMfhIa{}qnlJOFtxhMVXA3+?{HwKNxNd$^%pS?S+Ky#1zjie+b-h9Lp_=r;N67KdM~ONm<)WQ03Ioi`p(Z^JKsBiS4bOF^7VF6MTSR&BTyzC}l`|H|c5)@iGensF^5m3b^Og3W!^WX)NR9Y{rPXoa zGLW!z6&5Zf#aJtL^y=1~0;PXuLsGS4<*o|S&VK0M!0o)H0VJfBhl2;yheKZLae-&v zCkM7f#z~&JsQ4dGR4d^yqr6HyJXb?x8o=0yM(LxnwDtcHz+#WGqkCuJtAOma^yCS{ z{|F&f0C=3MeFlN)1Omr>Ue;-h{VH1C(9dXme!-i8%Nt8=Y(#PCWmkY4w3HQwecA4R zb{BikqSEWX=dc}=KZi4=w1*!Nc|L|-$;kX{ZfPsLcnmyYKlaR(A zv`h{ZMgC2JfdfhDdGaY=C@Y>m+#bv`wNV*5dye#YsST~Izg{#wj5`a&9zvhlW0k2) zD*Y)JMNndEl+D;vLnHq?+2~1`cmC9%S7VtKiL5NG+FN}~$aBZmn!z#G{^TiJN}~Jh z-(8?Xa@M2Hm(rVXw-bBU9T|j*`%wfyq&wW8jACZV!c*B0*Ge6qllR-r#)WRo^_v!w zaYPEziHE#KGYrf96SHy4^up;~de6rSBTRQN8_u3${4TAMx;np!yT0j3^`iJGP^*O~ ziF6uP5-!kKYKuKK7Bib^!XezUw+6b~p|qtK?sPQX8V4`qW9D&0r=l|9(Zkg9K^cM| zkeRC^LIbJ$9H_2;f-3Q24~_?42W8xA9y3QX3X$aZuHpBz7s*^Y9DUitXbxov}=0DC#{!sk;=g}}9-lJQ{e_o~gDIoPo+EXKsMZ2V?&4BsN88oL|Q+l(V| zVGH&3g#a*#2dPYc8ztm=&+;SC{w?l3qLKQkCQ&^yylA9aE^lS@f)%XTH znyVcnl{iGV2FY0!Ojm!m<$w}Q4a?OPe0@Hv|ItLE>)LPT-4{bFqN7SqPksFdv^38i z=g3Zl$(ESWXL4@8qnT~h*qK2O)`WO)@*1u(PKV0U($efA_E3Y+Z{Ev#7b9Y<3@I}H zbKF^20Kb)Kk{)2+bib064I1Q&?ACfe!=Dm#{>zjNmY?5fi!$^M^exWsw!rDIFASCp zKA-wUUqQ)CqsG@~me%K{F~G4B)W3O;WS?S+E6iq}C!M$X)wn<#HMwuHp`l|fmd_8G z>r1{8RT_$BS{2%+S%7ZHkD?8Ngs-q7?(`)RgRXc@f-!^jpxOB*_)1O5*Hm*5nm&M1 z^Fl>yK88lZQ8`7}Jh#y&37{=|4Gd1Ltjj;rRh-or7`p>>%6>95uyDu2%Ec)uGp#y+ zab@Y3Q$D5zr@owBiqw5eP2flYBfV2}!eB4AfI;Y3-Rz~*b&pOU4WGGoA<)Y~B+1*w zzUe6aO`mJ}dH!UHHgj&B7sWO2Y17(qXVtA=LT2z`|9TV*uyXo!kqQEuVbmhZNZ%c0!8!nR- zQHFH3W;=hT0IyS!wR(wFt2pLwm+XJv-8$RPC17BoP5JVPiH;;IcG+KFV&R8#zHFQ# z)?=A9!eAvfMbk8D&7fB=Htq{(J_+! z#+#gSe6dG4^e4ONfZK~P>b^^L<(yV41;lKh*)^r?D-vQjfmuGJiL(lfDtZUD0F(}2 zYE@3mS9e}>M`X(Vj~&7EuCs0Dskpsq1rKOm;-uQ3F_mG zgi8%uFA5Ok&vaa*S{J|gG5}@-V-|Q7e&|+^XxJ!!EsS6&1$1X6tj+53noq;Y_OV} z)j834uVys5wIcGDcOPR%kE4p+x}NBUT-~LLj3Bu*C$n)1d6{-A7}mBjmBcnMED|&I zRC;Y^U|@q)<;`qdO{9{mW~VN(zmt&ag3LXO#_`g!rpDI6se#O*h}*VW@CTD%9wRp( z%geSezBPpwcj!uXhO(4Yg~`|vkS<+gCN@*o!Je-f-cm9jp=LU+Sipe$l}C@j;Tk47`Z7y}`7_y}6fZxLE2VBasKI07>MqWQD~C1N-ME^; zr@-_^=en2kZ7L*ET?k{PN!o)mPYAwh)C1(S>qDq3QE`yt(dD@w=D)Zo%Z#j+g%Y$x zxlOmm{gVhtPdu?JoH{re(WNw4bU42{k+AsWkCD}8Idt-uFFMnBwKbhJ$5e~#LV*ysN0?ah#kbUdn z=W(#Wqh0eZ4m8*h^B$IAn9*?FBOUQrFe|)&)zlEa=}WQCu%_NOZfeJ|cE>Pekm6q} z5U(QD;Cc#tbAogqLZ$-N(B^QjI?GIRTwIKsYpfFMr|sFvY#?Y1$Co2?j{xo_p7{HTOnM`=6%C zg+$@17eKK9!)cItE_HxEDA(QRbtqJLOJV%>nT(qs?KcdL-3QlQHUY>pSC~>Jr=(Gx zXk#^zI65*C>@;HJnj+cS>u$CKSQdhe{w3SmGV$Eish)Mtitpg6Oz6ChnV2%eMe42E ztrvWZ^h9KO2PvCH;)e7UiD~sMswV7CU?Rfbf-i}GT@!v4tDMhlKLyeX>RzN|D@L;< zQF7g0^=AS6ya~*38l)k^)2foMzCJ4XlWvO1NH_)RWDCUg4HTI**Fv!`PfWR3^28eS z@%e8zY|TDies!?CaiBKZUUSSFq!7AI^H4;6f>F4>CDXUI=l~tCwl3vt2szVPaDk>K zymMR>lNB2YZw2{5^E~wdx(*dSJN+t3>xuU8lUYNBhV5XURCt>|Kz1&Tw@8sJ@x|-z zr1TN9y-4p}qj2s(YBChI`EZvKBjQn&D+j};Y_1G58byq2#oFrP`X>#!8)CB|{3z*# z;}tU7rs)8z@a^fX%aXsFwWS=6%P2%G~)k3a^|6Qv=;w|%MZx6fa{%+hT``p)0Y z0)QQ^u?UdG&J_&INf%C5IdqM~J!q*~(vI^Yl2EiPOL2G#^ z)U#4t$aX@Gq~-Z2ZdB*I+@i$oLIjH%vFH3ITIjWGsPqrpo#0AKpwXoD-U1GWebm3n2felW}ChT z2bY=A@Lp%IIB#$X}90#fjhDk>@g-K%UryX@2Lfl(sVxmkatliX-icRFdxq%LcnbLJ)K<_m&Oal%yD| zmKa)?nJ&DRkI5p?bK!ta)&Ufsgy0q)u;UZR5N{0|yEE_^eKf@LabER$a32huk7W}j ziG{xL!AH%A9m$gk8EhE!_n7rmqZ?zKWCBqj;YMzZa0NE@(kAo1$px+x02fg9+0u#x z4o{-i_$H^~lE8(9c`Erpt(zZ0RSnpYqL~nWZ3e32o0#IPnwpPop)$=<8wnSOtl9oW zKB!mT?Dv+uLR`+v1fV;f^$*M`Iux|ZphE4oN^}pB(plRvaOpDnKE-Zz@4*!>POef; zNc#9>|4sMERz-!Roxo;NJP_^5c)-F97BhQW>k`J+g8PCJ%U2@T@NO_Q&)!{YGKQ@w z)B>a0V{<$W8M^82dxn^uAcv>zZrFmq49=w@->Lb5b=6=@blvGVP){cnW%g3?gg7ga z+gH^?<_W6f7O1!v=fRx|6eEFt$c#-_YVx<&b^0hAd=**)wv!Kr_v!jC1}D1;iBS{7 z^JFIMb|9XPE$;7}kgQLF(}7C>TK!I1QgdI5m@#f3k$Dnx;BH?e?k!LUt-ITm2U9k* z%)q>H%zrk5TX`O%Z7&9&lx&>Irn75JNC2n=1ysU!Z0ZIR?$;H^5u1gL!4=rFo>5+u zt?J;(Xfd-&qTF0Fs1mNU>B@+7pc11nS?f3BRLa+E!_55E&!HQN8BIASzt;Gb5^j2Xr&JfB$Yxtm?Q9-9IP8o8=>moKc;L_SIJkZpJ+QJf6td@@1lyHwn$SHq6(2}zbU0IhEWg*8%A zUrFc-L+!=);B^f3_CB*sCS=kG4bCUC9}r(sQs#y?2dX$$1D*%3x4(2t3IKze34N2i z(fwN0xAp_({(aQR$V6s4yDeFACHf%W+{H+LA9}ek@q-yPwzVbHP3^u^6fUls)Y)6C zKXcbtomk~Q6tWrIg^Hyl*CwbvteL$O=YK!7(v$3e+*!aS*vKzrW;yv5y)U~t?7R@t0Y(OuyZ1i=%Q>bfD}gvlXe0@aXa3uLDJ~tF=pziAS}5$=i_y zn5WqwcP?6HX>e>J^Yqs}Xy1`U^;X{4Wd~P+o3MOt?I2jZ=$Xk-fK{uC4~p0`!3_SB z(c7?uZ(!R%<@R;`qe+vjbqEt8IlPrrHQ7|DdN3JKXRl`3q>)_5Wcm4G}@?cb2uZ~d|T0C0h#*Qxxkr9L{z!5oAK0cH&!+Meu2-?)(?Ef>OY+&`W>wgWQX0hNZIznm!U(ES}w5ya?FOH z)s?*H=`uO?>APMQvj-kL##OI2j}IzBcs8D`bPiC!xW3}3K12cZU=B6>xD-o#gZA-zIN z|CAk5<7ex1Ce4%Ym0)68(7w-SHkcnKk*kJMb{fydY{<|Y>_(deZf}`UG>g7>)LeDP zw3`tr72{clseAOXRgIcBbYCu-PV36h->Hnb-dpfxrcC%lMEH%3GSV%T4IC|@p&HxH z8{9WCl91j(XzY=L;elQbJPqiN)w&|OgibbAj(5rx*J#NDZ3YO$;LQ9V z0Z>W;aGwzWWo`(%9{l55_wJ=2zx#nKY>oU(34*M^O)-cYFolef!;l64Tl}q1hy#NB zR%QaxE(eb+$@AaW2y*~F6SB4(&_?7D1JH2-&k2A}+{gmK{}z7>#R+~D#Q>PU1g`~; z;O~K7D6kP&JhYJ?+}sBjNq+j4KG$*xY6$T_wgMhmdbvM#0Ir^b)$;c6gV%yb7K`BD z<<=vj_ h1:first-child { + font-size: 2.5rem; + margin-bottom: 0.5rem; +} + +/* Logo in header - make it slightly larger */ +.md-header__button.md-logo img { + height: 1.8rem; +} + +/* Simulator link button styling */ +.simulator-link { + display: inline-block; + padding: 0.75rem 1.5rem; + background: linear-gradient(135deg, var(--md-primary-fg-color), var(--md-accent-fg-color)); + color: white !important; + text-decoration: none; + border-radius: 2rem; + font-weight: 600; + font-size: 1.1rem; + transition: transform 0.2s ease, box-shadow 0.2s ease; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.simulator-link:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); + text-decoration: none; +} + +/* Code block improvements */ +.highlight code { + font-size: 0.85rem; +} + +/* Table improvements */ +.md-typeset table:not([class]) { + font-size: 0.8rem; +} + +.md-typeset table:not([class]) th { + font-weight: 700; +} diff --git a/docs/automation-examples.md b/docs/automation-examples.md new file mode 100644 index 00000000..e8ed0e7d --- /dev/null +++ b/docs/automation-examples.md @@ -0,0 +1,115 @@ +--- +icon: lucide/bot +--- + +# Automation Examples + +Real-world automation examples showing how to integrate Adaptive Lighting with your Home Assistant setup. + + + + + + + +
+Reset the manual_control status of a light after an hour. + +```yaml +- alias: "Adaptive lighting: reset manual_control after 1 hour" + mode: parallel + trigger: + platform: event + event_type: adaptive_lighting.manual_control + variables: + light: "{{ trigger.event.data.entity_id }}" + switch: "{{ trigger.event.data.switch }}" + action: + - delay: "01:00:00" + - condition: template + value_template: "{{ light in state_attr(switch, 'manual_control') }}" + - service: adaptive_lighting.set_manual_control + data: + entity_id: "{{ switch }}" + lights: "{{ light }}" + manual_control: false +``` + +
+ +
+Toggle multiple Adaptive Lighting switches to "sleep mode" using an input_boolean.sleep_mode. + +```yaml +- alias: "Adaptive lighting: toggle 'sleep mode'" + trigger: + - platform: state + entity_id: input_boolean.sleep_mode + - platform: homeassistant + event: start # in case the states aren't properly restored + variables: + sleep_mode: "{{ states('input_boolean.sleep_mode') }}" + action: + service: "switch.turn_{{ sleep_mode }}" + entity_id: + - switch.adaptive_lighting_sleep_mode_living_room + - switch.adaptive_lighting_sleep_mode_bedroom +``` + +Set your sunrise and sunset time based on your alarm. The below script sets sunset_time exactly 12 hours after the custom sunrise time. + +```yaml +iphone_carly_wakeup: + alias: iPhone Carly Wakeup + sequence: + - condition: state + entity_id: input_boolean.carly_iphone_wakeup + state: "off" + - service: input_datetime.set_datetime + target: + entity_id: input_datetime.carly_iphone_wakeup + data: + time: '{{ now().strftime("%H:%M:%S") }}' + - service: input_boolean.turn_on + target: + entity_id: input_boolean.carly_iphone_wakeup + - repeat: + count: > + {{ (states.switch + | map(attribute="entity_id") + | select(">","switch.adaptive_lighting_al_") + | select("<", "switch.adaptive_lighting_al_z") + | join(",") + ).split(",") | length }} + sequence: + - service: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_al_den_ceilingfan_lights + sunrise_time: '{{ now().strftime("%H:%M:%S") }}' + sunset_time: > + {{ (as_timestamp(now()) + 12*60*60) | timestamp_custom("%H:%M:%S") }} + - service: script.turn_on + target: + entity_id: script.run_wakeup_routine + - service: input_boolean.turn_off + target: + entity_id: + - input_boolean.carly_iphone_winddown + - input_boolean.carly_iphone_bedtime + - service: input_datetime.set_datetime + target: + entity_id: input_datetime.wakeup_time + data: + time: '{{ now().strftime("%H:%M:%S") }}' + - service: script.adaptive_lighting_disable_sleep_mode + mode: queued + icon: mdi:weather-sunset + max: 10 +``` + +
+ + + +> [!TIP] +> **Have a useful automation?** Share your automation examples by [opening an issue](https://github.com/basnijholt/adaptive-lighting/issues) or submitting a pull request to the README. diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 00000000..622c1416 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,150 @@ +--- +icon: lucide/settings +--- + +# Configuration + +Adaptive Lighting supports configuration through both YAML and the Home Assistant UI, with identical option names in both methods. + +## Basic Configuration + +The minimal configuration requires only adding the integration to your `configuration.yaml`: + +```yaml +adaptive_lighting: +``` + +You can then configure everything through the UI at **Settings** → **Devices & Services** → **Adaptive Lighting** → **Configure**. + +## YAML Configuration + +For YAML configuration, you can specify lights and options directly: + +```yaml +adaptive_lighting: + - name: "Living Room" + lights: + - light.living_room_ceiling + - light.living_room_lamp +``` + +## All Options + +All configuration options are listed below with their default values. These options work identically in both YAML and the UI. + + + + + + + +| Variable name | Description | Default | Type | +|:-------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:----------------------------------------| +| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | +| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | +| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | +| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | +| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | +| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | +| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | +| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | +| `take_over_control` | Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒 | `True` | `bool` | +| `take_over_control_mode` | The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed. | `pause_all` | one of `['pause_all', 'pause_changed']` | +| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues. | `False` | `bool` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | +| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | +| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | +| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | +| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | + + + +## Full Configuration Example + + + + + + + +Full example: + +```yaml +# Example configuration.yaml entry +adaptive_lighting: +- name: "default" + lights: [] + prefer_rgb_color: false + transition: 45 + initial_transition: 1 + interval: 90 + min_brightness: 1 + max_brightness: 100 + min_color_temp: 2000 + max_color_temp: 5500 + sleep_brightness: 1 + sleep_color_temp: 1000 + sunrise_time: "08:00:00" # override the sunrise time + sunrise_offset: + sunset_time: + sunset_offset: 1800 # in seconds or '00:30:00' + take_over_control: true + detect_non_ha_changes: false + only_once: false + +``` + + + +## Multiple Configurations + +You can create multiple Adaptive Lighting configurations for different areas or use cases: + +```yaml +adaptive_lighting: + - name: "Daytime Spaces" + lights: + - light.living_room + - light.kitchen + - light.office + min_brightness: 30 + max_brightness: 100 + + - name: "Bedroom" + lights: + - light.bedroom_ceiling + - light.bedroom_lamp + min_brightness: 5 + max_brightness: 80 + sleep_brightness: 1 + sleep_color_temp: 1000 +``` + +## Related Topics + +- [Brightness Modes](advanced/brightness-modes.md) - Detailed explanation of brightness calculation modes +- [Sleep Mode](advanced/sleep-mode.md) - Sleep mode configuration +- [Manual Control](advanced/manual-control.md) - How manual control detection works diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..68c253eb --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,117 @@ +--- +icon: lucide/rocket +--- + +# Getting Started + +This guide will help you install and configure Adaptive Lighting for the first time. + +## Prerequisites + +- [Home Assistant](https://www.home-assistant.io/) 2024.12.0 or newer +- [HACS](https://hacs.xyz/) (Home Assistant Community Store) installed + +## Installation + +### Via HACS (Recommended) + +1. Open HACS in your Home Assistant instance +2. Click on **Integrations** +3. Click the **+ Explore & Download Repositories** button +4. Search for "Adaptive Lighting" +5. Click **Download** +6. Restart Home Assistant + +Or use this button to open HACS directly: + +[![Open your Home Assistant instance and open the Adaptive Lighting integration inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=basnijholt&repository=adaptive-lighting&category=integration) + +### Manual Installation + +1. Download the latest release from [GitHub](https://github.com/basnijholt/adaptive-lighting/releases) +2. Extract the `adaptive_lighting` folder to your `config/custom_components/` directory +3. Restart Home Assistant + +## Configuration + +### Step 1: Add to configuration.yaml + +Add the following to your `configuration.yaml`: + +```yaml +adaptive_lighting: +``` + +> [!NOTE] +> This entry is required even if you plan to configure everything through the UI. + +### Step 2: Restart Home Assistant + +Restart Home Assistant for the changes to take effect. + +### Step 3: Add the Integration + +1. Go to **Settings** → **Devices & Services** +2. Click **+ Add Integration** +3. Search for "Adaptive Lighting" +4. Follow the setup wizard to select your lights + +### Step 4: Configure Your Lights + +You can configure Adaptive Lighting in two ways: + +=== "Via UI" + + 1. Go to **Settings** → **Devices & Services** + 2. Find Adaptive Lighting and click **Configure** + 3. Adjust settings as needed + +=== "Via YAML" + + ```yaml + adaptive_lighting: + - name: "Living Room" + lights: + - light.living_room_ceiling + - light.living_room_lamp + min_brightness: 20 + max_brightness: 100 + min_color_temp: 2200 + max_color_temp: 5500 + ``` + +## Basic Configuration Example + +Here's a simple configuration to get you started: + +```yaml +adaptive_lighting: + - name: "Main Lights" + lights: + - light.living_room + - light.bedroom + - light.kitchen + transition: 30 + min_brightness: 10 + max_brightness: 100 + min_color_temp: 2000 + max_color_temp: 5500 +``` + +## Verifying Installation + +After configuration, you should see new switches in Home Assistant: + +- `switch.adaptive_lighting_main_lights` +- `switch.adaptive_lighting_sleep_mode_main_lights` +- `switch.adaptive_lighting_adapt_brightness_main_lights` +- `switch.adaptive_lighting_adapt_color_main_lights` + +Turn on `switch.adaptive_lighting_main_lights` to start adapting your lights! + +## Next Steps + +- [Configuration Reference](configuration.md) - Explore all available options +- [Services](services.md) - Learn about service calls for automations +- [Automation Examples](automation-examples.md) - See real-world automation recipes +- [Troubleshooting](troubleshooting.md) - Common issues and solutions diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..28820274 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,85 @@ +--- +icon: lucide/sun +--- + +# Adaptive Lighting + +**Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting** + +
+ Adaptive Lighting Logo +
+ +[Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) is a custom component for [Home Assistant](https://www.home-assistant.io/) that intelligently adjusts the brightness and color of your lights based on the sun's position, while still allowing for manual control. + +
+ +By automatically adapting the settings of your lights throughout the day, Adaptive Lighting helps maintain your natural circadian rhythm, which can lead to improved sleep, mood, and overall well-being. Experience cooler color temperatures at noon, gradually transitioning to warmer colors at sunset and sunrise. + +## Features + + + + + + + +When initially turning on a light that is controlled by Adaptive Lighting, the `light.turn_on` service call is intercepted, and the light's brightness and color are automatically adjusted based on the sun's position. +After that, the light's brightness and color are automatically adjusted at a regular interval. + +Adaptive Lighting provides four switches (using "living_room" as an example component name): + +- `switch.adaptive_lighting_living_room`: Turn Adaptive Lighting on or off and view current light settings through its attributes. +- `switch.adaptive_lighting_sleep_mode_living_room`: Activate "sleep mode" 😴 and set custom sleep_brightness and sleep_color_temp. +- `switch.adaptive_lighting_adapt_brightness_living_room`: Enable or disable brightness adaptation 🔆 for supported lights. +- `switch.adaptive_lighting_adapt_color_living_room`: Enable or disable color adaptation 🌈 for supported lights. + + + +## Quick Start + +1. **Install via HACS**: Search for "Adaptive Lighting" in the [Home Assistant Community Store](https://hacs.xyz/) +2. **Add to configuration**: Add `adaptive_lighting:` to your `configuration.yaml` +3. **Configure**: Go to **Settings** → **Devices & Services** → **Add Integration** → **Adaptive Lighting** +4. **Select your lights**: Choose which lights to control and enjoy automatic adaptation! + +```yaml +# Minimal configuration.yaml entry +adaptive_lighting: + lights: + - light.living_room +``` + +> [!TIP] +> **Using the UI exclusively?** Even if you plan to configure everything through the UI, the `adaptive_lighting:` entry must still be present in your `configuration.yaml`. + +[Get Started →](getting-started.md){ .md-button .md-button--primary } +[View All Options →](configuration.md){ .md-button } + +## How It Works + +Adaptive Lighting provides four switches for each configuration (using "living_room" as an example): + +| Switch | Purpose | +|--------|---------| +| `switch.adaptive_lighting_living_room` | Main on/off control | +| `switch.adaptive_lighting_sleep_mode_living_room` | Activate sleep mode | +| `switch.adaptive_lighting_adapt_brightness_living_room` | Enable/disable brightness adaptation | +| `switch.adaptive_lighting_adapt_color_living_room` | Enable/disable color adaptation | + +## Interactive Simulator + +Visualize how Adaptive Lighting will work with your settings using the interactive simulator: + + diff --git a/docs/overrides/partials/integrations/analytics/custom.html b/docs/overrides/partials/integrations/analytics/custom.html new file mode 100644 index 00000000..c5e49c15 --- /dev/null +++ b/docs/overrides/partials/integrations/analytics/custom.html @@ -0,0 +1,6 @@ + + + diff --git a/docs/run_markdown_code_runner.py b/docs/run_markdown_code_runner.py new file mode 100755 index 00000000..3a4b59f8 --- /dev/null +++ b/docs/run_markdown_code_runner.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# ruff: noqa: T201, S603, S607 +"""Update all markdown files that use markdown-code-runner for auto-generation. + +Run from repo root: uv run python docs/run_markdown_code_runner.py +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + + +def find_markdown_files_with_code_blocks(docs_dir: Path) -> list[Path]: + """Find all markdown files containing markdown-code-runner markers.""" + files_with_code = [] + for md_file in docs_dir.rglob("*.md"): + content = md_file.read_text() + if "" in content: + files_with_code.append(md_file) + return sorted(files_with_code) + + +def run_markdown_code_runner(files: list[Path], repo_root: Path) -> bool: + """Run markdown-code-runner on all files. Returns True if all succeeded.""" + if not files: + print("No files with CODE:START markers found.") + return True + + print(f"Found {len(files)} file(s) with auto-generated content:") + for f in files: + print(f" - {f.relative_to(repo_root)}") + print() + + all_success = True + for file in files: + rel_path = file.relative_to(repo_root) + print(f"Updating {rel_path}...", end=" ", flush=True) + result = subprocess.run( + ["markdown-code-runner", str(file)], + check=False, + capture_output=True, + text=True, + ) + if result.returncode == 0: + print("✓") + else: + print("✗") + print(f" Error: {result.stderr}") + all_success = False + + return all_success + + +def main() -> int: + """Main entry point.""" + repo_root = Path(__file__).parent.parent + + # Process docs/ files and README.md + files = find_markdown_files_with_code_blocks(repo_root / "docs") + readme = repo_root / "README.md" + if readme.exists() and "" in readme.read_text(): + files.append(readme) + + success = run_markdown_code_runner(files, repo_root) + return 0 if success else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/see-also.md b/docs/see-also.md new file mode 100644 index 00000000..8c1a2613 --- /dev/null +++ b/docs/see-also.md @@ -0,0 +1,73 @@ +--- +icon: lucide/external-link +--- + +# See Also + +Resources, tutorials, and related projects for Adaptive Lighting. + +## Tutorials & Articles + + + + + + + +- [*Sleep better with Adaptive Lighting in Home Assistant*](https://wartner.io/sleep-better-with-adaptive-lightning-in-home-assistant/) by Florian Wartner on 2023-02-23 (blog post 📜) +- [*Automatic smart light brightness and color based on the sun*](https://www.youtube.com/watch?v=Rg3zI1Oyk3c) by Home Automation Guy on 2022-08-31 (YouTube video 📺) +- [*Adaptive Lighting Blew My Mind in Home Assistant - How to set it up*](https://www.youtube.com/watch?v=c1cnccmgl3k) by Smart Home Junkie on 2022-06-26 (YouTube video 📺) + + + +## Interactive Tools + +### Adaptive Lighting Simulator + +Visualize how different settings affect your lighting throughout the day: + + + +The simulator lets you: + +- Adjust all configuration parameters in real-time +- See how brightness and color temperature change throughout the day +- Visualize the effects of different `brightness_mode` settings +- Test sleep mode transitions + +## Related Projects + +### Circadian Lighting + +Adaptive Lighting was initially inspired by [hass-circadian_lighting](https://github.com/claytonjn/hass-circadian_lighting) by @claytonjn, but has since been entirely rewritten and expanded with many new features. + +## Official Documentation + +- [Home Assistant Documentation (PR Preview)](https://deploy-preview-14877--home-assistant-docs.netlify.app/integrations/adaptive_lighting/) +- [Home Assistant Community Store (HACS)](https://hacs.xyz/) + +## Community + +- [Home Assistant Community Forums](https://community.home-assistant.io/) +- [Home Assistant Discord](https://discord.gg/home-assistant) +- [Reddit r/homeassistant](https://www.reddit.com/r/homeassistant/) + +## Contributing + +Interested in contributing to Adaptive Lighting? + +- [GitHub Repository](https://github.com/basnijholt/adaptive-lighting) +- [Issue Tracker](https://github.com/basnijholt/adaptive-lighting/issues) +- [Translation via Weblate](https://hosted.weblate.org/engage/adaptive-lighting/) + +### Translation + +Help translate Adaptive Lighting into your language on [Hosted Weblate](https://hosted.weblate.org/engage/adaptive-lighting/). No programming knowledge required! + + +Translation status + diff --git a/docs/services.md b/docs/services.md new file mode 100644 index 00000000..5b0ccf58 --- /dev/null +++ b/docs/services.md @@ -0,0 +1,203 @@ +--- +icon: lucide/zap +--- + +# Services + +Adaptive Lighting provides three services for programmatic control, allowing you to integrate with automations and scripts. + +## adaptive_lighting.apply + +Applies the current Adaptive Lighting settings to lights on demand. Useful for forcing an immediate update or applying settings to lights that aren't in the regular adaptation cycle. + +### Parameters + + + + + + + +| Service data attribute | Description | Required | Type | +|:-------------------------|:--------------------------------------------------------------------------------------|:-----------|:---------------------| +| `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ✅ | list of `entity_id`s | +| `lights` | A light (or list of lights) to apply the settings to. 💡 | ❌ | list of `entity_id`s | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | ❌ | `float` 0-6553 | +| `adapt_brightness` | Whether to adapt the brightness of the light. 🌞 | ❌ | bool | +| `adapt_color` | Whether to adapt the color on supporting lights. 🌈 | ❌ | bool | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | ❌ | bool | +| `turn_on_lights` | Whether to turn on lights that are currently off. 🔆 | ❌ | bool | + + + +### Example Usage + +```yaml +# Apply current settings to specific lights +service: adaptive_lighting.apply +data: + entity_id: switch.adaptive_lighting_living_room + lights: + - light.floor_lamp + - light.desk_lamp + turn_on_lights: false +``` + +```yaml +# Force apply with custom transition +service: adaptive_lighting.apply +data: + entity_id: switch.adaptive_lighting_bedroom + transition: 5 + adapt_brightness: true + adapt_color: true +``` + +--- + +## adaptive_lighting.set_manual_control + +Marks or unmarks a light as "manually controlled". When a light is marked as manually controlled, Adaptive Lighting will not adjust it until the manual control flag is cleared. + +### Parameters + + + + + + + +| Service data attribute | Description | Required | Type | +|:-------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:-----------------------------------------| +| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s | +| `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s | +| `manual_control` | Whether to add ("true") or remove ("false") all adapted attributes of the light from the "manual_control" list, or the name of an attribute for selective addition. 🔒 | ❌ | bool or one of `['brightness', 'color']` | + + + +### Example Usage + +```yaml +# Remove manual control from a light (resume adaptation) +service: adaptive_lighting.set_manual_control +data: + entity_id: switch.adaptive_lighting_living_room + lights: + - light.floor_lamp + manual_control: false +``` + +```yaml +# Mark a light as manually controlled (pause adaptation) +service: adaptive_lighting.set_manual_control +data: + entity_id: switch.adaptive_lighting_living_room + lights: + - light.floor_lamp + manual_control: true +``` + +```yaml +# Only pause brightness adaptation, continue color adaptation +service: adaptive_lighting.set_manual_control +data: + entity_id: switch.adaptive_lighting_living_room + lights: + - light.floor_lamp + manual_control: brightness +``` + +--- + +## adaptive_lighting.change_switch_settings + + + + + + + +#### `adaptive_lighting.change_switch_settings` + +`adaptive_lighting.change_switch_settings` (new in 1.7.0) Change any of the above configuration options of Adaptive Lighting (such as `sunrise_time` or `prefer_rgb_color`) with a service call directly from your script/automation. + +> [!WARNING] +> These settings will **not** be written to your config and will be reset on restart of Home Assistant! You can see the current settings in the `switch.adaptive_lighting_XXX` attributes if `include_config_in_attributes` is enabled. + +| Service data attribute | Required | Description | +| --------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `use_defaults` | ❌ | (default: `current` for current settings) Choose from `factory`, `configuration`, or `current` to reset variables not being set with this service call. `current` leaves them as they are, `configuration` resets to initial startup values, `factory` resets to default values listed in the documentation. | +| **all other keys** (except the ones in the table below ⚠️) | ❌ | See the table below for disallowed keys. | + +The following keys are disallowed: + +| **DISALLOWED** service data | Description | +| --------------------------- | ----------------------------------------------------------------------------------------------- | +| `entity_id` | You cannot change the switch's `entity_id`, as it has already been registered. | +| `lights` | You may call `adaptive_lighting.apply` with your lights or create a new config instead. | +| `name` | You can rename your switch's display name in Home Assistant's UI. | +| `interval` | The interval is used only once when the config loads. A config change and restart are required. | + + + +### Example Usage + +```yaml +# Temporarily change color temperature range +service: adaptive_lighting.change_switch_settings +data: + entity_id: switch.adaptive_lighting_living_room + min_color_temp: 2500 + max_color_temp: 4000 +``` + +```yaml +# Override sunrise time for the day +service: adaptive_lighting.change_switch_settings +data: + entity_id: switch.adaptive_lighting_bedroom + sunrise_time: "07:00:00" + use_defaults: current +``` + +```yaml +# Reset to configuration defaults +service: adaptive_lighting.change_switch_settings +data: + entity_id: switch.adaptive_lighting_living_room + use_defaults: configuration +``` + +--- + +## Events + +Adaptive Lighting also fires events that you can use in automations. + +### adaptive_lighting.manual_control + +Fired when a light is marked as "manually controlled" due to a detected manual change. + +**Event Data:** + +| Attribute | Description | +|-----------|-------------| +| `entity_id` | The light that was marked as manually controlled | +| `switch` | The Adaptive Lighting switch entity | + +### Example Automation + +```yaml +automation: + - alias: "Log manual control events" + trigger: + platform: event + event_type: adaptive_lighting.manual_control + action: + - service: notify.mobile_app + data: + title: "Adaptive Lighting" + message: "{{ trigger.event.data.entity_id }} was manually controlled" +``` + +See [Automation Examples](automation-examples.md) for more use cases. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 00000000..e644f385 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,106 @@ +--- +icon: lucide/life-buoy +--- + +# Troubleshooting + +This guide covers common issues and their solutions when using Adaptive Lighting. + +## Enable Debug Logging + + + + + + + +Encountering issues? Enable debug logging in your `configuration.yaml`: + +```yaml +logger: + default: warning + logs: + custom_components.adaptive_lighting: debug +``` + +After the issue occurs, create a new issue report with the log (`/config/home-assistant.log`). + + + +## Common Problems & Solutions + + + + + + + +#### :bulb: Lights Not Responding or Turning On by Themselves + +Adaptive Lighting sends more commands to lights than a typical human user would. If your light control network is unhealthy, you may experience: + +- Laggy manual commands (e.g., turning lights on or off). +- Unresponsive lights. +- Home Assistant reporting incorrect light states, causing Adaptive Lighting to inadvertently turn lights back on. + +Most issues that appear to be caused by Adaptive Lighting are actually due to unrelated problems. +Addressing these issues will significantly improve your Home Assistant experience. + +In case lights are suddenly turning on by themselves, this is most likely due to the light incorrectly reporting an "on" state to Home Assistant, leading to an undesired Adaptive Lighting action. +To prevent adapting in cases *where the state of the light is suddenly "on" and only adapt if there is an associated `light.turn_on` service call*, set `detect_non_ha_changes: false`. + +#### :signal_strength: WiFi Networks + +Ensure your light bulbs have a strong WiFi connection. If the signal strength is less than -70dBm, the connection may be weak and prone to dropping messages. + +#### :spider_web: Zigbee, Z-Wave, and Other Mesh Networks + +Mesh networks typically require powered devices to act as routers, relaying messages back to the central coordinator (the radio connected to Home Assistant). +Philips lights usually function as routers, while Ikea, Sengled, and generic Tuya bulbs often do not. +If devices become unresponsive or fail to respond to commands, Adaptive Lighting can exacerbate the issue. +Use network maps (available in ZHA, zigbee2mqtt, deCONZ, and ZWaveJS UI) to evaluate your network health. +Smart plugs can be an affordable way to add more routers to your network. + +For most Zigbee networks, **using groups is essential for optimal performance**. +For example, if you want to use Adaptive Lighting in a hallway with six bulbs, adding each bulb individually to the Adaptive Lighting configuration could overwhelm the network with commands. +Instead, create a group in your Zigbee software (not a regular Home Assistant group) and add that single group to the Adaptive Lighting configuration. +This sends a single broadcast command to adjust all bulbs, improving response times and keeping the bulbs in sync. + +As a rule of thumb, if you always control lights together (e.g., bulbs in a ceiling fixture), they should be in a Zigbee group. +Expose only the group (not individual bulbs) in Home Assistant Dashboards and external systems like Google Home or Apple HomeKit. + +> :warning: **If you control lights individually, `manual_control` cannot behave correctly! If you need to control lights individually as well, use a [Home Assistant Light Group](https://www.home-assistant.io/integrations/group/).** + +#### :rainbow: Light Colors Not Matching + +Bulbs from different manufacturers or models may have varying color temperature specifications. For instance, if you have two Adaptive Lighting configurations—one with only Philips Hue White Ambiance bulbs and another with a mix of Philips Hue White Ambiance and Sengled bulbs—the Philips Hue bulbs may appear to have different color temperatures despite having identical settings. + +To resolve this: + +1. Include only bulbs of the same make and model in a single Adaptive Lighting configuration. +2. Rearrange bulbs so that different color temperatures are not visible simultaneously. + +#### :bulb: Bulb-Specific Issues + +These lights are known to exhibit disadvantageous behaviour due to firmware bugs, insufficient functionality, or hardware limitations: + +- [Sengled Z01-A19NAE26](https://www.zigbee2mqtt.io/devices/Z01-A19NAE26.html#sengled-z01-a19nae26) + - Unexpected turn-ons: If Adaptive Lighting sends a long transition time (like the default 45 seconds), and the bulb is turned off during that time, it may turn back on after approximately 10 seconds to continue the transition command. Since the bulb is turning itself on, there will be no obvious trigger in Home Assistant or other logs indicating the cause of the light turning on. To fix this, set a much shorter `transition` time, such as 1 second. + - Heat sensitivity: Additionally, these bulbs may perform poorly in enclosed "dome" style ceiling lights, particularly when hot. While most LEDs (even non-smart ones) state in the fine print that they do not support working in enclosed fixtures, in practice, more expensive bulbs like Philips Hue generally perform better. To resolve this issue, move the problematic bulbs to open-air fixtures. +- Ikea Tradfri bulbs/drivers (and related Ikea smart light products) + - Unsupported simultaneous transition of brightness and color: When receiving such a command, they switch the brightness instantly and only transition the color. To get smooth transitions of both brightness and color, enable `separate_turn_on_commands`. + - Unresponsiveness during color transitions: No other commands are processed during an ongoing color transition, e.g., turn-off commands are ignored and lights stay on despite being reported as off to Home Assistant. The default config with long transitions thus results in long periods of unresponsiveness. To work around this, disable transitions by setting `transition` to `0`, and increase the adaptation frequency by setting `interval` to a short time, e.g., `15` seconds, to retain the impression of smooth continuous adaptations. Keeping the `initial_transition` is recommended for a smooth fade-in (lights are usually not turned off momentarily after being turned on, in which case a short period of unresponsiveness is tolerable). + + + +## Getting Help + +If you're still having issues: + +1. Enable debug logging and capture the relevant logs +2. Open an issue on [GitHub](https://github.com/basnijholt/adaptive-lighting/issues) +3. Include: + - Your configuration (redact sensitive info) + - Debug logs + - Home Assistant version + - Description of expected vs actual behavior diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..7021ac48 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,66 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "adaptive-lighting" +version = "1.30.1" +description = "Automatically adjust brightness and color of lights based on the sun position" +readme = "README.md" +license = "Apache-2.0" +requires-python = ">=3.12" + +[dependency-groups] +docs = [ + "astral", + "homeassistant", + "markdown-code-runner", + "markdown-gfm-admonition", + "pandas", + "shinylive", + "tabulate", + "ulid-transform", + "voluptuous", + "zensical", +] +dev = [ + "mypy", + "pre-commit", + "ruff", +] + +[tool.hatch.build.targets.wheel] +packages = ["custom_components/adaptive_lighting"] + +[tool.ruff] +target-version = "py312" +line-length = 88 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # Pyflakes + "I", # isort + "UP", # pyupgrade + "RUF", # Ruff-specific rules + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "SIM", # flake8-simplify +] +ignore = [ + "E501", # line too long (handled by formatter) +] + +[tool.ruff.lint.isort] +known-first-party = ["adaptive_lighting"] + +[tool.mypy] +python_version = "3.12" +warn_return_any = true +warn_unused_configs = true +ignore_missing_imports = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +asyncio_mode = "auto" diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..5dd33777 --- /dev/null +++ b/uv.lock @@ -0,0 +1,6079 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] + +[[package]] +name = "acme" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "josepy", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "pyopenssl", version = "24.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "pyrfc3339", marker = "python_full_version < '3.13'" }, + { name = "pytz", marker = "python_full_version < '3.13'" }, + { name = "requests", version = "2.32.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "setuptools", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/34/d6d7064afabe02f0043e5015aab664b67fc8bf049e81ac9220526afed3ce/acme-3.0.1.tar.gz", hash = "sha256:2f4ae207c8a6791a2bc74cd18d60274766f483c2059145b0142cbb43e761331c", size = 91828, upload-time = "2024-11-14T19:13:31.682Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/7d/2daaaacf8dea7012fc680e4c1cf5dad426b6cc97296ba5b6431d313b83ab/acme-3.0.1-py3-none-any.whl", hash = "sha256:6b5f88681ead76f8c7de313ac6c7ee1c567fdcc61d48cfad2f5cb3606778529b", size = 95884, upload-time = "2024-11-14T19:13:08.565Z" }, +] + +[[package]] +name = "acme" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "josepy", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyopenssl", version = "25.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyrfc3339", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pytz", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "requests", version = "2.32.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/6a/c94be0e8ee157f3c7721844863589c627c40bfa10d8838faeb6b28b59bb2/acme-3.2.0.tar.gz", hash = "sha256:e11d0ccf43ec19244ada40df1dc4ca49c9ce407749f3771d2cefe0674e206d84", size = 92875, upload-time = "2025-02-11T21:35:57.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/1c/da759f277879f5b8aa0b6355689e091224b8af500c6983a1c8fb66bad5b0/acme-3.2.0-py3-none-any.whl", hash = "sha256:201b118d12426f746d936efc61706d30dc2f9e2635aebab0c86ec7f80eca5f30", size = 97444, upload-time = "2025-02-11T21:35:25.465Z" }, +] + +[[package]] +name = "acme" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "josepy", version = "2.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "pyopenssl", version = "25.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "pyrfc3339", marker = "python_full_version >= '3.13.2'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/f6/897be0abeb0e64f0e6136a8a6369a54d2a603a44cb7a411f6d77dbafb4ac/acme-5.1.0.tar.gz", hash = "sha256:7b97820857d9baffed98bca50ab82bb6a636e447865d7a013a7bdd7972f03cda", size = 89982, upload-time = "2025-10-07T17:30:38.579Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/0b/4d0421412bb063f4393ae7ebf3a9a6fde621aed187a1140ccf7f9e22b823/acme-5.1.0-py3-none-any.whl", hash = "sha256:80e9c315d82302bb97279f4516ff31230d29195ab9d4a6c9411ceec20481b61e", size = 94151, upload-time = "2025-10-07T17:30:15.994Z" }, +] + +[[package]] +name = "adaptive-lighting" +version = "1.30.1" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pre-commit" }, + { name = "ruff" }, +] +docs = [ + { name = "astral" }, + { name = "homeassistant", version = "2024.12.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "homeassistant", version = "2025.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "homeassistant", version = "2026.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "markdown-code-runner" }, + { name = "markdown-gfm-admonition" }, + { name = "pandas" }, + { name = "shinylive" }, + { name = "tabulate" }, + { name = "ulid-transform", version = "1.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "ulid-transform", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "ulid-transform", version = "1.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "voluptuous", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13.2'" }, + { name = "voluptuous", version = "0.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "zensical" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy" }, + { name = "pre-commit" }, + { name = "ruff" }, +] +docs = [ + { name = "astral" }, + { name = "homeassistant" }, + { name = "markdown-code-runner" }, + { name = "markdown-gfm-admonition" }, + { name = "pandas" }, + { name = "shinylive" }, + { name = "tabulate" }, + { name = "ulid-transform" }, + { name = "voluptuous" }, + { name = "zensical" }, +] + +[[package]] +name = "aiodns" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +dependencies = [ + { name = "pycares", marker = "python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/84/41a6a2765abc124563f5380e76b9b24118977729e25a84112f8dfb2b33dc/aiodns-3.2.0.tar.gz", hash = "sha256:62869b23409349c21b072883ec8998316b234c9a9e36675756e8e317e8768f72", size = 7823, upload-time = "2024-03-31T11:27:30.639Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/14/13c65b1bd59f7e707e0cc0964fbab45c003f90292ed267d159eeeeaa2224/aiodns-3.2.0-py3-none-any.whl", hash = "sha256:e443c0c27b07da3174a109fd9e736d69058d808f144d3c9d56dbd1776964c5f5", size = 5735, upload-time = "2024-03-31T11:27:28.615Z" }, +] + +[[package]] +name = "aiodns" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "pycares", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2f/9d1ee4f937addda60220f47925dac6c6b3782f6851fd578987284a8d2491/aiodns-3.6.1.tar.gz", hash = "sha256:b0e9ce98718a5b8f7ca8cd16fc393163374bc2412236b91f6c851d066e3324b6", size = 15143, upload-time = "2025-12-11T12:53:07.785Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/e3/9f777774ebe8f664bcd564f9de3936490a16effa82a969372161c9b0fb21/aiodns-3.6.1-py3-none-any.whl", hash = "sha256:46233ccad25f2037903828c5d05b64590eaa756e51d12b4a5616e2defcbc98c7", size = 7975, upload-time = "2025-12-11T12:53:06.387Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohasupervisor" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "aiohttp", version = "3.11.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "mashumaro", marker = "python_full_version < '3.13'" }, + { name = "orjson", version = "3.10.12", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "yarl", version = "1.18.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/c8/7eed41d183ae5290761b101651baf688014c4ecf4e398311dd2255a0e6b0/aiohasupervisor-0.2.1.tar.gz", hash = "sha256:a1242165fd255796c961dadfbb88fc1f0d45f8441f8af10f42899ab478b1cbd9", size = 34806, upload-time = "2024-10-31T11:25:15.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/48/3a96f8b3510d82260d0912929ef5337262659688659e1e2e87386deec8e8/aiohasupervisor-0.2.1-py3-none-any.whl", hash = "sha256:b1e17c916bc1cec13611f74851305c8901525612ac6261e6aba2b225f2b15016", size = 33420, upload-time = "2024-10-31T11:25:14.611Z" }, +] + +[[package]] +name = "aiohasupervisor" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "aiohttp", version = "3.11.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "mashumaro", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "orjson", version = "3.10.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "yarl", version = "1.18.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/23/eceea174c1d827adea8a8b23f1428454157288fd58e6a9231e8861a45383/aiohasupervisor-0.3.0.tar.gz", hash = "sha256:91bf0b051f28582196f900a31c9bcbebec6de9e3ed1a32a2947a892c04748ce2", size = 40542, upload-time = "2025-02-05T14:41:08.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/09/98c83e4a20ae951d49720caeb6daf784293498fab0450af5b2236ca0c079/aiohasupervisor-0.3.0-py3-none-any.whl", hash = "sha256:f85b45c80ee24b381523e5a84a39f962f25e72c90026a3dcef2becea1d7f5501", size = 38550, upload-time = "2025-02-05T14:41:06.838Z" }, +] + +[[package]] +name = "aiohasupervisor" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "aiohttp", version = "3.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "mashumaro", marker = "python_full_version >= '3.13.2'" }, + { name = "orjson", version = "3.11.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e0/f8865efa28ce22e44e3526f18654c7a69a6f0d0e8523e2aaf743f2798fd8/aiohasupervisor-0.3.3.tar.gz", hash = "sha256:24e268f58f37f9d8dafadba2ef9d860292ff622bc6e78b1ca4ef5e5095d1bbc8", size = 44696, upload-time = "2025-10-01T14:55:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/97/b811d22148e7227e6f02a1f0f13f60d959bb163c806feab853544da07c3e/aiohasupervisor-0.3.3-py3-none-any.whl", hash = "sha256:bc185dbb81bb8ec6ba91b5512df7fd3bf99db15e648b20aed3f8ce7dc3203f1f", size = 40486, upload-time = "2025-10-01T14:55:56.52Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.11.11" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "aiohappyeyeballs", marker = "python_full_version < '3.13'" }, + { name = "aiosignal", marker = "python_full_version < '3.13'" }, + { name = "attrs", version = "24.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "frozenlist", marker = "python_full_version < '3.13'" }, + { name = "multidict", marker = "python_full_version < '3.13'" }, + { name = "propcache", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "yarl", version = "1.18.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/ed/f26db39d29cd3cb2f5a3374304c713fe5ab5a0e4c8ee25a0c45cc6adf844/aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e", size = 7669618, upload-time = "2024-12-18T21:20:50.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/cf/4bda538c502f9738d6b95ada11603c05ec260807246e15e869fc3ec5de97/aiohttp-3.11.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886", size = 704666, upload-time = "2024-12-18T21:18:49.254Z" }, + { url = "https://files.pythonhosted.org/packages/46/7b/87fcef2cad2fad420ca77bef981e815df6904047d0a1bd6aeded1b0d1d66/aiohttp-3.11.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2", size = 464057, upload-time = "2024-12-18T21:18:51.375Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a6/789e1f17a1b6f4a38939fbc39d29e1d960d5f89f73d0629a939410171bc0/aiohttp-3.11.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c", size = 455996, upload-time = "2024-12-18T21:18:53.11Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dd/485061fbfef33165ce7320db36e530cd7116ee1098e9c3774d15a732b3fd/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a", size = 1682367, upload-time = "2024-12-18T21:18:55.053Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d7/9ec5b3ea9ae215c311d88b2093e8da17e67b8856673e4166c994e117ee3e/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231", size = 1736989, upload-time = "2024-12-18T21:18:56.933Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fb/ea94927f7bfe1d86178c9d3e0a8c54f651a0a655214cce930b3c679b8f64/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e", size = 1793265, upload-time = "2024-12-18T21:19:00.174Z" }, + { url = "https://files.pythonhosted.org/packages/40/7f/6de218084f9b653026bd7063cd8045123a7ba90c25176465f266976d8c82/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8", size = 1691841, upload-time = "2024-12-18T21:19:02.3Z" }, + { url = "https://files.pythonhosted.org/packages/77/e2/992f43d87831cbddb6b09c57ab55499332f60ad6fdbf438ff4419c2925fc/aiohttp-3.11.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8", size = 1619317, upload-time = "2024-12-18T21:19:04.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/74/879b23cdd816db4133325a201287c95bef4ce669acde37f8f1b8669e1755/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c", size = 1641416, upload-time = "2024-12-18T21:19:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/30/98/b123f6b15d87c54e58fd7ae3558ff594f898d7f30a90899718f3215ad328/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab", size = 1646514, upload-time = "2024-12-18T21:19:12.154Z" }, + { url = "https://files.pythonhosted.org/packages/d7/38/257fda3dc99d6978ab943141d5165ec74fd4b4164baa15e9c66fa21da86b/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da", size = 1702095, upload-time = "2024-12-18T21:19:15.51Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f4/ddab089053f9fb96654df5505c0a69bde093214b3c3454f6bfdb1845f558/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853", size = 1734611, upload-time = "2024-12-18T21:19:18.849Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d6/f30b2bc520c38c8aa4657ed953186e535ae84abe55c08d0f70acd72ff577/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e", size = 1694576, upload-time = "2024-12-18T21:19:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/bc/97/b0a88c3f4c6d0020b34045ee6d954058abc870814f6e310c4c9b74254116/aiohttp-3.11.11-cp312-cp312-win32.whl", hash = "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600", size = 411363, upload-time = "2024-12-18T21:19:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/7f/23/cc36d9c398980acaeeb443100f0216f50a7cfe20c67a9fd0a2f1a5a846de/aiohttp-3.11.11-cp312-cp312-win_amd64.whl", hash = "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d", size = 437666, upload-time = "2024-12-18T21:19:26.425Z" }, + { url = "https://files.pythonhosted.org/packages/49/d1/d8af164f400bad432b63e1ac857d74a09311a8334b0481f2f64b158b50eb/aiohttp-3.11.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:541d823548ab69d13d23730a06f97460f4238ad2e5ed966aaf850d7c369782d9", size = 697982, upload-time = "2024-12-18T21:19:28.454Z" }, + { url = "https://files.pythonhosted.org/packages/92/d1/faad3bf9fa4bfd26b95c69fc2e98937d52b1ff44f7e28131855a98d23a17/aiohttp-3.11.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:929f3ed33743a49ab127c58c3e0a827de0664bfcda566108989a14068f820194", size = 460662, upload-time = "2024-12-18T21:19:31.077Z" }, + { url = "https://files.pythonhosted.org/packages/db/61/0d71cc66d63909dabc4590f74eba71f91873a77ea52424401c2498d47536/aiohttp-3.11.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0882c2820fd0132240edbb4a51eb8ceb6eef8181db9ad5291ab3332e0d71df5f", size = 452950, upload-time = "2024-12-18T21:19:33.108Z" }, + { url = "https://files.pythonhosted.org/packages/07/db/6d04bc7fd92784900704e16b745484ef45b77bd04e25f58f6febaadf7983/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63de12e44935d5aca7ed7ed98a255a11e5cb47f83a9fded7a5e41c40277d104", size = 1665178, upload-time = "2024-12-18T21:19:36.556Z" }, + { url = "https://files.pythonhosted.org/packages/54/5c/e95ade9ae29f375411884d9fd98e50535bf9fe316c9feb0f30cd2ac8f508/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa54f8ef31d23c506910c21163f22b124facb573bff73930735cf9fe38bf7dff", size = 1717939, upload-time = "2024-12-18T21:19:40.081Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/1e7d5c5daea9e409ed70f7986001b8c9e3a49a50b28404498d30860edab6/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a344d5dc18074e3872777b62f5f7d584ae4344cd6006c17ba12103759d407af3", size = 1775125, upload-time = "2024-12-18T21:19:43.578Z" }, + { url = "https://files.pythonhosted.org/packages/5d/66/890987e44f7d2f33a130e37e01a164168e6aff06fce15217b6eaf14df4f6/aiohttp-3.11.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7fb429ab1aafa1f48578eb315ca45bd46e9c37de11fe45c7f5f4138091e2f1", size = 1677176, upload-time = "2024-12-18T21:19:46.239Z" }, + { url = "https://files.pythonhosted.org/packages/8f/dc/e2ba57d7a52df6cdf1072fd5fa9c6301a68e1cd67415f189805d3eeb031d/aiohttp-3.11.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c341c7d868750e31961d6d8e60ff040fb9d3d3a46d77fd85e1ab8e76c3e9a5c4", size = 1603192, upload-time = "2024-12-18T21:19:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9e/8d08a57de79ca3a358da449405555e668f2c8871a7777ecd2f0e3912c272/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed9ee95614a71e87f1a70bc81603f6c6760128b140bc4030abe6abaa988f1c3d", size = 1618296, upload-time = "2024-12-18T21:19:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/56/51/89822e3ec72db352c32e7fc1c690370e24e231837d9abd056490f3a49886/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de8d38f1c2810fa2a4f1d995a2e9c70bb8737b18da04ac2afbf3971f65781d87", size = 1616524, upload-time = "2024-12-18T21:19:52.542Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/e2e6d9398f462ffaa095e84717c1732916a57f1814502929ed67dd7568ef/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a9b7371665d4f00deb8f32208c7c5e652059b0fda41cf6dbcac6114a041f1cc2", size = 1685471, upload-time = "2024-12-18T21:19:54.683Z" }, + { url = "https://files.pythonhosted.org/packages/ae/5f/6bb976e619ca28a052e2c0ca7b0251ccd893f93d7c24a96abea38e332bf6/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:620598717fce1b3bd14dd09947ea53e1ad510317c85dda2c9c65b622edc96b12", size = 1715312, upload-time = "2024-12-18T21:19:56.824Z" }, + { url = "https://files.pythonhosted.org/packages/79/c1/756a7e65aa087c7fac724d6c4c038f2faaa2a42fe56dbc1dd62a33ca7213/aiohttp-3.11.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf8d9bfee991d8acc72d060d53860f356e07a50f0e0d09a8dfedea1c554dd0d5", size = 1672783, upload-time = "2024-12-18T21:19:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/73/ba/a6190ebb02176c7f75e6308da31f5d49f6477b651a3dcfaaaca865a298e2/aiohttp-3.11.11-cp313-cp313-win32.whl", hash = "sha256:9d73ee3725b7a737ad86c2eac5c57a4a97793d9f442599bea5ec67ac9f4bdc3d", size = 410229, upload-time = "2024-12-18T21:20:02.469Z" }, + { url = "https://files.pythonhosted.org/packages/b8/62/c9fa5bafe03186a0e4699150a7fed9b1e73240996d0d2f0e5f70f3fdf471/aiohttp-3.11.11-cp313-cp313-win_amd64.whl", hash = "sha256:c7a06301c2fb096bdb0bd25fe2011531c1453b9f2c163c8031600ec73af1cc99", size = 436081, upload-time = "2024-12-18T21:20:04.557Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.11.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "aiohappyeyeballs", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiosignal", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "attrs", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "frozenlist", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "multidict", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "propcache", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "yarl", version = "1.18.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/d9/1c4721d143e14af753f2bf5e3b681883e1f24b592c0482df6fa6e33597fa/aiohttp-3.11.16.tar.gz", hash = "sha256:16f8a2c9538c14a557b4d309ed4d0a7c60f0253e8ed7b6c9a2859a7582f8b1b8", size = 7676826, upload-time = "2025-04-02T02:17:44.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/38/100d01cbc60553743baf0fba658cb125f8ad674a8a771f765cdc155a890d/aiohttp-3.11.16-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:911a6e91d08bb2c72938bc17f0a2d97864c531536b7832abee6429d5296e5b27", size = 704881, upload-time = "2025-04-02T02:16:09.26Z" }, + { url = "https://files.pythonhosted.org/packages/21/ed/b4102bb6245e36591209e29f03fe87e7956e54cb604ee12e20f7eb47f994/aiohttp-3.11.16-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac13b71761e49d5f9e4d05d33683bbafef753e876e8e5a7ef26e937dd766713", size = 464564, upload-time = "2025-04-02T02:16:10.781Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e1/a9ab6c47b62ecee080eeb33acd5352b40ecad08fb2d0779bcc6739271745/aiohttp-3.11.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fd36c119c5d6551bce374fcb5c19269638f8d09862445f85a5a48596fd59f4bb", size = 456548, upload-time = "2025-04-02T02:16:12.764Z" }, + { url = "https://files.pythonhosted.org/packages/80/ad/216c6f71bdff2becce6c8776f0aa32cb0fa5d83008d13b49c3208d2e4016/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d489d9778522fbd0f8d6a5c6e48e3514f11be81cb0a5954bdda06f7e1594b321", size = 1691749, upload-time = "2025-04-02T02:16:14.304Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ea/7df7bcd3f4e734301605f686ffc87993f2d51b7acb6bcc9b980af223f297/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69a2cbd61788d26f8f1e626e188044834f37f6ae3f937bd9f08b65fc9d7e514e", size = 1736874, upload-time = "2025-04-02T02:16:16.538Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/c7724b9c87a29b7cfd1202ec6446bae8524a751473d25e2ff438bc9a02bf/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd464ba806e27ee24a91362ba3621bfc39dbbb8b79f2e1340201615197370f7c", size = 1786885, upload-time = "2025-04-02T02:16:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/86/b3/f61f8492fa6569fa87927ad35a40c159408862f7e8e70deaaead349e2fba/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce63ae04719513dd2651202352a2beb9f67f55cb8490c40f056cea3c5c355ce", size = 1698059, upload-time = "2025-04-02T02:16:20.234Z" }, + { url = "https://files.pythonhosted.org/packages/ce/be/7097cf860a9ce8bbb0e8960704e12869e111abcd3fbd245153373079ccec/aiohttp-3.11.16-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b00dd520d88eac9d1768439a59ab3d145065c91a8fab97f900d1b5f802895e", size = 1626527, upload-time = "2025-04-02T02:16:22.092Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1d/aaa841c340e8c143a8d53a1f644c2a2961c58cfa26e7b398d6bf75cf5d23/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f6428fee52d2bcf96a8aa7b62095b190ee341ab0e6b1bcf50c615d7966fd45b", size = 1644036, upload-time = "2025-04-02T02:16:23.707Z" }, + { url = "https://files.pythonhosted.org/packages/2c/88/59d870f76e9345e2b149f158074e78db457985c2b4da713038d9da3020a8/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:13ceac2c5cdcc3f64b9015710221ddf81c900c5febc505dbd8f810e770011540", size = 1685270, upload-time = "2025-04-02T02:16:25.874Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b1/c6686948d4c79c3745595efc469a9f8a43cab3c7efc0b5991be65d9e8cb8/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fadbb8f1d4140825069db3fedbbb843290fd5f5bc0a5dbd7eaf81d91bf1b003b", size = 1650852, upload-time = "2025-04-02T02:16:27.556Z" }, + { url = "https://files.pythonhosted.org/packages/fe/94/3e42a6916fd3441721941e0f1b8438e1ce2a4c49af0e28e0d3c950c9b3c9/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6a792ce34b999fbe04a7a71a90c74f10c57ae4c51f65461a411faa70e154154e", size = 1704481, upload-time = "2025-04-02T02:16:29.573Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6d/6ab5854ff59b27075c7a8c610597d2b6c38945f9a1284ee8758bc3720ff6/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f4065145bf69de124accdd17ea5f4dc770da0a6a6e440c53f6e0a8c27b3e635c", size = 1735370, upload-time = "2025-04-02T02:16:31.191Z" }, + { url = "https://files.pythonhosted.org/packages/73/2a/08a68eec3c99a6659067d271d7553e4d490a0828d588e1daa3970dc2b771/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa73e8c2656a3653ae6c307b3f4e878a21f87859a9afab228280ddccd7369d71", size = 1697619, upload-time = "2025-04-02T02:16:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/61/d5/fea8dbbfb0cd68fbb56f0ae913270a79422d9a41da442a624febf72d2aaf/aiohttp-3.11.16-cp312-cp312-win32.whl", hash = "sha256:f244b8e541f414664889e2c87cac11a07b918cb4b540c36f7ada7bfa76571ea2", size = 411710, upload-time = "2025-04-02T02:16:34.525Z" }, + { url = "https://files.pythonhosted.org/packages/33/fb/41cde15fbe51365024550bf77b95a4fc84ef41365705c946da0421f0e1e0/aiohttp-3.11.16-cp312-cp312-win_amd64.whl", hash = "sha256:23a15727fbfccab973343b6d1b7181bfb0b4aa7ae280f36fd2f90f5476805682", size = 438012, upload-time = "2025-04-02T02:16:36.103Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/7c712b2d9fb4d5e5fd6d12f9ab76e52baddfee71e3c8203ca7a7559d7f51/aiohttp-3.11.16-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a3814760a1a700f3cfd2f977249f1032301d0a12c92aba74605cfa6ce9f78489", size = 698005, upload-time = "2025-04-02T02:16:37.923Z" }, + { url = "https://files.pythonhosted.org/packages/51/3e/61057814f7247666d43ac538abcd6335b022869ade2602dab9bf33f607d2/aiohttp-3.11.16-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b751a6306f330801665ae69270a8a3993654a85569b3469662efaad6cf5cc50", size = 461106, upload-time = "2025-04-02T02:16:39.961Z" }, + { url = "https://files.pythonhosted.org/packages/4f/85/6b79fb0ea6e913d596d5b949edc2402b20803f51b1a59e1bbc5bb7ba7569/aiohttp-3.11.16-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ad497f38a0d6c329cb621774788583ee12321863cd4bd9feee1effd60f2ad133", size = 453394, upload-time = "2025-04-02T02:16:41.562Z" }, + { url = "https://files.pythonhosted.org/packages/4b/04/e1bb3fcfbd2c26753932c759593a32299aff8625eaa0bf8ff7d9c0c34a36/aiohttp-3.11.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca37057625693d097543bd88076ceebeb248291df9d6ca8481349efc0b05dcd0", size = 1666643, upload-time = "2025-04-02T02:16:43.62Z" }, + { url = "https://files.pythonhosted.org/packages/0e/27/97bc0fdd1f439b8f060beb3ba8fb47b908dc170280090801158381ad7942/aiohttp-3.11.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5abcbba9f4b463a45c8ca8b7720891200658f6f46894f79517e6cd11f3405ca", size = 1721948, upload-time = "2025-04-02T02:16:45.617Z" }, + { url = "https://files.pythonhosted.org/packages/2c/4f/bc4c5119e75c05ef15c5670ef1563bbe25d4ed4893b76c57b0184d815e8b/aiohttp-3.11.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f420bfe862fb357a6d76f2065447ef6f484bc489292ac91e29bc65d2d7a2c84d", size = 1774454, upload-time = "2025-04-02T02:16:48.562Z" }, + { url = "https://files.pythonhosted.org/packages/73/5b/54b42b2150bb26fdf795464aa55ceb1a49c85f84e98e6896d211eabc6670/aiohttp-3.11.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58ede86453a6cf2d6ce40ef0ca15481677a66950e73b0a788917916f7e35a0bb", size = 1677785, upload-time = "2025-04-02T02:16:50.367Z" }, + { url = "https://files.pythonhosted.org/packages/10/ee/a0fe68916d3f82eae199b8535624cf07a9c0a0958c7a76e56dd21140487a/aiohttp-3.11.16-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fdec0213244c39973674ca2a7f5435bf74369e7d4e104d6c7473c81c9bcc8c4", size = 1608456, upload-time = "2025-04-02T02:16:52.158Z" }, + { url = "https://files.pythonhosted.org/packages/8b/48/83afd779242b7cf7e1ceed2ff624a86d3221e17798061cf9a79e0b246077/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72b1b03fb4655c1960403c131740755ec19c5898c82abd3961c364c2afd59fe7", size = 1622424, upload-time = "2025-04-02T02:16:54.386Z" }, + { url = "https://files.pythonhosted.org/packages/6f/27/452f1d5fca1f516f9f731539b7f5faa9e9d3bf8a3a6c3cd7c4b031f20cbd/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:780df0d837276276226a1ff803f8d0fa5f8996c479aeef52eb040179f3156cbd", size = 1660943, upload-time = "2025-04-02T02:16:56.887Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e1/5c7d63143b8d00c83b958b9e78e7048c4a69903c760c1e329bf02bac57a1/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ecdb8173e6c7aa09eee342ac62e193e6904923bd232e76b4157ac0bfa670609f", size = 1622797, upload-time = "2025-04-02T02:16:58.676Z" }, + { url = "https://files.pythonhosted.org/packages/46/9e/2ac29cca2746ee8e449e73cd2fcb3d454467393ec03a269d50e49af743f1/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6db7458ab89c7d80bc1f4e930cc9df6edee2200127cfa6f6e080cf619eddfbd", size = 1687162, upload-time = "2025-04-02T02:17:01.076Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6b/eaa6768e02edebaf37d77f4ffb74dd55f5cbcbb6a0dbf798ccec7b0ac23b/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2540ddc83cc724b13d1838026f6a5ad178510953302a49e6d647f6e1de82bc34", size = 1718518, upload-time = "2025-04-02T02:17:03.388Z" }, + { url = "https://files.pythonhosted.org/packages/e5/18/dda87cbad29472a51fa058d6d8257dfce168289adaeb358b86bd93af3b20/aiohttp-3.11.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b4e6db8dc4879015b9955778cfb9881897339c8fab7b3676f8433f849425913", size = 1675254, upload-time = "2025-04-02T02:17:05.579Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/d2fb08c614df401d92c12fcbc60e6e879608d5e8909ef75c5ad8d4ad8aa7/aiohttp-3.11.16-cp313-cp313-win32.whl", hash = "sha256:493910ceb2764f792db4dc6e8e4b375dae1b08f72e18e8f10f18b34ca17d0979", size = 410698, upload-time = "2025-04-02T02:17:07.499Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ed/853e36d5a33c24544cfa46585895547de152dfef0b5c79fa675f6e4b7b87/aiohttp-3.11.16-cp313-cp313-win_amd64.whl", hash = "sha256:42864e70a248f5f6a49fdaf417d9bc62d6e4d8ee9695b24c5916cb4bb666c802", size = 436395, upload-time = "2025-04-02T02:17:09.566Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "aiohappyeyeballs", marker = "python_full_version >= '3.13.2'" }, + { name = "aiosignal", marker = "python_full_version >= '3.13.2'" }, + { name = "attrs", version = "25.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "frozenlist", marker = "python_full_version >= '3.13.2'" }, + { name = "multidict", marker = "python_full_version >= '3.13.2'" }, + { name = "propcache", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "yarl", version = "1.22.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +] + +[[package]] +name = "aiohttp-asyncmdnsresolver" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiodns", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiodns", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "aiohttp", version = "3.11.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiohttp", version = "3.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "zeroconf", version = "0.146.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "zeroconf", version = "0.148.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/83/09fb97705e7308f94197a09b486669696ea20f28074c14b5811a38bdedc3/aiohttp_asyncmdnsresolver-0.1.1.tar.gz", hash = "sha256:8c65d4b08b42c8a260717a2766bd5967a1d437cee852a9b21f3928b5171a7c81", size = 36129, upload-time = "2025-02-14T14:46:44.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/d1/4f61508a43de82bb5c60cede3bb89cc57c5e8af7978d93ca03ad60b99368/aiohttp_asyncmdnsresolver-0.1.1-py3-none-any.whl", hash = "sha256:d04ded993e9f0e07c07a1bc687cde447d9d32e05bcf55ecbf94f63b33dcab93e", size = 13582, upload-time = "2025-02-14T14:46:41.985Z" }, +] + +[[package]] +name = "aiohttp-cors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +dependencies = [ + { name = "aiohttp", version = "3.11.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "aiohttp", version = "3.11.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/9e/6cdce7c3f346d8fd487adf68761728ad8cd5fbc296a7b07b92518350d31f/aiohttp-cors-0.7.0.tar.gz", hash = "sha256:4d39c6d7100fd9764ed1caf8cebf0eb01bf5e3f24e2e073fda6234bc48b19f5d", size = 35966, upload-time = "2018-03-06T15:45:42.936Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/e7/e436a0c0eb5127d8b491a9b83ecd2391c6ff7dcd5548dfaec2080a2340fd/aiohttp_cors-0.7.0-py3-none-any.whl", hash = "sha256:0451ba59fdf6909d0e2cd21e4c0a43752bc0703d33fc78ae94d9d9321710193e", size = 27564, upload-time = "2018-03-06T15:45:42.034Z" }, +] + +[[package]] +name = "aiohttp-cors" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "aiohttp", version = "3.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/d89e846a5444b3d5eb8985a6ddb0daef3774928e1bfbce8e84ec97b0ffa7/aiohttp_cors-0.8.1.tar.gz", hash = "sha256:ccacf9cb84b64939ea15f859a146af1f662a6b1d68175754a07315e305fb1403", size = 38626, upload-time = "2025-03-31T14:16:20.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/3b/40a68de458904bcc143622015fff2352b6461cd92fd66d3527bf1c6f5716/aiohttp_cors-0.8.1-py3-none-any.whl", hash = "sha256:3180cf304c5c712d626b9162b195b1db7ddf976a2a25172b35bb2448b890a80d", size = 25231, upload-time = "2025-03-31T14:16:18.478Z" }, +] + +[[package]] +name = "aiohttp-fast-zlib" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "aiohttp", version = "3.11.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/af/fb60f9f5f7c619478346c456262c491493e1c73f3e1681ce73cdd204ef9f/aiohttp_fast_zlib-0.2.0.tar.gz", hash = "sha256:e2e6c27a7ffc825cdd50d6f80e302ebbc025b43c876c00f01dc2ae759905dce8", size = 8671, upload-time = "2024-11-14T15:45:15.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/54/35b33e95e01878b3252e686d050502d04dd4cc3192b4ee13cd300ab2a1aa/aiohttp_fast_zlib-0.2.0-py3-none-any.whl", hash = "sha256:ff50de72e95da3d1b7e6dd6fd64a3aedf743f488ad9202a8fde3baccf0fa1161", size = 8422, upload-time = "2024-11-14T15:45:14.089Z" }, +] + +[[package]] +name = "aiohttp-fast-zlib" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "aiohttp", version = "3.11.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/73/c93543264f745202a6fe78ad8ddb7c13a9d3e3ea47cde26501d683bd46a4/aiohttp_fast_zlib-0.2.3.tar.gz", hash = "sha256:d7e34621f2ac47155d9ad5d78f15ffb066a4ee849cb3d55df0077395ab4b3eff", size = 8591, upload-time = "2025-02-22T17:52:51.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/55/9aebf9f5dac1a34bb0a4f300d2ec4692f86df44e458f3061a659dec2b98f/aiohttp_fast_zlib-0.2.3-py3-none-any.whl", hash = "sha256:41a93670f88042faff3ebbd039fd2fc37a0c956193c20eb758be45b1655a7e04", size = 8421, upload-time = "2025-02-22T17:52:49.971Z" }, +] + +[[package]] +name = "aiohttp-fast-zlib" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "aiohttp", version = "3.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/a6/982f3a013b42e914a2420631afcaecb729c49525cc6cc58e15d27ee4cb4b/aiohttp_fast_zlib-0.3.0.tar.gz", hash = "sha256:963a09de571b67fa0ef9cb44c5a32ede5cb1a51bc79fc21181b1cddd56b58b28", size = 8770, upload-time = "2025-06-07T12:41:49.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/11/ea9ecbcd6cf68c5de690fd39b66341405ab091aa0c3598277e687aa65901/aiohttp_fast_zlib-0.3.0-py3-none-any.whl", hash = "sha256:d4cb20760a3e1137c93cb42c13871cbc9cd1fdc069352f2712cd650d6c0e537e", size = 8615, upload-time = "2025-06-07T12:41:47.454Z" }, +] + +[[package]] +name = "aiooui" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/b7/ad0f86010bbabc4e556e98dd2921a923677188223cc524432695966f14fa/aiooui-0.1.9.tar.gz", hash = "sha256:e8c8bc59ab352419e0747628b4cce7c4e04d492574c1971e223401126389c5d8", size = 369276, upload-time = "2025-01-19T00:12:44.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/fa/b1310457adbea7adb84d2c144159f3b41341c40c80df3c10ce6b266874b3/aiooui-0.1.9-py3-none-any.whl", hash = "sha256:737a5e62d8726540218c2b70e5f966d9912121e4644f3d490daf8f3c18b182e5", size = 367404, upload-time = "2025-01-19T00:12:42.57Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "aiozoneinfo" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "tzdata", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/05/fe5c1f5f72ca7fbb88b05eb9d47b90bfd898d494a1099e1ec1c3d0e5d44b/aiozoneinfo-0.2.1.tar.gz", hash = "sha256:457e2c665a2c7e093119efb87cc5e0da29e6f59aac504a544bec822c5be1cb6b", size = 8472, upload-time = "2024-06-24T12:30:11.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/a1/7f94ff464f01a65d30ebdb00b815b2cf9613b3c4314828d2aad576b0ff21/aiozoneinfo-0.2.1-py3-none-any.whl", hash = "sha256:04579f855f030cd0edb1758659c513142ef1aaf7fcc97b59eb2262ed0c453cce", size = 8011, upload-time = "2024-06-24T12:30:10.017Z" }, +] + +[[package]] +name = "aiozoneinfo" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "tzdata", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/00/e437a179ab78ed24780ded10bbb5d7e10832c07f62eab1d44ee2f335c95c/aiozoneinfo-0.2.3.tar.gz", hash = "sha256:987ce2a7d5141f3f4c2e9d50606310d0bf60d688ad9f087aa7267433ba85fff3", size = 8381, upload-time = "2025-02-04T19:32:06.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/a4/99e13bb4006999de2a4d63cee7497c3eb7f616b0aefc660c4c316179af3a/aiozoneinfo-0.2.3-py3-none-any.whl", hash = "sha256:5423f0354c9eed982e3f1c35edeeef1458d4cc6a10f106616891a089a8455661", size = 8009, upload-time = "2025-02-04T19:32:04.74Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "annotatedyaml" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "propcache", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyyaml", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "voluptuous", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/b6/e24fb814108d0a708cc8b26d67e61d5fee0735373dcaa8cd61cb140caf02/annotatedyaml-0.4.5.tar.gz", hash = "sha256:e251929cd7e741fa2e9ece13e24e29bb8f1b5c6ca3a9ef7292a66a3ae8b9390f", size = 15321, upload-time = "2025-03-22T17:50:37.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/1c/dc1159b82db421cd71ce09f877781aa8c3cc623c8d094b3532b5519ff149/annotatedyaml-0.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bf113cfe1c7a85f0e61ea39a6d2f3fdcf12fe528e7d563f8eff4a89afdfaa7a1", size = 62194, upload-time = "2025-03-22T17:54:04.903Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/9bb9b9dabb8468627ae42384b666687b3b7541cf7e080d50d129bd93c48b/annotatedyaml-0.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c6f64ebe3a81d7ddc6bc261feca2092905043e493da369bd93ad6aab58399a0a", size = 61480, upload-time = "2025-03-22T17:54:06.075Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7e/35038f9eeea279c31cbb7181800db1f6e00f42e83b2dc30bc7bf9d686cb4/annotatedyaml-0.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:480670331a3f906ddc760f21b302984ace4c674cfa3e6c48fdf76841dd0cdc1e", size = 73749, upload-time = "2025-03-22T17:54:07.643Z" }, + { url = "https://files.pythonhosted.org/packages/c7/09/5154fef033fe9595f603f2fa55b1a55057a80e105bfb17ea9557ca68aa44/annotatedyaml-0.4.5-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:981b1dc193163d17757a8b8016b048e6d315de93055671a989f327276e1acd30", size = 79159, upload-time = "2025-03-22T17:54:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/02/f9/3c524ac4ba2a977640e221154deb6276a3b7463fa2525b5a0b7edd2d9ccc/annotatedyaml-0.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6acea1969910a3a956fdb818734bfb0e5f7a377c18d1080846372c281d930dd9", size = 76137, upload-time = "2025-03-22T17:54:10.733Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4b/c066e26ffe6d472c4abf6cfdaf2e1760e2959a94a873559622407d953cf0/annotatedyaml-0.4.5-cp312-cp312-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:649a256ae447e97f075943ab6cfc15d582490f994ffc5523225ebdeaffd24164", size = 70327, upload-time = "2025-03-22T17:54:11.881Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0c/b521a82c88917b393aba710330a33b5083e2ce938917e21a312db8c7be3c/annotatedyaml-0.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a91b433c5250d3a42bbf5a72e38e2cd04f1fd48c82eae7f6dec3ecb3b4cc121e", size = 74790, upload-time = "2025-03-22T17:54:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/24/ec/1c17c3ea482acf68ad4490c6b6b70c43097ebd3b96ae9ea652dd38282968/annotatedyaml-0.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5a0ecba3df7c5fd4f2256669b4d375a08e7e48db1092f44e98fe35505121f1ea", size = 71456, upload-time = "2025-03-22T17:54:14.529Z" }, + { url = "https://files.pythonhosted.org/packages/5c/40/b7e6e9fe71456eec5502e24cf943d7dd33c116dc6858319ccc9135be1fd5/annotatedyaml-0.4.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2b0c706df48c8b96250b1f18728f815f3c7bdb6ad86310f3bc433cd21ca063ce", size = 82390, upload-time = "2025-03-22T17:54:15.754Z" }, + { url = "https://files.pythonhosted.org/packages/74/f8/d548281bbd2d3aa5fa232007f02ea165c145e72666a81b277ac2af698430/annotatedyaml-0.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ce311f389a6f149f0d7e76ba789ab5c543ed23c83fcd16e6f05e75934039f75", size = 78542, upload-time = "2025-03-22T17:54:16.86Z" }, + { url = "https://files.pythonhosted.org/packages/6e/81/8dda9d9700ac886ce4b5eec7203ebdf7c690ff178f81fe64eb9921ad1f3a/annotatedyaml-0.4.5-cp312-cp312-win32.whl", hash = "sha256:9ce177a6a1c751ac08beaa8b9e449d4b3ef759ab23ad88847970d55b625f58d9", size = 58103, upload-time = "2025-03-22T17:54:18.115Z" }, + { url = "https://files.pythonhosted.org/packages/3e/76/beba5536e610b5a06bf209379650bdb2767821ca757fc1b331f3ac043e00/annotatedyaml-0.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:6ca77b171137f8a2939c3fc4eae70d26fcefa4fa7e7a839d84f0bb1f4b979b4a", size = 63133, upload-time = "2025-03-22T17:54:19.225Z" }, + { url = "https://files.pythonhosted.org/packages/60/d4/262c3ebf8266595975f810998c6a82633eddc373764a927d919d33f3d3ce/annotatedyaml-0.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:971293ef07be457554ee97bcd6f7b0cb13df1c8d8ab1a2554880d78d9dc5d27a", size = 60968, upload-time = "2025-03-22T17:54:21.021Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b2/fd26ed4aa50c8a6670ae0909f8075262d50fa959eeff2185074f00cdc8aa/annotatedyaml-0.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8100a47d37b766f850bf8659fc6f973b14633f5d4a1957195af0a0e36449ffbe", size = 60414, upload-time = "2025-03-22T17:54:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/f5/96/0c52b99fb8cf39b585fca4a4656b829c1b0eec38943eef40c97044ed114b/annotatedyaml-0.4.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51a053d426ce1d1d7a783cea5185f5f5b3a4c3c2f269cd9cd2dfb07bd6671ee0", size = 72011, upload-time = "2025-03-22T17:54:23.316Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a6/7a77d92db7df4f491f5a90218c1d327bf32d37bfa18c99d3a9588d219d0f/annotatedyaml-0.4.5-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:2ca45e75b3091680553f21dca3f776075fb029f1a8499de61801cb0712f29de5", size = 77028, upload-time = "2025-03-22T17:54:24.433Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a0/bd6dc6eab687ab98a182cdf5fadb8a9456b6dab25cb1260857f324abcda0/annotatedyaml-0.4.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7354a88931bc73e05d4e1b24dd6c26b8618ea6412553b4c8084a7481932482bc", size = 74145, upload-time = "2025-03-22T17:54:25.988Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e1/ad12626d5096835d583455a02165f1d0cabdfd1796f5b07854f86fc61083/annotatedyaml-0.4.5-cp313-cp313-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75c3a91402dcfcf45967dcbbcd3ee151222c4881202be87f00c17cf0d627caae", size = 68149, upload-time = "2025-03-22T17:54:27.414Z" }, + { url = "https://files.pythonhosted.org/packages/25/48/a871c4c3c6e45b002a6f04a17b758e8db0120f79b43a494b298dff43ebfa/annotatedyaml-0.4.5-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:3d76ca28122fd063f27f298aa76f074f4bb8dd84501cf74cfec51931f0ed7ae0", size = 74388, upload-time = "2025-03-22T17:50:36.089Z" }, + { url = "https://files.pythonhosted.org/packages/03/b2/7ff9c2c479883a7f583ba5f0c380d937caf065eb994cbf671a656c6847b7/annotatedyaml-0.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea47e128d2a8f549fad47b4a579f9d0a0e11733130419cb5071eb242caf5e66e", size = 73542, upload-time = "2025-03-22T17:54:28.527Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5d/a9cb90c65717226cf7eb3f5f0808befb9c80e05641c8857e305a02bc6393/annotatedyaml-0.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b0b21600607faea68a6a8e99fab7671119a672c454b153aec3fc3410347650ee", size = 69904, upload-time = "2025-03-22T17:54:29.694Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f0/a8d04e2cf8d743c5364af8a41dd2110a4fee70489142114f4f99a87124f7/annotatedyaml-0.4.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:233864f23f89a43457759a526a01cccc9f60409b08070b806b5122ee5cc4cb9c", size = 80000, upload-time = "2025-03-22T17:54:30.826Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d6/24c949543c2378390856912ccf66d2b82b06ab68ec43ff8da48dd2e072e3/annotatedyaml-0.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:35e0be8088e81b60be70da401da23db5420795e1e3ba7451d232a02dd9a81f30", size = 76820, upload-time = "2025-03-22T17:54:31.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ca/8c85cf1f87234cf99a44ac2c9859e7446015932bcc205d06a95b0197739a/annotatedyaml-0.4.5-cp313-cp313-win32.whl", hash = "sha256:967fddfa8af4864f09190bde7905f05ab5bdd5f32fcca672e86033a39b0afbe8", size = 57338, upload-time = "2025-03-22T17:54:33.093Z" }, + { url = "https://files.pythonhosted.org/packages/78/57/2cb75df5189ee009278895afa77941ba701d4fc72f5b6ce44b6f97295159/annotatedyaml-0.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:f53f9f8e4ae92081653337be56265cf7085a5bc216f5e15c4531b36de5cba365", size = 62040, upload-time = "2025-03-22T17:54:34.617Z" }, +] + +[[package]] +name = "annotatedyaml" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "propcache", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "voluptuous", version = "0.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/4b/973067092ee348e331d125acd60c45245f11663373c219650814b43d0025/annotatedyaml-1.0.2.tar.gz", hash = "sha256:f9a49952994ef1952ca17d27bb6478342eb1189d2c28e4c0ddbbb32065471fb0", size = 15366, upload-time = "2025-10-04T14:36:26.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/0f/4482333d679e7174b74655d17b3969ab3754ae4d581752bac1002fe316c0/annotatedyaml-1.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:359a964daf3fccbb4818e6f08478d2e6712a2417a261cbd6472826ce5e8f1503", size = 58944, upload-time = "2025-10-04T14:41:49.516Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/ab5f9c67dd13b54e0100e8a4cdfd371c45ecfea1ba776a971d7b728087fe/annotatedyaml-1.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6d2dcf741bdedf893d04f958f3f1ad0b5b12b1fe27746f9918a24e2f347eac1", size = 60030, upload-time = "2025-10-04T14:41:50.859Z" }, + { url = "https://files.pythonhosted.org/packages/47/3f/785a22acee2fc16049ac00a9f708f11b1354e40578ae4e5076b989dc5f82/annotatedyaml-1.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:139533a395301f219bfd4ba2265b7a8c55cb4931aac7f730a8ff204a465e76d3", size = 70701, upload-time = "2025-10-04T14:41:52.091Z" }, + { url = "https://files.pythonhosted.org/packages/e8/93/712a6170903b6dd2a30aa59f76e39569f260fde38e9277d3a40ddbdf53f4/annotatedyaml-1.0.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:623f571e0d3819a3cbadce592a2c691274ccd46b09ad770f9271201d7476ea88", size = 65011, upload-time = "2025-10-04T14:41:53.35Z" }, + { url = "https://files.pythonhosted.org/packages/34/a0/5f3e9b72d871d67f89d70166d0e2affdbcf0cf87cd20276c84b6db968a52/annotatedyaml-1.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75927ec682f188efe25309e259c115e3976b702900ce1be93a971b328c87a10a", size = 71370, upload-time = "2025-10-04T14:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/a4/32/143d643d9f5d21f1b66666713d24adf68677790fb61700bc727078bdef2c/annotatedyaml-1.0.2-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:106ac5eaa022df4dfa42e307932aa2a197a19151de3bb41e98840cfc7f1745e1", size = 69434, upload-time = "2025-10-04T14:36:24.962Z" }, + { url = "https://files.pythonhosted.org/packages/83/b0/1ce75b81e42e033914f94159f633b923e57c507690eb0bba966475cab9a1/annotatedyaml-1.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:139d2626fe8faccba9cc79b4d8dca25a4d59e4a274508612842d78945bddeebe", size = 71333, upload-time = "2025-10-04T14:41:55.836Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/89451246b115dfc5fcfb3b3ca966f9fcdfc647b10a13eb517fa11f2e3ffd/annotatedyaml-1.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6f2fb86c18064f0dcfb01e3d1096f0575cdff509a24b748c2994f97eb0b70156", size = 65905, upload-time = "2025-10-04T14:41:57.121Z" }, + { url = "https://files.pythonhosted.org/packages/70/57/7008f39f1af0e0b36668cd9affe8a68846797ee1119ec36daac428ade742/annotatedyaml-1.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:56d7235147b58d155b4ee93f2a92920b4c0be6a6852dc3fd810c67f6e56f8c15", size = 72181, upload-time = "2025-10-04T14:41:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/18/48/abfed2c0d5ff9d09aff2bb85d5035b56066925826972e0f703f59c2c0cb5/annotatedyaml-1.0.2-cp313-cp313-win32.whl", hash = "sha256:e53c74051a82c4cbd68db1371918a6399650f165579a2bf1f7e0a2ed58300564", size = 56097, upload-time = "2025-10-04T14:41:59.016Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7e/24d0e04d148340950aa26e9e2bea1d4047d6bd3588d0db86d2107d7ca2a8/annotatedyaml-1.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:13ca5d3a325103fd0a0c5a27af05f22118935f3e731e2df26f620ee85b56e85b", size = 60283, upload-time = "2025-10-04T14:41:59.955Z" }, + { url = "https://files.pythonhosted.org/packages/1b/91/0acf5b74926c6964812d9ed752af77531ab4daa06fba1cb668d9006e9e1f/annotatedyaml-1.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c42f385c3f04f425d5948c16afbb94a876da867be276dbf2c2e7436b9a80792d", size = 58962, upload-time = "2025-10-04T14:42:01.183Z" }, + { url = "https://files.pythonhosted.org/packages/71/f6/5dac1ce125984db4cb99d883f234e6a8c0e49358a9136047a490bc2ba51a/annotatedyaml-1.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b5d9d24ba907fd2e905eac69c88e651310c480980a17aa57faf0599ff21f586f", size = 60252, upload-time = "2025-10-04T14:42:02.095Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/81dea3e4272927518abb9c96ab299b8c4346c40267740bfb8d6b0cdb317f/annotatedyaml-1.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2572b7c3c630dae1dd163d6c6ba847493a7f987437941b32d0ad8354615f358a", size = 71219, upload-time = "2025-10-04T14:42:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/42/fb/5aa3d7767cb92e8ba34cba582e5b088f42746e6d075f7d387fcdc4e5dd62/annotatedyaml-1.0.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b28fe13eb0014a0dd06c9a292466feed0cd298ab10525ef9a37089df01c7d333", size = 64459, upload-time = "2025-10-04T14:42:03.967Z" }, + { url = "https://files.pythonhosted.org/packages/a0/eb/b29b84eec6d3a1fc3278ff2959388f347e7853a3f82fc5275c591a523835/annotatedyaml-1.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:987f73a13f121c775bcdb082214c17f114447fee7dad37db2f86b035893ad58d", size = 71175, upload-time = "2025-10-04T14:42:05.22Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7c/4f4bf854f4b62cade7485a9572773d4440ee535e905f166b441b2d3f19a7/annotatedyaml-1.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5cb4ee79c08da2b8f4f24b1775732ca6c497682f3c9b3fd65dee4ea084fc925c", size = 71824, upload-time = "2025-10-04T14:42:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/68/ac/1f903eeccde636723fcf664b372a6ab253b7f13c3de446ff5bce6852d696/annotatedyaml-1.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:da065a8c29556219fce1aa81b406e84f73bc2181067658e57428a8b2e662fc1b", size = 65343, upload-time = "2025-10-04T14:42:07.727Z" }, + { url = "https://files.pythonhosted.org/packages/e0/aa/43e83b50a42ad5c51abf1a335cfc249e182f66542d7c7306ee07397b1956/annotatedyaml-1.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ba9937418c1b189b267540b47fa0dc24c148292739d06a6ca31c2ca8482f16", size = 72328, upload-time = "2025-10-04T14:42:08.656Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6a/f5d9c29633c499973f10330af31a8b135a564a4a2e056c32a6ff2c901559/annotatedyaml-1.0.2-cp314-cp314-win32.whl", hash = "sha256:003e16e91b40176dd8fe77d56c6c936106b408b62953e88ce3506e8ba10bf4e1", size = 57286, upload-time = "2025-10-04T14:42:09.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/43/d8c7464676094658f1caaee6762536ab43867d7153f7c637207c63fc4c97/annotatedyaml-1.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:17e64a7dde47a678db8aa4e934c3ed8da9a52ab1bc6946d12be86f323e6bd8c7", size = 61363, upload-time = "2025-10-04T14:42:10.968Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5d/7e384f4115a7bc113162f7b6eb5d561031e303f840f304b68e3f1b0541a1/annotatedyaml-1.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8698bbbd1d38f8c9ba95a107d7597f5af3f2ba295d1d14227f85b62377998ffc", size = 104776, upload-time = "2025-10-04T14:42:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/77bebdd30118c1e85f11d5a83a3bb5955409bba74d81cfb0f7b551273513/annotatedyaml-1.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cbc661dbc7c5f7ddf69fbf879da6a96745b8cd39ae1338dab3a0aa8eb208367", size = 107716, upload-time = "2025-10-04T14:42:14.151Z" }, + { url = "https://files.pythonhosted.org/packages/dc/19/bfc798abb154e398d5210304ba3beff9ad9c7b6ec4574ffb705493b8e2d5/annotatedyaml-1.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db1c3ca021bbd390354037ede5c255657afb2a7544b7cfa0e091b62b888aa462", size = 130361, upload-time = "2025-10-04T14:42:15.494Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "appdirs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, +] + +[[package]] +name = "asgiref" +version = "3.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" }, +] + +[[package]] +name = "astral" +version = "2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/c3/76dfe55a68c48a1a6f3d2eeab2793ebffa9db8adfba82774a7e0f5f43980/astral-2.2.tar.gz", hash = "sha256:e41d9967d5c48be421346552f0f4dedad43ff39a83574f5ff2ad32b6627b6fbe", size = 578223, upload-time = "2020-05-20T14:23:17.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/60/7cc241b9c3710ebadddcb323e77dd422c693183aec92449a1cf1fb59e1ba/astral-2.2-py2.py3-none-any.whl", hash = "sha256:b9ef70faf32e81a8ba174d21e8f29dc0b53b409ef035f27e0749ddc13cb5982a", size = 30775, upload-time = "2020-05-20T14:23:14.866Z" }, +] + +[[package]] +name = "async-interrupt" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/5a2d74465037b33ccdaf830e3d9ac008bccdbe4b0657983b90dc89191626/async_interrupt-1.2.0.tar.gz", hash = "sha256:d147559e2478501ad45ea43f52df23b246456715a7cb96e1aebdb4b71aed43d5", size = 8584, upload-time = "2024-08-21T13:23:54.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/f6/5638f86da774d30dae619a8d0d48df24cb17981b43948a8a3ee241b8b695/async_interrupt-1.2.0-py3-none-any.whl", hash = "sha256:a0126e882b9991d1c77839ab53e0e1b9f41f1b3d151a7032243f15011df5e4dc", size = 8898, upload-time = "2024-08-21T13:23:52.816Z" }, +] + +[[package]] +name = "async-interrupt" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/56/79/732a581e3ceb09f938d33ad8ab3419856181d95bb621aa2441a10f281e10/async_interrupt-1.2.2.tar.gz", hash = "sha256:be4331a029b8625777905376a6dc1370984c8c810f30b79703f3ee039d262bf7", size = 8484, upload-time = "2025-02-22T17:15:04.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/77/060b972fa7819fa9eea9a70acf8c7c0c58341a1e300ee5ccb063e757a4a7/async_interrupt-1.2.2-py3-none-any.whl", hash = "sha256:0a8deb884acfb5fe55188a693ae8a4381bbbd2cb6e670dac83869489513eec2c", size = 8907, upload-time = "2025-02-22T17:15:01.971Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "atomicwrites-homeassistant" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/5a/10ff0fd9aa04f78a0b31bb617c8d29796a12bea33f1e48aa54687d635e44/atomicwrites-homeassistant-1.4.1.tar.gz", hash = "sha256:256a672106f16745445228d966240b77b55f46a096d20305901a57aa5d1f4c2f", size = 12223, upload-time = "2022-07-08T20:56:46.35Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/1b/872dd3b11939edb4c0a27d2569a9b7e77d3b88995a45a331f376e13528c0/atomicwrites_homeassistant-1.4.1-py2.py3-none-any.whl", hash = "sha256:01457de800961db7d5b575f3c92e7fb56e435d88512c366afb0873f4f092bb0d", size = 7128, upload-time = "2022-07-08T20:56:44.186Z" }, +] + +[[package]] +name = "attrs" +version = "24.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678, upload-time = "2024-08-06T14:37:38.364Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001, upload-time = "2024-08-06T14:37:36.958Z" }, +] + +[[package]] +name = "attrs" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562, upload-time = "2025-01-25T11:30:12.508Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152, upload-time = "2025-01-25T11:30:10.164Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "audioop-lts" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/3b/69ff8a885e4c1c42014c2765275c4bd91fe7bc9847e9d8543dbcbb09f820/audioop_lts-0.2.1.tar.gz", hash = "sha256:e81268da0baa880431b68b1308ab7257eb33f356e57a5f9b1f915dfb13dd1387", size = 30204, upload-time = "2024-08-04T21:14:43.957Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/91/a219253cc6e92db2ebeaf5cf8197f71d995df6f6b16091d1f3ce62cb169d/audioop_lts-0.2.1-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd1345ae99e17e6910f47ce7d52673c6a1a70820d78b67de1b7abb3af29c426a", size = 46252, upload-time = "2024-08-04T21:13:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f6/3cb21e0accd9e112d27cee3b1477cd04dafe88675c54ad8b0d56226c1e0b/audioop_lts-0.2.1-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:e175350da05d2087e12cea8e72a70a1a8b14a17e92ed2022952a4419689ede5e", size = 27183, upload-time = "2024-08-04T21:13:59.966Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7e/f94c8a6a8b2571694375b4cf94d3e5e0f529e8e6ba280fad4d8c70621f27/audioop_lts-0.2.1-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:4a8dd6a81770f6ecf019c4b6d659e000dc26571b273953cef7cd1d5ce2ff3ae6", size = 26726, upload-time = "2024-08-04T21:14:00.846Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f8/a0e8e7a033b03fae2b16bc5aa48100b461c4f3a8a38af56d5ad579924a3a/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cd3c0b6f2ca25c7d2b1c3adeecbe23e65689839ba73331ebc7d893fcda7ffe", size = 80718, upload-time = "2024-08-04T21:14:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ea/a98ebd4ed631c93b8b8f2368862cd8084d75c77a697248c24437c36a6f7e/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff3f97b3372c97782e9c6d3d7fdbe83bce8f70de719605bd7ee1839cd1ab360a", size = 88326, upload-time = "2024-08-04T21:14:03.509Z" }, + { url = "https://files.pythonhosted.org/packages/33/79/e97a9f9daac0982aa92db1199339bd393594d9a4196ad95ae088635a105f/audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a351af79edefc2a1bd2234bfd8b339935f389209943043913a919df4b0f13300", size = 80539, upload-time = "2024-08-04T21:14:04.679Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d3/1051d80e6f2d6f4773f90c07e73743a1e19fcd31af58ff4e8ef0375d3a80/audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aeb6f96f7f6da80354330470b9134d81b4cf544cdd1c549f2f45fe964d28059", size = 78577, upload-time = "2024-08-04T21:14:09.038Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/54f4c58bae8dc8c64a75071c7e98e105ddaca35449376fcb0180f6e3c9df/audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c589f06407e8340e81962575fcffbba1e92671879a221186c3d4662de9fe804e", size = 82074, upload-time = "2024-08-04T21:14:09.99Z" }, + { url = "https://files.pythonhosted.org/packages/36/89/2e78daa7cebbea57e72c0e1927413be4db675548a537cfba6a19040d52fa/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fbae5d6925d7c26e712f0beda5ed69ebb40e14212c185d129b8dfbfcc335eb48", size = 84210, upload-time = "2024-08-04T21:14:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/3ff8a74df2ec2fa6d2ae06ac86e4a27d6412dbb7d0e0d41024222744c7e0/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_i686.whl", hash = "sha256:d2d5434717f33117f29b5691fbdf142d36573d751716249a288fbb96ba26a281", size = 85664, upload-time = "2024-08-04T21:14:12.394Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/21cc4e5878f6edbc8e54be4c108d7cb9cb6202313cfe98e4ece6064580dd/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f626a01c0a186b08f7ff61431c01c055961ee28769591efa8800beadd27a2959", size = 93255, upload-time = "2024-08-04T21:14:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/3e/28/7f7418c362a899ac3b0bf13b1fde2d4ffccfdeb6a859abd26f2d142a1d58/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:05da64e73837f88ee5c6217d732d2584cf638003ac72df124740460531e95e47", size = 87760, upload-time = "2024-08-04T21:14:14.74Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/577a8be87dc7dd2ba568895045cee7d32e81d85a7e44a29000fe02c4d9d4/audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:56b7a0a4dba8e353436f31a932f3045d108a67b5943b30f85a5563f4d8488d77", size = 84992, upload-time = "2024-08-04T21:14:19.155Z" }, + { url = "https://files.pythonhosted.org/packages/ef/9a/4699b0c4fcf89936d2bfb5425f55f1a8b86dff4237cfcc104946c9cd9858/audioop_lts-0.2.1-cp313-abi3-win32.whl", hash = "sha256:6e899eb8874dc2413b11926b5fb3857ec0ab55222840e38016a6ba2ea9b7d5e3", size = 26059, upload-time = "2024-08-04T21:14:20.438Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1c/1f88e9c5dd4785a547ce5fd1eb83fff832c00cc0e15c04c1119b02582d06/audioop_lts-0.2.1-cp313-abi3-win_amd64.whl", hash = "sha256:64562c5c771fb0a8b6262829b9b4f37a7b886c01b4d3ecdbae1d629717db08b4", size = 30412, upload-time = "2024-08-04T21:14:21.342Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e9/c123fd29d89a6402ad261516f848437472ccc602abb59bba522af45e281b/audioop_lts-0.2.1-cp313-abi3-win_arm64.whl", hash = "sha256:c45317debeb64002e980077642afbd977773a25fa3dfd7ed0c84dccfc1fafcb0", size = 23578, upload-time = "2024-08-04T21:14:22.193Z" }, + { url = "https://files.pythonhosted.org/packages/7a/99/bb664a99561fd4266687e5cb8965e6ec31ba4ff7002c3fce3dc5ef2709db/audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3827e3fce6fee4d69d96a3d00cd2ab07f3c0d844cb1e44e26f719b34a5b15455", size = 46827, upload-time = "2024-08-04T21:14:23.034Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e3/f664171e867e0768ab982715e744430cf323f1282eb2e11ebfb6ee4c4551/audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:161249db9343b3c9780ca92c0be0d1ccbfecdbccac6844f3d0d44b9c4a00a17f", size = 27479, upload-time = "2024-08-04T21:14:23.922Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0d/2a79231ff54eb20e83b47e7610462ad6a2bea4e113fae5aa91c6547e7764/audioop_lts-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5b7b4ff9de7a44e0ad2618afdc2ac920b91f4a6d3509520ee65339d4acde5abf", size = 27056, upload-time = "2024-08-04T21:14:28.061Z" }, + { url = "https://files.pythonhosted.org/packages/86/46/342471398283bb0634f5a6df947806a423ba74b2e29e250c7ec0e3720e4f/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72e37f416adb43b0ced93419de0122b42753ee74e87070777b53c5d2241e7fab", size = 87802, upload-time = "2024-08-04T21:14:29.586Z" }, + { url = "https://files.pythonhosted.org/packages/56/44/7a85b08d4ed55517634ff19ddfbd0af05bf8bfd39a204e4445cd0e6f0cc9/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534ce808e6bab6adb65548723c8cbe189a3379245db89b9d555c4210b4aaa9b6", size = 95016, upload-time = "2024-08-04T21:14:30.481Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2a/45edbca97ea9ee9e6bbbdb8d25613a36e16a4d1e14ae01557392f15cc8d3/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2de9b6fb8b1cf9f03990b299a9112bfdf8b86b6987003ca9e8a6c4f56d39543", size = 87394, upload-time = "2024-08-04T21:14:31.883Z" }, + { url = "https://files.pythonhosted.org/packages/14/ae/832bcbbef2c510629593bf46739374174606e25ac7d106b08d396b74c964/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f24865991b5ed4b038add5edbf424639d1358144f4e2a3e7a84bc6ba23e35074", size = 84874, upload-time = "2024-08-04T21:14:32.751Z" }, + { url = "https://files.pythonhosted.org/packages/26/1c/8023c3490798ed2f90dfe58ec3b26d7520a243ae9c0fc751ed3c9d8dbb69/audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bdb3b7912ccd57ea53197943f1bbc67262dcf29802c4a6df79ec1c715d45a78", size = 88698, upload-time = "2024-08-04T21:14:34.147Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/5379d953d4918278b1f04a5a64b2c112bd7aae8f81021009da0dcb77173c/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:120678b208cca1158f0a12d667af592e067f7a50df9adc4dc8f6ad8d065a93fb", size = 90401, upload-time = "2024-08-04T21:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/99/6e/3c45d316705ab1aec2e69543a5b5e458d0d112a93d08994347fafef03d50/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:54cd4520fc830b23c7d223693ed3e1b4d464997dd3abc7c15dce9a1f9bd76ab2", size = 91864, upload-time = "2024-08-04T21:14:36.158Z" }, + { url = "https://files.pythonhosted.org/packages/08/58/6a371d8fed4f34debdb532c0b00942a84ebf3e7ad368e5edc26931d0e251/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:d6bd20c7a10abcb0fb3d8aaa7508c0bf3d40dfad7515c572014da4b979d3310a", size = 98796, upload-time = "2024-08-04T21:14:37.185Z" }, + { url = "https://files.pythonhosted.org/packages/ee/77/d637aa35497e0034ff846fd3330d1db26bc6fd9dd79c406e1341188b06a2/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:f0ed1ad9bd862539ea875fb339ecb18fcc4148f8d9908f4502df28f94d23491a", size = 94116, upload-time = "2024-08-04T21:14:38.145Z" }, + { url = "https://files.pythonhosted.org/packages/1a/60/7afc2abf46bbcf525a6ebc0305d85ab08dc2d1e2da72c48dbb35eee5b62c/audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e1af3ff32b8c38a7d900382646e91f2fc515fd19dea37e9392275a5cbfdbff63", size = 91520, upload-time = "2024-08-04T21:14:39.128Z" }, + { url = "https://files.pythonhosted.org/packages/65/6d/42d40da100be1afb661fd77c2b1c0dfab08af1540df57533621aea3db52a/audioop_lts-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:f51bb55122a89f7a0817d7ac2319744b4640b5b446c4c3efcea5764ea99ae509", size = 26482, upload-time = "2024-08-04T21:14:40.269Z" }, + { url = "https://files.pythonhosted.org/packages/01/09/f08494dca79f65212f5b273aecc5a2f96691bf3307cac29acfcf84300c01/audioop_lts-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f0f2f336aa2aee2bce0b0dcc32bbba9178995454c7b979cf6ce086a8801e14c7", size = 30780, upload-time = "2024-08-04T21:14:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/be73b6015511aa0173ec595fc579133b797ad532996f2998fd6b8d1bbe6b/audioop_lts-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:78bfb3703388c780edf900be66e07de5a3d4105ca8e8720c5c4d67927e0b15d0", size = 23918, upload-time = "2024-08-04T21:14:42.803Z" }, +] + +[[package]] +name = "awesomeversion" +version = "24.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/e9/1baaf8619a3d66b467ba105976897e67b36dbad93b619753768357dbd475/awesomeversion-24.6.0.tar.gz", hash = "sha256:aee7ccbaed6f8d84e0f0364080c7734a0166d77ea6ccfcc4900b38917f1efc71", size = 11997, upload-time = "2024-06-24T11:09:27.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/a5/258ffce7048e8be24c6f402bcbf5d1b3933d5d63421d000a55e74248481b/awesomeversion-24.6.0-py3-none-any.whl", hash = "sha256:6768415b8954b379a25cebf21ed4f682cab10aebf3f82a6640aaaa15ec6821f2", size = 14716, upload-time = "2024-06-24T11:09:26.133Z" }, +] + +[[package]] +name = "awesomeversion" +version = "25.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/3a/c97ef69b8209aa9d7209b143345fe49c1e20126f62a775038ab6dcd78fd5/awesomeversion-25.8.0.tar.gz", hash = "sha256:e6cd08c90292a11f30b8de401863dcde7bc66a671d8173f9066ebd15d9310453", size = 70873, upload-time = "2025-08-03T08:54:07.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/b3/c6be343010721bfdd3058b708eb4868fa1a207534a3b6c80de74d35fb568/awesomeversion-25.8.0-py3-none-any.whl", hash = "sha256:1c314683abfcc3e26c62af9e609b585bbcbf2ec19568df2f60ff1034fb1dae28", size = 15919, upload-time = "2025-08-03T08:54:06.265Z" }, +] + +[[package]] +name = "bcrypt" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/7e/d95e7d96d4828e965891af92e43b52a4cd3395dc1c1ef4ee62748d0471d0/bcrypt-4.2.0.tar.gz", hash = "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221", size = 24294, upload-time = "2024-07-22T18:09:10.445Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/81/4e8f5bc0cd947e91fb720e1737371922854da47a94bc9630454e7b2845f8/bcrypt-4.2.0-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb", size = 471568, upload-time = "2024-07-22T18:08:55.603Z" }, + { url = "https://files.pythonhosted.org/packages/05/d2/1be1e16aedec04bcf8d0156e01b987d16a2063d38e64c3f28030a3427d61/bcrypt-4.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00", size = 277372, upload-time = "2024-07-22T18:08:51.446Z" }, + { url = "https://files.pythonhosted.org/packages/e3/96/7a654027638ad9b7589effb6db77eb63eba64319dfeaf9c0f4ca953e5f76/bcrypt-4.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d", size = 273488, upload-time = "2024-07-22T18:09:02.005Z" }, + { url = "https://files.pythonhosted.org/packages/46/54/dc7b58abeb4a3d95bab653405935e27ba32f21b812d8ff38f271fb6f7f55/bcrypt-4.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291", size = 277759, upload-time = "2024-07-22T18:08:50.017Z" }, + { url = "https://files.pythonhosted.org/packages/ac/be/da233c5f11fce3f8adec05e8e532b299b64833cc962f49331cdd0e614fa9/bcrypt-4.2.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328", size = 273796, upload-time = "2024-07-22T18:09:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b8/8b4add88d55a263cf1c6b8cf66c735280954a04223fcd2880120cc767ac3/bcrypt-4.2.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7", size = 311082, upload-time = "2024-07-22T18:08:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/7b/76/2aa660679abbdc7f8ee961552e4bb6415a81b303e55e9374533f22770203/bcrypt-4.2.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399", size = 305912, upload-time = "2024-07-22T18:08:40.049Z" }, + { url = "https://files.pythonhosted.org/packages/00/03/2af7c45034aba6002d4f2b728c1a385676b4eab7d764410e34fd768009f2/bcrypt-4.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060", size = 325185, upload-time = "2024-07-22T18:08:41.833Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5d/6843443ce4ab3af40bddb6c7c085ed4a8418b3396f7a17e60e6d9888416c/bcrypt-4.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7", size = 335188, upload-time = "2024-07-22T18:08:29.25Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4c/ff8ca83d816052fba36def1d24e97d9a85739b9bbf428c0d0ecd296a07c8/bcrypt-4.2.0-cp37-abi3-win32.whl", hash = "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458", size = 156481, upload-time = "2024-07-22T18:09:00.303Z" }, + { url = "https://files.pythonhosted.org/packages/65/f1/e09626c88a56cda488810fb29d5035f1662873777ed337880856b9d204ae/bcrypt-4.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5", size = 151336, upload-time = "2024-07-22T18:08:48.473Z" }, + { url = "https://files.pythonhosted.org/packages/96/86/8c6a84daed4dd878fbab094400c9174c43d9b838ace077a2f8ee8bc3ae12/bcrypt-4.2.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841", size = 472414, upload-time = "2024-07-22T18:08:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f6/05/e394515f4e23c17662e5aeb4d1859b11dc651be01a3bd03c2e919a155901/bcrypt-4.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68", size = 277599, upload-time = "2024-07-22T18:08:53.974Z" }, + { url = "https://files.pythonhosted.org/packages/4b/3b/ad784eac415937c53da48983756105d267b91e56aa53ba8a1b2014b8d930/bcrypt-4.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe", size = 273491, upload-time = "2024-07-22T18:08:45.231Z" }, + { url = "https://files.pythonhosted.org/packages/cc/14/b9ff8e0218bee95e517b70e91130effb4511e8827ac1ab00b4e30943a3f6/bcrypt-4.2.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2", size = 277934, upload-time = "2024-07-22T18:09:09.189Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/31938bb697600a04864246acde4918c4190a938f891fd11883eaaf41327a/bcrypt-4.2.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c", size = 273804, upload-time = "2024-07-22T18:09:04.618Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c3/dae866739989e3f04ae304e1201932571708cb292a28b2f1b93283e2dcd8/bcrypt-4.2.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae", size = 311275, upload-time = "2024-07-22T18:08:43.317Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2c/019bc2c63c6125ddf0483ee7d914a405860327767d437913942b476e9c9b/bcrypt-4.2.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d", size = 306355, upload-time = "2024-07-22T18:09:06.053Z" }, + { url = "https://files.pythonhosted.org/packages/75/fe/9e137727f122bbe29771d56afbf4e0dbc85968caa8957806f86404a5bfe1/bcrypt-4.2.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e", size = 325381, upload-time = "2024-07-22T18:08:33.904Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d4/586b9c18a327561ea4cd336ff4586cca1a7aa0f5ee04e23a8a8bb9ca64f1/bcrypt-4.2.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8", size = 335685, upload-time = "2024-07-22T18:08:56.897Z" }, + { url = "https://files.pythonhosted.org/packages/24/55/1a7127faf4576138bb278b91e9c75307490178979d69c8e6e273f74b974f/bcrypt-4.2.0-cp39-abi3-win32.whl", hash = "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34", size = 155857, upload-time = "2024-07-22T18:08:30.827Z" }, + { url = "https://files.pythonhosted.org/packages/1c/2a/c74052e54162ec639266d91539cca7cbf3d1d3b8b36afbfeaee0ea6a1702/bcrypt-4.2.0-cp39-abi3-win_amd64.whl", hash = "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9", size = 151717, upload-time = "2024-07-22T18:08:52.781Z" }, +] + +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806, upload-time = "2025-09-25T19:49:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" }, + { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" }, + { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" }, + { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" }, + { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498, upload-time = "2025-09-25T19:49:24.134Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853, upload-time = "2025-09-25T19:49:25.702Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626, upload-time = "2025-09-25T19:49:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, + { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" }, + { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, +] + +[[package]] +name = "bleak" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dbus-fast", marker = "sys_platform == 'linux'" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corebluetooth", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-libdispatch", marker = "sys_platform == 'darwin'" }, + { name = "winrt-runtime", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-devices-bluetooth", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-devices-bluetooth-advertisement", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-devices-bluetooth-genericattributeprofile", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-devices-enumeration", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-devices-radios", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-foundation", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-foundation-collections", marker = "sys_platform == 'win32'" }, + { name = "winrt-windows-storage-streams", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/8a/5acbd4da6a5a301fab56ff6d6e9e6b6945e6e4a2d1d213898c21b1d3a19b/bleak-2.1.1.tar.gz", hash = "sha256:4600cc5852f2392ce886547e127623f188e689489c5946d422172adf80635cf9", size = 120634, upload-time = "2025-12-31T20:43:28.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/fe/22aec895f040c1e457d6e6fcc79286fbb17d54602600ab2a58837bec7be1/bleak-2.1.1-py3-none-any.whl", hash = "sha256:61ac1925073b580c896a92a8c404088c5e5ec9dc3c5bd6fc17554a15779d83de", size = 141258, upload-time = "2025-12-31T20:43:27.302Z" }, +] + +[[package]] +name = "bleak-retry-connector" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bleak" }, + { name = "bluetooth-adapters", marker = "sys_platform == 'linux'" }, + { name = "dbus-fast", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/e1/28c82e31f4f1c555c029598225b8d895bb3ccdc6c99b0974a1322f9725a9/bleak_retry_connector-4.5.0.tar.gz", hash = "sha256:5db81f8510c63cbea7b85d94bfa2b0fd9a24f0704474a49727a634488623fa17", size = 18648, upload-time = "2026-01-07T18:34:24.504Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/52/24ef8647f2e0dab293ec68fc58c4f0ad4e00652b631d83835cf32c06d101/bleak_retry_connector-4.5.0-py3-none-any.whl", hash = "sha256:ea63420e8f20117ef04202ea13d5215bffb736720cf865ce9a5aa556ea677afe", size = 18638, upload-time = "2026-01-07T18:34:23.119Z" }, +] + +[[package]] +name = "bluetooth-adapters" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiooui" }, + { name = "bleak" }, + { name = "dbus-fast", marker = "sys_platform == 'linux'" }, + { name = "uart-devices" }, + { name = "usb-devices" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/b37a52f5243cf8bd30cc7e25c1128750d129db15a7b2c7ef107ddb7429f9/bluetooth_adapters-2.1.1.tar.gz", hash = "sha256:f289e0f08814f74252a28862f488283680584744430d7eac45820f9c20ba041a", size = 17234, upload-time = "2025-09-12T17:18:48.906Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/11/8f344d5379df2d31eea73052128136702630f28b5fb55a8d30250c8112e2/bluetooth_adapters-2.1.1-py3-none-any.whl", hash = "sha256:1f93026e530dcb2f4515a92955fa6f85934f928b009a181ee57edc8b4affd25c", size = 20276, upload-time = "2025-09-12T17:18:47.763Z" }, +] + +[[package]] +name = "bluetooth-auto-recovery" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bluetooth-adapters" }, + { name = "btsocket" }, + { name = "pyric" }, + { name = "usb-devices" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/8b/1d6f338ced9b47965382c8f1325bf3d22be65e6f838b4e465227c52d333c/bluetooth_auto_recovery-1.5.3.tar.gz", hash = "sha256:0b36aa6be84474fff81c1ce328f016a6553272ac47050b1fa60f03e36a8db46d", size = 12798, upload-time = "2025-09-13T17:17:09.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/ab/518f14a3c3e43c34c638485cd29bfa80bd35da5a151a434f7ac3c86e1e83/bluetooth_auto_recovery-1.5.3-py3-none-any.whl", hash = "sha256:5d66b859a54ef20fdf1bd3cf6762f153e86651babe716836770da9d9c47b01c4", size = 11750, upload-time = "2025-09-13T17:17:07.681Z" }, +] + +[[package]] +name = "bluetooth-data-tools" +version = "1.28.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/90/46dfa84798ca4e5c2f66d9a756bb207ed21d89a32b8ef8d3ea89e079455f/bluetooth_data_tools-1.28.4.tar.gz", hash = "sha256:0617a879c30e0410c3506e263ee9e9bd51b06d64db13b4ad0bfd765f794b756f", size = 16488, upload-time = "2025-10-28T15:23:05.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/ed/9bb3a560ff073b5ea6f1fe9ef66d4af0fb8071cd3f569ba58769217114bb/bluetooth_data_tools-1.28.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e3184f43c52ed1e39f9ad412c586c84b4e0841f052608e6ed7ef81daf656fb64", size = 385017, upload-time = "2025-10-28T15:35:58.534Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cb/8237718607f1daeec1f4aefb3dfdd7b7b56bc1422fdc4e5f9ef991e3a9b2/bluetooth_data_tools-1.28.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1c5e524df9afae40142c3a3dcf128983df99e73158a2bc98f1709024ff185a22", size = 386609, upload-time = "2025-10-28T15:35:59.73Z" }, + { url = "https://files.pythonhosted.org/packages/35/8e/e6136f790c68261610160c0b8dfadd874d38bff8e3da0feb4bf1428b89fd/bluetooth_data_tools-1.28.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6bf7eb8b41995466af3401db3387726afda42487b291b94ab90e7d26aadb72ac", size = 414363, upload-time = "2025-10-28T15:36:00.975Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c9/af729a472e5e9274480b34c8218fda915b3d7add9247d766dee79143eebf/bluetooth_data_tools-1.28.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b3a1e9838f147d6e80b5d9cf7e33c9e736f1f1bda9db00b4ea5ed45fd57d2e8", size = 132276, upload-time = "2025-10-28T15:36:02.622Z" }, + { url = "https://files.pythonhosted.org/packages/70/eb/111c66cc73fd4ec29071641ba6f8b68db033f3d6b9611aa332565c0e3286/bluetooth_data_tools-1.28.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8688f54fd344f17f0c04bca6c2b4351c9fcb211d16becada60f5656305c04238", size = 414494, upload-time = "2025-10-28T15:36:03.709Z" }, + { url = "https://files.pythonhosted.org/packages/dc/72/0cb024304121380d374c62cf119647b77c88be3bea435291a71d98e956d4/bluetooth_data_tools-1.28.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8f4233a9d8983ede1d4b319783266b5ae89dbd0f8ac48dcc9b0c2a1d6a60a0ca", size = 414311, upload-time = "2025-10-28T15:36:04.921Z" }, + { url = "https://files.pythonhosted.org/packages/53/00/e11af70293608c36507f14bf893f20b072d753ab1c929dc8103209cf9555/bluetooth_data_tools-1.28.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:df2948eae3bd32242322d7f1a7f0d74e2d2f79e5e3254c7e06ec2ddbcacabe7b", size = 135039, upload-time = "2025-10-28T15:36:06.483Z" }, + { url = "https://files.pythonhosted.org/packages/a4/24/47344c86c8abef13a7b39240c3ee5789e0c5fde16ea65c97f3a9a669c121/bluetooth_data_tools-1.28.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e98f7bcd491711f5be161a0400721c9ecb782308f0eeb030f3bd450450f53d0", size = 416584, upload-time = "2025-10-28T15:36:07.925Z" }, + { url = "https://files.pythonhosted.org/packages/b8/26/45358ddee23e4eed23c3e39959cb070351bcdce6698818334e029af7ab8f/bluetooth_data_tools-1.28.4-cp312-cp312-win32.whl", hash = "sha256:324fc45aad6e9a3115a5612959460eb82156579f5925009cb482427a0931207b", size = 287103, upload-time = "2025-10-28T15:36:09.101Z" }, + { url = "https://files.pythonhosted.org/packages/9e/bc/171fabeff862a94f9926eebd5f9722cdfcd2b7134e2162c471d115cd31cf/bluetooth_data_tools-1.28.4-cp312-cp312-win_amd64.whl", hash = "sha256:c9192bcca07a926599a8221e6354a3ef628a8ecb3d904e437ea216a3aafcbcc7", size = 287106, upload-time = "2025-10-28T15:36:10.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/cd/fa868c3bed326976813c04bd89e833cb0032a6a18ffc03f843947caa29d3/bluetooth_data_tools-1.28.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ade5a22f394cee6b428474f5c23f8ce086ebc618b30fa478fc53703b5dc1bf09", size = 383602, upload-time = "2025-10-28T15:36:11.963Z" }, + { url = "https://files.pythonhosted.org/packages/d3/37/ef120dcce334ba8e3d97c06c9d46ab1db3b7474fad1fb867097b7c0a9355/bluetooth_data_tools-1.28.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ea8569f42699e94e18a1be32e45c737f2795c7509f09fa27dd5d342a7855473c", size = 385073, upload-time = "2025-10-28T15:36:13.522Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b5/1ce2f4d2ce6a04a6a1be490cd2b975777fb76f5230818cefe24b7ed7ba9d/bluetooth_data_tools-1.28.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dfaecb4269bc4830a7bd6f823e8a0a4c368d9135ee6805e6db5eecf1211a2e4", size = 412028, upload-time = "2025-10-28T15:36:14.709Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/f525cc4d4da3555f820a6ce79a3877424ba73f69f4d44a4389b19f7aaf15/bluetooth_data_tools-1.28.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ff3d43804f3510bd11a267268c567b7fe5653b10243be48527ac01d8e15b3faa", size = 130572, upload-time = "2025-10-28T15:36:16.184Z" }, + { url = "https://files.pythonhosted.org/packages/6d/01/2c4b89de730e71c94f3552948aa8adf0a0b5a4dc21e642805bc8e014f41d/bluetooth_data_tools-1.28.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e99be62bdcd2b94778eb230c6d73f4da4ad1493ccc33c09efc8432c5a242c071", size = 412805, upload-time = "2025-10-28T15:36:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/6ca04f0225b51ef27e79f369e9b9fff4bf104025a4e51d6fb2d943c38645/bluetooth_data_tools-1.28.4-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:f85fbc0c540c3e64b5fc925f6b60d8c96d521548c7bfa3b1e8998ea4e5a59054", size = 140133, upload-time = "2025-10-28T15:23:03.49Z" }, + { url = "https://files.pythonhosted.org/packages/c8/3e/675f9037c7b23df43229d95e8627fe10759f8c3c4a1ef6919b8d1683d4df/bluetooth_data_tools-1.28.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c9dd29f39bddbcfa1dcfca13dcfd2a1111d5a0fbba708a8c98feb98bca10b7a", size = 411910, upload-time = "2025-10-28T15:36:19.207Z" }, + { url = "https://files.pythonhosted.org/packages/a1/12/4f2086f879c0595e065e62dd1bfbe8d371336308654e466ca10b6cf61d86/bluetooth_data_tools-1.28.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8bbcd287a1d5b249639fc1ba99c7ab8f0d7257d43104cf349fab2c747b84b3cd", size = 132814, upload-time = "2025-10-28T15:36:20.789Z" }, + { url = "https://files.pythonhosted.org/packages/e2/72/56a3b3a15cd6c601c3c22cf8c58db788ea59e669db1af123a4113983302b/bluetooth_data_tools-1.28.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7decde5838ccccf71ec626c3f0421a6265054cb1e5ced121bb6448434a0bb72f", size = 414926, upload-time = "2025-10-28T15:36:21.897Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7f/280e0fb57c8569ee04057231e17bc5b47fb037470d62af72b9e7d908fdd2/bluetooth_data_tools-1.28.4-cp313-cp313-win32.whl", hash = "sha256:7d4d65ee4cb3c0616d411f2352b9da8c97f789a42fe9c14a68b6d4b458d62d9a", size = 287105, upload-time = "2025-10-28T15:36:23.089Z" }, + { url = "https://files.pythonhosted.org/packages/79/a2/bcccc7fcaabd74a717663dbe4f4909c4f37edef6c95bfcde7b2548b04ec1/bluetooth_data_tools-1.28.4-cp313-cp313-win_amd64.whl", hash = "sha256:5f3bb83e8755d0ce2e3d62e70a35b73c569ddc63d7200658740e311042c60777", size = 287107, upload-time = "2025-10-28T15:36:24.252Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba322c36376532f3133c7d56bc80dd2859df9c78aa52de19ed7627b9fb/bluetooth_data_tools-1.28.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:81c6c2b7c844d30a0fd1527e38e47cdb0f350c0297fb11516bfa255b37241fbf", size = 383677, upload-time = "2025-10-28T15:36:33.905Z" }, + { url = "https://files.pythonhosted.org/packages/a4/2e/74e7b4857ba10a524cd00177fbd78764c50810fb523020b7d5cbf0fdbac8/bluetooth_data_tools-1.28.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:99896987f48d762694cdea7a8a7091031cdf40dc65e8e934a7422746264865ba", size = 385890, upload-time = "2025-10-28T15:36:35.196Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/35bc257ed1935e55ac7bfb56172a290f094f8b982f65f68aadb0f03ceab5/bluetooth_data_tools-1.28.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebac9d60786bd7c403f472fcda871cb74d0aef0d4e713715af2e5e095d15a625", size = 412966, upload-time = "2025-10-28T15:36:36.398Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2c/2ed3dff30e85029e631a211d93e11aab7dc4a899d9c96a15eca18541e66e/bluetooth_data_tools-1.28.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:06a2750e49fed2310ddd7b51388b891cbd4457ee7392f3a17c387591cbb74ace", size = 129887, upload-time = "2025-10-28T15:36:38.429Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ff/824f3b34b0fab4e57efd457ea8b9bdf41d279a44eb19cfde5ede159d90b3/bluetooth_data_tools-1.28.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5dccfe237237463c3d74fa425aaf8a9d78b26a5177e6777b10039699313a335", size = 412909, upload-time = "2025-10-28T15:36:39.552Z" }, + { url = "https://files.pythonhosted.org/packages/eb/88/f2217b88c32b470e5f9dc9fbce38f24b9548c0776be7c5e0db1249c42ae9/bluetooth_data_tools-1.28.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4a071d7af2614af9a00f65063adaacda94f4357cc2dfedda7057c005f437dacd", size = 413005, upload-time = "2025-10-28T15:36:41.572Z" }, + { url = "https://files.pythonhosted.org/packages/6d/da/cde7557972e50cbb8a92291cc34e5de07f0e2bbc28a388151e738e9efe84/bluetooth_data_tools-1.28.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:bd84c4f2d24103ff43044ccd3cf8c0e05ee285bd6f9eddc9772b2069cfb6c271", size = 131426, upload-time = "2025-10-28T15:36:42.645Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7f/925fd28e2695ba810b1f7f02f2d5ab8635a11d6e415ac4039446145f9e48/bluetooth_data_tools-1.28.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e3895dbbdad2a39de5a7b36a4ddb5e2f8ad38029628e3eddfde31a5c56d81b5", size = 414955, upload-time = "2025-10-28T15:36:43.775Z" }, + { url = "https://files.pythonhosted.org/packages/03/b1/cbf3a2c8404862605e487200d45aefb130c0c0ce3df219230155eeb95199/bluetooth_data_tools-1.28.4-cp314-cp314-win32.whl", hash = "sha256:1d9b22827144329e3ca1348b8473fe6b48127707a81539848232847c4cb08e1d", size = 286157, upload-time = "2025-10-28T15:36:45.171Z" }, + { url = "https://files.pythonhosted.org/packages/c7/68/eb168b986eebc0c98fb0a6a521719a33d218bafc46c48c5279322d15e9b2/bluetooth_data_tools-1.28.4-cp314-cp314-win_amd64.whl", hash = "sha256:04c91b6f2dfaa419652356488fa50dfb0f54cb20b1f90f9e5e1d6911430d9688", size = 286151, upload-time = "2025-10-28T15:36:46.414Z" }, + { url = "https://files.pythonhosted.org/packages/57/37/f2ce46cf82b32d6a62171753a2d6550d633af5b27f0ad2c2ff5fef1980a4/bluetooth_data_tools-1.28.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a44c48bf163606a2915d12ffb3ac1b022548e566c062907f98266e8a19c6173c", size = 488264, upload-time = "2025-10-28T15:36:47.582Z" }, + { url = "https://files.pythonhosted.org/packages/ba/32/c3bbee5b7c66190f0729e71fefe44adb49e7bb94407b110d972d817561a2/bluetooth_data_tools-1.28.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b76a6c8c6d610844c8712cecf207c16373cad3361fb29e6dbcdcb12f2700bcb9", size = 492846, upload-time = "2025-10-28T15:36:48.846Z" }, + { url = "https://files.pythonhosted.org/packages/71/5c/751028e7fab907c0c2fc7749f088d19bf2b938e5cdd7d0e68ddbcacb7b79/bluetooth_data_tools-1.28.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61b827616075ecee12c374b04b14d81575403849435bf915c9a3812138f046b7", size = 548041, upload-time = "2025-10-28T15:36:50.066Z" }, + { url = "https://files.pythonhosted.org/packages/77/02/4d8f4a9cb2a2beaaedda71fb3017f6bb5eb3de08656adfb9a8a773ec7912/bluetooth_data_tools-1.28.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:525646baaf5f741ea071aa4babd8313e4e9bae75b46757c4b0f6aeadfa71b52a", size = 517778, upload-time = "2025-10-28T15:36:51.628Z" }, + { url = "https://files.pythonhosted.org/packages/89/9b/90d65fed47b531b0f0f4c8be012d35c97950c97fb7b74501bfe938c7f7ca/bluetooth_data_tools-1.28.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c06b66ef406c68a95052a87640fa34d402d31120a8b0b62f99080169621697a", size = 546643, upload-time = "2025-10-28T15:36:52.971Z" }, + { url = "https://files.pythonhosted.org/packages/d3/6b/c15363ccfc208a34cd6d627610350c72633e2a6764d37d04a1340fb13844/bluetooth_data_tools-1.28.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:152232c157f2f6d8265c0141e56423bbedd9e84044fb815e69d786a73fb195c7", size = 548872, upload-time = "2025-10-28T15:36:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/85/2a/b649eeea14e6330da34f42dc1407424cd929af3ae1298b5651459d0c4bb8/bluetooth_data_tools-1.28.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:243163028565955e73f19c0c462b619fd0f56e31875c30f5f3af2a48b43adb67", size = 524783, upload-time = "2025-10-28T15:36:55.815Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6e/96c762f8a49f65348748d72c515c5a79c9179c685d3e02694c380bdafa72/bluetooth_data_tools-1.28.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0a1608bca00e24b6ca3b98ed7d797a03988a44285d74286e045446c8161a62ea", size = 551318, upload-time = "2025-10-28T15:36:57.062Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7d/796cbb679d19425ff381ebbe7a5238217b3f3e5c65b9a46e7be57ba105fc/bluetooth_data_tools-1.28.4-cp314-cp314t-win32.whl", hash = "sha256:25918d7ece36f29ebde21aaf70f3c1e1c63501206dd1c7713bbd8911d43d0dce", size = 286158, upload-time = "2025-10-28T15:36:58.717Z" }, + { url = "https://files.pythonhosted.org/packages/c3/74/639329ba05947018ba928162042dfb162a31b85757e27591bb6aa96c1f42/bluetooth_data_tools-1.28.4-cp314-cp314t-win_amd64.whl", hash = "sha256:276528d7ea2419ccab14ddf044ee7f65a5b6bc35c49264625560ad0c184dc67a", size = 286163, upload-time = "2025-10-28T15:36:59.861Z" }, +] + +[[package]] +name = "boto3" +version = "1.42.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/ad/06f48f2d0e9ec91d136602c7009f5f68c84be3655cc6e7e2b59aff82ead4/boto3-1.42.26.tar.gz", hash = "sha256:0fbcf1922e62d180f3644bc1139425821b38d93c1e6ec27409325d2ae86131aa", size = 112877, upload-time = "2026-01-12T20:36:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/0c/094a63b0ab893995b1f2e7ddb5425e11f97403feb90cea0eb770c8905487/boto3-1.42.26-py3-none-any.whl", hash = "sha256:f116cfbe7408e0a9153da363f134d2f1b5008f17ee86af104f0ce59a62be1833", size = 140576, upload-time = "2026-01-12T20:36:38.244Z" }, +] + +[[package]] +name = "botocore" +version = "1.42.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13.2'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/c9/6ce745d4233aeb3abdb18205739b394f7955087f7603cb324a797adbf8d2/botocore-1.42.26.tar.gz", hash = "sha256:1c8855e3e811f015d930ccfe8751d4be295aae0562133d14b6f0b247cd6fd8d3", size = 14882582, upload-time = "2026-01-12T20:36:29.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/43/5993eab2114c0de7bbc21985b745aafe3b912f98fc63726c2a54680bb69d/botocore-1.42.26-py3-none-any.whl", hash = "sha256:71171c2d09ac07739f4efce398b15a4a8bc8769c17fb3bc99625e43ed11ad8b7", size = 14554661, upload-time = "2026-01-12T20:36:26.891Z" }, +] + +[[package]] +name = "btsocket" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/b1/0ae262ecf936f5d2472ff7387087ca674e3b88d8c76b3e0e55fbc0c6e956/btsocket-0.3.0.tar.gz", hash = "sha256:7ea495de0ff883f0d9f8eea59c72ca7fed492994df668fe476b84d814a147a0d", size = 19563, upload-time = "2024-06-10T07:05:27.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/2b/9bf3481131a24cb29350d69469448349362f6102bed9ae4a0a5bb228d731/btsocket-0.3.0-py2.py3-none-any.whl", hash = "sha256:949821c1b580a88e73804ad610f5173d6ae258e7b4e389da4f94d614344f1a9c", size = 14807, upload-time = "2024-06-10T07:05:26.381Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "chevron" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/1f/ca74b65b19798895d63a6e92874162f44233467c9e7c1ed8afd19016ebe9/chevron-0.14.0.tar.gz", hash = "sha256:87613aafdf6d77b6a90ff073165a61ae5086e21ad49057aa0e53681601800ebf", size = 11440, upload-time = "2021-01-02T22:47:59.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/93/342cc62a70ab727e093ed98e02a725d85b746345f05d2b5e5034649f4ec8/chevron-0.14.0-py3-none-any.whl", hash = "sha256:fbf996a709f8da2e745ef763f482ce2d311aa817d287593a5b990d6d6e4f0443", size = 11595, upload-time = "2021-01-02T22:47:57.847Z" }, +] + +[[package]] +name = "ciso8601" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/bc/cf42c1b0042f91c90a6b00244f63b6fb137af15e43e29f07bb72cf955be8/ciso8601-2.3.1.tar.gz", hash = "sha256:3212c7ffe5d8080270548b5f2692ffd2039683b6628a8d2ad456122cc5793c4c", size = 31225, upload-time = "2023-10-30T19:54:34.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/3d/f6496a260ba6e58135fb3dd1108799f6dd9cadf634372e020bfbf0d27fea/ciso8601-2.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f39bb5936debf21c52e5d52b89f26857c303da80c43a72883946096a6ef5e561", size = 24211, upload-time = "2023-10-30T19:53:42.011Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a9/24ffa848a5878a50009d6177826c36b60e2e8807e4d54ee94817e790897b/ciso8601-2.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:21cf83ca945bb26ecd95364ae2c9ed0276378e5fe35ce1b64d4c6d5b33038ea3", size = 15591, upload-time = "2023-10-30T19:53:43.317Z" }, + { url = "https://files.pythonhosted.org/packages/ea/74/77fd6e67a2a3489a1ac449570142e2f4137289be25027e235d4688470d56/ciso8601-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:013410263cba46748d2de29e9894341ae41223356cde7970478c32bd0984d10c", size = 15643, upload-time = "2023-10-30T19:53:44.806Z" }, + { url = "https://files.pythonhosted.org/packages/5b/bd/fef5524974e3ba376e16df35e4197152edffdf7ac9d5d99bc173a9fcf256/ciso8601-2.3.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b26935687ef1837b56997d8c61f1d789e698be58b261410e629eda9c89812141", size = 39805, upload-time = "2023-10-30T19:53:46.036Z" }, + { url = "https://files.pythonhosted.org/packages/50/1c/d1cb5b2d2173abfc4d5a068981acddff763cce318e896ec87c140412c72d/ciso8601-2.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0d980a2a88030d4d8b2434623c250866a75b4979d289eba69bec445c51ace99f", size = 48335, upload-time = "2023-10-30T19:53:47.275Z" }, + { url = "https://files.pythonhosted.org/packages/76/ef/5ef06ccb6f6c023573634119f93df237687e3f4263e2f307b51a7208d103/ciso8601-2.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:87721de54e008fb1c4c3978553b05a9c417aa25b76ddf5702d6f7e8d9b109288", size = 49449, upload-time = "2023-10-30T19:53:48.646Z" }, + { url = "https://files.pythonhosted.org/packages/e6/45/f981bbd51f1c9fa3d730c3fbcb74b1f8928463e6ea8283257f6b164847e6/ciso8601-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:9f107a4c051e7c0416824279264d94f4ed3da0fbd82bd96ec3c3293426826de4", size = 17084, upload-time = "2023-10-30T19:53:49.745Z" }, +] + +[[package]] +name = "ciso8601" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/09/e9/d83711081c997540aee59ad2f49d81f01d33e8551d766b0ebde346f605af/ciso8601-2.3.2.tar.gz", hash = "sha256:ec1616969aa46c51310b196022e5d3926f8d3fa52b80ec17f6b4133623bd5434", size = 28214, upload-time = "2024-12-09T12:26:40.768Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/fc/e852e664bb90bf1112e17778512d6cbc5fa5f49b7c22969e4ee131f13d06/ciso8601-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:75870a1e496a17e9e8d2ac90125600e1bafe51679d2836b2f6cb66908fef7ad6", size = 15755, upload-time = "2024-12-09T12:26:07.259Z" }, + { url = "https://files.pythonhosted.org/packages/22/da/c82e665c627836be4d7d0a8ed38518f9833124a6fd85735881cac72427b8/ciso8601-2.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:c117c415c43aa3db68ee16a2446cb85c5e88459650421d773f6f6444ce5e5819", size = 24291, upload-time = "2024-12-09T12:26:08.23Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6a/822b178b6c473533e5023aab6447b05d1683f95c3210eda5680f9262c93c/ciso8601-2.3.2-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:ce5f76297b6138dc5c085d4c5a0a631afded99f250233fe583dc365f67fe8a8d", size = 15713, upload-time = "2024-12-09T12:26:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/de/c3/63b89c7ec2a4f9bcbdeb3401485992d13eeb4da943accef58f0820c62552/ciso8601-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e3205e4cfd63100f454ea67100c7c6123af32da0022bdc6e81058e95476a8ad", size = 40253, upload-time = "2024-12-09T12:26:10.149Z" }, + { url = "https://files.pythonhosted.org/packages/96/01/b12f356afaa6dfc339c4b964f01c7b78f7d844dfe087cbbc9c68a5f048c0/ciso8601-2.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5308a14ac72898f91332ccfded2f18a6c558ccd184ccff84c4fb36c7e4c2a0e6", size = 40087, upload-time = "2024-12-09T12:26:11.066Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/de5f920ebf5cdb2ef28237bdb48ac9ea980d794e16f1fbedffc430064208/ciso8601-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e825cb5ecd232775a94ef3c456ab19752ee8e66eaeb20562ea45472eaa8614ec", size = 40908, upload-time = "2024-12-09T12:26:12.092Z" }, + { url = "https://files.pythonhosted.org/packages/b2/3c/cd79c9305480cc9bf8dce286bd7ec2035a3d140b3f3ae0b1232087a65240/ciso8601-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7a8f96f91bdeabee7ebca2c6e48185bea45e195f406ff748c87a3c9ecefb25cc", size = 40881, upload-time = "2024-12-09T12:26:12.997Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f7/f1b64a6dac1ff824ad6eee9c2b540fbe411f288b40218a06644fa2e4f075/ciso8601-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:3fe497819e50a245253a3b2d62ec4c68f8cf337d79dc18e2f3b0a74d24dc5e93", size = 17278, upload-time = "2024-12-09T12:26:14.027Z" }, +] + +[[package]] +name = "ciso8601" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/8a/075724aea06c98626109bfd670c27c248c87b9ba33e637f069bf46e8c4c3/ciso8601-2.3.3.tar.gz", hash = "sha256:db5d78d9fb0de8686fbad1c1c2d168ed52efb6e8bf8774ae26226e5034a46dae", size = 31909, upload-time = "2025-08-20T16:31:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/aa/b723a6981cfc42bbe992da23179f5dd1556e9054067985108ec6cbe34dd3/ciso8601-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e7ef14610446211c4102bf6c67f32619ab341e56db15bad6884385b43c12b064", size = 16111, upload-time = "2025-08-20T16:30:36.781Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e9/e547ec4dd75f28d8d217488130fa07767bc42fd643d61a18870487133c0e/ciso8601-2.3.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:523901aec6b0ccdf255c863ef161f476197f177c5cd33f2fbb35955c5f97fdb4", size = 24193, upload-time = "2025-08-20T16:30:38.067Z" }, + { url = "https://files.pythonhosted.org/packages/14/c8/801b78e30667cb31b4524e9dc26cbc2c03c012f9aa3f5ae21676461dc622/ciso8601-2.3.3-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:45f8254d1fb0a41e20f98e93075db7b56504adddf65e4c8b397671feba4861ca", size = 15917, upload-time = "2025-08-20T16:30:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/44/6b/dfc56a2a4e572a2a3f8c88a66dea6a9186a8e10da7c36cc84abc31bf795c/ciso8601-2.3.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:202ca99077577683e6a84d394ff2677ec19d9f406fbf35734f68be85d2bcd3f1", size = 41324, upload-time = "2025-08-20T16:30:40.321Z" }, + { url = "https://files.pythonhosted.org/packages/7c/57/cf66171cb5807fe345b03ce9e32fd91b3a8b6e5bd95710618a9a1b0f3fab/ciso8601-2.3.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7cec4e31c363e87221f2561e7083ce055a82de041e822e7c3775f8ce6250a7e", size = 41804, upload-time = "2025-08-20T16:30:41.204Z" }, + { url = "https://files.pythonhosted.org/packages/75/91/15e8871d7ae2ff0f756128e246348bdede58c08edba13cd886450ceeb304/ciso8601-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:389fef3ccc3065fa21cb6ef7d03aee63ab980591b5d87b9f0bbe349f52b16bdc", size = 41209, upload-time = "2025-08-20T16:30:42.46Z" }, + { url = "https://files.pythonhosted.org/packages/30/54/7563e20a158a4bdf3e8d13c63e02b71f9b73c662edc83cb4d5ab67171a7d/ciso8601-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4499cfbe4da092dea95ab81aefc78b98e2d7464518e6e80107cf2b9b1f65fa2", size = 41368, upload-time = "2025-08-20T16:30:43.397Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d5/6182006dd86365bb21d1f658f70c41e266ce0f97eaf353f9d7069c51851f/ciso8601-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:1df1ca3791c6f2d543f091d88e728a60a31681ff900d9eb02f1403cf31e9c177", size = 17566, upload-time = "2025-08-20T16:30:44.706Z" }, + { url = "https://files.pythonhosted.org/packages/01/16/88154fe8247e4dcfdbaed8c6b8ccf32b1dd4389c6c95b1986bf31649eb00/ciso8601-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8afa073802c926c3244e1e5fcc5818afd3acb90fb7826a90f91ddbda0636ea70", size = 16109, upload-time = "2025-08-20T16:30:45.655Z" }, + { url = "https://files.pythonhosted.org/packages/be/46/8d46372b3802c7201c20c8b316569f27253aaafba0cdd2cd033985e8b77e/ciso8601-2.3.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:8a04e518b4adf8e35e030feaecdb4a835d39b9bb44d207e926aea8ce3447ad7c", size = 24189, upload-time = "2025-08-20T16:30:46.958Z" }, + { url = "https://files.pythonhosted.org/packages/13/80/1890e097cb76e41995de82f29c0289ca590d7135e0be3707e5b78f54350d/ciso8601-2.3.3-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:f79ad8372463ba4265981016d1648bc05f4922bc8044c4243fcbaef7a12ee9f7", size = 15925, upload-time = "2025-08-20T16:30:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e9/690a2a6beefd9d982c20adde3f09ff54a23291a699b0df7cf0c59027d9cf/ciso8601-2.3.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d5894a33f119b5ac1082df187dc58c74fe13c9c092e19ba36495c2b7cee3540b", size = 41352, upload-time = "2025-08-20T16:30:49.294Z" }, + { url = "https://files.pythonhosted.org/packages/2f/34/9a498ceb0ebd23f538e6685721c9fc4666701372c651874ed22ec46b1423/ciso8601-2.3.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09deebf3e326ec59d80019b4ad35175c90b99cde789c644b1496811fe3340587", size = 41866, upload-time = "2025-08-20T16:30:50.262Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0a/ee0981502aa1c9f28f7e89cf6cee08bdff2c6ed9d4289b00cceb8a1c500e/ciso8601-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3aa43ed59b2117baccc5bb760e5e53dad77cacba671d757c1e82e0a367b1f42a", size = 41271, upload-time = "2025-08-20T16:30:51.198Z" }, + { url = "https://files.pythonhosted.org/packages/fb/65/24a888240324188d8350bc24fb58a6d759c0ca43adfa77210f3d60370b56/ciso8601-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:289515aa3a3b86a9c3450bf482f634138b98788332d136751507bfdfe46e6031", size = 41411, upload-time = "2025-08-20T16:30:52.439Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1f/febc9de191acb461e02e616e5366bc2b7757277a11b4bf215d4fb79516a8/ciso8601-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:e7288068a5bffbcc50cbe9cdaf3971f541fcd209c194fa6a59ad06066a3dcff0", size = 17573, upload-time = "2025-08-20T16:30:53.759Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3a/54ad0ae2257870076b4990545a8f16221470fecea0aa7a4e1f39506db8c5/ciso8601-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82db4047d74d8b1d129e7a8da578518729912c3bd19cb71541b147e41f426381", size = 16115, upload-time = "2025-08-20T16:30:54.971Z" }, + { url = "https://files.pythonhosted.org/packages/23/fb/9fe767d44520691e2b706769466852fbdeb44a82dc294c2766bce1049d22/ciso8601-2.3.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a553f3fc03a2ed5ca6f5716de0b314fa166461df01b45d8b36043ccac3a5e79f", size = 24214, upload-time = "2025-08-20T16:30:56.359Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ac/984fd3948f372c46c436a2b48da43f4fb7bc6f156a6f4bc858adaab79d42/ciso8601-2.3.3-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:ff59c26083b7bef6df4f0d96e4b649b484806d3d7bcc2de14ad43147c3aafb04", size = 15929, upload-time = "2025-08-20T16:30:58.352Z" }, + { url = "https://files.pythonhosted.org/packages/de/3a/5572917d4e0bec2c1ef0eda8652f9dc8d1850d29d3eef9e5e82ffe5d6791/ciso8601-2.3.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99a1fa5a730790431d0bfcd1f3a6387f60cddc6853d8dcc5c2e140cd4d67a928", size = 41578, upload-time = "2025-08-20T16:30:59.351Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cf/07321ce5cf099b98de0c02cd4bab4818610da69743003e94c8fb6e8a59cb/ciso8601-2.3.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c35265c1b0bd2ac30ed29b49818dd38b0d1dfda43086af605d8b91722727dec0", size = 42085, upload-time = "2025-08-20T16:31:00.338Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c7/3c521d6779ee433d9596eb3fcded79549bbe371843f25e62006c04f74dc9/ciso8601-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aa9df2f84ab25454f14df92b2dd4f9aae03dbfa581565a716b3e89b8e2110c03", size = 41313, upload-time = "2025-08-20T16:31:01.313Z" }, + { url = "https://files.pythonhosted.org/packages/f9/93/efd40db0d6b512be1cbe4e7e750882c2e88f580e17f35b3e9cc9c23004b5/ciso8601-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:32e06a35eb251cfc4bbe01a858c598da0a160e4ad7f42ff52477157ceaf48061", size = 41443, upload-time = "2025-08-20T16:31:02.357Z" }, + { url = "https://files.pythonhosted.org/packages/21/8e/515f9404faa39af8df5e2b899cafbca5dbe7cd2ffe5cc124ef393ffdaf1c/ciso8601-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:7657ba9730dc1340d73b9e61eca14f341c41dd308128c808b8b084d2b85bc03e", size = 17977, upload-time = "2025-08-20T16:31:03.429Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cronsim" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/d8/cfb8d51a51f6076ffa09902c02978c7db9764cca78f4ee832e691d20f44b/cronsim-2.6.tar.gz", hash = "sha256:5aab98716ef90ab5ac6be294b2c3965dbf76dc869f048846a0af74ebb506c10d", size = 20315, upload-time = "2024-11-02T14:34:02.475Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/dd/9c40c4e0f4d3cb6cf52eb335e9cc1fa140c1f3a87146fb6987f465b069da/cronsim-2.6-py3-none-any.whl", hash = "sha256:5e153ff8ed64da7ee8d5caac470dbeda8024ab052c3010b1be149772b4801835", size = 13500, upload-time = "2024-12-04T12:53:57.443Z" }, +] + +[[package]] +name = "cronsim" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/1a/02f105147f7f2e06ed4f734ff5a6439590bb275a53dd91fc73df6312298a/cronsim-2.7-py3-none-any.whl", hash = "sha256:1e1431fa08c51dc7f72e67e571c7c7a09af26420169b607badd4ca9677ffad1e", size = 14213, upload-time = "2025-10-21T16:38:20.431Z" }, +] + +[[package]] +name = "cryptography" +version = "43.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "cffi", marker = "python_full_version < '3.13' and platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ba/0664727028b37e249e73879348cc46d45c5c1a2a2e81e8166462953c5755/cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d", size = 686927, upload-time = "2024-09-03T20:04:20.788Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/28/b92c98a04ba762f8cdeb54eba5c4c84e63cac037a7c5e70117d337b15ad6/cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d", size = 6223222, upload-time = "2024-09-03T20:04:14.466Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/1193774705783ba364121aa2a60132fa31a668b8ababd5edfa1662354ccd/cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062", size = 3794751, upload-time = "2024-09-03T20:04:16.725Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4b/39bb3c4c8cfb3e94e736b8d8859ce5c81536e91a1033b1d26770c4249000/cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962", size = 3981827, upload-time = "2024-09-03T20:03:55.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/dc/1471d4d56608e1013237af334b8a4c35d53895694fbb73882d1c4fd3f55e/cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277", size = 3780034, upload-time = "2024-09-03T20:03:58.972Z" }, + { url = "https://files.pythonhosted.org/packages/ad/43/7a9920135b0d5437cc2f8f529fa757431eb6a7736ddfadfdee1cc5890800/cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", size = 3993407, upload-time = "2024-09-03T20:03:36.682Z" }, + { url = "https://files.pythonhosted.org/packages/cc/42/9ab8467af6c0b76f3d9b8f01d1cf25b9c9f3f2151f4acfab888d21c55a72/cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042", size = 3886457, upload-time = "2024-09-03T20:03:52.995Z" }, + { url = "https://files.pythonhosted.org/packages/a4/65/430509e31700286ec02868a2457d2111d03ccefc20349d24e58d171ae0a7/cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494", size = 4081499, upload-time = "2024-09-03T20:03:32.522Z" }, + { url = "https://files.pythonhosted.org/packages/bb/18/a04b6467e6e09df8c73b91dcee8878f4a438a43a3603dc3cd6f8003b92d8/cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2", size = 2616504, upload-time = "2024-09-03T20:04:09.459Z" }, + { url = "https://files.pythonhosted.org/packages/cc/73/0eacbdc437202edcbdc07f3576ed8fb8b0ab79d27bf2c5d822d758a72faa/cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d", size = 3067456, upload-time = "2024-09-03T20:03:40.775Z" }, + { url = "https://files.pythonhosted.org/packages/8a/b6/bc54b371f02cffd35ff8dc6baba88304d7cf8e83632566b4b42e00383e03/cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d", size = 6225263, upload-time = "2024-09-03T20:03:43.181Z" }, + { url = "https://files.pythonhosted.org/packages/00/0e/8217e348a1fa417ec4c78cd3cdf24154f5e76fd7597343a35bd403650dfd/cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806", size = 3794368, upload-time = "2024-09-03T20:03:18.051Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ed/38b6be7254d8f7251fde8054af597ee8afa14f911da67a9410a45f602fc3/cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85", size = 3981750, upload-time = "2024-09-03T20:04:18.775Z" }, + { url = "https://files.pythonhosted.org/packages/64/f3/b7946c3887cf7436f002f4cbb1e6aec77b8d299b86be48eeadfefb937c4b/cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c", size = 3778925, upload-time = "2024-09-03T20:03:45.022Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7e/ebda4dd4ae098a0990753efbb4b50954f1d03003846b943ea85070782da7/cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1", size = 3993152, upload-time = "2024-09-03T20:03:30.108Z" }, + { url = "https://files.pythonhosted.org/packages/43/f6/feebbd78a3e341e3913846a3bb2c29d0b09b1b3af1573c6baabc2533e147/cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa", size = 3886392, upload-time = "2024-09-03T20:03:34.543Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4c/ab0b9407d5247576290b4fd8abd06b7f51bd414f04eef0f2800675512d61/cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4", size = 4082606, upload-time = "2024-09-03T20:03:27.836Z" }, + { url = "https://files.pythonhosted.org/packages/05/36/e532a671998d6fcfdb9122da16434347a58a6bae9465e527e450e0bc60a5/cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47", size = 2617948, upload-time = "2024-09-03T20:03:25.446Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c6/c09cee6968add5ff868525c3815e5dccc0e3c6e89eec58dc9135d3c40e88/cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb", size = 3070445, upload-time = "2024-09-03T20:03:21.179Z" }, +] + +[[package]] +name = "cryptography" +version = "44.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "cffi", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2' and platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/67/545c79fe50f7af51dbad56d16b23fe33f63ee6a5d956b3cb68ea110cbe64/cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14", size = 710819, upload-time = "2025-02-11T15:50:58.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/27/5e3524053b4c8889da65cf7814a9d0d8514a05194a25e1e34f46852ee6eb/cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009", size = 6642022, upload-time = "2025-02-11T15:49:32.752Z" }, + { url = "https://files.pythonhosted.org/packages/34/b9/4d1fa8d73ae6ec350012f89c3abfbff19fc95fe5420cf972e12a8d182986/cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f", size = 3943865, upload-time = "2025-02-11T15:49:36.659Z" }, + { url = "https://files.pythonhosted.org/packages/6e/57/371a9f3f3a4500807b5fcd29fec77f418ba27ffc629d88597d0d1049696e/cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2", size = 4162562, upload-time = "2025-02-11T15:49:39.541Z" }, + { url = "https://files.pythonhosted.org/packages/c5/1d/5b77815e7d9cf1e3166988647f336f87d5634a5ccecec2ffbe08ef8dd481/cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911", size = 3951923, upload-time = "2025-02-11T15:49:42.461Z" }, + { url = "https://files.pythonhosted.org/packages/28/01/604508cd34a4024467cd4105887cf27da128cba3edd435b54e2395064bfb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69", size = 3685194, upload-time = "2025-02-11T15:49:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3d/d3c55d4f1d24580a236a6753902ef6d8aafd04da942a1ee9efb9dc8fd0cb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026", size = 4187790, upload-time = "2025-02-11T15:49:48.215Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a6/44d63950c8588bfa8594fd234d3d46e93c3841b8e84a066649c566afb972/cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd", size = 3951343, upload-time = "2025-02-11T15:49:50.313Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/f5282661b57301204cbf188254c1a0267dbd8b18f76337f0a7ce1038888c/cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0", size = 4187127, upload-time = "2025-02-11T15:49:52.051Z" }, + { url = "https://files.pythonhosted.org/packages/f3/68/abbae29ed4f9d96596687f3ceea8e233f65c9645fbbec68adb7c756bb85a/cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf", size = 4070666, upload-time = "2025-02-11T15:49:56.56Z" }, + { url = "https://files.pythonhosted.org/packages/0f/10/cf91691064a9e0a88ae27e31779200b1505d3aee877dbe1e4e0d73b4f155/cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864", size = 4288811, upload-time = "2025-02-11T15:49:59.248Z" }, + { url = "https://files.pythonhosted.org/packages/38/78/74ea9eb547d13c34e984e07ec8a473eb55b19c1451fe7fc8077c6a4b0548/cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a", size = 2771882, upload-time = "2025-02-11T15:50:01.478Z" }, + { url = "https://files.pythonhosted.org/packages/cf/6c/3907271ee485679e15c9f5e93eac6aa318f859b0aed8d369afd636fafa87/cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00", size = 3206989, upload-time = "2025-02-11T15:50:03.312Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f1/676e69c56a9be9fd1bffa9bc3492366901f6e1f8f4079428b05f1414e65c/cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008", size = 6643714, upload-time = "2025-02-11T15:50:05.555Z" }, + { url = "https://files.pythonhosted.org/packages/ba/9f/1775600eb69e72d8f9931a104120f2667107a0ee478f6ad4fe4001559345/cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862", size = 3943269, upload-time = "2025-02-11T15:50:08.54Z" }, + { url = "https://files.pythonhosted.org/packages/25/ba/e00d5ad6b58183829615be7f11f55a7b6baa5a06910faabdc9961527ba44/cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3", size = 4166461, upload-time = "2025-02-11T15:50:11.419Z" }, + { url = "https://files.pythonhosted.org/packages/b3/45/690a02c748d719a95ab08b6e4decb9d81e0ec1bac510358f61624c86e8a3/cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7", size = 3950314, upload-time = "2025-02-11T15:50:14.181Z" }, + { url = "https://files.pythonhosted.org/packages/e6/50/bf8d090911347f9b75adc20f6f6569ed6ca9b9bff552e6e390f53c2a1233/cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a", size = 3686675, upload-time = "2025-02-11T15:50:16.3Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e7/cfb18011821cc5f9b21efb3f94f3241e3a658d267a3bf3a0f45543858ed8/cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c", size = 4190429, upload-time = "2025-02-11T15:50:19.302Z" }, + { url = "https://files.pythonhosted.org/packages/07/ef/77c74d94a8bfc1a8a47b3cafe54af3db537f081742ee7a8a9bd982b62774/cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62", size = 3950039, upload-time = "2025-02-11T15:50:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b9/8be0ff57c4592382b77406269b1e15650c9f1a167f9e34941b8515b97159/cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41", size = 4189713, upload-time = "2025-02-11T15:50:24.261Z" }, + { url = "https://files.pythonhosted.org/packages/78/e1/4b6ac5f4100545513b0847a4d276fe3c7ce0eacfa73e3b5ebd31776816ee/cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b", size = 4071193, upload-time = "2025-02-11T15:50:26.18Z" }, + { url = "https://files.pythonhosted.org/packages/3d/cb/afff48ceaed15531eab70445abe500f07f8f96af2bb35d98af6bfa89ebd4/cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7", size = 4289566, upload-time = "2025-02-11T15:50:28.221Z" }, + { url = "https://files.pythonhosted.org/packages/30/6f/4eca9e2e0f13ae459acd1ca7d9f0257ab86e68f44304847610afcb813dc9/cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9", size = 2772371, upload-time = "2025-02-11T15:50:29.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/05/5533d30f53f10239616a357f080892026db2d550a40c393d0a8a7af834a9/cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f", size = 3207303, upload-time = "2025-02-11T15:50:32.258Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "cffi", marker = "python_full_version >= '3.13.2' and platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/9b/e301418629f7bfdf72db9e80ad6ed9d1b83c487c471803eaa6464c511a01/cryptography-46.0.2.tar.gz", hash = "sha256:21b6fc8c71a3f9a604f028a329e5560009cc4a3a828bfea5fcba8eb7647d88fe", size = 749293, upload-time = "2025-10-01T00:29:11.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/98/7a8df8c19a335c8028414738490fc3955c0cecbfdd37fcc1b9c3d04bd561/cryptography-46.0.2-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3e32ab7dd1b1ef67b9232c4cf5e2ee4cd517d4316ea910acaaa9c5712a1c663", size = 7261255, upload-time = "2025-10-01T00:27:22.947Z" }, + { url = "https://files.pythonhosted.org/packages/c6/38/b2adb2aa1baa6706adc3eb746691edd6f90a656a9a65c3509e274d15a2b8/cryptography-46.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fd1a69086926b623ef8126b4c33d5399ce9e2f3fac07c9c734c2a4ec38b6d02", size = 4297596, upload-time = "2025-10-01T00:27:25.258Z" }, + { url = "https://files.pythonhosted.org/packages/e4/27/0f190ada240003119488ae66c897b5e97149292988f556aef4a6a2a57595/cryptography-46.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb7fb9cd44c2582aa5990cf61a4183e6f54eea3172e54963787ba47287edd135", size = 4450899, upload-time = "2025-10-01T00:27:27.458Z" }, + { url = "https://files.pythonhosted.org/packages/85/d5/e4744105ab02fdf6bb58ba9a816e23b7a633255987310b4187d6745533db/cryptography-46.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9066cfd7f146f291869a9898b01df1c9b0e314bfa182cef432043f13fc462c92", size = 4300382, upload-time = "2025-10-01T00:27:29.091Z" }, + { url = "https://files.pythonhosted.org/packages/33/fb/bf9571065c18c04818cb07de90c43fc042c7977c68e5de6876049559c72f/cryptography-46.0.2-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:97e83bf4f2f2c084d8dd792d13841d0a9b241643151686010866bbd076b19659", size = 4017347, upload-time = "2025-10-01T00:27:30.767Z" }, + { url = "https://files.pythonhosted.org/packages/35/72/fc51856b9b16155ca071080e1a3ad0c3a8e86616daf7eb018d9565b99baa/cryptography-46.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:4a766d2a5d8127364fd936572c6e6757682fc5dfcbdba1632d4554943199f2fa", size = 4983500, upload-time = "2025-10-01T00:27:32.741Z" }, + { url = "https://files.pythonhosted.org/packages/c1/53/0f51e926799025e31746d454ab2e36f8c3f0d41592bc65cb9840368d3275/cryptography-46.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fab8f805e9675e61ed8538f192aad70500fa6afb33a8803932999b1049363a08", size = 4482591, upload-time = "2025-10-01T00:27:34.869Z" }, + { url = "https://files.pythonhosted.org/packages/86/96/4302af40b23ab8aa360862251fb8fc450b2a06ff24bc5e261c2007f27014/cryptography-46.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1e3b6428a3d56043bff0bb85b41c535734204e599c1c0977e1d0f261b02f3ad5", size = 4300019, upload-time = "2025-10-01T00:27:37.029Z" }, + { url = "https://files.pythonhosted.org/packages/9b/59/0be12c7fcc4c5e34fe2b665a75bc20958473047a30d095a7657c218fa9e8/cryptography-46.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:1a88634851d9b8de8bb53726f4300ab191d3b2f42595e2581a54b26aba71b7cc", size = 4950006, upload-time = "2025-10-01T00:27:40.272Z" }, + { url = "https://files.pythonhosted.org/packages/55/1d/42fda47b0111834b49e31590ae14fd020594d5e4dadd639bce89ad790fba/cryptography-46.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:be939b99d4e091eec9a2bcf41aaf8f351f312cd19ff74b5c83480f08a8a43e0b", size = 4482088, upload-time = "2025-10-01T00:27:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/60f583f69aa1602c2bdc7022dae86a0d2b837276182f8c1ec825feb9b874/cryptography-46.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f13b040649bc18e7eb37936009b24fd31ca095a5c647be8bb6aaf1761142bd1", size = 4425599, upload-time = "2025-10-01T00:27:44.616Z" }, + { url = "https://files.pythonhosted.org/packages/d1/57/d8d4134cd27e6e94cf44adb3f3489f935bde85f3a5508e1b5b43095b917d/cryptography-46.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bdc25e4e01b261a8fda4e98618f1c9515febcecebc9566ddf4a70c63967043b", size = 4697458, upload-time = "2025-10-01T00:27:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2b/531e37408573e1da33adfb4c58875013ee8ac7d548d1548967d94a0ae5c4/cryptography-46.0.2-cp311-abi3-win32.whl", hash = "sha256:8b9bf67b11ef9e28f4d78ff88b04ed0929fcd0e4f70bb0f704cfc32a5c6311ee", size = 3056077, upload-time = "2025-10-01T00:27:48.424Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cd/2f83cafd47ed2dc5a3a9c783ff5d764e9e70d3a160e0df9a9dcd639414ce/cryptography-46.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:758cfc7f4c38c5c5274b55a57ef1910107436f4ae842478c4989abbd24bd5acb", size = 3512585, upload-time = "2025-10-01T00:27:50.521Z" }, + { url = "https://files.pythonhosted.org/packages/00/36/676f94e10bfaa5c5b86c469ff46d3e0663c5dc89542f7afbadac241a3ee4/cryptography-46.0.2-cp311-abi3-win_arm64.whl", hash = "sha256:218abd64a2e72f8472c2102febb596793347a3e65fafbb4ad50519969da44470", size = 2927474, upload-time = "2025-10-01T00:27:52.91Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cc/47fc6223a341f26d103cb6da2216805e08a37d3b52bee7f3b2aee8066f95/cryptography-46.0.2-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:bda55e8dbe8533937956c996beaa20266a8eca3570402e52ae52ed60de1faca8", size = 7198626, upload-time = "2025-10-01T00:27:54.8Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/d66a8591207c28bbe4ac7afa25c4656dc19dc0db29a219f9809205639ede/cryptography-46.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7155c0b004e936d381b15425273aee1cebc94f879c0ce82b0d7fecbf755d53a", size = 4287584, upload-time = "2025-10-01T00:27:57.018Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/fac3ab6302b928e0398c269eddab5978e6c1c50b2b77bb5365ffa8633b37/cryptography-46.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a61c154cc5488272a6c4b86e8d5beff4639cdb173d75325ce464d723cda0052b", size = 4433796, upload-time = "2025-10-01T00:27:58.631Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/24392e5d3c58e2d83f98fe5a2322ae343360ec5b5b93fe18bc52e47298f5/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:9ec3f2e2173f36a9679d3b06d3d01121ab9b57c979de1e6a244b98d51fea1b20", size = 4292126, upload-time = "2025-10-01T00:28:00.643Z" }, + { url = "https://files.pythonhosted.org/packages/ed/38/3d9f9359b84c16c49a5a336ee8be8d322072a09fac17e737f3bb11f1ce64/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2fafb6aa24e702bbf74de4cb23bfa2c3beb7ab7683a299062b69724c92e0fa73", size = 3993056, upload-time = "2025-10-01T00:28:02.8Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a3/4c44fce0d49a4703cc94bfbe705adebf7ab36efe978053742957bc7ec324/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0c7ffe8c9b1fcbb07a26d7c9fa5e857c2fe80d72d7b9e0353dcf1d2180ae60ee", size = 4967604, upload-time = "2025-10-01T00:28:04.783Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/49d73218747c8cac16bb8318a5513fde3129e06a018af3bc4dc722aa4a98/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5840f05518caa86b09d23f8b9405a7b6d5400085aa14a72a98fdf5cf1568c0d2", size = 4465367, upload-time = "2025-10-01T00:28:06.864Z" }, + { url = "https://files.pythonhosted.org/packages/1b/64/9afa7d2ee742f55ca6285a54386ed2778556a4ed8871571cb1c1bfd8db9e/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:27c53b4f6a682a1b645fbf1cd5058c72cf2f5aeba7d74314c36838c7cbc06e0f", size = 4291678, upload-time = "2025-10-01T00:28:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/50/48/1696d5ea9623a7b72ace87608f6899ca3c331709ac7ebf80740abb8ac673/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:512c0250065e0a6b286b2db4bbcc2e67d810acd53eb81733e71314340366279e", size = 4931366, upload-time = "2025-10-01T00:28:10.74Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/9dfc778401a334db3b24435ee0733dd005aefb74afe036e2d154547cb917/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:07c0eb6657c0e9cca5891f4e35081dbf985c8131825e21d99b4f440a8f496f36", size = 4464738, upload-time = "2025-10-01T00:28:12.491Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b1/abcde62072b8f3fd414e191a6238ce55a0050e9738090dc6cded24c12036/cryptography-46.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48b983089378f50cba258f7f7aa28198c3f6e13e607eaf10472c26320332ca9a", size = 4419305, upload-time = "2025-10-01T00:28:14.145Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1f/3d2228492f9391395ca34c677e8f2571fb5370fe13dc48c1014f8c509864/cryptography-46.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e6f6775eaaa08c0eec73e301f7592f4367ccde5e4e4df8e58320f2ebf161ea2c", size = 4681201, upload-time = "2025-10-01T00:28:15.951Z" }, + { url = "https://files.pythonhosted.org/packages/de/77/b687745804a93a55054f391528fcfc76c3d6bfd082ce9fb62c12f0d29fc1/cryptography-46.0.2-cp314-cp314t-win32.whl", hash = "sha256:e8633996579961f9b5a3008683344c2558d38420029d3c0bc7ff77c17949a4e1", size = 3022492, upload-time = "2025-10-01T00:28:17.643Z" }, + { url = "https://files.pythonhosted.org/packages/60/a5/8d498ef2996e583de0bef1dcc5e70186376f00883ae27bf2133f490adf21/cryptography-46.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:48c01988ecbb32979bb98731f5c2b2f79042a6c58cc9a319c8c2f9987c7f68f9", size = 3496215, upload-time = "2025-10-01T00:28:19.272Z" }, + { url = "https://files.pythonhosted.org/packages/56/db/ee67aaef459a2706bc302b15889a1a8126ebe66877bab1487ae6ad00f33d/cryptography-46.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:8e2ad4d1a5899b7caa3a450e33ee2734be7cc0689010964703a7c4bcc8dd4fd0", size = 2919255, upload-time = "2025-10-01T00:28:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/d5/bb/fa95abcf147a1b0bb94d95f53fbb09da77b24c776c5d87d36f3d94521d2c/cryptography-46.0.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a08e7401a94c002e79dc3bc5231b6558cd4b2280ee525c4673f650a37e2c7685", size = 7248090, upload-time = "2025-10-01T00:28:22.846Z" }, + { url = "https://files.pythonhosted.org/packages/b7/66/f42071ce0e3ffbfa80a88feadb209c779fda92a23fbc1e14f74ebf72ef6b/cryptography-46.0.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d30bc11d35743bf4ddf76674a0a369ec8a21f87aaa09b0661b04c5f6c46e8d7b", size = 4293123, upload-time = "2025-10-01T00:28:25.072Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/1fdbd2e5c1ba822828d250e5a966622ef00185e476d1cd2726b6dd135e53/cryptography-46.0.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bca3f0ce67e5a2a2cf524e86f44697c4323a86e0fd7ba857de1c30d52c11ede1", size = 4439524, upload-time = "2025-10-01T00:28:26.808Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/5e4989a7d102d4306053770d60f978c7b6b1ea2ff8c06e0265e305b23516/cryptography-46.0.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ff798ad7a957a5021dcbab78dfff681f0cf15744d0e6af62bd6746984d9c9e9c", size = 4297264, upload-time = "2025-10-01T00:28:29.327Z" }, + { url = "https://files.pythonhosted.org/packages/28/78/b56f847d220cb1d6d6aef5a390e116ad603ce13a0945a3386a33abc80385/cryptography-46.0.2-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cb5e8daac840e8879407acbe689a174f5ebaf344a062f8918e526824eb5d97af", size = 4011872, upload-time = "2025-10-01T00:28:31.479Z" }, + { url = "https://files.pythonhosted.org/packages/e1/80/2971f214b066b888944f7b57761bf709ee3f2cf805619a18b18cab9b263c/cryptography-46.0.2-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:3f37aa12b2d91e157827d90ce78f6180f0c02319468a0aea86ab5a9566da644b", size = 4978458, upload-time = "2025-10-01T00:28:33.267Z" }, + { url = "https://files.pythonhosted.org/packages/a5/84/0cb0a2beaa4f1cbe63ebec4e97cd7e0e9f835d0ba5ee143ed2523a1e0016/cryptography-46.0.2-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e38f203160a48b93010b07493c15f2babb4e0f2319bbd001885adb3f3696d21", size = 4472195, upload-time = "2025-10-01T00:28:36.039Z" }, + { url = "https://files.pythonhosted.org/packages/30/8b/2b542ddbf78835c7cd67b6fa79e95560023481213a060b92352a61a10efe/cryptography-46.0.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d19f5f48883752b5ab34cff9e2f7e4a7f216296f33714e77d1beb03d108632b6", size = 4296791, upload-time = "2025-10-01T00:28:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/78/12/9065b40201b4f4876e93b9b94d91feb18de9150d60bd842a16a21565007f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:04911b149eae142ccd8c9a68892a70c21613864afb47aba92d8c7ed9cc001023", size = 4939629, upload-time = "2025-10-01T00:28:39.654Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9e/6507dc048c1b1530d372c483dfd34e7709fc542765015425f0442b08547f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8b16c1ede6a937c291d41176934268e4ccac2c6521c69d3f5961c5a1e11e039e", size = 4471988, upload-time = "2025-10-01T00:28:41.822Z" }, + { url = "https://files.pythonhosted.org/packages/b1/86/d025584a5f7d5c5ec8d3633dbcdce83a0cd579f1141ceada7817a4c26934/cryptography-46.0.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:747b6f4a4a23d5a215aadd1d0b12233b4119c4313df83ab4137631d43672cc90", size = 4422989, upload-time = "2025-10-01T00:28:43.608Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/536370418b38a15a61bbe413006b79dfc3d2b4b0eafceb5581983f973c15/cryptography-46.0.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b275e398ab3a7905e168c036aad54b5969d63d3d9099a0a66cc147a3cc983be", size = 4685578, upload-time = "2025-10-01T00:28:45.361Z" }, + { url = "https://files.pythonhosted.org/packages/15/52/ea7e2b1910f547baed566c866fbb86de2402e501a89ecb4871ea7f169a81/cryptography-46.0.2-cp38-abi3-win32.whl", hash = "sha256:0b507c8e033307e37af61cb9f7159b416173bdf5b41d11c4df2e499a1d8e007c", size = 3036711, upload-time = "2025-10-01T00:28:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/71/9e/171f40f9c70a873e73c2efcdbe91e1d4b1777a03398fa1c4af3c56a2477a/cryptography-46.0.2-cp38-abi3-win_amd64.whl", hash = "sha256:f9b2dc7668418fb6f221e4bf701f716e05e8eadb4f1988a2487b11aedf8abe62", size = 3500007, upload-time = "2025-10-01T00:28:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/3e/7c/15ad426257615f9be8caf7f97990cf3dcbb5b8dd7ed7e0db581a1c4759dd/cryptography-46.0.2-cp38-abi3-win_arm64.whl", hash = "sha256:91447f2b17e83c9e0c89f133119d83f94ce6e0fb55dd47da0a959316e6e9cfa1", size = 2918153, upload-time = "2025-10-01T00:28:51.003Z" }, +] + +[[package]] +name = "dbus-fast" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/a4/e54607cf8b0a696beba591f1a543cff5b6a9e4b4f842fd55f7ba741d678d/dbus_fast-3.1.2.tar.gz", hash = "sha256:6c9e1b45e4b5e7df0c021bf1bf3f27649374e47c3de1afdba6d00a7d7bba4b3a", size = 73191, upload-time = "2025-11-17T03:41:10.475Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/87/1aa99b4ab3e051962071a0e443cfa725e80802c950484a501f01014ab0df/dbus_fast-3.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91362a0f2151926a882c652ee2ae7c41495a82228b045e7461e1ce687ab4b173", size = 789953, upload-time = "2025-11-17T03:49:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/fc/99/59bc4854b2a2355352373d08d07b89d5318c181a1356de307b07b8ae5d99/dbus_fast-3.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439c300cf0f1b9b4b81c1a55ac1ed65c2b90f203570c4d0243d2fc3eac8fc7cc", size = 839666, upload-time = "2025-11-17T03:49:33.81Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/69ae337cd516bce574fc1dd592ce6c3fdd46acae0168c9c20b26ce284396/dbus_fast-3.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9290039b2454357735a35cf81b98c208c19c1b4a244532bbb52135c5dc0b7f8c", size = 796378, upload-time = "2025-11-17T03:49:35.125Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/a6178c07c769ecdba3d46f8c458adfab25db9a4e5d16a8b567b2c61c3e03/dbus_fast-3.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9d275923c4ec24b63b1edf4871f05fc673fc08e1a838a9ddd02938b9c28fa44", size = 847893, upload-time = "2025-11-17T03:49:36.79Z" }, + { url = "https://files.pythonhosted.org/packages/48/65/81e1acbe08e195f38a3ef863702dba88ade8fe56594041ff7e5a6ef9d137/dbus_fast-3.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdaa7c1cf132b72a8c66fd36c612b112063296d2d518463064ff44dc670d452a", size = 787581, upload-time = "2025-11-17T03:49:39.581Z" }, + { url = "https://files.pythonhosted.org/packages/1a/59/da668ad9947db726747e29aa1978642919a5cfbe633a473f76446ea3915d/dbus_fast-3.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:973afa96fcb97c680d50a66163ad2aa7327177e136a29fbeae280c660584536a", size = 836242, upload-time = "2025-11-17T03:49:40.854Z" }, + { url = "https://files.pythonhosted.org/packages/de/c4/ecb51606a7f5f41e6eeb2e347757b6ffbd83b9e51c85bb7787a737016ee4/dbus_fast-3.1.2-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:cea152a01991cb8b77eeb2403b156e5a8ba4300b729636aa732fc891c22e44d4", size = 812530, upload-time = "2025-11-17T03:41:08.634Z" }, + { url = "https://files.pythonhosted.org/packages/fd/47/42eac517b9fe3949f3cb66dcc1d2b5fa4380bf606bef2374f3c77d7f4cb2/dbus_fast-3.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:618b819b19724477b77f5bf3f300d92fa51d0974bd25499e10c3417eadc4a732", size = 793323, upload-time = "2025-11-17T03:49:42.765Z" }, + { url = "https://files.pythonhosted.org/packages/92/ef/0956c728d7a4963b6503f201f4aa3c3b366ec21490b526bf155b8fa0ce52/dbus_fast-3.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66279b8491ba9d593c4793b423abbf1dce14dbb3f3e6d9967bb62be8c39244b4", size = 845052, upload-time = "2025-11-17T03:49:44.157Z" }, + { url = "https://files.pythonhosted.org/packages/e0/87/2d322dbf5357393a426ae2873d60016008566b0a60a2c7f6e4f1783add50/dbus_fast-3.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8116564196c7e83cfc81be186378da7f093d36fbfef0669e1fe1f20ac891c50a", size = 801269, upload-time = "2025-11-17T03:49:47.418Z" }, + { url = "https://files.pythonhosted.org/packages/72/27/1bc75fcd4ea5fb298a28b9d204b65b0d00c7c89ba13a3e47185d48b8db8b/dbus_fast-3.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c55db7b62878bc039736d2687b1bd5eb4a5596b97a4b230c9d919daa961a1d9c", size = 842854, upload-time = "2025-11-17T03:49:48.986Z" }, + { url = "https://files.pythonhosted.org/packages/40/c4/f042243ff28cf083c189f7eff5cc04b86c150a33b3cdef7ef21b520d93a5/dbus_fast-3.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8064b36900098c31a3fe8dab7ef3931c853cbcf9f163ccb437a7379c61e6acc3", size = 808090, upload-time = "2025-11-17T03:49:50.697Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/1e1443cb8a35e11e24009882463c461ee5103c20c5fc7f04a9e3443462ce/dbus_fast-3.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:038d3e8803f62b1d789ce0c602cc8c317c47c21e67bb2dd544b9c0fc97b4b2e2", size = 850179, upload-time = "2025-11-17T03:49:51.956Z" }, + { url = "https://files.pythonhosted.org/packages/23/fb/9ebef541d192c22529af38d151a818ca18f6c3699dfc3644ff0e66e44599/dbus_fast-3.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71c99fb09c3a5637a0729230ac5f888b61abf754e10f23c629be476da830887c", size = 1529979, upload-time = "2025-11-17T03:49:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4f/0e0b0359d41ffaa652b066759321d8f2608246090c827061c43b72dcefae/dbus_fast-3.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a78eb3f19ff81fb7a8b16075160ebd1edc6135c59c929da0832511f315b5ede", size = 1606236, upload-time = "2025-11-17T03:49:56.667Z" }, + { url = "https://files.pythonhosted.org/packages/8e/18/1ad08968773f1d1822a1e147dbc98c9af220b582d2c8b95705c7e8f8019f/dbus_fast-3.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:366550946b281a5b4bb8d70815667d24565141e3c23dc7d40267a315b16def2c", size = 1545417, upload-time = "2025-11-17T03:49:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/89/94/b7ff6279e642b014cd4aef4d914b9fca3917c2c9c35df49db062023cbdfc/dbus_fast-3.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1d7cc1315586e4c50875c9a2d56b9ad2e056ec75e2f27c43cd80392f72d0f6e3", size = 1623709, upload-time = "2025-11-17T03:49:59.571Z" }, +] + +[[package]] +name = "deepmerge" +version = "2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/3a/b0ba594708f1ad0bc735884b3ad854d3ca3bdc1d741e56e40bbda6263499/deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20", size = 19890, upload-time = "2024-08-30T05:31:50.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "envs" +version = "1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/7f/2098df91ff1499860935b4276ea0c27d3234170b03f803a8b9c97e42f0e9/envs-1.4.tar.gz", hash = "sha256:9d8435c6985d1cdd68299e04c58e2bdb8ae6cf66b2596a8079e6f9a93f2a0398", size = 9230, upload-time = "2021-12-09T22:16:52.616Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/bc/f8c625a084b6074c2295f7eab967f868d424bb8ca30c7a656024b26fe04e/envs-1.4-py3-none-any.whl", hash = "sha256:4a1fcf85e4d4443e77c348ff7cdd3bfc4c0178b181d447057de342e4172e5ed1", size = 10988, upload-time = "2021-12-09T22:16:51.127Z" }, +] + +[[package]] +name = "fastuuid" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, + { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, + { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" }, + { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532, upload-time = "2025-10-19T22:35:18.217Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457, upload-time = "2025-10-19T22:33:44.579Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, +] + +[[package]] +name = "fnv-hash-fast" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "fnvhash", version = "0.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/35/a0f2baec714caa5e865dc284382bac7f959c643a03d7f2c2f8c38573b2b2/fnv_hash_fast-1.0.2.tar.gz", hash = "sha256:d4c528bfb0daa751afb17419a244b913b094b9f0634f9bd19aeffcdc60192589", size = 5808, upload-time = "2024-08-23T13:00:08.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/76/08c20543421f368f5d0215381b745268f5ba51916a286896fed62ed583e2/fnv_hash_fast-1.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d306b606c1686f7902f2da3193535e3523934ddf10cc540427d5a1d96a9818c4", size = 65081, upload-time = "2024-08-23T13:05:34.981Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ba/4cadf8a40268bed1c777f9d8e6cfe18a38056d46c2cc844ce34560bb9796/fnv_hash_fast-1.0.2-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:d1dfd66728c70b6b3184729a8e2b98cf8d3548b65bc09ab49fff156d86095e62", size = 149973, upload-time = "2024-08-23T13:05:35.891Z" }, + { url = "https://files.pythonhosted.org/packages/40/9b/0b27c3116dcbde287961c413d9cddc9bfaa62783f5c43cf6a8a6743bd1da/fnv_hash_fast-1.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdeaed747d4af60c0ae4cd336ee349db0bba2e1bd46d7d94c8c6a1a7cf3ecbf4", size = 154213, upload-time = "2024-08-23T13:05:37.305Z" }, + { url = "https://files.pythonhosted.org/packages/14/ee/6b6079fd618a4ae9e1762ff525d204d7689aa8e7e378d24ca5df7e02a4c9/fnv_hash_fast-1.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e9f303ce7c394119cb205fe54124f956b3feefd388700f2268b209d78fa9a88c", size = 1237178, upload-time = "2024-08-23T13:05:39.099Z" }, + { url = "https://files.pythonhosted.org/packages/78/36/60431380699e98371a985bdbfc181a4adb32317d5ada1c5f10c84087afaa/fnv_hash_fast-1.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55b9ccbfb87aafc76ef133c70e76a5061a48432f6ba846263ef122a774bce09c", size = 1147938, upload-time = "2024-08-23T13:05:40.686Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d5/d2ef0509776ba09378f90ea87657b8c33112c2c24429e8596439472eb6ea/fnv_hash_fast-1.0.2-cp312-cp312-win32.whl", hash = "sha256:57507e52829dd463f2f755ca22fc9dc4a8d9a9c5d8cf1b0d5ec4eeddf90c9c48", size = 64267, upload-time = "2024-08-23T13:05:42.121Z" }, + { url = "https://files.pythonhosted.org/packages/1f/46/eb073c248b42b8137d47dff385b30c0ebfb3542536fc4581a944372a0c82/fnv_hash_fast-1.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:ef4118d57d27a13271feb47b0ffef95a5122aaa2c4e15b4979cc8bf1bc81c14b", size = 66293, upload-time = "2024-08-23T13:05:43.392Z" }, + { url = "https://files.pythonhosted.org/packages/54/af/9bb74b23610d3f10b64ac41c6ac9a59402619ef13a4d880975d1dc07dfd3/fnv_hash_fast-1.0.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ea6d4fb666684d7e15f2eb1aa92235b25286ea3081cdfb469ffcc7ee34c74b67", size = 64342, upload-time = "2024-08-23T13:05:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/a6/47/327e75a72d6f0de00491b8867953878a4c72c058e0f9ee37c0c64111a297/fnv_hash_fast-1.0.2-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:f91ec27fbe3fc43440a250d3b8dac3f0ebd8cea91ffa432bea40ef611b089eeb", size = 145037, upload-time = "2024-08-23T13:05:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5f/90f485875dee1a7fd2252aacef6c10a7dcf991fcfee44f4e65ba13ab477e/fnv_hash_fast-1.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fb40cc6685a81595434c6cf1ef79d92d4d899e8bc823d9ad6a30287d612de0d", size = 149134, upload-time = "2024-08-23T13:05:46.624Z" }, + { url = "https://files.pythonhosted.org/packages/05/d3/79bc1b00223aada2bf906c99bb619d730bdbff293433f08ef3ba153ef198/fnv_hash_fast-1.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7b7f491a7c3cd679bda8fcd693234812a488bcb3dae0952ba312366b6f69796d", size = 1232050, upload-time = "2024-08-23T13:05:47.591Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0d/b523ee18b8dea412f0b1d711917b030a32edc1e183b36d7419d83030b7f5/fnv_hash_fast-1.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a318cb86ea4a91c95eb42bd49e9144fbdc83e0bb91a1e6f672f197f15a450d01", size = 1143237, upload-time = "2024-08-23T13:05:49.209Z" }, + { url = "https://files.pythonhosted.org/packages/1a/aa/6cc3d9da7ca14341be9a9effe32b2bf105f6d08b3971b007c51f18b22d9a/fnv_hash_fast-1.0.2-cp313-cp313-win32.whl", hash = "sha256:0ac9b5da8fbb9f670a7ce877dfa9bccc942f6499e25801d63427e0f55e1aa902", size = 63690, upload-time = "2024-08-23T13:05:50.556Z" }, + { url = "https://files.pythonhosted.org/packages/87/f6/1d7f2fd7c2163e661774c07f8cceb1db434f2c5440ab644379d6de76753b/fnv_hash_fast-1.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:a5935a91ae5cc9edd2bd7a9028b0e5b1e371e5a383034357541b559a2e235e57", size = 65514, upload-time = "2024-08-23T13:05:51.41Z" }, +] + +[[package]] +name = "fnv-hash-fast" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "fnvhash", version = "0.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/36/fa1cab334dc1228235d76a22bdeab67b6895e08eb1870821c500e86b240b/fnv_hash_fast-1.4.0.tar.gz", hash = "sha256:12a2a437263f08815bd2d5759c12e881408718bb82cfffceb0341575f2c43f0a", size = 5661, upload-time = "2025-03-05T01:09:25.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/8e/dfb9f6a3bb99df495c5d0a7ac801ff930cb7800306afb74e856b6517e1f0/fnv_hash_fast-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:10e55bf0987b2e74af39211223e6d4e7c17380ad91e5bd9d799eb5ec78b3f19d", size = 19151, upload-time = "2025-03-05T01:15:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1e/450ce8cc0c668c529544605b8d3fa8789ad32450b086d68f39e4312f4bd3/fnv_hash_fast-1.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b49f0f8df92efc3f7d8894b1c5217379cb25d826716690b42692b93661b3eea0", size = 19214, upload-time = "2025-03-05T01:15:33.145Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7e/1ddd3a6449fa6c10de63f1409cd34f680e7a0c320449594c655bc0a0d702/fnv_hash_fast-1.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e489052acf4f279ffbd3ad7326b7306df9e91388295b207077e939fc10fdb9", size = 22469, upload-time = "2025-03-05T01:15:34.407Z" }, + { url = "https://files.pythonhosted.org/packages/89/f2/17edeb023de13afc9382ffa26458ceee78e9be36af88365a6f7f7ad77cc9/fnv_hash_fast-1.4.0-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:4472a130fa7eb740c995f39c7c882e5b587a23489dcce5c8c96423f5adc443fb", size = 23998, upload-time = "2025-03-05T01:15:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/b3/62/a8ef494256b4822b0f5a012fbad9d3e65d1e738a696bc9eb06e9d657a9da/fnv_hash_fast-1.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6acb872d1026293814ac5ca31adb80fb898e8b4823fc19dbb38cff12f1adad18", size = 22903, upload-time = "2025-03-05T01:15:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/a9/47/258d6ce1e2540152859ecf3894de68463cffc2e6dfa446cf3b95534a89b3/fnv_hash_fast-1.4.0-cp312-cp312-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:64f1a35e860442a24c36503a5bf5442bb548fe2f4bc8c06ae7c873dd387d7e96", size = 20304, upload-time = "2025-03-05T01:15:40.731Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/30f1e8cd83ad2502a0928712c2925abcbc58935e0a92176b2b2cb8bce29b/fnv_hash_fast-1.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e609e8412520328c454418fd355ea6e558a66684fef9a91c55a1f90524977134", size = 23084, upload-time = "2025-03-05T01:15:42.778Z" }, + { url = "https://files.pythonhosted.org/packages/85/46/0077cf8fae9d17077b9f5d0fa7b236b1ed9a68199f1de785f08f7255173f/fnv_hash_fast-1.4.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:66f37b3fb877fd0719eaddcb924375c2a5a3d1dde443857720d41f69fe0a791c", size = 20806, upload-time = "2025-03-05T01:15:44.294Z" }, + { url = "https://files.pythonhosted.org/packages/63/8a/247182a9e4b9d1843f406d33ec590f1c9e6340702d40edfb1a50cfa5f208/fnv_hash_fast-1.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c53060f9d50164040649489f19c11b1b89bd04a061320e1bd7d1b6af714c236b", size = 24829, upload-time = "2025-03-05T01:15:46.201Z" }, + { url = "https://files.pythonhosted.org/packages/35/ba/0986c9c16a8ff70e57e09d5750cbda0a355772c0c84c5818be8ea63d48c2/fnv_hash_fast-1.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9fe7aff4247cdc66e099b2bb5470cc2bceb4e1ae10be3adb89a2f0e83c7ef919", size = 23597, upload-time = "2025-03-05T01:15:47.282Z" }, + { url = "https://files.pythonhosted.org/packages/b9/df/42b524e0289b3f9d19e0e6e1343f4a00fe1f09a252ef394efe5d49ed4d6c/fnv_hash_fast-1.4.0-cp312-cp312-win32.whl", hash = "sha256:1db95120a50ebddd9bca8eaf8d20883334f3aa6f743d4651a0368058110a8ceb", size = 19508, upload-time = "2025-03-05T01:15:48.401Z" }, + { url = "https://files.pythonhosted.org/packages/b6/64/e176358d760d2eed7366557dba235ac1fcb282afed3c52961ff2f2fa033f/fnv_hash_fast-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:72a61703e02fcd2571631a07f09146352ec6f08320ce1604a24cba54bf310642", size = 21244, upload-time = "2025-03-05T01:15:49.588Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/178cf5b827f0d54caa3fbb28e6e7493ad5e0d75a165b57f8c2fe9fcd3519/fnv_hash_fast-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7f1bbfe718df3aa10aafa2c40cf6bb9581a267c914a4005fc0f5e3ce21a01a12", size = 18552, upload-time = "2025-03-05T01:15:50.836Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5b/88866809455c974b5cf8c8c18c2ad293df796083703c0da8ec6c4e50135f/fnv_hash_fast-1.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:25dd05a0e7f8381b2d881b122400862da4f855b80fa9d6f5a20cda0706fde9b7", size = 18585, upload-time = "2025-03-05T01:15:52.297Z" }, + { url = "https://files.pythonhosted.org/packages/da/60/effbe5965ab6da6e6805ca18729f588bee486b8e4127b8bf52037908a3a3/fnv_hash_fast-1.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1038ab67c143f1119b2cad9fb3d909439e88f72f3b137015eb642ad91245734", size = 21761, upload-time = "2025-03-05T01:15:54.267Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/d80f730b9f535cec9a6db8c11e9e38413e0a398d9e7f2e0abde80b3587b3/fnv_hash_fast-1.4.0-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:c5ae87deee0204f2aaabfc861bf322f0f1dea078c847e10e35bc67c7becfefd6", size = 23126, upload-time = "2025-03-05T01:15:55.387Z" }, + { url = "https://files.pythonhosted.org/packages/ce/70/0c795f5a92a58f6d105078c632a9a5cf1de4bcdd42d3fec20592ed7c59f4/fnv_hash_fast-1.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a07b27755378f08e399fc124f724f3af58f8f54167f6fd525068e51ba7e9e9f", size = 22152, upload-time = "2025-03-05T01:15:56.579Z" }, + { url = "https://files.pythonhosted.org/packages/9f/72/14a8bb3844037cc6afe0ba147020ba2d450f44d29a32a0d2863583822ee2/fnv_hash_fast-1.4.0-cp313-cp313-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37cb8e67d8d4df670a699d928a4ed74b7284724d73df736a8ff9f57178e6a720", size = 19755, upload-time = "2025-03-05T01:15:57.696Z" }, + { url = "https://files.pythonhosted.org/packages/c6/f8/7cce1f63cd07c0c3d998ddd5f193b56247edd0ccb049c7549d05f559c889/fnv_hash_fast-1.4.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:5fa7945986ae71c68eef522335d33a671ed2c33952272ea4360d0c44331f90eb", size = 21792, upload-time = "2025-03-05T01:09:23.45Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/5e3d38f4ef3c7c862ba8cbda1d64b48b8ba314d937fe967815ee8e1d85bb/fnv_hash_fast-1.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e6dd8f20122d0fba69171858438eb74f1f12f4630178ab0db9d05bd0dfc68054", size = 22437, upload-time = "2025-03-05T01:15:58.782Z" }, + { url = "https://files.pythonhosted.org/packages/89/85/bb90b080e30c4e082e3120874e218956a0b8a78c5913139b38c8056be4da/fnv_hash_fast-1.4.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c410adea6d70c8663a168b1f499bb3cb9ff743675921aa8b6fb99e07cd83eb45", size = 20259, upload-time = "2025-03-05T01:15:59.894Z" }, + { url = "https://files.pythonhosted.org/packages/83/00/788bb2e0f369c543a8bdbc5621a7070f31b13ee6992420e198636b42276a/fnv_hash_fast-1.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:579e5ba4265f4fc41dbe6fbe12d133d22aa7948455ef8a16fbe7bc5d1666d6d8", size = 24059, upload-time = "2025-03-05T01:16:01.121Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9d/ede35e832d6801ae8373abc8926c631cb6739e7d5e432366be44a9bf98d2/fnv_hash_fast-1.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c9568b00ab3e91af8a8409c2955b5d781f9a338c0c00428e788ff77f3baac3ec", size = 22927, upload-time = "2025-03-05T01:16:02.228Z" }, + { url = "https://files.pythonhosted.org/packages/df/5c/2aa4c0b1b1648d80567499062bfe0414384df2487e794885d5ff4dc235a4/fnv_hash_fast-1.4.0-cp313-cp313-win32.whl", hash = "sha256:c04e54d919b5e0ef2cb6a2de0fbabb3d075ee2609324a678d2471c87542bbacb", size = 18938, upload-time = "2025-03-05T01:16:04.28Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7a/a666e222003f52941aa4b620f2658cc1f665a84657886e584a69613bfb58/fnv_hash_fast-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:5eec9b18aee7ed014ba0b8a2cf59d5c2f2d83af683c4ad87e9c03dd2f4f5d573", size = 20453, upload-time = "2025-03-05T01:16:08.226Z" }, +] + +[[package]] +name = "fnv-hash-fast" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "fnvhash", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/19/10f4e1b4bbfe7cf162d20bb4d54bd62935d652e2ea107ddb0b5a6c4e8b75/fnv_hash_fast-1.6.0.tar.gz", hash = "sha256:a09feefad2c827192dc4306826df3ffb7c6288f25ab7976d4588fdae9cbb7661", size = 5675, upload-time = "2025-10-04T19:35:00.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/8e/a250c545faccf828772830ae3db234ad8741ed57832d96b3aa9d6ebe8616/fnv_hash_fast-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8054c711283a4d598d516c17445d6c12304c93e48f7e97aba970549e0d5b413b", size = 13373, upload-time = "2025-10-04T19:45:09.032Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/fb0f0e516a46c2288f6dd4afea0ba1c7c3065ac0565fc12d9c662c7d2434/fnv_hash_fast-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:135bc62ec5d61a9a38222afce83d3d0c9d4d52fa8a2670f6acc86ee6818d2bfa", size = 13901, upload-time = "2025-10-04T19:45:10.25Z" }, + { url = "https://files.pythonhosted.org/packages/22/76/f4c7c8784b836da32fa84b1ef78455b3d26950c46da99cfeffa9dfdfbb95/fnv_hash_fast-1.6.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:34cd7e776fd515dfcab4cf84bdb63d879653cc3f77f6f086822ed9c50645f75c", size = 15210, upload-time = "2025-10-04T19:45:11.198Z" }, + { url = "https://files.pythonhosted.org/packages/cb/43/a8c154b9b1fae5ab2ded2a389efef18c7332d93a1ab260f5cae02e34b3d8/fnv_hash_fast-1.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9189a9a1b820658c791728b4558b4aadad6b35429b1784bcaf57183788d3f489", size = 16299, upload-time = "2025-10-04T19:45:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/ee/14/674c4d4ac7bcc9145f351707d2d944c76c7bdc955700707468854e6cf00b/fnv_hash_fast-1.6.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:893306251f69bf9591b51e75dfac2bac9703c4156b9c7e78dd2930e33b184f1f", size = 14208, upload-time = "2025-10-04T19:45:13.647Z" }, + { url = "https://files.pythonhosted.org/packages/73/90/54752bc1cfcc281aef379c5926c00b127006e6a4197c701796dd382f81c6/fnv_hash_fast-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7107073d69dcf68b602bddf37a6ab2af6e9afcfc31239e215b75dcc5049667ae", size = 16197, upload-time = "2025-10-04T19:45:14.876Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4f/971d08ab32e41d7cc4255dc0d2e94be8b907def305b45fbe9c62f1c75db6/fnv_hash_fast-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f0b9cbb8538e981d4e0895f01af6510bae5dd6bcb1a8bf3db8cecd9877079e66", size = 14634, upload-time = "2025-10-04T19:45:15.822Z" }, + { url = "https://files.pythonhosted.org/packages/16/bf/ff169383b41176a15828aec88a39a8b1397041e6d3948273aec010688712/fnv_hash_fast-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:11b93131512147038d54c92106a4035b1ae71488abe730952bdeb55a9d7dcb18", size = 15542, upload-time = "2025-10-04T19:45:17.064Z" }, + { url = "https://files.pythonhosted.org/packages/84/9d/3d98e9ab97db03666ddd9b98f5ae4b45cc3e5746e8987f7513b54ae49a50/fnv_hash_fast-1.6.0-cp312-cp312-win32.whl", hash = "sha256:df7666d14e01352a22351344fdbcb8916b873ea91d598b53e07736b0e64fb66c", size = 15140, upload-time = "2025-10-04T19:45:18.283Z" }, + { url = "https://files.pythonhosted.org/packages/b1/14/d629342b4b1ff8b2daaf8fdba74103b5786a69e071bd525b9c612b2fd3f5/fnv_hash_fast-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:60d7c0a89ee63076de139f0b619f5cc55378f3c4ed67488dde456dbf93479530", size = 15971, upload-time = "2025-10-04T19:45:19.219Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a9/c73abc05dd01434442dbd38a2e50166e9ba59f8db41cdf82649410c37d12/fnv_hash_fast-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d34e4f2acc41aacd97877d396948b38efc7197a2dd91c15e818c049c4d48b0a0", size = 13350, upload-time = "2025-10-04T19:45:20.184Z" }, + { url = "https://files.pythonhosted.org/packages/75/f8/a79d5a29dcf3b0e41635056ee37fff9e2bc46e3625d44b163a4ac2b9160c/fnv_hash_fast-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1a1fe55163d38052ec90aaf16f190bb807342aa09f9680185b9772ce0407b62", size = 13864, upload-time = "2025-10-04T19:45:21.174Z" }, + { url = "https://files.pythonhosted.org/packages/c1/41/fabca5bf0c5b36405517908974e93f1832780d692271295efaf8dba40afc/fnv_hash_fast-1.6.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d7c3a18e7aa483d18ff569554b07b5238403775f8e401245ab8b3c27bcb34cf", size = 15206, upload-time = "2025-10-04T19:45:22.242Z" }, + { url = "https://files.pythonhosted.org/packages/04/7f/1c5c4e451c0213b44235b39737cecf3e58f4195332b173f45e2c95a9b0d8/fnv_hash_fast-1.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd8fdeee59431cc03afdb8a04c3c46b452dc2ded85973953b7077715e897a85b", size = 16301, upload-time = "2025-10-04T19:45:23.188Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7c/095bb6f7ed9bbb85d7451312388fd61dfcde194aad5a2e3902e8fd908a78/fnv_hash_fast-1.6.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6d8284c7ad0339def03252905f3456195ec9d77d8329225e5b09b226e3eb79ec", size = 14175, upload-time = "2025-10-04T19:45:24.397Z" }, + { url = "https://files.pythonhosted.org/packages/73/52/ced7073eaee3479b4e09fed73c1db5a51a9bc72f7546324126b76b4c2f9b/fnv_hash_fast-1.6.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:03642803cc4567dada952d7b1490d6eedd97cd960a83ebbb4a4b7c545629f33f", size = 14581, upload-time = "2025-10-04T19:34:59.064Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a3/877d7f9bce7efccb70607307b25abec35f1206f5dcb3b5a898ad67d61dbf/fnv_hash_fast-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3a540bae99086d3942a2976c16480916cb86d9f06a632023176fe4fa56d298b5", size = 16214, upload-time = "2025-10-04T19:45:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e5/f26eb6e262a8d2329aad6d618b102c238009a11e00d6c5914cb510d1d968/fnv_hash_fast-1.6.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9d6a447404ddfc0035a52de80747c36dce8ba0cc24c27610ca4be9c0ba46d783", size = 14649, upload-time = "2025-10-04T19:45:26.568Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/6791aa693e9400d00ef56be40586bc9de7b826756f0156a3f4e5b3b6d40b/fnv_hash_fast-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d016ee85cd9faccb2f958e5017eb60c8c6410b1700f85052f5dbf2b34084c7ef", size = 15554, upload-time = "2025-10-04T19:45:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/be/4e/65ce211d9cb8333fddba5b38a18014b2928b4b7a5678d8501cb764a89285/fnv_hash_fast-1.6.0-cp313-cp313-win32.whl", hash = "sha256:9a3751dc38c33b0be4fc4a5a5947ab6d9acbdb1017dfeff55ab3d1fa3ed6c03e", size = 15103, upload-time = "2025-10-04T19:45:28.391Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d8/8ff48a6beec92576d9fbae2b9b69db61503f062fb3bff4921495323a6847/fnv_hash_fast-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:1e8fb4c1cd62bc8d559dabeaf69fb25ba647232d980ffdb8e5f679d4aef8d03a", size = 15956, upload-time = "2025-10-04T19:45:29.418Z" }, + { url = "https://files.pythonhosted.org/packages/ef/17/9c724ac795f53578dd6be61d6a0466c4cd51550485b301764ddfc6ed5ad1/fnv_hash_fast-1.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:07bb79eaa44f91db2aab3b641194f68dc4ddd15701756f687c1a7a294bfa9c06", size = 13296, upload-time = "2025-10-04T19:45:31.164Z" }, + { url = "https://files.pythonhosted.org/packages/0c/11/a2eb0a7fbfb5d5cb5d27df7f6d4e395ce2f328da16d32702909af00ffe82/fnv_hash_fast-1.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4176315430f9fcf5346a0339b0f55982e1715452345d70c2887755bfd5aa2b64", size = 13879, upload-time = "2025-10-04T19:45:32.063Z" }, + { url = "https://files.pythonhosted.org/packages/0e/85/3a297faae2416916f7a5cb858b08b500296bbc7d7136faf2cfbadde61e33/fnv_hash_fast-1.6.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c31db9d944c91d286475870855b9203f4fb4794cb0674de5458e9d1231e07f37", size = 15222, upload-time = "2025-10-04T19:45:33.334Z" }, + { url = "https://files.pythonhosted.org/packages/e4/27/9c81426e4a22d15dc9c1a73536c6a7e2aeb8a71ac0b398d841ebd287e8e5/fnv_hash_fast-1.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6fc1bbec5871060c6efa6a444a554496f372f1f4a7e83b99989be5ea6b97435f", size = 16379, upload-time = "2025-10-04T19:45:34.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/7f1454ebc9dee224d6ee5360111e3855802ce79f48f1808117998771ffaa/fnv_hash_fast-1.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:91ed6df63ab2082b5b48a6b8f5d7eb7b51d39c2eeffd64821301bf6d9662ff11", size = 16252, upload-time = "2025-10-04T19:45:35.243Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5c/cedd70c2e09ba09f5834c7e50f8fed4a37bba38c0c2471849bb4dac91148/fnv_hash_fast-1.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6d34541e15bbc3877da7541f059fb1eadf53031abe7fc4318b28421e02eff383", size = 15570, upload-time = "2025-10-04T19:45:36.516Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/9c68ad33e254af9809bbd504b9895a93cb67472fc39bcd656f02c2703637/fnv_hash_fast-1.6.0-cp314-cp314-win32.whl", hash = "sha256:74320b9033c13e851174edf959c167619907eb985176e795d17d7fbe29cf3a45", size = 15484, upload-time = "2025-10-04T19:45:37.392Z" }, + { url = "https://files.pythonhosted.org/packages/e1/3a/8ead2c631323c8a755c8437641e832ba2eaf27bf2577535cf40d57b62def/fnv_hash_fast-1.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:540670ff837824939d2af90dd89cddbd02d238d778999a403cdb4a4de8c65a73", size = 16345, upload-time = "2025-10-04T19:45:38.345Z" }, + { url = "https://files.pythonhosted.org/packages/da/7a/b5bd2b9a06269098af059e79e05ceff320a405b1c49b9f3d29708324179b/fnv_hash_fast-1.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:83aa2d791193e3b3f4132741c4dc09eed4f7df8000d76ad77fb9d24db8e59a88", size = 21338, upload-time = "2025-10-04T19:45:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/21/07/1688d543a7688529857cd43bcff3ac324c69fd2923a9b40a1adc120cef20/fnv_hash_fast-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b8d33f002bb336f9f0949a32d7da07cc9d340a9d07e4f16cc9ece982842eb4e0", size = 22455, upload-time = "2025-10-04T19:45:40.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/f7/588f43d8dd122fc884c3556f993a3e3db953afecc62fa812d439f69ec067/fnv_hash_fast-1.6.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0042af2a1cb7ffae412ec3cb6ae8c581a73610fd523f7e17ed58a5359505ffec", size = 25053, upload-time = "2025-10-04T19:45:41.74Z" }, + { url = "https://files.pythonhosted.org/packages/e4/28/6209457f59e0ff43b066ca8cbfeb800bc0af478e221e74beadaf0b58effa/fnv_hash_fast-1.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73308e11c0e5a2dba433fc5645672de4756a52b323de1dab20e45d4fe5e83994", size = 27875, upload-time = "2025-10-04T19:45:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/4a/6e/c4796f6b1ee6cb778620663d00eadb970a8271fb537ce75774d5acfeecdb/fnv_hash_fast-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96282ecb75bec190af0111e82ddd38afc98e9cb867a1689e873ab6802af951b7", size = 27443, upload-time = "2025-10-04T19:45:44.274Z" }, + { url = "https://files.pythonhosted.org/packages/9c/5b/846b8f977dda4f0e7f1ec4ffff6707b9e666dabb9eb203c4c2bfc4b0b6fe/fnv_hash_fast-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cae16753c1d85ed358df13824bd8a474bfa9da34daddc1a90c72b25ff4177f51", size = 25765, upload-time = "2025-10-04T19:45:45.565Z" }, + { url = "https://files.pythonhosted.org/packages/2f/98/1371f0a765a3160a4c864de1b6d5ea696ba3ca822e3cea74357e15aca85d/fnv_hash_fast-1.6.0-cp314-cp314t-win32.whl", hash = "sha256:e2efb5953475a5a0529ca9757d6782c5174a3b8a3fbdc4e1c1273ac1d293316b", size = 26343, upload-time = "2025-10-04T19:45:46.566Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/99586ce163eeead7373db6dc3aa01998c42211ad11bbd7f6d21824fc5c80/fnv_hash_fast-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6eb03cd17c134d412fed9f05dc6f9ff9a8aa3b8e69c0135603a521e77720c93", size = 28057, upload-time = "2025-10-04T19:45:48.033Z" }, +] + +[[package]] +name = "fnvhash" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/01/14ef74ea03ac12e8a80d43bbad5356ae809b125cd2072766e459bcc7d388/fnvhash-0.1.0.tar.gz", hash = "sha256:3e82d505054f9f3987b2b5b649f7e7b6f48349f6af8a1b8e4d66779699c85a8e", size = 1902, upload-time = "2015-11-28T12:21:00.722Z" } + +[[package]] +name = "fnvhash" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/43/30d2dd2b14621b2004f658ba5335e5a6f5a9c1338ed37678d7fd247b7a9c/fnvhash-0.2.1.tar.gz", hash = "sha256:0c7e885f44c8f06de07f442befebc590ee9ca0cc88846681f608496284ce9cd5", size = 19057, upload-time = "2025-05-05T16:59:10.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/92/7c8abc21a1de7159013c0b0bd2ecf06530959bb14fd5c3bf0045e788c6d9/fnvhash-0.2.1-py3-none-any.whl", hash = "sha256:00fab14bec841e4cb29b4fd2ed9358f8bf9f4600d9d8149cde27a191193a33e8", size = 18115, upload-time = "2025-05-05T16:59:09.269Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/7d/5df2650c57d47c57232af5ef4b4fdbff182070421e405e0d62c6cdbfaa87/fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b", size = 310496, upload-time = "2026-01-09T15:21:35.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, +] + +[[package]] +name = "future" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" }, + { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" }, + { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" }, + { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" }, + { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, + { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, + { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, + { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, + { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, +] + +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" }, + { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" }, + { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" }, + { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" }, + { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" }, + { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" }, + { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" }, + { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" }, + { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" }, + { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" }, + { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" }, + { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "ha-ffmpeg" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/3b/bd1284a9bc39cc119b0da551a81be6cf30dc3cfb369ce8c62fb648d7a2ea/ha_ffmpeg-3.2.2.tar.gz", hash = "sha256:80e4a77b3eda73df456ec9cc3295a898ed7cbb8cd2d59798f10e8c10a8e6c401", size = 7608, upload-time = "2024-11-08T13:32:14.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/66/7863e5a3713bb71c02f050f14a751b02e7a2d50eaf2109c96a1202e65d8b/ha_ffmpeg-3.2.2-py3-none-any.whl", hash = "sha256:4fd4a4f4cdaf3243d2737942f3f41f141e4437d2af1167655815dc03283b1652", size = 8749, upload-time = "2024-11-08T13:32:12.69Z" }, +] + +[[package]] +name = "habluetooth" +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-interrupt", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "async-interrupt", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "bleak" }, + { name = "bleak-retry-connector" }, + { name = "bluetooth-adapters" }, + { name = "bluetooth-auto-recovery" }, + { name = "bluetooth-data-tools" }, + { name = "btsocket" }, + { name = "dbus-fast", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/89/0da109c6ed1704c991f9d466a3dbee99f8a921d006d6f44eac84fff95da7/habluetooth-5.8.0.tar.gz", hash = "sha256:7ecbe1ad6a4d3610f918dbe573bb9bee16064e7a4a61c95c37ef22b0c4533493", size = 49177, upload-time = "2025-12-02T19:30:53.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/e4/5f41bb8367db0f704bf27b7a346dda3e0e4965b1a2107c33af1078ca5271/habluetooth-5.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c5bd5ac723d2b38f5d56506408d56fa1de0f9a063410b6ce8061961849a1b63", size = 570041, upload-time = "2025-12-02T19:47:27.104Z" }, + { url = "https://files.pythonhosted.org/packages/4f/20/22dea6dd139f6addf78d02bbd840ba565353aee29ca812a2488b14b6b4ff/habluetooth-5.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e920fe6dd4fbb601f22043c205d3baaae5ac9804206d16a424cd19e00493d88d", size = 563353, upload-time = "2025-12-02T19:47:28.371Z" }, + { url = "https://files.pythonhosted.org/packages/39/e0/e2ddf93dbe2cbd536c9d66c7189ce394ca40133ab84420748e94afda7be0/habluetooth-5.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1086a8b76364e8c008b3ddc54e420f0aa74187231edfa987d362912cae339350", size = 656230, upload-time = "2025-12-02T19:47:29.712Z" }, + { url = "https://files.pythonhosted.org/packages/0b/af/be98ead575eced913fa396fd985ed47786db20ae11f9e1a3cc954161be40/habluetooth-5.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1702528676be2ebe17a2bd357fe7dc09f2855b70fd83de64392f040b18f404c5", size = 643444, upload-time = "2025-12-02T19:47:30.977Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6a/cd096b7c405b867a1864b5326c4a250942c8fa734ae319ba0e0ba485aab4/habluetooth-5.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b875a4de30861d3a9359c154fa21532c235d337ca48d203ea0dc74df3e60885c", size = 700091, upload-time = "2025-12-02T19:47:32.281Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3c/aebb70fdad26c7f547773cd95d381610606e0bd36c45bfc7652f593f08ac/habluetooth-5.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c184a80d342f01087478ba643e5e35e04c57a364e55b7f3092742d98a18707b3", size = 664128, upload-time = "2025-12-02T19:47:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5b/22ecdabd000c0cd1e9984f0dec257549b96e011a978b973797e024cc9063/habluetooth-5.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e431d2c833c83e748490b3d2bb37a4625d36c89e7d3faac03286b916fca910e3", size = 647860, upload-time = "2025-12-02T19:47:35.671Z" }, + { url = "https://files.pythonhosted.org/packages/88/ac/7e87cbd3f5a34361d37b23c8ec8ed48bea7c81246f01db88e2b41ad7f752/habluetooth-5.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cbbe296186c7e66ef79dca12e4e69326f8748db405e7203451119c9d7c1e5a2e", size = 705598, upload-time = "2025-12-02T19:47:37.102Z" }, + { url = "https://files.pythonhosted.org/packages/a2/36/bd836cb9943eb0370c4fd96c5db44d9349e7320a767384bc5afb2a11a075/habluetooth-5.8.0-cp312-cp312-win32.whl", hash = "sha256:7ad8929500e12df7860ed5ef1b984ead31484779c33f61d641ca5adc7677f52b", size = 455051, upload-time = "2025-12-02T19:47:38.765Z" }, + { url = "https://files.pythonhosted.org/packages/cd/61/2c8fd5a43e31b87247863efe5e4e60232accf002f288ccba7ca5cf97f6b9/habluetooth-5.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e039e84b02b7d31e9f7ab7b15937dfaa47f39b31bfb73b04772cf3e378d23430", size = 525980, upload-time = "2025-12-02T19:47:40.324Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/6bb983149530899647ee0807e5fc46124b56f0b326c6a952df48ed5c2b21/habluetooth-5.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8a88e3f586427642355927d43ad80190964589ae99d0d579ac4a39395d60f9a7", size = 566214, upload-time = "2025-12-02T19:47:41.767Z" }, + { url = "https://files.pythonhosted.org/packages/7c/16/18ff974967494402f827dab3d69d9a7d51d7d62153d8872206c0d4c4b058/habluetooth-5.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bb028a5d46fdead2ad687182db10667da201f54cc0f8eeba54870a3c2d1797f", size = 560005, upload-time = "2025-12-02T19:47:43.324Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d1/77ca3c8b3213f1f8429902329cad1b2e2a637cd32df70748ff5e187aad3b/habluetooth-5.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c425044ee873d5730571601097bb55020a7ff293cf96972e8924cf1719aa5345", size = 656376, upload-time = "2025-12-02T19:47:45.138Z" }, + { url = "https://files.pythonhosted.org/packages/f6/50/b06804e511552f1e5553b5abfb48f7bec795ffbe6c3290d95abfe35e51b4/habluetooth-5.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dc8baf5eee8835cfe436b14a6b4dd087f8134a8fd5d6ec4072ea992ab5aaae41", size = 639827, upload-time = "2025-12-02T19:47:46.447Z" }, + { url = "https://files.pythonhosted.org/packages/ab/34/88fde875eb1df89f870aa11aa1b0ab8e1cfca104cd8e9af802dd831c4068/habluetooth-5.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d56f3c73f5c74bfc3817a56343f5ef38ef4e584b825cf1d0d45e148b8fc2ce20", size = 699639, upload-time = "2025-12-02T19:47:48.144Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/74c352a234cf9faca007e1d43af79493de8ce19416b9bdddb9d2894d801b/habluetooth-5.8.0-cp313-cp313-manylinux_2_41_x86_64.whl", hash = "sha256:b651fa1d34a4086bd4bab27e528a0ea11dc310e806e86bd877c1b77a8b58ff7c", size = 698468, upload-time = "2025-12-02T19:30:51.699Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fe/8a28d94411c0cc19409091692ec893a603d71e66ada35c2c5db6c716f265/habluetooth-5.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:468e04e601a457d0097f57a2b16f65debfe4ed8b219270d52ebfba5cf0051b5c", size = 663619, upload-time = "2025-12-02T19:47:49.452Z" }, + { url = "https://files.pythonhosted.org/packages/7a/be/fb3d81a1cad478120f6ea7d5085eff98e1336331fbf1a7ceca11ca0a6982/habluetooth-5.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:91d34f82e36a2292aac5465db2c5b268d31df25303870af6687ad824275648bd", size = 647900, upload-time = "2025-12-02T19:47:50.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a0/1a738c6675d41193e80defe425f630c5a4db6da4ef5ee4cbce60fb998a28/habluetooth-5.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b25442a1ab5ba73c7da0726491a8a96637a546433f45d39f195415219bf11bef", size = 705834, upload-time = "2025-12-02T19:47:52.379Z" }, + { url = "https://files.pythonhosted.org/packages/ea/78/5bcd6c57f963a6b26ff725e32e8b0183a5a12e1e09f583e5bed481622555/habluetooth-5.8.0-cp313-cp313-win32.whl", hash = "sha256:ad59ce59ea06750aa7bb7905421db8654216a6b4a535d18f65b3aa9c23f892c0", size = 454143, upload-time = "2025-12-02T19:47:53.927Z" }, + { url = "https://files.pythonhosted.org/packages/46/99/519e5030747e4c3fed7ef3789a5c04e507208c7ec9ed5ec8007549c4f173/habluetooth-5.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:4a12f07f7d0c770da9586e6c50a444553c2921766a51281e383a4ad23dfdbe2b", size = 524424, upload-time = "2025-12-02T19:47:55.289Z" }, + { url = "https://files.pythonhosted.org/packages/00/9c/a219114293d6d0b83d2c2b3cb505125de96edb73037b9f4f71c203e0b231/habluetooth-5.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:12148fb6c41a4464c1336cd5bc127ed1c97cbfd0f2bedb2dbb16fecadd54e1c0", size = 568992, upload-time = "2025-12-02T19:47:56.921Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/5783e448e310f235550ef5b682d0518ea196e19f82484815943fee69640e/habluetooth-5.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7962af2e456676df1c09eb0e7a95b5c27696b76a82c2971a0dff25f017168453", size = 566035, upload-time = "2025-12-02T19:47:58.294Z" }, + { url = "https://files.pythonhosted.org/packages/97/60/80ea750a3af4d84455315eb038100e49419d8c5b8b14bb966a46ba8a8971/habluetooth-5.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84abc0819e462e6210620778ff9cf6ace8df04b6e9c04f41dd56abdaf9dade72", size = 666209, upload-time = "2025-12-02T19:47:59.656Z" }, + { url = "https://files.pythonhosted.org/packages/2c/58/0b45dfffbabfa5982e9721946e43f2fc7ff982c8d6598cb148c527797983/habluetooth-5.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a74652fe7ae5cb571138832cdf16945d17a9dc564e7c48e0c2173ca9796a7e90", size = 632911, upload-time = "2025-12-02T19:48:01.028Z" }, + { url = "https://files.pythonhosted.org/packages/25/0c/e3c9158eb988917f00fc93001a86bfccdf59c383ad95d90a5f897121bf98/habluetooth-5.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ddbe3e7c03d5df6d6064cd622d2235f1db2cb19282705d48393535ed1f3547b", size = 703092, upload-time = "2025-12-02T19:48:02.358Z" }, + { url = "https://files.pythonhosted.org/packages/1a/13/79858645697ebd3b87819d8b08a168d49532dccf38a0d8a66def0c6744b2/habluetooth-5.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c5f650be8b36e47016f418c73b36011754c742c0e4b8975b178a99fb40f94900", size = 674337, upload-time = "2025-12-02T19:48:03.643Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d3/55793994bd8500e459e5b5ecd1290b77c87d978df4a93a08abe031c353fd/habluetooth-5.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6c0ffe030bef47c268c658c2e0a1dcf8db3841727761f617953caee2831af6c", size = 637932, upload-time = "2025-12-02T19:48:05.126Z" }, + { url = "https://files.pythonhosted.org/packages/60/bf/118c76a8fda261ae42d2d15696c4277ec1a1c4da47c5ab030f7b50cce53b/habluetooth-5.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2248235680aa591bcba3fd626f859cb0fd89d84e03f1a596cf281d6802ba3d1b", size = 709054, upload-time = "2025-12-02T19:48:06.903Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b5/15a3807d972658bf22ee90f7bcc91ed8b470c2f9615f3c43eed836174bc9/habluetooth-5.8.0-cp314-cp314-win32.whl", hash = "sha256:56718953e05300e633f1f1f4588aa2b81ea327dc276228d21d0472038596352b", size = 463435, upload-time = "2025-12-02T19:48:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/1f/dd/5f761526da52753c14f1852137cd14cd9e2372266589eeda9d6ab7d09eaf/habluetooth-5.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:b4f48cf6485a39ae72eb3ce68b46ee5b991ab28fdb4c5daea97bfa2dfb17e432", size = 536193, upload-time = "2025-12-02T19:48:10.325Z" }, + { url = "https://files.pythonhosted.org/packages/46/2a/e8d31c5a53c1aa9c2af9bbe1760ee1976b9642828d31b9b8b5aa4a01245b/habluetooth-5.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aded66250091ca6f05740317b295197d9387ca88bcec138e01f7fbd145d3406c", size = 1124956, upload-time = "2025-12-02T19:48:11.74Z" }, + { url = "https://files.pythonhosted.org/packages/39/a0/41ebef6913efcfe1ebe22c62423a013d2978c0a890bb849f4702ab8fbee5/habluetooth-5.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c3a5dc4f2e01931fce07018ccc46584483c44fc24369098ec8af253def6421db", size = 1123851, upload-time = "2025-12-02T19:48:13.202Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cf/68ab568ac01c054d32ecbd4790fceafd8587d22dd3382c441a905d993e29/habluetooth-5.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:289af0d5a67bef760203d3cbdfa9e9aa9cc1f7da29112dbc50474a16f3d66735", size = 1275887, upload-time = "2025-12-02T19:48:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/3e68a905858e9945027b49f872bd6d6c92a916fd26f1c9c16e0b1056609b/habluetooth-5.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d96008b6c09d3617016801aa4dba74d065e118d9a4cb84e913e16fa3f379e597", size = 1192706, upload-time = "2025-12-02T19:48:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bb/d7483c7e57f0c34acb6bfb83fd32bac2c31ffbfc111b7511dffd1610c752/habluetooth-5.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e569435c561e79267dd6b2e0718d5411c1face47a810bb1d8fe42a7a9a9d8ee4", size = 1335960, upload-time = "2025-12-02T19:48:17.911Z" }, + { url = "https://files.pythonhosted.org/packages/27/6d/03fda3288621b9ba3221f37b1e2c877b2e73fd0f82591f751a058e83b2ef/habluetooth-5.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:60b5eed41f69ce6ca6c98df7c5f3ba1185737f2008225cd19e43bef0dab55074", size = 1294245, upload-time = "2025-12-02T19:48:19.437Z" }, + { url = "https://files.pythonhosted.org/packages/d3/df/8d2df48c24c175f2dafa7026f8fb0f24793e69cc4e73f01f51ac5c5480c7/habluetooth-5.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:63b6d82223d64d19cd2feb23fafa7246b42574c628014ce21b010bb31fced5f1", size = 1215522, upload-time = "2025-12-02T19:48:21.197Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/5c0f86a9f35593102ca2a0a7fef6943e1400a0ea97fa9f296aec945b5992/habluetooth-5.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a106c1809516c86c7ff10a78ac68017dc23f73e5613735295eac628ca95a97b", size = 1350369, upload-time = "2025-12-02T19:48:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/ec8ae0810fafb3ff5ff00e312f0b99da91f22e547e2bc2b453f730b399ec/habluetooth-5.8.0-cp314-cp314t-win32.whl", hash = "sha256:ab75ee35c050d872377afd9ed48372538b218edc52a911aae32c5e58c5ae896b", size = 958959, upload-time = "2025-12-02T19:48:24.521Z" }, + { url = "https://files.pythonhosted.org/packages/fd/92/32d8279f955c2e5dd216a2a13ad4953bb1fd9a62512b280458dab07ad088/habluetooth-5.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:635ee560a003f884230d9600e536da4466bd632ae90572a454498b4795926271", size = 1129467, upload-time = "2025-12-02T19:48:25.979Z" }, +] + +[[package]] +name = "hass-nabucasa" +version = "0.86.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "acme", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "aiohttp", version = "3.11.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "atomicwrites-homeassistant", marker = "python_full_version < '3.13'" }, + { name = "attrs", version = "24.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "ciso8601", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "pycognito", marker = "python_full_version < '3.13'" }, + { name = "pyjwt", marker = "python_full_version < '3.13'" }, + { name = "snitun", version = "0.39.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "webrtc-models", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/4a/46bdeada82c7ea394c47c3d33a8b50a53b5c45330b8b26f54fd304161ec7/hass_nabucasa-0.86.0.tar.gz", hash = "sha256:d6951af95796a92a6e7e6d64f7ddac1ecbdda65f3d65374c8f3216650a579b89", size = 65176, upload-time = "2024-12-05T15:50:35.003Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/97/0691cf7e92fcc283b2ce50bb658de51b2a8fe8121b7129fec56c5200fa8d/hass_nabucasa-0.86.0-py3-none-any.whl", hash = "sha256:75153e438a451ea9653304f6011719974911325bd7dca5f0ba6297491901c8b0", size = 55818, upload-time = "2024-12-05T15:50:33.556Z" }, +] + +[[package]] +name = "hass-nabucasa" +version = "0.94.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "acme", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiohttp", version = "3.11.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "async-timeout", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "atomicwrites-homeassistant", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "attrs", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "ciso8601", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pycognito", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyjwt", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "snitun", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "webrtc-models", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/33/ac792655794278f3d429b821a5a6f4628eb9de75fdacc222da0a13cfc9a0/hass_nabucasa-0.94.0.tar.gz", hash = "sha256:2ae8ca877dbd7c128fd49f64383e69bd86a395ed175cf73d6f33478d07491947", size = 72357, upload-time = "2025-03-03T10:54:57.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/cd/fabe44c4513600e153082e15a0b98c69d728e8fa0ed5bac7658b330f8979/hass_nabucasa-0.94.0-py3-none-any.whl", hash = "sha256:5acbe999373b81e7f6cc8d2f5918fe9db0a9e18e52e6ddb19d7857818c039c46", size = 61929, upload-time = "2025-03-03T10:54:55.93Z" }, +] + +[[package]] +name = "hass-nabucasa" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "acme", version = "5.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "aiohttp", version = "3.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "async-timeout", marker = "python_full_version >= '3.13.2'" }, + { name = "atomicwrites-homeassistant", marker = "python_full_version >= '3.13.2'" }, + { name = "attrs", version = "25.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "ciso8601", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "grpcio", marker = "python_full_version >= '3.13.2'" }, + { name = "josepy", version = "2.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "litellm", marker = "python_full_version >= '3.13.2'" }, + { name = "pycognito", marker = "python_full_version >= '3.13.2'" }, + { name = "pyjwt", marker = "python_full_version >= '3.13.2'" }, + { name = "sentence-stream", marker = "python_full_version >= '3.13.2'" }, + { name = "snitun", version = "0.45.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "voluptuous", version = "0.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "webrtc-models", marker = "python_full_version >= '3.13.2'" }, + { name = "yarl", version = "1.22.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/63/132c981f9615db0681d2a08a6af3294b6568af833df7d5075368f41b9518/hass_nabucasa-1.7.0.tar.gz", hash = "sha256:a6d25a02a538e316625ea48c44e70def10687def7b05e08da91da72004dd3ea4", size = 106571, upload-time = "2025-12-03T12:24:40.352Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/9a/c3295290c3f616cd2271157971357e085a50d858a1bed6862ecf13a967b9/hass_nabucasa-1.7.0-py3-none-any.whl", hash = "sha256:2256974ce3f9b4f170ed19e902427557d08ea2ac2b88c5feafd6a572d3e552ed", size = 82666, upload-time = "2025-12-03T12:24:38.71Z" }, +] + +[[package]] +name = "hassil" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "unicode-rbnf", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/f4/bf2f642321114c4ca4586efb194274905388a09b1c95e52529eba2fd4d51/hassil-2.2.3.tar.gz", hash = "sha256:8516ebde2caf72362ea566cd677cb382138be3f5d36889fee21bb313bfd7d0d8", size = 46867, upload-time = "2025-02-04T17:36:22.142Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/ae/684cf7117bdd757bb7d92c20deb528db2d42a3d018fc788f1c415421d809/hassil-2.2.3-py3-none-any.whl", hash = "sha256:d22032c5268e6bdfc7fb60fa8f52f3a955d5ca982ccbfe535ed074c593e66bdf", size = 42097, upload-time = "2025-02-04T17:36:21.09Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +] + +[[package]] +name = "home-assistant-bluetooth" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "habluetooth", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/86/2e339e75b8e00c121e3de766718bc4363b3c41b3b0d6c9cb666479dddb3c/home_assistant_bluetooth-1.13.0.tar.gz", hash = "sha256:3fa8a0d05a844063501a37e0b98501337e7035623b345d5c285a778e9416fd93", size = 7760, upload-time = "2024-10-05T23:12:03.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/c9/96c4491583c328773873887065960a538da90fc08aa2a7e81cfec738bb2a/home_assistant_bluetooth-1.13.0-py3-none-any.whl", hash = "sha256:caec3d6ced580d3bd015ab74a9ee7e91693650d0548637c5f294101167fc6e82", size = 7915, upload-time = "2024-10-05T23:12:01.65Z" }, +] + +[[package]] +name = "home-assistant-bluetooth" +version = "1.13.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "habluetooth", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/0e/c05ee603cab1adb847a305bc8f1034cbdbc0a5d15169fcf68c0d6d21e33f/home_assistant_bluetooth-1.13.1.tar.gz", hash = "sha256:0ae0e2a8491cc762ee9e694b8bc7665f1e2b4618926f63969a23a2e3a48ce55e", size = 7607, upload-time = "2025-02-04T16:11:15.259Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/9b/9904cec885cc32c45e8c22cd7e19d9c342e30074fdb7c58f3d5b33ea1adb/home_assistant_bluetooth-1.13.1-py3-none-any.whl", hash = "sha256:cdf13b5b45f7744165677831e309ee78fbaf0c2866c6b5931e14d1e4e7dae5d7", size = 7915, upload-time = "2025-02-04T16:11:13.163Z" }, +] + +[[package]] +name = "home-assistant-intents" +version = "2025.3.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/f1/9c13e5535bbcf4801f81d88f452581b113246e485d8ff9f9d64faffcf50f/home_assistant_intents-2025.3.28.tar.gz", hash = "sha256:3b93717525ae738f9163a2215bb0628321b86bd8418bfd64e1d5ce571b84fef4", size = 451905, upload-time = "2025-03-28T14:26:00.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/e5/627c5cb34ed05bbe3227834702327fab6cbed6c5d6f0c6f053a85cc2b10f/home_assistant_intents-2025.3.28-py3-none-any.whl", hash = "sha256:14f589a5a188f8b0c52f06ff8998c171fda25f8729de7a4011636295d90e7295", size = 470049, upload-time = "2025-03-28T14:25:59.107Z" }, +] + +[[package]] +name = "homeassistant" +version = "2024.12.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "aiodns", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "aiohasupervisor", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "aiohttp", version = "3.11.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "aiohttp-cors", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "aiohttp-fast-zlib", version = "0.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "aiozoneinfo", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "astral", marker = "python_full_version < '3.13'" }, + { name = "async-interrupt", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "atomicwrites-homeassistant", marker = "python_full_version < '3.13'" }, + { name = "attrs", version = "24.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "awesomeversion", version = "24.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "bcrypt", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "certifi", marker = "python_full_version < '3.13'" }, + { name = "ciso8601", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "fnv-hash-fast", version = "1.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "hass-nabucasa", version = "0.86.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "home-assistant-bluetooth", version = "1.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "httpx", version = "0.27.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "ifaddr", marker = "python_full_version < '3.13'" }, + { name = "jinja2", version = "3.1.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "lru-dict", marker = "python_full_version < '3.13'" }, + { name = "orjson", version = "3.10.12", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "packaging", marker = "python_full_version < '3.13'" }, + { name = "pillow", version = "11.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "propcache", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "psutil-home-assistant", marker = "python_full_version < '3.13'" }, + { name = "pyjwt", marker = "python_full_version < '3.13'" }, + { name = "pyopenssl", version = "24.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "python-slugify", marker = "python_full_version < '3.13'" }, + { name = "pyyaml", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "requests", version = "2.32.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "securetar", version = "2024.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "sqlalchemy", version = "2.0.36", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "ulid-transform", version = "1.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "uv", version = "0.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "voluptuous", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "voluptuous-openapi", version = "0.0.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "voluptuous-serialize", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "webrtc-models", marker = "python_full_version < '3.13'" }, + { name = "yarl", version = "1.18.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/d8/973badc57e1e494c01e184b817bcea4c264c35a2cbefd18d5c845fbb5cc9/homeassistant-2024.12.5.tar.gz", hash = "sha256:3df79d8a02561cbbf606a1ce81579d0f2271c714a67477b60af3cd6025a2e9a0", size = 22691228, upload-time = "2024-12-20T11:05:11.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/c4/e2ffd3e6701f0e729d5ebc6b9e47061e4e7bd97c86872f57345d3493ea85/homeassistant-2024.12.5-py3-none-any.whl", hash = "sha256:a7b3e13ac8c2b93a79807c51e3715b36c8459c9cbddb8bf448d69d18a49b703d", size = 38984357, upload-time = "2024-12-20T11:05:03.426Z" }, +] + +[[package]] +name = "homeassistant" +version = "2025.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "aiodns", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiohasupervisor", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiohttp", version = "3.11.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiohttp-asyncmdnsresolver", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiohttp-cors", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiohttp-fast-zlib", version = "0.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "aiozoneinfo", version = "0.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "annotatedyaml", version = "0.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "astral", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "async-interrupt", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "atomicwrites-homeassistant", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "attrs", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "awesomeversion", version = "24.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "bcrypt", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "certifi", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "ciso8601", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "cronsim", version = "2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "fnv-hash-fast", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "ha-ffmpeg", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "hass-nabucasa", version = "0.94.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "hassil", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "home-assistant-bluetooth", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "home-assistant-intents", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "httpx", version = "0.28.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "ifaddr", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "jinja2", version = "3.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "lru-dict", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "mutagen", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "numpy", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "orjson", version = "3.10.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "packaging", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pillow", version = "11.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "propcache", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "psutil-home-assistant", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyjwt", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pymicro-vad", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyopenssl", version = "25.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyspeex-noise", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "python-slugify", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyturbojpeg", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyyaml", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "requests", version = "2.32.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "securetar", version = "2025.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "sqlalchemy", version = "2.0.39", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "standard-aifc", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "standard-telnetlib", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "ulid-transform", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "uv", version = "0.6.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "voluptuous", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "voluptuous-openapi", version = "0.0.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "voluptuous-serialize", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "webrtc-models", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "yarl", version = "1.18.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "zeroconf", version = "0.146.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/09/9a651106e22f9ca121c6586106ab857e17da89db7258070695df231cbf59/homeassistant-2025.4.4.tar.gz", hash = "sha256:a8e84eb3a236271ffb19655e08127a77bef0dd10858ecbc5c4351b47ad1471fd", size = 25116020, upload-time = "2025-04-25T09:19:13.986Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/a9/2e778738a2c4e45d795a076796b341e8af6d3919e591b0fafa1ff2564702/homeassistant-2025.4.4-py3-none-any.whl", hash = "sha256:b7970fe3ce3a9d032a3a5dee3cbf71b227e7f23fad14a23855ee252542ef2631", size = 43206054, upload-time = "2025-04-25T09:19:07.669Z" }, +] + +[[package]] +name = "homeassistant" +version = "2026.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "aiodns", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "aiohasupervisor", version = "0.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "aiohttp", version = "3.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "aiohttp-asyncmdnsresolver", marker = "python_full_version >= '3.13.2'" }, + { name = "aiohttp-cors", version = "0.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "aiohttp-fast-zlib", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "aiozoneinfo", version = "0.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "annotatedyaml", version = "1.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "astral", marker = "python_full_version >= '3.13.2'" }, + { name = "async-interrupt", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "atomicwrites-homeassistant", marker = "python_full_version >= '3.13.2'" }, + { name = "attrs", version = "25.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13.2'" }, + { name = "awesomeversion", version = "25.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "bcrypt", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "certifi", marker = "python_full_version >= '3.13.2'" }, + { name = "ciso8601", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "cronsim", version = "2.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "fnv-hash-fast", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "hass-nabucasa", version = "1.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "home-assistant-bluetooth", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "httpx", version = "0.28.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "ifaddr", marker = "python_full_version >= '3.13.2'" }, + { name = "jinja2", version = "3.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "lru-dict", marker = "python_full_version >= '3.13.2'" }, + { name = "orjson", version = "3.11.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "packaging", marker = "python_full_version >= '3.13.2'" }, + { name = "pillow", version = "12.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "propcache", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "psutil-home-assistant", marker = "python_full_version >= '3.13.2'" }, + { name = "pyjwt", marker = "python_full_version >= '3.13.2'" }, + { name = "pyopenssl", version = "25.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "python-slugify", marker = "python_full_version >= '3.13.2'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "securetar", version = "2025.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "sqlalchemy", version = "2.0.41", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "standard-aifc", marker = "python_full_version >= '3.13.2'" }, + { name = "standard-telnetlib", marker = "python_full_version >= '3.13.2'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, + { name = "ulid-transform", version = "1.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "uv", version = "0.9.17", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "voluptuous", version = "0.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "voluptuous-openapi", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "voluptuous-serialize", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "webrtc-models", marker = "python_full_version >= '3.13.2'" }, + { name = "yarl", version = "1.22.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "zeroconf", version = "0.148.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/96/b2ccc2497cda9085f53106a01164da3eed57f6065f05bdcf0a3c0167e1a2/homeassistant-2026.1.0.tar.gz", hash = "sha256:0faa7fa4c37b8ccf1eedc91794c69042f8f8559f2e7c205f1c279068262bdec6", size = 30167957, upload-time = "2026-01-07T18:15:25.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/3f/57e3c7e3daf7cc8509eb139f4faca9198474f8c4f50871d8fe9479adf3da/homeassistant-2026.1.0-py3-none-any.whl", hash = "sha256:29148b15f12fdf398ed8290ad1c23e5241011b225d2f96febcc7390855f18061", size = 50115149, upload-time = "2026-01-07T18:15:19.236Z" }, +] + +[[package]] +name = "htmltools" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/1d/c568d17e9fb5ad5aa0ca3531c58d36fe69deb8eee4e53ff6425c1f99f210/htmltools-0.6.0.tar.gz", hash = "sha256:e8a3fb023d748935035db7ff17f620612ffc814a6a80b6ae388f7b7ab182adf7", size = 97152, upload-time = "2024-10-29T20:21:43.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/ba/aa99706246f1938ca905eb6eeb7db832ac2e157aa4b805acb5cd4cd1791a/htmltools-0.6.0-py3-none-any.whl", hash = "sha256:072a274ff5e2851e0acce13fc5bb2bbdbbad8268dc8b123f881c05012ce7dce0", size = 84954, upload-time = "2024-10-29T20:21:42.067Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.27.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version < '3.13'" }, + { name = "certifi", marker = "python_full_version < '3.13'" }, + { name = "httpcore", marker = "python_full_version < '3.13'" }, + { name = "idna", marker = "python_full_version < '3.13'" }, + { name = "sniffio", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189, upload-time = "2024-08-27T12:54:01.334Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395, upload-time = "2024-08-27T12:53:59.653Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.13'" }, + { name = "certifi", marker = "python_full_version >= '3.13'" }, + { name = "httpcore", marker = "python_full_version >= '3.13'" }, + { name = "idna", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", marker = "python_full_version >= '3.13.2'" }, + { name = "fsspec", marker = "python_full_version >= '3.13.2'" }, + { name = "hf-xet", marker = "(python_full_version >= '3.13.2' and platform_machine == 'AMD64') or (python_full_version >= '3.13.2' and platform_machine == 'aarch64') or (python_full_version >= '3.13.2' and platform_machine == 'amd64') or (python_full_version >= '3.13.2' and platform_machine == 'arm64') or (python_full_version >= '3.13.2' and platform_machine == 'x86_64')" }, + { name = "httpx", version = "0.28.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "packaging", marker = "python_full_version >= '3.13.2'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "shellingham", marker = "python_full_version >= '3.13.2'" }, + { name = "tqdm", marker = "python_full_version >= '3.13.2'" }, + { name = "typer-slim", marker = "python_full_version >= '3.13.2'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/dd/1cc985c5dda36298b152f75e82a1c81f52243b78fb7e9cad637a29561ad1/huggingface_hub-1.3.1.tar.gz", hash = "sha256:e80e0cfb4a75557c51ab20d575bdea6bb6106c2f97b7c75d8490642f1efb6df5", size = 622356, upload-time = "2026-01-09T14:08:16.888Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/fb/cb8fe5f71d5622427f20bcab9e06a696a5aaf21bfe7bd0a8a0c63c88abf5/huggingface_hub-1.3.1-py3-none-any.whl", hash = "sha256:efbc7f3153cb84e2bb69b62ed90985e21ecc9343d15647a419fc0ee4b85f0ac3", size = 533351, upload-time = "2026-01-09T14:08:14.519Z" }, +] + +[[package]] +name = "identify" +version = "2.6.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "ifaddr" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4", size = 10485, upload-time = "2022-06-15T21:40:27.561Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "markupsafe", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245, upload-time = "2024-05-05T23:42:02.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271, upload-time = "2024-05-05T23:41:59.928Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "markupsafe", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, + { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, + { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, + { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, + { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, + { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, + { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, + { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, + { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, + { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, + { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, + { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, + { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, + { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, + { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, +] + +[[package]] +name = "josepy" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +dependencies = [ + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "pyopenssl", version = "24.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "pyopenssl", version = "25.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/8a/cd416f56cd4492878e8d62701b4ad32407c5ce541f247abf31d6e5f3b79b/josepy-1.15.0.tar.gz", hash = "sha256:46c9b13d1a5104ffbfa5853e555805c915dcde71c2cd91ce5386e84211281223", size = 59310, upload-time = "2025-01-22T23:56:23.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/74/fc54f4b03cb66b0b351131fcf1797fe9d7c1e6ce9a38fd940d9bc2d9531b/josepy-1.15.0-py3-none-any.whl", hash = "sha256:878c08cedd0a892c98c6d1a90b3cb869736f9c751f68ec8901e7b05a0c040fed", size = 32774, upload-time = "2025-01-22T23:56:21.524Z" }, +] + +[[package]] +name = "josepy" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/ad/6f520aee9cc9618d33430380741e9ef859b2c560b1e7915e755c084f6bc0/josepy-2.2.0.tar.gz", hash = "sha256:74c033151337c854f83efe5305a291686cef723b4b970c43cfe7270cf4a677a9", size = 56500, upload-time = "2025-10-14T14:54:42.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/b2/b5caed897fbb1cc286c62c01feca977e08d99a17230ff3055b9a98eccf1d/josepy-2.2.0-py3-none-any.whl", hash = "sha256:63e9dd116d4078778c25ca88f880cc5d95f1cab0099bebe3a34c2e299f65d10b", size = 29211, upload-time = "2025-10-14T14:54:41.144Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", version = "25.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "jsonschema-specifications", marker = "python_full_version >= '3.13.2'" }, + { name = "referencing", marker = "python_full_version >= '3.13.2'" }, + { name = "rpds-py", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "librt" +version = "0.7.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/29/47f29026ca17f35cf299290292d5f8331f5077364974b7675a353179afa2/librt-0.7.7.tar.gz", hash = "sha256:81d957b069fed1890953c3b9c3895c7689960f233eea9a1d9607f71ce7f00b2c", size = 145910, upload-time = "2026-01-01T23:52:22.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/72/1cd9d752070011641e8aee046c851912d5f196ecd726fffa7aed2070f3e0/librt-0.7.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a85a1fc4ed11ea0eb0a632459ce004a2d14afc085a50ae3463cd3dfe1ce43fc", size = 55687, upload-time = "2026-01-01T23:51:16.291Z" }, + { url = "https://files.pythonhosted.org/packages/50/aa/d5a1d4221c4fe7e76ae1459d24d6037783cb83c7645164c07d7daf1576ec/librt-0.7.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c87654e29a35938baead1c4559858f346f4a2a7588574a14d784f300ffba0efd", size = 57136, upload-time = "2026-01-01T23:51:17.363Z" }, + { url = "https://files.pythonhosted.org/packages/23/6f/0c86b5cb5e7ef63208c8cc22534df10ecc5278efc0d47fb8815577f3ca2f/librt-0.7.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c9faaebb1c6212c20afd8043cd6ed9de0a47d77f91a6b5b48f4e46ed470703fe", size = 165320, upload-time = "2026-01-01T23:51:18.455Z" }, + { url = "https://files.pythonhosted.org/packages/16/37/df4652690c29f645ffe405b58285a4109e9fe855c5bb56e817e3e75840b3/librt-0.7.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1908c3e5a5ef86b23391448b47759298f87f997c3bd153a770828f58c2bb4630", size = 174216, upload-time = "2026-01-01T23:51:19.599Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d6/d3afe071910a43133ec9c0f3e4ce99ee6df0d4e44e4bddf4b9e1c6ed41cc/librt-0.7.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbc4900e95a98fc0729523be9d93a8fedebb026f32ed9ffc08acd82e3e181503", size = 189005, upload-time = "2026-01-01T23:51:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/74060a870fe2d9fd9f47824eba6717ce7ce03124a0d1e85498e0e7efc1b2/librt-0.7.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7ea4e1fbd253e5c68ea0fe63d08577f9d288a73f17d82f652ebc61fa48d878d", size = 183961, upload-time = "2026-01-01T23:51:22.493Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5e/918a86c66304af66a3c1d46d54df1b2d0b8894babc42a14fb6f25511497f/librt-0.7.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef7699b7a5a244b1119f85c5bbc13f152cd38240cbb2baa19b769433bae98e50", size = 177610, upload-time = "2026-01-01T23:51:23.874Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d7/b5e58dc2d570f162e99201b8c0151acf40a03a39c32ab824dd4febf12736/librt-0.7.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:955c62571de0b181d9e9e0a0303c8bc90d47670a5eff54cf71bf5da61d1899cf", size = 199272, upload-time = "2026-01-01T23:51:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/87/8202c9bd0968bdddc188ec3811985f47f58ed161b3749299f2c0dd0f63fb/librt-0.7.7-cp312-cp312-win32.whl", hash = "sha256:1bcd79be209313b270b0e1a51c67ae1af28adad0e0c7e84c3ad4b5cb57aaa75b", size = 43189, upload-time = "2026-01-01T23:51:26.799Z" }, + { url = "https://files.pythonhosted.org/packages/61/8d/80244b267b585e7aa79ffdac19f66c4861effc3a24598e77909ecdd0850e/librt-0.7.7-cp312-cp312-win_amd64.whl", hash = "sha256:4353ee891a1834567e0302d4bd5e60f531912179578c36f3d0430f8c5e16b456", size = 49462, upload-time = "2026-01-01T23:51:27.813Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1f/75db802d6a4992d95e8a889682601af9b49d5a13bbfa246d414eede1b56c/librt-0.7.7-cp312-cp312-win_arm64.whl", hash = "sha256:a76f1d679beccccdf8c1958e732a1dfcd6e749f8821ee59d7bec009ac308c029", size = 42828, upload-time = "2026-01-01T23:51:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5e/d979ccb0a81407ec47c14ea68fb217ff4315521730033e1dd9faa4f3e2c1/librt-0.7.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4a0b0a3c86ba9193a8e23bb18f100d647bf192390ae195d84dfa0a10fb6244", size = 55746, upload-time = "2026-01-01T23:51:29.828Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/3b65861fb32f802c3783d6ac66fc5589564d07452a47a8cf9980d531cad3/librt-0.7.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5335890fea9f9e6c4fdf8683061b9ccdcbe47c6dc03ab8e9b68c10acf78be78d", size = 57174, upload-time = "2026-01-01T23:51:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/030b50614b29e443607220097ebaf438531ea218c7a9a3e21ea862a919cd/librt-0.7.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b4346b1225be26def3ccc6c965751c74868f0578cbcba293c8ae9168483d811", size = 165834, upload-time = "2026-01-01T23:51:32.278Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e1/bd8d1eacacb24be26a47f157719553bbd1b3fe812c30dddf121c0436fd0b/librt-0.7.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a10b8eebdaca6e9fdbaf88b5aefc0e324b763a5f40b1266532590d5afb268a4c", size = 174819, upload-time = "2026-01-01T23:51:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/91d6c3372acf54a019c1ad8da4c9ecf4fc27d039708880bf95f48dbe426a/librt-0.7.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:067be973d90d9e319e6eb4ee2a9b9307f0ecd648b8a9002fa237289a4a07a9e7", size = 189607, upload-time = "2026-01-01T23:51:34.604Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ac/44604d6d3886f791fbd1c6ae12d5a782a8f4aca927484731979f5e92c200/librt-0.7.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23d2299ed007812cccc1ecef018db7d922733382561230de1f3954db28433977", size = 184586, upload-time = "2026-01-01T23:51:35.845Z" }, + { url = "https://files.pythonhosted.org/packages/5c/26/d8a6e4c17117b7f9b83301319d9a9de862ae56b133efb4bad8b3aa0808c9/librt-0.7.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b6f8ea465524aa4c7420c7cc4ca7d46fe00981de8debc67b1cc2e9957bb5b9d", size = 178251, upload-time = "2026-01-01T23:51:37.018Z" }, + { url = "https://files.pythonhosted.org/packages/99/ab/98d857e254376f8e2f668e807daccc1f445e4b4fc2f6f9c1cc08866b0227/librt-0.7.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8df32a99cc46eb0ee90afd9ada113ae2cafe7e8d673686cf03ec53e49635439", size = 199853, upload-time = "2026-01-01T23:51:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/7c/55/4523210d6ae5134a5da959900be43ad8bab2e4206687b6620befddb5b5fd/librt-0.7.7-cp313-cp313-win32.whl", hash = "sha256:86f86b3b785487c7760247bcdac0b11aa8bf13245a13ed05206286135877564b", size = 43247, upload-time = "2026-01-01T23:51:39.629Z" }, + { url = "https://files.pythonhosted.org/packages/25/40/3ec0fed5e8e9297b1cf1a3836fb589d3de55f9930e3aba988d379e8ef67c/librt-0.7.7-cp313-cp313-win_amd64.whl", hash = "sha256:4862cb2c702b1f905c0503b72d9d4daf65a7fdf5a9e84560e563471e57a56949", size = 49419, upload-time = "2026-01-01T23:51:40.674Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/aab5f0fb122822e2acbc776addf8b9abfb4944a9056c00c393e46e543177/librt-0.7.7-cp313-cp313-win_arm64.whl", hash = "sha256:0996c83b1cb43c00e8c87835a284f9057bc647abd42b5871e5f941d30010c832", size = 42828, upload-time = "2026-01-01T23:51:41.731Z" }, + { url = "https://files.pythonhosted.org/packages/69/9c/228a5c1224bd23809a635490a162e9cbdc68d99f0eeb4a696f07886b8206/librt-0.7.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:23daa1ab0512bafdd677eb1bfc9611d8ffbe2e328895671e64cb34166bc1b8c8", size = 55188, upload-time = "2026-01-01T23:51:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c2/0e7c6067e2b32a156308205e5728f4ed6478c501947e9142f525afbc6bd2/librt-0.7.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:558a9e5a6f3cc1e20b3168fb1dc802d0d8fa40731f6e9932dcc52bbcfbd37111", size = 56895, upload-time = "2026-01-01T23:51:44.534Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/de50ff70c80855eb79d1d74035ef06f664dd073fb7fb9d9fb4429651b8eb/librt-0.7.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2567cb48dc03e5b246927ab35cbb343376e24501260a9b5e30b8e255dca0d1d2", size = 163724, upload-time = "2026-01-01T23:51:45.571Z" }, + { url = "https://files.pythonhosted.org/packages/6e/19/f8e4bf537899bdef9e0bb9f0e4b18912c2d0f858ad02091b6019864c9a6d/librt-0.7.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6066c638cdf85ff92fc6f932d2d73c93a0e03492cdfa8778e6d58c489a3d7259", size = 172470, upload-time = "2026-01-01T23:51:46.823Z" }, + { url = "https://files.pythonhosted.org/packages/42/4c/dcc575b69d99076768e8dd6141d9aecd4234cba7f0e09217937f52edb6ed/librt-0.7.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a609849aca463074c17de9cda173c276eb8fee9e441053529e7b9e249dc8b8ee", size = 186806, upload-time = "2026-01-01T23:51:48.009Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f8/4094a2b7816c88de81239a83ede6e87f1138477d7ee956c30f136009eb29/librt-0.7.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:add4e0a000858fe9bb39ed55f31085506a5c38363e6eb4a1e5943a10c2bfc3d1", size = 181809, upload-time = "2026-01-01T23:51:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/821b7c0ab1b5a6cd9aee7ace8309c91545a2607185101827f79122219a7e/librt-0.7.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a3bfe73a32bd0bdb9a87d586b05a23c0a1729205d79df66dee65bb2e40d671ba", size = 175597, upload-time = "2026-01-01T23:51:50.636Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/27f6bfbcc764805864c04211c6ed636fe1d58f57a7b68d1f4ae5ed74e0e0/librt-0.7.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0ecce0544d3db91a40f8b57ae26928c02130a997b540f908cefd4d279d6c5848", size = 196506, upload-time = "2026-01-01T23:51:52.535Z" }, + { url = "https://files.pythonhosted.org/packages/46/ba/c9b9c6fc931dd7ea856c573174ccaf48714905b1a7499904db2552e3bbaf/librt-0.7.7-cp314-cp314-win32.whl", hash = "sha256:8f7a74cf3a80f0c3b0ec75b0c650b2f0a894a2cec57ef75f6f72c1e82cdac61d", size = 39747, upload-time = "2026-01-01T23:51:53.683Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/cd1269337c4cde3ee70176ee611ab0058aa42fc8ce5c9dce55f48facfcd8/librt-0.7.7-cp314-cp314-win_amd64.whl", hash = "sha256:3d1fe2e8df3268dd6734dba33ededae72ad5c3a859b9577bc00b715759c5aaab", size = 45971, upload-time = "2026-01-01T23:51:54.697Z" }, + { url = "https://files.pythonhosted.org/packages/79/fd/e0844794423f5583108c5991313c15e2b400995f44f6ec6871f8aaf8243c/librt-0.7.7-cp314-cp314-win_arm64.whl", hash = "sha256:2987cf827011907d3dfd109f1be0d61e173d68b1270107bb0e89f2fca7f2ed6b", size = 39075, upload-time = "2026-01-01T23:51:55.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/02/211fd8f7c381e7b2a11d0fdfcd410f409e89967be2e705983f7c6342209a/librt-0.7.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e92c8de62b40bfce91d5e12c6e8b15434da268979b1af1a6589463549d491e6", size = 57368, upload-time = "2026-01-01T23:51:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/aca257affae73ece26041ae76032153266d110453173f67d7603058e708c/librt-0.7.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f683dcd49e2494a7535e30f779aa1ad6e3732a019d80abe1309ea91ccd3230e3", size = 59238, upload-time = "2026-01-01T23:51:58.066Z" }, + { url = "https://files.pythonhosted.org/packages/96/47/7383a507d8e0c11c78ca34c9d36eab9000db5989d446a2f05dc40e76c64f/librt-0.7.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b15e5d17812d4d629ff576699954f74e2cc24a02a4fc401882dd94f81daba45", size = 183870, upload-time = "2026-01-01T23:51:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/50f3d8eec8efdaf79443963624175c92cec0ba84827a66b7fcfa78598e51/librt-0.7.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c084841b879c4d9b9fa34e5d5263994f21aea7fd9c6add29194dbb41a6210536", size = 194608, upload-time = "2026-01-01T23:52:00.419Z" }, + { url = "https://files.pythonhosted.org/packages/23/d9/1b6520793aadb59d891e3b98ee057a75de7f737e4a8b4b37fdbecb10d60f/librt-0.7.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c8fb9966f84737115513fecbaf257f9553d067a7dd45a69c2c7e5339e6a8dc", size = 206776, upload-time = "2026-01-01T23:52:01.705Z" }, + { url = "https://files.pythonhosted.org/packages/ff/db/331edc3bba929d2756fa335bfcf736f36eff4efcb4f2600b545a35c2ae58/librt-0.7.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5fb1ecb2c35362eab2dbd354fd1efa5a8440d3e73a68be11921042a0edc0ff", size = 203206, upload-time = "2026-01-01T23:52:03.315Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e1/6af79ec77204e85f6f2294fc171a30a91bb0e35d78493532ed680f5d98be/librt-0.7.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d1454899909d63cc9199a89fcc4f81bdd9004aef577d4ffc022e600c412d57f3", size = 196697, upload-time = "2026-01-01T23:52:04.857Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/de55ecce4b2796d6d243295c221082ca3a944dc2fb3a52dcc8660ce7727d/librt-0.7.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7ef28f2e7a016b29792fe0a2dd04dec75725b32a1264e390c366103f834a9c3a", size = 217193, upload-time = "2026-01-01T23:52:06.159Z" }, + { url = "https://files.pythonhosted.org/packages/41/61/33063e271949787a2f8dd33c5260357e3d512a114fc82ca7890b65a76e2d/librt-0.7.7-cp314-cp314t-win32.whl", hash = "sha256:5e419e0db70991b6ba037b70c1d5bbe92b20ddf82f31ad01d77a347ed9781398", size = 40277, upload-time = "2026-01-01T23:52:07.625Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/1abd972349f83a696ea73159ac964e63e2d14086fdd9bc7ca878c25fced4/librt-0.7.7-cp314-cp314t-win_amd64.whl", hash = "sha256:d6b7d93657332c817b8d674ef6bf1ab7796b4f7ce05e420fd45bd258a72ac804", size = 46765, upload-time = "2026-01-01T23:52:08.647Z" }, + { url = "https://files.pythonhosted.org/packages/51/0e/b756c7708143a63fca65a51ca07990fa647db2cc8fcd65177b9e96680255/librt-0.7.7-cp314-cp314t-win_arm64.whl", hash = "sha256:142c2cd91794b79fd0ce113bd658993b7ede0fe93057668c2f98a45ca00b7e91", size = 39724, upload-time = "2026-01-01T23:52:09.745Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" }, +] + +[[package]] +name = "litellm" +version = "1.80.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp", version = "3.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "click", marker = "python_full_version >= '3.13.2'" }, + { name = "fastuuid", marker = "python_full_version >= '3.13.2'" }, + { name = "grpcio", marker = "python_full_version >= '3.13.2'" }, + { name = "httpx", version = "0.28.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "importlib-metadata", marker = "python_full_version >= '3.13.2'" }, + { name = "jinja2", version = "3.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "jsonschema", marker = "python_full_version >= '3.13.2'" }, + { name = "openai", marker = "python_full_version >= '3.13.2'" }, + { name = "pydantic", marker = "python_full_version >= '3.13.2'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.13.2'" }, + { name = "tiktoken", marker = "python_full_version >= '3.13.2'" }, + { name = "tokenizers", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/41/9b28df3e4739df83ddb32dfb2bccb12ad271d986494c9fd60e4927a0a6c3/litellm-1.80.15.tar.gz", hash = "sha256:759d09f33c9c6028c58dcdf71781b17b833ee926525714e09a408602be27f54e", size = 13376508, upload-time = "2026-01-11T18:31:44.95Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3b/b1bd693721ccb3c9a37c8233d019a643ac57bef5a93f279e5a63839ee4db/litellm-1.80.15-py3-none-any.whl", hash = "sha256:f354e49456985a235b9ed99df1c19d686d30501f96e68882dcc5b29b1e7c59d9", size = 11670707, upload-time = "2026-01-11T18:31:41.67Z" }, +] + +[[package]] +name = "lru-dict" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/e3/42c87871920602a3c8300915bd0292f76eccc66c38f782397acbf8a62088/lru-dict-1.3.0.tar.gz", hash = "sha256:54fd1966d6bd1fcde781596cb86068214edeebff1db13a2cea11079e3fd07b6b", size = 13123, upload-time = "2023-11-06T01:40:12.951Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/5c/385f080747eb3083af87d8e4c9068f3c4cab89035f6982134889940dafd8/lru_dict-1.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c279068f68af3b46a5d649855e1fb87f5705fe1f744a529d82b2885c0e1fc69d", size = 17174, upload-time = "2023-11-06T01:39:07.923Z" }, + { url = "https://files.pythonhosted.org/packages/3c/de/5ef2ed75ce55d7059d1b96177ba04fa7ee1f35564f97bdfcd28fccfbe9d2/lru_dict-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:350e2233cfee9f326a0d7a08e309372d87186565e43a691b120006285a0ac549", size = 10742, upload-time = "2023-11-06T01:39:08.871Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/f69a6abb0062d2cf2ce0aaf0284b105b97d1da024ca6d3d0730e6151242e/lru_dict-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4eafb188a84483b3231259bf19030859f070321b00326dcb8e8c6cbf7db4b12f", size = 11079, upload-time = "2023-11-06T01:39:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/ea/59/cf891143abe58a455b8eaa9175f0e80f624a146a2bf9a1ca842ee0ef930a/lru_dict-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73593791047e36b37fdc0b67b76aeed439fcea80959c7d46201240f9ec3b2563", size = 32469, upload-time = "2023-11-06T01:39:11.091Z" }, + { url = "https://files.pythonhosted.org/packages/59/88/d5976e9f70107ce11e45d93c6f0c2d5eaa1fc30bb3c8f57525eda4510dff/lru_dict-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1958cb70b9542773d6241974646e5410e41ef32e5c9e437d44040d59bd80daf2", size = 33496, upload-time = "2023-11-06T01:39:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f8/94d6e910d54fc1fa05c0ee1cd608c39401866a18cf5e5aff238449b33c11/lru_dict-1.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc1cd3ed2cee78a47f11f3b70be053903bda197a873fd146e25c60c8e5a32cd6", size = 29914, upload-time = "2023-11-06T01:39:13.395Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b9/9db79780c8a3cfd66bba6847773061e5cf8a3746950273b9985d47bbfe53/lru_dict-1.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82eb230d48eaebd6977a92ddaa6d788f14cf4f4bcf5bbffa4ddfd60d051aa9d4", size = 32241, upload-time = "2023-11-06T01:39:14.612Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b6/08a623019daec22a40c4d6d2c40851dfa3d129a53b2f9469db8eb13666c1/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5ad659cbc349d0c9ba8e536b5f40f96a70c360f43323c29f4257f340d891531c", size = 37320, upload-time = "2023-11-06T01:39:15.875Z" }, + { url = "https://files.pythonhosted.org/packages/70/0b/d3717159c26155ff77679cee1b077d22e1008bf45f19921e193319cd8e46/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ba490b8972531d153ac0d4e421f60d793d71a2f4adbe2f7740b3c55dce0a12f1", size = 35054, upload-time = "2023-11-06T01:39:17.063Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f2ae00de7c27984a19b88d2b09ac877031c525b01199d7841ec8fa657fd6/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:c0131351b8a7226c69f1eba5814cbc9d1d8daaf0fdec1ae3f30508e3de5262d4", size = 38613, upload-time = "2023-11-06T01:39:18.136Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0b/e30236aafe31b4247aa9ae61ba8aac6dde75c3ea0e47a8fb7eef53f6d5ce/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0e88dba16695f17f41701269fa046197a3fd7b34a8dba744c8749303ddaa18df", size = 37143, upload-time = "2023-11-06T01:39:19.571Z" }, + { url = "https://files.pythonhosted.org/packages/1c/28/b59bcebb8d76ba8147a784a8be7eab6a4ad3395b9236e73740ff675a5a52/lru_dict-1.3.0-cp312-cp312-win32.whl", hash = "sha256:6ffaf595e625b388babc8e7d79b40f26c7485f61f16efe76764e32dce9ea17fc", size = 12653, upload-time = "2023-11-06T01:39:20.574Z" }, + { url = "https://files.pythonhosted.org/packages/bd/18/06d9710cb0a0d3634f8501e4bdcc07abe64a32e404d82895a6a36fab97f6/lru_dict-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf9da32ef2582434842ab6ba6e67290debfae72771255a8e8ab16f3e006de0aa", size = 13811, upload-time = "2023-11-06T01:39:21.599Z" }, +] + +[[package]] +name = "lzstring" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "future" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/0c/28347673b45e5f0975cdf1f6d69ede6ad049be873194c4e164d79aecd34c/lzstring-1.0.4.tar.gz", hash = "sha256:1afa61e598193fbcc211e0899f09a9679e33f9102bccc37fbfda0b7fef4d9ea2", size = 4256, upload-time = "2018-06-01T02:32:12.639Z" } + +[[package]] +name = "markdown" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, +] + +[[package]] +name = "markdown-code-runner" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/91/6b7030f873b0c49f38131c7a5ac9432238cf1e1cd47104c05b07cf5c32ed/markdown_code_runner-2.4.0.tar.gz", hash = "sha256:7d6229c437f0c71e5c0442585663af4bfe4beacddf884a64ea8c09fd5dbc31dd", size = 19072, upload-time = "2025-08-23T19:07:41.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/3c/8857763070aa0f39591b012f92ab40bd5fe906914085e925eb117e28f306/markdown_code_runner-2.4.0-py3-none-any.whl", hash = "sha256:5be62e75ab35188b37f2d440c19b6683df68615ea24bed4955d95a745e56b483", size = 12292, upload-time = "2025-08-23T19:07:40.972Z" }, +] + +[[package]] +name = "markdown-gfm-admonition" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/5b/1e7114e8fac6aadf2a25dab2e24a1eba54acd504d6a5747928a3eb09d7fd/markdown_gfm_admonition-0.3.0.tar.gz", hash = "sha256:20a37febc79222badb6dfbfcbf0a74b94c0a6259b0f27e9f783601fea59094b6", size = 5817, upload-time = "2025-11-28T09:01:38.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/86/ce4288122111c11bb2483a962357baa9793817d48ac1e99ce5ff47384efe/markdown_gfm_admonition-0.3.0-py3-none-any.whl", hash = "sha256:8e49bfea892f4c220b7f7b9479462360b69c68ae7cae7739e288b5382d0ae9b5", size = 6416, upload-time = "2025-11-28T09:01:37.543Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mashumaro" +version = "3.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/67/c4e235256baf6837106d2620c7123eb1e5786c704c7f7d7fa488ad6afc61/mashumaro-3.17.tar.gz", hash = "sha256:de1d8b1faffee58969c7f97e35963a92480a38d4c9858e92e0721efec12258ed", size = 189877, upload-time = "2025-10-03T21:09:27.281Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/2d/edc147aa1dce9e0e377687d453e62fdfcbccc08cd35d0b7413e3485c4b92/mashumaro-3.17-py3-none-any.whl", hash = "sha256:3964e2c804f62de9e4c58fb985de71dcd716f9507cc18374b1bd5c4f1a1b879b", size = 94198, upload-time = "2025-10-03T21:09:25.436Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, + { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, + { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, + { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, + { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, + { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, + { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, + { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, + { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, + { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, + { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, + { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, + { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, + { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, + { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, + { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, + { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, + { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, + { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, + { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, + { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, + { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, + { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, + { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, + { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, + { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, + { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, + { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, + { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, + { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +] + +[[package]] +name = "mutagen" +version = "1.47.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e6/64bc71b74eef4b68e61eb921dcf72dabd9e4ec4af1e11891bbd312ccbb77/mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99", size = 1274186, upload-time = "2023-09-03T16:33:33.411Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/7a/620f945b96be1f6ee357d211d5bf74ab1b7fe72a9f1525aafbfe3aee6875/mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719", size = 194391, upload-time = "2023-09-03T16:33:29.955Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "narwhals" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/6d/b57c64e5038a8cf071bce391bb11551657a74558877ac961e7fa905ece27/narwhals-2.15.0.tar.gz", hash = "sha256:a9585975b99d95084268445a1fdd881311fa26ef1caa18020d959d5b2ff9a965", size = 603479, upload-time = "2026-01-06T08:10:13.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-py3-none-any.whl", hash = "sha256:cbfe21ca19d260d9fd67f995ec75c44592d1f106933b03ddd375df7ac841f9d6", size = 432856, upload-time = "2026-01-06T08:10:11.511Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "numpy" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/25/ca/1166b75c21abd1da445b97bf1fa2f14f423c6cfb4fc7c4ef31dccf9f6a94/numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", size = 20166090, upload-time = "2024-11-02T17:48:55.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/f0/385eb9970309643cbca4fc6eebc8bb16e560de129c91258dfaa18498da8b/numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", size = 20849658, upload-time = "2024-11-02T17:37:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/54/4a/765b4607f0fecbb239638d610d04ec0a0ded9b4951c56dc68cef79026abf/numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", size = 13492258, upload-time = "2024-11-02T17:37:45.252Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a7/2332679479c70b68dccbf4a8eb9c9b5ee383164b161bee9284ac141fbd33/numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", size = 5090249, upload-time = "2024-11-02T17:37:54.252Z" }, + { url = "https://files.pythonhosted.org/packages/c1/67/4aa00316b3b981a822c7a239d3a8135be2a6945d1fd11d0efb25d361711a/numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", size = 6621704, upload-time = "2024-11-02T17:38:05.127Z" }, + { url = "https://files.pythonhosted.org/packages/5e/da/1a429ae58b3b6c364eeec93bf044c532f2ff7b48a52e41050896cf15d5b1/numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", size = 13606089, upload-time = "2024-11-02T17:38:25.997Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3e/3757f304c704f2f0294a6b8340fcf2be244038be07da4cccf390fa678a9f/numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", size = 16043185, upload-time = "2024-11-02T17:38:51.07Z" }, + { url = "https://files.pythonhosted.org/packages/43/97/75329c28fea3113d00c8d2daf9bc5828d58d78ed661d8e05e234f86f0f6d/numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", size = 16410751, upload-time = "2024-11-02T17:39:15.801Z" }, + { url = "https://files.pythonhosted.org/packages/ad/7a/442965e98b34e0ae9da319f075b387bcb9a1e0658276cc63adb8c9686f7b/numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", size = 14082705, upload-time = "2024-11-02T17:39:38.274Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b6/26108cf2cfa5c7e03fb969b595c93131eab4a399762b51ce9ebec2332e80/numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", size = 6239077, upload-time = "2024-11-02T17:39:49.299Z" }, + { url = "https://files.pythonhosted.org/packages/a6/84/fa11dad3404b7634aaab50733581ce11e5350383311ea7a7010f464c0170/numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", size = 12566858, upload-time = "2024-11-02T17:40:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0b/620591441457e25f3404c8057eb924d04f161244cb8a3680d529419aa86e/numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f", size = 20836263, upload-time = "2024-11-02T17:40:39.528Z" }, + { url = "https://files.pythonhosted.org/packages/45/e1/210b2d8b31ce9119145433e6ea78046e30771de3fe353f313b2778142f34/numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598", size = 13507771, upload-time = "2024-11-02T17:41:01.368Z" }, + { url = "https://files.pythonhosted.org/packages/55/44/aa9ee3caee02fa5a45f2c3b95cafe59c44e4b278fbbf895a93e88b308555/numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57", size = 5075805, upload-time = "2024-11-02T17:41:11.213Z" }, + { url = "https://files.pythonhosted.org/packages/78/d6/61de6e7e31915ba4d87bbe1ae859e83e6582ea14c6add07c8f7eefd8488f/numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe", size = 6608380, upload-time = "2024-11-02T17:41:22.19Z" }, + { url = "https://files.pythonhosted.org/packages/3e/46/48bdf9b7241e317e6cf94276fe11ba673c06d1fdf115d8b4ebf616affd1a/numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43", size = 13602451, upload-time = "2024-11-02T17:41:43.094Z" }, + { url = "https://files.pythonhosted.org/packages/70/50/73f9a5aa0810cdccda9c1d20be3cbe4a4d6ea6bfd6931464a44c95eef731/numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56", size = 16039822, upload-time = "2024-11-02T17:42:07.595Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cd/098bc1d5a5bc5307cfc65ee9369d0ca658ed88fbd7307b0d49fab6ca5fa5/numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a", size = 16411822, upload-time = "2024-11-02T17:42:32.48Z" }, + { url = "https://files.pythonhosted.org/packages/83/a2/7d4467a2a6d984549053b37945620209e702cf96a8bc658bc04bba13c9e2/numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef", size = 14079598, upload-time = "2024-11-02T17:42:53.773Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6a/d64514dcecb2ee70bfdfad10c42b76cab657e7ee31944ff7a600f141d9e9/numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f", size = 6236021, upload-time = "2024-11-02T17:46:19.171Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f9/12297ed8d8301a401e7d8eb6b418d32547f1d700ed3c038d325a605421a4/numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed", size = 12560405, upload-time = "2024-11-02T17:46:38.177Z" }, + { url = "https://files.pythonhosted.org/packages/a7/45/7f9244cd792e163b334e3a7f02dff1239d2890b6f37ebf9e82cbe17debc0/numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f", size = 20859062, upload-time = "2024-11-02T17:43:24.599Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b4/a084218e7e92b506d634105b13e27a3a6645312b93e1c699cc9025adb0e1/numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4", size = 13515839, upload-time = "2024-11-02T17:43:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/27/45/58ed3f88028dcf80e6ea580311dc3edefdd94248f5770deb980500ef85dd/numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e", size = 5116031, upload-time = "2024-11-02T17:43:54.585Z" }, + { url = "https://files.pythonhosted.org/packages/37/a8/eb689432eb977d83229094b58b0f53249d2209742f7de529c49d61a124a0/numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0", size = 6629977, upload-time = "2024-11-02T17:44:05.31Z" }, + { url = "https://files.pythonhosted.org/packages/42/a3/5355ad51ac73c23334c7caaed01adadfda49544f646fcbfbb4331deb267b/numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408", size = 13575951, upload-time = "2024-11-02T17:44:25.881Z" }, + { url = "https://files.pythonhosted.org/packages/c4/70/ea9646d203104e647988cb7d7279f135257a6b7e3354ea6c56f8bafdb095/numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6", size = 16022655, upload-time = "2024-11-02T17:44:50.115Z" }, + { url = "https://files.pythonhosted.org/packages/14/ce/7fc0612903e91ff9d0b3f2eda4e18ef9904814afcae5b0f08edb7f637883/numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f", size = 16399902, upload-time = "2024-11-02T17:45:15.685Z" }, + { url = "https://files.pythonhosted.org/packages/ef/62/1d3204313357591c913c32132a28f09a26357e33ea3c4e2fe81269e0dca1/numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", size = 14067180, upload-time = "2024-11-02T17:45:37.234Z" }, + { url = "https://files.pythonhosted.org/packages/24/d7/78a40ed1d80e23a774cb8a34ae8a9493ba1b4271dde96e56ccdbab1620ef/numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", size = 6291907, upload-time = "2024-11-02T17:45:48.951Z" }, + { url = "https://files.pythonhosted.org/packages/86/09/a5ab407bd7f5f5599e6a9261f964ace03a73e7c6928de906981c31c38082/numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", size = 12644098, upload-time = "2024-11-02T17:46:07.941Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/d0/c12ddfd3a02274be06ffc71f3efc6d0e457b0409c4481596881e748cb264/numpy-2.2.2.tar.gz", hash = "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f", size = 20233295, upload-time = "2025-01-19T00:02:09.581Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/e6/847d15770ab7a01e807bdfcd4ead5bdae57c0092b7dc83878171b6af97bb/numpy-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467", size = 20912636, upload-time = "2025-01-18T23:23:58.337Z" }, + { url = "https://files.pythonhosted.org/packages/d1/af/f83580891577b13bd7e261416120e036d0d8fb508c8a43a73e38928b794b/numpy-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a", size = 14098403, upload-time = "2025-01-18T23:25:26.896Z" }, + { url = "https://files.pythonhosted.org/packages/2b/86/d019fb60a9d0f1d4cf04b014fe88a9135090adfadcc31c1fadbb071d7fa7/numpy-2.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825", size = 5128938, upload-time = "2025-01-18T23:25:37.21Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1b/50985edb6f1ec495a1c36452e860476f5b7ecdc3fc59ea89ccad3c4926c5/numpy-2.2.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37", size = 6661937, upload-time = "2025-01-18T23:26:05.86Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1b/17efd94cad1b9d605c3f8907fb06bcffc4ce4d1d14d46b95316cccccf2b9/numpy-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748", size = 14049518, upload-time = "2025-01-18T23:26:33.364Z" }, + { url = "https://files.pythonhosted.org/packages/5b/73/65d2f0b698df1731e851e3295eb29a5ab8aa06f763f7e4188647a809578d/numpy-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0", size = 16099146, upload-time = "2025-01-18T23:27:15.132Z" }, + { url = "https://files.pythonhosted.org/packages/d5/69/308f55c0e19d4b5057b5df286c5433822e3c8039ede06d4051d96f1c2c4e/numpy-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278", size = 15246336, upload-time = "2025-01-18T23:28:09.658Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d8/d8d333ad0d8518d077a21aeea7b7c826eff766a2b1ce1194dea95ca0bacf/numpy-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba", size = 17863507, upload-time = "2025-01-18T23:28:56.146Z" }, + { url = "https://files.pythonhosted.org/packages/82/6e/0b84ad3103ffc16d6673e63b5acbe7901b2af96c2837174c6318c98e27ab/numpy-2.2.2-cp312-cp312-win32.whl", hash = "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283", size = 6276491, upload-time = "2025-01-18T23:29:09.409Z" }, + { url = "https://files.pythonhosted.org/packages/fc/84/7f801a42a67b9772a883223a0a1e12069a14626c81a732bd70aac57aebc1/numpy-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb", size = 12616372, upload-time = "2025-01-18T23:29:46.645Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/df5624001f4f5c3e0b78e9017bfab7fdc18a8d3b3d3161da3d64924dd659/numpy-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc", size = 20899188, upload-time = "2025-01-18T23:31:15.292Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/d349c3b5ed66bd3cb0214be60c27e32b90a506946857b866838adbe84040/numpy-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369", size = 14113972, upload-time = "2025-01-18T23:31:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/9d/50/949ec9cbb28c4b751edfa64503f0913cbfa8d795b4a251e7980f13a8a655/numpy-2.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd", size = 5114294, upload-time = "2025-01-18T23:31:54.219Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f3/399c15629d5a0c68ef2aa7621d430b2be22034f01dd7f3c65a9c9666c445/numpy-2.2.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be", size = 6648426, upload-time = "2025-01-18T23:32:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/2c/03/c72474c13772e30e1bc2e558cdffd9123c7872b731263d5648b5c49dd459/numpy-2.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84", size = 14045990, upload-time = "2025-01-18T23:32:38.031Z" }, + { url = "https://files.pythonhosted.org/packages/83/9c/96a9ab62274ffafb023f8ee08c88d3d31ee74ca58869f859db6845494fa6/numpy-2.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff", size = 16096614, upload-time = "2025-01-18T23:33:12.265Z" }, + { url = "https://files.pythonhosted.org/packages/d5/34/cd0a735534c29bec7093544b3a509febc9b0df77718a9b41ffb0809c9f46/numpy-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0", size = 15242123, upload-time = "2025-01-18T23:33:46.412Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6d/541717a554a8f56fa75e91886d9b79ade2e595918690eb5d0d3dbd3accb9/numpy-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de", size = 17859160, upload-time = "2025-01-18T23:34:37.857Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a5/fbf1f2b54adab31510728edd06a05c1b30839f37cf8c9747cb85831aaf1b/numpy-2.2.2-cp313-cp313-win32.whl", hash = "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9", size = 6273337, upload-time = "2025-01-18T23:40:10.83Z" }, + { url = "https://files.pythonhosted.org/packages/56/e5/01106b9291ef1d680f82bc47d0c5b5e26dfed15b0754928e8f856c82c881/numpy-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369", size = 12609010, upload-time = "2025-01-18T23:40:31.34Z" }, + { url = "https://files.pythonhosted.org/packages/9f/30/f23d9876de0f08dceb707c4dcf7f8dd7588266745029debb12a3cdd40be6/numpy-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391", size = 20924451, upload-time = "2025-01-18T23:35:26.639Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ec/6ea85b2da9d5dfa1dbb4cb3c76587fc8ddcae580cb1262303ab21c0926c4/numpy-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39", size = 14122390, upload-time = "2025-01-18T23:36:30.596Z" }, + { url = "https://files.pythonhosted.org/packages/68/05/bfbdf490414a7dbaf65b10c78bc243f312c4553234b6d91c94eb7c4b53c2/numpy-2.2.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317", size = 5156590, upload-time = "2025-01-18T23:36:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/fe2e91b2642b9d6544518388a441bcd65c904cea38d9ff998e2e8ebf808e/numpy-2.2.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49", size = 6671958, upload-time = "2025-01-18T23:37:05.361Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6f/6531a78e182f194d33ee17e59d67d03d0d5a1ce7f6be7343787828d1bd4a/numpy-2.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2", size = 14019950, upload-time = "2025-01-18T23:37:38.605Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fb/13c58591d0b6294a08cc40fcc6b9552d239d773d520858ae27f39997f2ae/numpy-2.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7", size = 16079759, upload-time = "2025-01-18T23:38:05.757Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/f2f8edd62abb4b289f65a7f6d1f3650273af00b91b7267a2431be7f1aec6/numpy-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb", size = 15226139, upload-time = "2025-01-18T23:38:38.458Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/14a177f1a90b8ad8a592ca32124ac06af5eff32889874e53a308f850290f/numpy-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648", size = 17856316, upload-time = "2025-01-18T23:39:11.454Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/242ae8d7b97f4e0e4ab8dd51231465fb23ed5e802680d629149722e3faf1/numpy-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4", size = 6329134, upload-time = "2025-01-18T23:39:28.128Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/cd9e9b04012c015cb6320ab3bf43bc615e248dddfeb163728e800a5d96f0/numpy-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576", size = 12696208, upload-time = "2025-01-18T23:39:51.85Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/6d/745dd1c1c5c284d17725e5c802ca4d45cfc6803519d777f087b71c9f4069/numpy-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc3186bea41fae9d8e90c2b4fb5f0a1f5a690682da79b92574d63f56b529080b", size = 20956420, upload-time = "2025-07-24T20:28:18.002Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/e7b533ea5740641dd62b07a790af5d9d8fec36000b8e2d0472bd7574105f/numpy-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f4f0215edb189048a3c03bd5b19345bdfa7b45a7a6f72ae5945d2a28272727f", size = 14184660, upload-time = "2025-07-24T20:28:39.522Z" }, + { url = "https://files.pythonhosted.org/packages/2b/53/102c6122db45a62aa20d1b18c9986f67e6b97e0d6fbc1ae13e3e4c84430c/numpy-2.3.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b1224a734cd509f70816455c3cffe13a4f599b1bf7130f913ba0e2c0b2006c0", size = 5113382, upload-time = "2025-07-24T20:28:48.544Z" }, + { url = "https://files.pythonhosted.org/packages/2b/21/376257efcbf63e624250717e82b4fae93d60178f09eb03ed766dbb48ec9c/numpy-2.3.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3dcf02866b977a38ba3ec10215220609ab9667378a9e2150615673f3ffd6c73b", size = 6647258, upload-time = "2025-07-24T20:28:59.104Z" }, + { url = "https://files.pythonhosted.org/packages/91/ba/f4ebf257f08affa464fe6036e13f2bf9d4642a40228781dc1235da81be9f/numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370", size = 14281409, upload-time = "2025-07-24T20:40:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/59/ef/f96536f1df42c668cbacb727a8c6da7afc9c05ece6d558927fb1722693e1/numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73", size = 16641317, upload-time = "2025-07-24T20:40:56.625Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a7/af813a7b4f9a42f498dde8a4c6fcbff8100eed00182cc91dbaf095645f38/numpy-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:103ea7063fa624af04a791c39f97070bf93b96d7af7eb23530cd087dc8dbe9dc", size = 16056262, upload-time = "2025-07-24T20:41:20.797Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5d/41c4ef8404caaa7f05ed1cfb06afe16a25895260eacbd29b4d84dff2920b/numpy-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc927d7f289d14f5e037be917539620603294454130b6de200091e23d27dc9be", size = 18579342, upload-time = "2025-07-24T20:41:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/9950e44c5a11636f4a3af6e825ec23003475cc9a466edb7a759ed3ea63bd/numpy-2.3.2-cp312-cp312-win32.whl", hash = "sha256:d95f59afe7f808c103be692175008bab926b59309ade3e6d25009e9a171f7036", size = 6320610, upload-time = "2025-07-24T20:42:01.551Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2f/244643a5ce54a94f0a9a2ab578189c061e4a87c002e037b0829dd77293b6/numpy-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:9e196ade2400c0c737d93465327d1ae7c06c7cb8a1756121ebf54b06ca183c7f", size = 12786292, upload-time = "2025-07-24T20:42:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/54/cd/7b5f49d5d78db7badab22d8323c1b6ae458fbf86c4fdfa194ab3cd4eb39b/numpy-2.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:ee807923782faaf60d0d7331f5e86da7d5e3079e28b291973c545476c2b00d07", size = 10194071, upload-time = "2025-07-24T20:42:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c0/c6bb172c916b00700ed3bf71cb56175fd1f7dbecebf8353545d0b5519f6c/numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3", size = 20949074, upload-time = "2025-07-24T20:43:07.813Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/c116466d22acaf4573e58421c956c6076dc526e24a6be0903219775d862e/numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b", size = 14177311, upload-time = "2025-07-24T20:43:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/d4698c182895af189c463fc91d70805d455a227261d950e4e0f1310c2550/numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6", size = 5106022, upload-time = "2025-07-24T20:43:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/9f/76/3e6880fef4420179309dba72a8c11f6166c431cf6dee54c577af8906f914/numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089", size = 6640135, upload-time = "2025-07-24T20:43:49.28Z" }, + { url = "https://files.pythonhosted.org/packages/34/fa/87ff7f25b3c4ce9085a62554460b7db686fef1e0207e8977795c7b7d7ba1/numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2", size = 14278147, upload-time = "2025-07-24T20:44:10.328Z" }, + { url = "https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f", size = 16635989, upload-time = "2025-07-24T20:44:34.88Z" }, + { url = "https://files.pythonhosted.org/packages/24/5a/84ae8dca9c9a4c592fe11340b36a86ffa9fd3e40513198daf8a97839345c/numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee", size = 16053052, upload-time = "2025-07-24T20:44:58.872Z" }, + { url = "https://files.pythonhosted.org/packages/57/7c/e5725d99a9133b9813fcf148d3f858df98511686e853169dbaf63aec6097/numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6", size = 18577955, upload-time = "2025-07-24T20:45:26.714Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7c546fcf42145f29b71e4d6f429e96d8d68e5a7ba1830b2e68d7418f0bbd/numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b", size = 6311843, upload-time = "2025-07-24T20:49:24.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6f/a428fd1cb7ed39b4280d057720fed5121b0d7754fd2a9768640160f5517b/numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56", size = 12782876, upload-time = "2025-07-24T20:49:43.227Z" }, + { url = "https://files.pythonhosted.org/packages/65/85/4ea455c9040a12595fb6c43f2c217257c7b52dd0ba332c6a6c1d28b289fe/numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2", size = 10192786, upload-time = "2025-07-24T20:49:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/80/23/8278f40282d10c3f258ec3ff1b103d4994bcad78b0cba9208317f6bb73da/numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab", size = 21047395, upload-time = "2025-07-24T20:45:58.821Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2d/624f2ce4a5df52628b4ccd16a4f9437b37c35f4f8a50d00e962aae6efd7a/numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2", size = 14300374, upload-time = "2025-07-24T20:46:20.207Z" }, + { url = "https://files.pythonhosted.org/packages/f6/62/ff1e512cdbb829b80a6bd08318a58698867bca0ca2499d101b4af063ee97/numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a", size = 5228864, upload-time = "2025-07-24T20:46:30.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8e/74bc18078fff03192d4032cfa99d5a5ca937807136d6f5790ce07ca53515/numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286", size = 6737533, upload-time = "2025-07-24T20:46:46.111Z" }, + { url = "https://files.pythonhosted.org/packages/19/ea/0731efe2c9073ccca5698ef6a8c3667c4cf4eea53fcdcd0b50140aba03bc/numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8", size = 14352007, upload-time = "2025-07-24T20:47:07.1Z" }, + { url = "https://files.pythonhosted.org/packages/cf/90/36be0865f16dfed20f4bc7f75235b963d5939707d4b591f086777412ff7b/numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a", size = 16701914, upload-time = "2025-07-24T20:47:32.459Z" }, + { url = "https://files.pythonhosted.org/packages/94/30/06cd055e24cb6c38e5989a9e747042b4e723535758e6153f11afea88c01b/numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91", size = 16132708, upload-time = "2025-07-24T20:47:58.129Z" }, + { url = "https://files.pythonhosted.org/packages/9a/14/ecede608ea73e58267fd7cb78f42341b3b37ba576e778a1a06baffbe585c/numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5", size = 18651678, upload-time = "2025-07-24T20:48:25.402Z" }, + { url = "https://files.pythonhosted.org/packages/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832, upload-time = "2025-07-24T20:48:37.181Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049, upload-time = "2025-07-24T20:48:56.24Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935, upload-time = "2025-07-24T20:49:13.136Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7c/7659048aaf498f7611b783e000c7268fcc4dcf0ce21cd10aad7b2e8f9591/numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a", size = 20950906, upload-time = "2025-07-24T20:50:30.346Z" }, + { url = "https://files.pythonhosted.org/packages/80/db/984bea9d4ddf7112a04cfdfb22b1050af5757864cfffe8e09e44b7f11a10/numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b", size = 14185607, upload-time = "2025-07-24T20:50:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/b3d6f414f4eca568f469ac112a3b510938d892bc5a6c190cb883af080b77/numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125", size = 5114110, upload-time = "2025-07-24T20:51:01.041Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d2/6f5e6826abd6bca52392ed88fe44a4b52aacb60567ac3bc86c67834c3a56/numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19", size = 6642050, upload-time = "2025-07-24T20:51:11.64Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/f12b2ade99199e39c73ad182f103f9d9791f48d885c600c8e05927865baf/numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f", size = 14296292, upload-time = "2025-07-24T20:51:33.488Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f9/77c07d94bf110a916b17210fac38680ed8734c236bfed9982fd8524a7b47/numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5", size = 16638913, upload-time = "2025-07-24T20:51:58.517Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d1/9d9f2c8ea399cc05cfff8a7437453bd4e7d894373a93cdc46361bbb49a7d/numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58", size = 16071180, upload-time = "2025-07-24T20:52:22.827Z" }, + { url = "https://files.pythonhosted.org/packages/4c/41/82e2c68aff2a0c9bf315e47d61951099fed65d8cb2c8d9dc388cb87e947e/numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0", size = 18576809, upload-time = "2025-07-24T20:52:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/4b4fd3efb0837ed252d0f583c5c35a75121038a8c4e065f2c259be06d2d8/numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2", size = 6366410, upload-time = "2025-07-24T20:56:44.949Z" }, + { url = "https://files.pythonhosted.org/packages/11/9e/b4c24a6b8467b61aced5c8dc7dcfce23621baa2e17f661edb2444a418040/numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b", size = 12918821, upload-time = "2025-07-24T20:57:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0f/0dc44007c70b1007c1cef86b06986a3812dd7106d8f946c09cfa75782556/numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910", size = 10477303, upload-time = "2025-07-24T20:57:22.879Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3e/075752b79140b78ddfc9c0a1634d234cfdbc6f9bbbfa6b7504e445ad7d19/numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e", size = 21047524, upload-time = "2025-07-24T20:53:22.086Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/60e8247564a72426570d0e0ea1151b95ce5bd2f1597bb878a18d32aec855/numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45", size = 14300519, upload-time = "2025-07-24T20:53:44.053Z" }, + { url = "https://files.pythonhosted.org/packages/4d/73/d8326c442cd428d47a067070c3ac6cc3b651a6e53613a1668342a12d4479/numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b", size = 5228972, upload-time = "2025-07-24T20:53:53.81Z" }, + { url = "https://files.pythonhosted.org/packages/34/2e/e71b2d6dad075271e7079db776196829019b90ce3ece5c69639e4f6fdc44/numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2", size = 6737439, upload-time = "2025-07-24T20:54:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/15/b0/d004bcd56c2c5e0500ffc65385eb6d569ffd3363cb5e593ae742749b2daa/numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0", size = 14352479, upload-time = "2025-07-24T20:54:25.819Z" }, + { url = "https://files.pythonhosted.org/packages/11/e3/285142fcff8721e0c99b51686426165059874c150ea9ab898e12a492e291/numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0", size = 16702805, upload-time = "2025-07-24T20:54:50.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/33b56b0e47e604af2c7cd065edca892d180f5899599b76830652875249a3/numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2", size = 16133830, upload-time = "2025-07-24T20:55:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/7b1476a1f4d6a48bc669b8deb09939c56dd2a439db1ab03017844374fb67/numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf", size = 18652665, upload-time = "2025-07-24T20:55:46.665Z" }, + { url = "https://files.pythonhosted.org/packages/14/ba/5b5c9978c4bb161034148ade2de9db44ec316fab89ce8c400db0e0c81f86/numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1", size = 6514777, upload-time = "2025-07-24T20:55:57.66Z" }, + { url = "https://files.pythonhosted.org/packages/eb/46/3dbaf0ae7c17cdc46b9f662c56da2054887b8d9e737c1476f335c83d33db/numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b", size = 13111856, upload-time = "2025-07-24T20:56:17.318Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9e/1652778bce745a67b5fe05adde60ed362d38eb17d919a540e813d30f6874/numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631", size = 10544226, upload-time = "2025-07-24T20:56:34.509Z" }, +] + +[[package]] +name = "openai" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.13.2'" }, + { name = "distro", marker = "python_full_version >= '3.13.2'" }, + { name = "httpx", version = "0.28.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "jiter", marker = "python_full_version >= '3.13.2'" }, + { name = "pydantic", marker = "python_full_version >= '3.13.2'" }, + { name = "sniffio", marker = "python_full_version >= '3.13.2'" }, + { name = "tqdm", marker = "python_full_version >= '3.13.2'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/f4/4690ecb5d70023ce6bfcfeabfe717020f654bde59a775058ec6ac4692463/openai-2.15.0.tar.gz", hash = "sha256:42eb8cbb407d84770633f31bf727d4ffb4138711c670565a41663d9439174fba", size = 627383, upload-time = "2026-01-09T22:10:08.603Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/df/c306f7375d42bafb379934c2df4c2fa3964656c8c782bac75ee10c102818/openai-2.15.0-py3-none-any.whl", hash = "sha256:6ae23b932cd7230f7244e52954daa6602716d6b9bf235401a107af731baea6c3", size = 1067879, upload-time = "2026-01-09T22:10:06.446Z" }, +] + +[[package]] +name = "orjson" +version = "3.10.12" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/04/bb9f72987e7f62fb591d6c880c0caaa16238e4e530cbc3bdc84a7372d75f/orjson-3.10.12.tar.gz", hash = "sha256:0a78bbda3aea0f9f079057ee1ee8a1ecf790d4f1af88dd67493c6b8ee52506ff", size = 5438647, upload-time = "2024-11-23T19:42:56.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/2f/989adcafad49afb535da56b95d8f87d82e748548b2a86003ac129314079c/orjson-3.10.12-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:53206d72eb656ca5ac7d3a7141e83c5bbd3ac30d5eccfe019409177a57634b0d", size = 248678, upload-time = "2024-11-23T19:41:33.346Z" }, + { url = "https://files.pythonhosted.org/packages/69/b9/8c075e21a50c387649db262b618ebb7e4d40f4197b949c146fc225dd23da/orjson-3.10.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac8010afc2150d417ebda810e8df08dd3f544e0dd2acab5370cfa6bcc0662f8f", size = 136763, upload-time = "2024-11-23T19:41:35.539Z" }, + { url = "https://files.pythonhosted.org/packages/87/d3/78edf10b4ab14c19f6d918cf46a145818f4aca2b5a1773c894c5490d3a4c/orjson-3.10.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed459b46012ae950dd2e17150e838ab08215421487371fa79d0eced8d1461d70", size = 149137, upload-time = "2024-11-23T19:41:36.937Z" }, + { url = "https://files.pythonhosted.org/packages/16/81/5db8852bdf990a0ddc997fa8f16b80895b8cc77c0fe3701569ed2b4b9e78/orjson-3.10.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dcb9673f108a93c1b52bfc51b0af422c2d08d4fc710ce9c839faad25020bb69", size = 140567, upload-time = "2024-11-23T19:41:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a6/9ce1e3e3db918512efadad489630c25841eb148513d21dab96f6b4157fa1/orjson-3.10.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22a51ae77680c5c4652ebc63a83d5255ac7d65582891d9424b566fb3b5375ee9", size = 156620, upload-time = "2024-11-23T19:41:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/05133d6bea24e292d2f7628b1e19986554f7d97b6412b3e51d812e38db2d/orjson-3.10.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910fdf2ac0637b9a77d1aad65f803bac414f0b06f720073438a7bd8906298192", size = 131555, upload-time = "2024-11-23T19:41:41.172Z" }, + { url = "https://files.pythonhosted.org/packages/b9/7a/b3fbffda8743135c7811e95dc2ab7cdbc5f04999b83c2957d046f1b3fac9/orjson-3.10.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24ce85f7100160936bc2116c09d1a8492639418633119a2224114f67f63a4559", size = 139743, upload-time = "2024-11-23T19:41:42.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/13/95bbcc9a6584aa083da5ce5004ce3d59ea362a542a0b0938d884fd8790b6/orjson-3.10.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a76ba5fc8dd9c913640292df27bff80a685bed3a3c990d59aa6ce24c352f8fc", size = 131733, upload-time = "2024-11-23T19:41:44.184Z" }, + { url = "https://files.pythonhosted.org/packages/e8/29/dddbb2ea6e7af426fcc3da65a370618a88141de75c6603313d70768d1df1/orjson-3.10.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ff70ef093895fd53f4055ca75f93f047e088d1430888ca1229393a7c0521100f", size = 415788, upload-time = "2024-11-23T19:41:45.612Z" }, + { url = "https://files.pythonhosted.org/packages/53/df/4aea59324ac539975919b4705ee086aced38e351a6eb3eea0f5071dd5661/orjson-3.10.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f4244b7018b5753ecd10a6d324ec1f347da130c953a9c88432c7fbc8875d13be", size = 142347, upload-time = "2024-11-23T19:41:48.128Z" }, + { url = "https://files.pythonhosted.org/packages/55/55/a52d83d7c49f8ff44e0daab10554490447d6c658771569e1c662aa7057fe/orjson-3.10.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16135ccca03445f37921fa4b585cff9a58aa8d81ebcb27622e69bfadd220b32c", size = 130829, upload-time = "2024-11-23T19:41:49.702Z" }, + { url = "https://files.pythonhosted.org/packages/a1/8b/b1beb1624dd4adf7d72e2d9b73c4b529e7851c0c754f17858ea13e368b33/orjson-3.10.12-cp312-none-win32.whl", hash = "sha256:2d879c81172d583e34153d524fcba5d4adafbab8349a7b9f16ae511c2cee8708", size = 143659, upload-time = "2024-11-23T19:41:51.122Z" }, + { url = "https://files.pythonhosted.org/packages/13/91/634c9cd0bfc6a857fc8fab9bf1a1bd9f7f3345e0d6ca5c3d4569ceb6dcfa/orjson-3.10.12-cp312-none-win_amd64.whl", hash = "sha256:fc23f691fa0f5c140576b8c365bc942d577d861a9ee1142e4db468e4e17094fb", size = 135221, upload-time = "2024-11-23T19:41:52.569Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bb/3f560735f46fa6f875a9d7c4c2171a58cfb19f56a633d5ad5037a924f35f/orjson-3.10.12-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:47962841b2a8aa9a258b377f5188db31ba49af47d4003a32f55d6f8b19006543", size = 248662, upload-time = "2024-11-23T19:41:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/a3/df/54817902350636cc9270db20486442ab0e4db33b38555300a1159b439d16/orjson-3.10.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6334730e2532e77b6054e87ca84f3072bee308a45a452ea0bffbbbc40a67e296", size = 126055, upload-time = "2024-11-23T19:41:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/2e/77/55835914894e00332601a74540840f7665e81f20b3e2b9a97614af8565ed/orjson-3.10.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:accfe93f42713c899fdac2747e8d0d5c659592df2792888c6c5f829472e4f85e", size = 131507, upload-time = "2024-11-23T19:41:57.942Z" }, + { url = "https://files.pythonhosted.org/packages/33/9e/b91288361898e3158062a876b5013c519a5d13e692ac7686e3486c4133ab/orjson-3.10.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7974c490c014c48810d1dede6c754c3cc46598da758c25ca3b4001ac45b703f", size = 131686, upload-time = "2024-11-23T19:41:59.351Z" }, + { url = "https://files.pythonhosted.org/packages/b2/15/08ce117d60a4d2d3fd24e6b21db463139a658e9f52d22c9c30af279b4187/orjson-3.10.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3f250ce7727b0b2682f834a3facff88e310f52f07a5dcfd852d99637d386e79e", size = 415710, upload-time = "2024-11-23T19:42:00.953Z" }, + { url = "https://files.pythonhosted.org/packages/71/af/c09da5ed58f9c002cf83adff7a4cdf3e6cee742aa9723395f8dcdb397233/orjson-3.10.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f31422ff9486ae484f10ffc51b5ab2a60359e92d0716fcce1b3593d7bb8a9af6", size = 142305, upload-time = "2024-11-23T19:42:02.56Z" }, + { url = "https://files.pythonhosted.org/packages/17/d1/8612038d44f33fae231e9ba480d273bac2b0383ce9e77cb06bede1224ae3/orjson-3.10.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5f29c5d282bb2d577c2a6bbde88d8fdcc4919c593f806aac50133f01b733846e", size = 130815, upload-time = "2024-11-23T19:42:04.868Z" }, + { url = "https://files.pythonhosted.org/packages/67/2c/d5f87834be3591555cfaf9aecdf28f480a6f0b4afeaac53bad534bf9518f/orjson-3.10.12-cp313-none-win32.whl", hash = "sha256:f45653775f38f63dc0e6cd4f14323984c3149c05d6007b58cb154dd080ddc0dc", size = 143664, upload-time = "2024-11-23T19:42:06.349Z" }, + { url = "https://files.pythonhosted.org/packages/6a/05/7d768fa3ca23c9b3e1e09117abeded1501119f1d8de0ab722938c91ab25d/orjson-3.10.12-cp313-none-win_amd64.whl", hash = "sha256:229994d0c376d5bdc91d92b3c9e6be2f1fbabd4cc1b59daae1443a46ee5e9825", size = 134944, upload-time = "2024-11-23T19:42:07.842Z" }, +] + +[[package]] +name = "orjson" +version = "3.10.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c7/03913cc4332174071950acf5b0735463e3f63760c80585ef369270c2b372/orjson-3.10.16.tar.gz", hash = "sha256:d2aaa5c495e11d17b9b93205f5fa196737ee3202f000aaebf028dc9a73750f10", size = 5410415, upload-time = "2025-03-24T17:00:23.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/15/67ce9d4c959c83f112542222ea3b9209c1d424231d71d74c4890ea0acd2b/orjson-3.10.16-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6d3444abbfa71ba21bb042caa4b062535b122248259fdb9deea567969140abca", size = 249325, upload-time = "2025-03-24T16:59:19.784Z" }, + { url = "https://files.pythonhosted.org/packages/da/2c/1426b06f30a1b9ada74b6f512c1ddf9d2760f53f61cdb59efeb9ad342133/orjson-3.10.16-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:30245c08d818fdcaa48b7d5b81499b8cae09acabb216fe61ca619876b128e184", size = 133621, upload-time = "2025-03-24T16:59:21.207Z" }, + { url = "https://files.pythonhosted.org/packages/9e/88/18d26130954bc73bee3be10f95371ea1dfb8679e0e2c46b0f6d8c6289402/orjson-3.10.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0ba1d0baa71bf7579a4ccdcf503e6f3098ef9542106a0eca82395898c8a500a", size = 138270, upload-time = "2025-03-24T16:59:22.514Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f9/6d8b64fcd58fae072e80ee7981be8ba0d7c26ace954e5cd1d027fc80518f/orjson-3.10.16-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb0beefa5ef3af8845f3a69ff2a4aa62529b5acec1cfe5f8a6b4141033fd46ef", size = 132346, upload-time = "2025-03-24T16:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/2513fd5bc786f40cd12af569c23cae6381aeddbefeed2a98f0a666eb5d0d/orjson-3.10.16-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6daa0e1c9bf2e030e93c98394de94506f2a4d12e1e9dadd7c53d5e44d0f9628e", size = 136845, upload-time = "2025-03-24T16:59:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/b0e7b36720f5ab722b48e8ccf06514d4f769358dd73c51abd8728ef58d0b/orjson-3.10.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da9019afb21e02410ef600e56666652b73eb3e4d213a0ec919ff391a7dd52aa", size = 138078, upload-time = "2025-03-24T16:59:27.288Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a8/d220afb8a439604be74fc755dbc740bded5ed14745ca536b304ed32eb18a/orjson-3.10.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:daeb3a1ee17b69981d3aae30c3b4e786b0f8c9e6c71f2b48f1aef934f63f38f4", size = 142712, upload-time = "2025-03-24T16:59:28.613Z" }, + { url = "https://files.pythonhosted.org/packages/8c/88/7e41e9883c00f84f92fe357a8371edae816d9d7ef39c67b5106960c20389/orjson-3.10.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fed80eaf0e20a31942ae5d0728849862446512769692474be5e6b73123a23b", size = 133136, upload-time = "2025-03-24T16:59:29.987Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ca/61116095307ad0be828ea26093febaf59e38596d84a9c8d765c3c5e4934f/orjson-3.10.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73390ed838f03764540a7bdc4071fe0123914c2cc02fb6abf35182d5fd1b7a42", size = 135258, upload-time = "2025-03-24T16:59:31.339Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1b/09493cf7d801505f094c9295f79c98c1e0af2ac01c7ed8d25b30fcb19ada/orjson-3.10.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a22bba012a0c94ec02a7768953020ab0d3e2b884760f859176343a36c01adf87", size = 412326, upload-time = "2025-03-24T16:59:32.709Z" }, + { url = "https://files.pythonhosted.org/packages/ea/02/125d7bbd7f7a500190ddc8ae5d2d3c39d87ed3ed28f5b37cfe76962c678d/orjson-3.10.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5385bbfdbc90ff5b2635b7e6bebf259652db00a92b5e3c45b616df75b9058e88", size = 152800, upload-time = "2025-03-24T16:59:34.134Z" }, + { url = "https://files.pythonhosted.org/packages/f9/09/7658a9e3e793d5b3b00598023e0fb6935d0e7bbb8ff72311c5415a8ce677/orjson-3.10.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02c6279016346e774dd92625d46c6c40db687b8a0d685aadb91e26e46cc33e1e", size = 137516, upload-time = "2025-03-24T16:59:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/29/87/32b7a4831e909d347278101a48d4cf9f3f25901b2295e7709df1651f65a1/orjson-3.10.16-cp312-cp312-win32.whl", hash = "sha256:7ca55097a11426db80f79378e873a8c51f4dde9ffc22de44850f9696b7eb0e8c", size = 141759, upload-time = "2025-03-24T16:59:37.509Z" }, + { url = "https://files.pythonhosted.org/packages/35/ce/81a27e7b439b807bd393585271364cdddf50dc281fc57c4feef7ccb186a6/orjson-3.10.16-cp312-cp312-win_amd64.whl", hash = "sha256:86d127efdd3f9bf5f04809b70faca1e6836556ea3cc46e662b44dab3fe71f3d6", size = 133944, upload-time = "2025-03-24T16:59:38.814Z" }, + { url = "https://files.pythonhosted.org/packages/87/b9/ff6aa28b8c86af9526160905593a2fe8d004ac7a5e592ee0b0ff71017511/orjson-3.10.16-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:148a97f7de811ba14bc6dbc4a433e0341ffd2cc285065199fb5f6a98013744bd", size = 249289, upload-time = "2025-03-24T16:59:40.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/81/6d92a586149b52684ab8fd70f3623c91d0e6a692f30fd8c728916ab2263c/orjson-3.10.16-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1d960c1bf0e734ea36d0adc880076de3846aaec45ffad29b78c7f1b7962516b8", size = 133640, upload-time = "2025-03-24T16:59:41.469Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/b72443f4793d2e16039ab85d0026677932b15ab968595fb7149750d74134/orjson-3.10.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a318cd184d1269f68634464b12871386808dc8b7c27de8565234d25975a7a137", size = 138286, upload-time = "2025-03-24T16:59:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3c/72a22d4b28c076c4016d5a52bd644a8e4d849d3bb0373d9e377f9e3b2250/orjson-3.10.16-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df23f8df3ef9223d1d6748bea63fca55aae7da30a875700809c500a05975522b", size = 132307, upload-time = "2025-03-24T16:59:44.143Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a2/f1259561bdb6ad7061ff1b95dab082fe32758c4bc143ba8d3d70831f0a06/orjson-3.10.16-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b94dda8dd6d1378f1037d7f3f6b21db769ef911c4567cbaa962bb6dc5021cf90", size = 136739, upload-time = "2025-03-24T16:59:45.995Z" }, + { url = "https://files.pythonhosted.org/packages/3d/af/c7583c4b34f33d8b8b90cfaab010ff18dd64e7074cc1e117a5f1eff20dcf/orjson-3.10.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12970a26666a8775346003fd94347d03ccb98ab8aa063036818381acf5f523e", size = 138076, upload-time = "2025-03-24T16:59:47.776Z" }, + { url = "https://files.pythonhosted.org/packages/d7/59/d7fc7fbdd3d4a64c2eae4fc7341a5aa39cf9549bd5e2d7f6d3c07f8b715b/orjson-3.10.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15a1431a245d856bd56e4d29ea0023eb4d2c8f71efe914beb3dee8ab3f0cd7fb", size = 142643, upload-time = "2025-03-24T16:59:49.258Z" }, + { url = "https://files.pythonhosted.org/packages/92/0e/3bd8f2197d27601f16b4464ae948826da2bcf128af31230a9dbbad7ceb57/orjson-3.10.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83655cfc247f399a222567d146524674a7b217af7ef8289c0ff53cfe8db09f0", size = 133168, upload-time = "2025-03-24T16:59:51.027Z" }, + { url = "https://files.pythonhosted.org/packages/af/a8/351fd87b664b02f899f9144d2c3dc848b33ac04a5df05234cbfb9e2a7540/orjson-3.10.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fa59ae64cb6ddde8f09bdbf7baf933c4cd05734ad84dcf4e43b887eb24e37652", size = 135271, upload-time = "2025-03-24T16:59:52.449Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b0/a6d42a7d412d867c60c0337d95123517dd5a9370deea705ea1be0f89389e/orjson-3.10.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ca5426e5aacc2e9507d341bc169d8af9c3cbe88f4cd4c1cf2f87e8564730eb56", size = 412444, upload-time = "2025-03-24T16:59:53.825Z" }, + { url = "https://files.pythonhosted.org/packages/79/ec/7572cd4e20863f60996f3f10bc0a6da64a6fd9c35954189a914cec0b7377/orjson-3.10.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6fd5da4edf98a400946cd3a195680de56f1e7575109b9acb9493331047157430", size = 152737, upload-time = "2025-03-24T16:59:55.599Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/ceb9e8fed5403b2e76a8ac15f581b9d25780a3be3c9b3aa54b7777a210d5/orjson-3.10.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:980ecc7a53e567169282a5e0ff078393bac78320d44238da4e246d71a4e0e8f5", size = 137482, upload-time = "2025-03-24T16:59:57.045Z" }, + { url = "https://files.pythonhosted.org/packages/1b/78/a78bb810f3786579dbbbd94768284cbe8f2fd65167cd7020260679665c17/orjson-3.10.16-cp313-cp313-win32.whl", hash = "sha256:28f79944dd006ac540a6465ebd5f8f45dfdf0948ff998eac7a908275b4c1add6", size = 141714, upload-time = "2025-03-24T16:59:58.666Z" }, + { url = "https://files.pythonhosted.org/packages/81/9c/b66ce9245ff319df2c3278acd351a3f6145ef34b4a2d7f4b0f739368370f/orjson-3.10.16-cp313-cp313-win_amd64.whl", hash = "sha256:fe0a145e96d51971407cb8ba947e63ead2aa915db59d6631a355f5f2150b56b7", size = 133954, upload-time = "2025-03-24T17:00:00.101Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/be/4d/8df5f83256a809c22c4d6792ce8d43bb503be0fb7a8e4da9025754b09658/orjson-3.11.3.tar.gz", hash = "sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a", size = 5482394, upload-time = "2025-08-26T17:46:43.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/b0/a7edab2a00cdcb2688e1c943401cb3236323e7bfd2839815c6131a3742f4/orjson-3.11.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b", size = 238259, upload-time = "2025-08-26T17:45:15.093Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c6/ff4865a9cc398a07a83342713b5932e4dc3cb4bf4bc04e8f83dedfc0d736/orjson-3.11.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2", size = 127633, upload-time = "2025-08-26T17:45:16.417Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e6/e00bea2d9472f44fe8794f523e548ce0ad51eb9693cf538a753a27b8bda4/orjson-3.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a", size = 123061, upload-time = "2025-08-26T17:45:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/31/9fbb78b8e1eb3ac605467cb846e1c08d0588506028b37f4ee21f978a51d4/orjson-3.11.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c", size = 127956, upload-time = "2025-08-26T17:45:19.172Z" }, + { url = "https://files.pythonhosted.org/packages/36/88/b0604c22af1eed9f98d709a96302006915cfd724a7ebd27d6dd11c22d80b/orjson-3.11.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064", size = 130790, upload-time = "2025-08-26T17:45:20.586Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/1c1238ae9fffbfed51ba1e507731b3faaf6b846126a47e9649222b0fd06f/orjson-3.11.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424", size = 132385, upload-time = "2025-08-26T17:45:22.036Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b5/c06f1b090a1c875f337e21dd71943bc9d84087f7cdf8c6e9086902c34e42/orjson-3.11.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23", size = 135305, upload-time = "2025-08-26T17:45:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/a0/26/5f028c7d81ad2ebbf84414ba6d6c9cac03f22f5cd0d01eb40fb2d6a06b07/orjson-3.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667", size = 132875, upload-time = "2025-08-26T17:45:25.182Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/b8df70d9cfb56e385bf39b4e915298f9ae6c61454c8154a0f5fd7efcd42e/orjson-3.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f", size = 130940, upload-time = "2025-08-26T17:45:27.209Z" }, + { url = "https://files.pythonhosted.org/packages/da/5e/afe6a052ebc1a4741c792dd96e9f65bf3939d2094e8b356503b68d48f9f5/orjson-3.11.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1", size = 403852, upload-time = "2025-08-26T17:45:28.478Z" }, + { url = "https://files.pythonhosted.org/packages/f8/90/7bbabafeb2ce65915e9247f14a56b29c9334003536009ef5b122783fe67e/orjson-3.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc", size = 146293, upload-time = "2025-08-26T17:45:29.86Z" }, + { url = "https://files.pythonhosted.org/packages/27/b3/2d703946447da8b093350570644a663df69448c9d9330e5f1d9cce997f20/orjson-3.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049", size = 135470, upload-time = "2025-08-26T17:45:31.243Z" }, + { url = "https://files.pythonhosted.org/packages/38/70/b14dcfae7aff0e379b0119c8a812f8396678919c431efccc8e8a0263e4d9/orjson-3.11.3-cp312-cp312-win32.whl", hash = "sha256:3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca", size = 136248, upload-time = "2025-08-26T17:45:32.567Z" }, + { url = "https://files.pythonhosted.org/packages/35/b8/9e3127d65de7fff243f7f3e53f59a531bf6bb295ebe5db024c2503cc0726/orjson-3.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1", size = 131437, upload-time = "2025-08-26T17:45:34.949Z" }, + { url = "https://files.pythonhosted.org/packages/51/92/a946e737d4d8a7fd84a606aba96220043dcc7d6988b9e7551f7f6d5ba5ad/orjson-3.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710", size = 125978, upload-time = "2025-08-26T17:45:36.422Z" }, + { url = "https://files.pythonhosted.org/packages/fc/79/8932b27293ad35919571f77cb3693b5906cf14f206ef17546052a241fdf6/orjson-3.11.3-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810", size = 238127, upload-time = "2025-08-26T17:45:38.146Z" }, + { url = "https://files.pythonhosted.org/packages/1c/82/cb93cd8cf132cd7643b30b6c5a56a26c4e780c7a145db6f83de977b540ce/orjson-3.11.3-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43", size = 127494, upload-time = "2025-08-26T17:45:39.57Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/2d9eb181a9b6bb71463a78882bcac1027fd29cf62c38a40cc02fc11d3495/orjson-3.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27", size = 123017, upload-time = "2025-08-26T17:45:40.876Z" }, + { url = "https://files.pythonhosted.org/packages/b4/14/a0e971e72d03b509190232356d54c0f34507a05050bd026b8db2bf2c192c/orjson-3.11.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f", size = 127898, upload-time = "2025-08-26T17:45:42.188Z" }, + { url = "https://files.pythonhosted.org/packages/8e/af/dc74536722b03d65e17042cc30ae586161093e5b1f29bccda24765a6ae47/orjson-3.11.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c", size = 130742, upload-time = "2025-08-26T17:45:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/7a3b63b6677bce089fe939353cda24a7679825c43a24e49f757805fc0d8a/orjson-3.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be", size = 132377, upload-time = "2025-08-26T17:45:45.525Z" }, + { url = "https://files.pythonhosted.org/packages/fc/cd/ce2ab93e2e7eaf518f0fd15e3068b8c43216c8a44ed82ac2b79ce5cef72d/orjson-3.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d", size = 135313, upload-time = "2025-08-26T17:45:46.821Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b4/f98355eff0bd1a38454209bbc73372ce351ba29933cb3e2eba16c04b9448/orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2", size = 132908, upload-time = "2025-08-26T17:45:48.126Z" }, + { url = "https://files.pythonhosted.org/packages/eb/92/8f5182d7bc2a1bed46ed960b61a39af8389f0ad476120cd99e67182bfb6d/orjson-3.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f", size = 130905, upload-time = "2025-08-26T17:45:49.414Z" }, + { url = "https://files.pythonhosted.org/packages/1a/60/c41ca753ce9ffe3d0f67b9b4c093bdd6e5fdb1bc53064f992f66bb99954d/orjson-3.11.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee", size = 403812, upload-time = "2025-08-26T17:45:51.085Z" }, + { url = "https://files.pythonhosted.org/packages/dd/13/e4a4f16d71ce1868860db59092e78782c67082a8f1dc06a3788aef2b41bc/orjson-3.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e", size = 146277, upload-time = "2025-08-26T17:45:52.851Z" }, + { url = "https://files.pythonhosted.org/packages/8d/8b/bafb7f0afef9344754a3a0597a12442f1b85a048b82108ef2c956f53babd/orjson-3.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633", size = 135418, upload-time = "2025-08-26T17:45:54.806Z" }, + { url = "https://files.pythonhosted.org/packages/60/d4/bae8e4f26afb2c23bea69d2f6d566132584d1c3a5fe89ee8c17b718cab67/orjson-3.11.3-cp313-cp313-win32.whl", hash = "sha256:2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b", size = 136216, upload-time = "2025-08-26T17:45:57.182Z" }, + { url = "https://files.pythonhosted.org/packages/88/76/224985d9f127e121c8cad882cea55f0ebe39f97925de040b75ccd4b33999/orjson-3.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae", size = 131362, upload-time = "2025-08-26T17:45:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cf/0dce7a0be94bd36d1346be5067ed65ded6adb795fdbe3abd234c8d576d01/orjson-3.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce", size = 125989, upload-time = "2025-08-26T17:45:59.95Z" }, + { url = "https://files.pythonhosted.org/packages/ef/77/d3b1fef1fc6aaeed4cbf3be2b480114035f4df8fa1a99d2dac1d40d6e924/orjson-3.11.3-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4", size = 238115, upload-time = "2025-08-26T17:46:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/468d21d49bb12f900052edcfbf52c292022d0a323d7828dc6376e6319703/orjson-3.11.3-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e", size = 127493, upload-time = "2025-08-26T17:46:03.466Z" }, + { url = "https://files.pythonhosted.org/packages/67/46/1e2588700d354aacdf9e12cc2d98131fb8ac6f31ca65997bef3863edb8ff/orjson-3.11.3-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d", size = 122998, upload-time = "2025-08-26T17:46:04.803Z" }, + { url = "https://files.pythonhosted.org/packages/3b/94/11137c9b6adb3779f1b34fd98be51608a14b430dbc02c6d41134fbba484c/orjson-3.11.3-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229", size = 132915, upload-time = "2025-08-26T17:46:06.237Z" }, + { url = "https://files.pythonhosted.org/packages/10/61/dccedcf9e9bcaac09fdabe9eaee0311ca92115699500efbd31950d878833/orjson-3.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451", size = 130907, upload-time = "2025-08-26T17:46:07.581Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/0e935539aa7b08b3ca0f817d73034f7eb506792aae5ecc3b7c6e679cdf5f/orjson-3.11.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167", size = 403852, upload-time = "2025-08-26T17:46:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2b/50ae1a5505cd1043379132fdb2adb8a05f37b3e1ebffe94a5073321966fd/orjson-3.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077", size = 146309, upload-time = "2025-08-26T17:46:10.576Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1d/a473c158e380ef6f32753b5f39a69028b25ec5be331c2049a2201bde2e19/orjson-3.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872", size = 135424, upload-time = "2025-08-26T17:46:12.386Z" }, + { url = "https://files.pythonhosted.org/packages/da/09/17d9d2b60592890ff7382e591aa1d9afb202a266b180c3d4049b1ec70e4a/orjson-3.11.3-cp314-cp314-win32.whl", hash = "sha256:0c6d7328c200c349e3a4c6d8c83e0a5ad029bdc2d417f234152bf34842d0fc8d", size = 136266, upload-time = "2025-08-26T17:46:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/15/58/358f6846410a6b4958b74734727e582ed971e13d335d6c7ce3e47730493e/orjson-3.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:317bbe2c069bbc757b1a2e4105b64aacd3bc78279b66a6b9e51e846e4809f804", size = 131351, upload-time = "2025-08-26T17:46:15.27Z" }, + { url = "https://files.pythonhosted.org/packages/28/01/d6b274a0635be0468d4dbd9cafe80c47105937a0d42434e805e67cd2ed8b/orjson-3.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:e8f6a7a27d7b7bec81bd5924163e9af03d49bbb63013f107b48eb5d16db711bc", size = 125985, upload-time = "2025-08-26T17:46:16.67Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, +] + +[[package]] +name = "pillow" +version = "11.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/26/0d95c04c868f6bdb0c447e3ee2de5564411845e36a858cfd63766bc7b563/pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", size = 46737780, upload-time = "2024-10-15T14:24:29.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/a3/26e606ff0b2daaf120543e537311fa3ae2eb6bf061490e4fea51771540be/pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", size = 3147642, upload-time = "2024-10-15T14:22:37.736Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d5/1caabedd8863526a6cfa44ee7a833bd97f945dc1d56824d6d76e11731939/pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", size = 2978999, upload-time = "2024-10-15T14:22:39.654Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ff/5a45000826a1aa1ac6874b3ec5a856474821a1b59d838c4f6ce2ee518fe9/pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", size = 4196794, upload-time = "2024-10-15T14:22:41.598Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/84c9f287d17180f26263b5f5c8fb201de0f88b1afddf8a2597a5c9fe787f/pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", size = 4300762, upload-time = "2024-10-15T14:22:45.952Z" }, + { url = "https://files.pythonhosted.org/packages/84/39/63fb87cd07cc541438b448b1fed467c4d687ad18aa786a7f8e67b255d1aa/pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9", size = 4210468, upload-time = "2024-10-15T14:22:47.789Z" }, + { url = "https://files.pythonhosted.org/packages/7f/42/6e0f2c2d5c60f499aa29be14f860dd4539de322cd8fb84ee01553493fb4d/pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", size = 4381824, upload-time = "2024-10-15T14:22:49.668Z" }, + { url = "https://files.pythonhosted.org/packages/31/69/1ef0fb9d2f8d2d114db982b78ca4eeb9db9a29f7477821e160b8c1253f67/pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", size = 4296436, upload-time = "2024-10-15T14:22:51.911Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/dad2818c675c44f6012289a7c4f46068c548768bc6c7f4e8c4ae5bbbc811/pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", size = 4429714, upload-time = "2024-10-15T14:22:53.967Z" }, + { url = "https://files.pythonhosted.org/packages/af/3a/da80224a6eb15bba7a0dcb2346e2b686bb9bf98378c0b4353cd88e62b171/pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", size = 2249631, upload-time = "2024-10-15T14:22:56.404Z" }, + { url = "https://files.pythonhosted.org/packages/57/97/73f756c338c1d86bb802ee88c3cab015ad7ce4b838f8a24f16b676b1ac7c/pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", size = 2567533, upload-time = "2024-10-15T14:22:58.087Z" }, + { url = "https://files.pythonhosted.org/packages/0b/30/2b61876e2722374558b871dfbfcbe4e406626d63f4f6ed92e9c8e24cac37/pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", size = 2254890, upload-time = "2024-10-15T14:22:59.918Z" }, + { url = "https://files.pythonhosted.org/packages/63/24/e2e15e392d00fcf4215907465d8ec2a2f23bcec1481a8ebe4ae760459995/pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", size = 3147300, upload-time = "2024-10-15T14:23:01.855Z" }, + { url = "https://files.pythonhosted.org/packages/43/72/92ad4afaa2afc233dc44184adff289c2e77e8cd916b3ddb72ac69495bda3/pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", size = 2978742, upload-time = "2024-10-15T14:23:03.749Z" }, + { url = "https://files.pythonhosted.org/packages/9e/da/c8d69c5bc85d72a8523fe862f05ababdc52c0a755cfe3d362656bb86552b/pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", size = 4194349, upload-time = "2024-10-15T14:23:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e8/686d0caeed6b998351d57796496a70185376ed9c8ec7d99e1d19ad591fc6/pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", size = 4298714, upload-time = "2024-10-15T14:23:07.919Z" }, + { url = "https://files.pythonhosted.org/packages/ec/da/430015cec620d622f06854be67fd2f6721f52fc17fca8ac34b32e2d60739/pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", size = 4208514, upload-time = "2024-10-15T14:23:10.19Z" }, + { url = "https://files.pythonhosted.org/packages/44/ae/7e4f6662a9b1cb5f92b9cc9cab8321c381ffbee309210940e57432a4063a/pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", size = 4380055, upload-time = "2024-10-15T14:23:12.08Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/1a807779ac8a0eeed57f2b92a3c32ea1b696e6140c15bd42eaf908a261cd/pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", size = 4296751, upload-time = "2024-10-15T14:23:13.836Z" }, + { url = "https://files.pythonhosted.org/packages/38/8c/5fa3385163ee7080bc13026d59656267daaaaf3c728c233d530e2c2757c8/pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", size = 4430378, upload-time = "2024-10-15T14:23:15.735Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1d/ad9c14811133977ff87035bf426875b93097fb50af747793f013979facdb/pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", size = 2249588, upload-time = "2024-10-15T14:23:17.905Z" }, + { url = "https://files.pythonhosted.org/packages/fb/01/3755ba287dac715e6afdb333cb1f6d69740a7475220b4637b5ce3d78cec2/pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", size = 2567509, upload-time = "2024-10-15T14:23:19.643Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/2c7d727079b6be1aba82d195767d35fcc2d32204c7a5820f822df5330152/pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", size = 2254791, upload-time = "2024-10-15T14:23:21.601Z" }, + { url = "https://files.pythonhosted.org/packages/eb/38/998b04cc6f474e78b563716b20eecf42a2fa16a84589d23c8898e64b0ffd/pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", size = 3150854, upload-time = "2024-10-15T14:23:23.91Z" }, + { url = "https://files.pythonhosted.org/packages/13/8e/be23a96292113c6cb26b2aa3c8b3681ec62b44ed5c2bd0b258bd59503d3c/pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", size = 2982369, upload-time = "2024-10-15T14:23:27.184Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/3db4eaabb7a2ae8203cd3a332a005e4aba00067fc514aaaf3e9721be31f1/pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", size = 4333703, upload-time = "2024-10-15T14:23:28.979Z" }, + { url = "https://files.pythonhosted.org/packages/28/ac/629ffc84ff67b9228fe87a97272ab125bbd4dc462745f35f192d37b822f1/pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", size = 4412550, upload-time = "2024-10-15T14:23:30.846Z" }, + { url = "https://files.pythonhosted.org/packages/d6/07/a505921d36bb2df6868806eaf56ef58699c16c388e378b0dcdb6e5b2fb36/pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", size = 4461038, upload-time = "2024-10-15T14:23:32.687Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b9/fb620dd47fc7cc9678af8f8bd8c772034ca4977237049287e99dda360b66/pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", size = 2253197, upload-time = "2024-10-15T14:23:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/df/86/25dde85c06c89d7fc5db17940f07aae0a56ac69aa9ccb5eb0f09798862a8/pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", size = 2572169, upload-time = "2024-10-15T14:23:37.33Z" }, + { url = "https://files.pythonhosted.org/packages/51/85/9c33f2517add612e17f3381aee7c4072779130c634921a756c97bc29fb49/pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", size = 2256828, upload-time = "2024-10-15T14:23:39.826Z" }, +] + +[[package]] +name = "pillow" +version = "11.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715, upload-time = "2025-01-02T08:13:58.407Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/20/9ce6ed62c91c073fcaa23d216e68289e19d95fb8188b9fb7a63d36771db8/pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a", size = 3226818, upload-time = "2025-01-02T08:11:22.518Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d8/f6004d98579a2596c098d1e30d10b248798cceff82d2b77aa914875bfea1/pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b", size = 3101662, upload-time = "2025-01-02T08:11:25.19Z" }, + { url = "https://files.pythonhosted.org/packages/08/d9/892e705f90051c7a2574d9f24579c9e100c828700d78a63239676f960b74/pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3", size = 4329317, upload-time = "2025-01-02T08:11:30.371Z" }, + { url = "https://files.pythonhosted.org/packages/8c/aa/7f29711f26680eab0bcd3ecdd6d23ed6bce180d82e3f6380fb7ae35fcf3b/pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a", size = 4412999, upload-time = "2025-01-02T08:11:33.499Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c4/8f0fe3b9e0f7196f6d0bbb151f9fba323d72a41da068610c4c960b16632a/pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1", size = 4368819, upload-time = "2025-01-02T08:11:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/38/0d/84200ed6a871ce386ddc82904bfadc0c6b28b0c0ec78176871a4679e40b3/pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f", size = 4496081, upload-time = "2025-01-02T08:11:39.598Z" }, + { url = "https://files.pythonhosted.org/packages/84/9c/9bcd66f714d7e25b64118e3952d52841a4babc6d97b6d28e2261c52045d4/pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91", size = 4296513, upload-time = "2025-01-02T08:11:43.083Z" }, + { url = "https://files.pythonhosted.org/packages/db/61/ada2a226e22da011b45f7104c95ebda1b63dcbb0c378ad0f7c2a710f8fd2/pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c", size = 4431298, upload-time = "2025-01-02T08:11:46.626Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fc6e86750523f367923522014b821c11ebc5ad402e659d8c9d09b3c9d70c/pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6", size = 2291630, upload-time = "2025-01-02T08:11:49.401Z" }, + { url = "https://files.pythonhosted.org/packages/08/5c/2104299949b9d504baf3f4d35f73dbd14ef31bbd1ddc2c1b66a5b7dfda44/pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf", size = 2626369, upload-time = "2025-01-02T08:11:52.02Z" }, + { url = "https://files.pythonhosted.org/packages/37/f3/9b18362206b244167c958984b57c7f70a0289bfb59a530dd8af5f699b910/pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5", size = 2375240, upload-time = "2025-01-02T08:11:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/b3/31/9ca79cafdce364fd5c980cd3416c20ce1bebd235b470d262f9d24d810184/pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", size = 3226640, upload-time = "2025-01-02T08:11:58.329Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0f/ff07ad45a1f172a497aa393b13a9d81a32e1477ef0e869d030e3c1532521/pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", size = 3101437, upload-time = "2025-01-02T08:12:01.797Z" }, + { url = "https://files.pythonhosted.org/packages/08/2f/9906fca87a68d29ec4530be1f893149e0cb64a86d1f9f70a7cfcdfe8ae44/pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", size = 4326605, upload-time = "2025-01-02T08:12:05.224Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0f/f3547ee15b145bc5c8b336401b2d4c9d9da67da9dcb572d7c0d4103d2c69/pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", size = 4411173, upload-time = "2025-01-02T08:12:08.281Z" }, + { url = "https://files.pythonhosted.org/packages/b1/df/bf8176aa5db515c5de584c5e00df9bab0713548fd780c82a86cba2c2fedb/pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", size = 4369145, upload-time = "2025-01-02T08:12:11.411Z" }, + { url = "https://files.pythonhosted.org/packages/de/7c/7433122d1cfadc740f577cb55526fdc39129a648ac65ce64db2eb7209277/pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", size = 4496340, upload-time = "2025-01-02T08:12:15.29Z" }, + { url = "https://files.pythonhosted.org/packages/25/46/dd94b93ca6bd555588835f2504bd90c00d5438fe131cf01cfa0c5131a19d/pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", size = 4296906, upload-time = "2025-01-02T08:12:17.485Z" }, + { url = "https://files.pythonhosted.org/packages/a8/28/2f9d32014dfc7753e586db9add35b8a41b7a3b46540e965cb6d6bc607bd2/pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", size = 4431759, upload-time = "2025-01-02T08:12:20.382Z" }, + { url = "https://files.pythonhosted.org/packages/33/48/19c2cbe7403870fbe8b7737d19eb013f46299cdfe4501573367f6396c775/pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", size = 2291657, upload-time = "2025-01-02T08:12:23.922Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ad/285c556747d34c399f332ba7c1a595ba245796ef3e22eae190f5364bb62b/pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", size = 2626304, upload-time = "2025-01-02T08:12:28.069Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/ef35a71163bf36db06e9c8729608f78dedf032fc8313d19bd4be5c2588f3/pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", size = 2375117, upload-time = "2025-01-02T08:12:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/77f54228401e84d6791354888549b45824ab0ffde659bafa67956303a09f/pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", size = 3230060, upload-time = "2025-01-02T08:12:32.362Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b1/56723b74b07dd64c1010fee011951ea9c35a43d8020acd03111f14298225/pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", size = 3106192, upload-time = "2025-01-02T08:12:34.361Z" }, + { url = "https://files.pythonhosted.org/packages/e1/cd/7bf7180e08f80a4dcc6b4c3a0aa9e0b0ae57168562726a05dc8aa8fa66b0/pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", size = 4446805, upload-time = "2025-01-02T08:12:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/97/42/87c856ea30c8ed97e8efbe672b58c8304dee0573f8c7cab62ae9e31db6ae/pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", size = 4530623, upload-time = "2025-01-02T08:12:41.912Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/026879e90c84a88e33fb00cc6bd915ac2743c67e87a18f80270dfe3c2041/pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", size = 4465191, upload-time = "2025-01-02T08:12:45.186Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494, upload-time = "2025-01-02T08:12:47.098Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595, upload-time = "2025-01-02T08:12:50.47Z" }, + { url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651, upload-time = "2025-01-02T08:12:53.356Z" }, +] + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13.2'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "propcache" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/20/c8/2a13f78d82211490855b2fb303b6721348d0787fdd9a12ac46d99d3acde1/propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64", size = 41735, upload-time = "2024-12-01T18:29:16.437Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/28/1d205fe49be8b1b4df4c50024e62480a442b1a7b818e734308bb0d17e7fb/propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a", size = 79588, upload-time = "2024-12-01T18:28:03.327Z" }, + { url = "https://files.pythonhosted.org/packages/21/ee/fc4d893f8d81cd4971affef2a6cb542b36617cd1d8ce56b406112cb80bf7/propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0", size = 45825, upload-time = "2024-12-01T18:28:06.78Z" }, + { url = "https://files.pythonhosted.org/packages/4a/de/bbe712f94d088da1d237c35d735f675e494a816fd6f54e9db2f61ef4d03f/propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d", size = 45357, upload-time = "2024-12-01T18:28:08.575Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/7ae06a6cf2a2f1cb382586d5a99efe66b0b3d0c6f9ac2f759e6f7af9d7cf/propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4", size = 241869, upload-time = "2024-12-01T18:28:10.396Z" }, + { url = "https://files.pythonhosted.org/packages/cc/59/227a78be960b54a41124e639e2c39e8807ac0c751c735a900e21315f8c2b/propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d", size = 247884, upload-time = "2024-12-01T18:28:11.746Z" }, + { url = "https://files.pythonhosted.org/packages/84/58/f62b4ffaedf88dc1b17f04d57d8536601e4e030feb26617228ef930c3279/propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5", size = 248486, upload-time = "2024-12-01T18:28:13.048Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/ebe102777a830bca91bbb93e3479cd34c2ca5d0361b83be9dbd93104865e/propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24", size = 243649, upload-time = "2024-12-01T18:28:14.297Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bc/4f7aba7f08f520376c4bb6a20b9a981a581b7f2e385fa0ec9f789bb2d362/propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff", size = 229103, upload-time = "2024-12-01T18:28:15.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d5/04ac9cd4e51a57a96f78795e03c5a0ddb8f23ec098b86f92de028d7f2a6b/propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f", size = 226607, upload-time = "2024-12-01T18:28:18.015Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f0/24060d959ea41d7a7cc7fdbf68b31852331aabda914a0c63bdb0e22e96d6/propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec", size = 221153, upload-time = "2024-12-01T18:28:19.937Z" }, + { url = "https://files.pythonhosted.org/packages/77/a7/3ac76045a077b3e4de4859a0753010765e45749bdf53bd02bc4d372da1a0/propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348", size = 222151, upload-time = "2024-12-01T18:28:21.186Z" }, + { url = "https://files.pythonhosted.org/packages/e7/af/5e29da6f80cebab3f5a4dcd2a3240e7f56f2c4abf51cbfcc99be34e17f0b/propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6", size = 233812, upload-time = "2024-12-01T18:28:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/8c/89/ebe3ad52642cc5509eaa453e9f4b94b374d81bae3265c59d5c2d98efa1b4/propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6", size = 238829, upload-time = "2024-12-01T18:28:24.071Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2f/6b32f273fa02e978b7577159eae7471b3cfb88b48563b1c2578b2d7ca0bb/propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518", size = 230704, upload-time = "2024-12-01T18:28:25.314Z" }, + { url = "https://files.pythonhosted.org/packages/5c/2e/f40ae6ff5624a5f77edd7b8359b208b5455ea113f68309e2b00a2e1426b6/propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246", size = 40050, upload-time = "2024-12-01T18:28:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/a92c3ef994e47180862b9d7d11e37624fb1c00a16d61faf55115d970628b/propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1", size = 44117, upload-time = "2024-12-01T18:28:27.643Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2a/329e0547cf2def8857157f9477669043e75524cc3e6251cef332b3ff256f/propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc", size = 77002, upload-time = "2024-12-01T18:28:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/12/2d/c4df5415e2382f840dc2ecbca0eeb2293024bc28e57a80392f2012b4708c/propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9", size = 44639, upload-time = "2024-12-01T18:28:30.199Z" }, + { url = "https://files.pythonhosted.org/packages/d0/5a/21aaa4ea2f326edaa4e240959ac8b8386ea31dedfdaa636a3544d9e7a408/propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439", size = 44049, upload-time = "2024-12-01T18:28:31.308Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3e/021b6cd86c0acc90d74784ccbb66808b0bd36067a1bf3e2deb0f3845f618/propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536", size = 224819, upload-time = "2024-12-01T18:28:32.755Z" }, + { url = "https://files.pythonhosted.org/packages/3c/57/c2fdeed1b3b8918b1770a133ba5c43ad3d78e18285b0c06364861ef5cc38/propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629", size = 229625, upload-time = "2024-12-01T18:28:34.083Z" }, + { url = "https://files.pythonhosted.org/packages/9d/81/70d4ff57bf2877b5780b466471bebf5892f851a7e2ca0ae7ffd728220281/propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b", size = 232934, upload-time = "2024-12-01T18:28:35.434Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b9/bb51ea95d73b3fb4100cb95adbd4e1acaf2cbb1fd1083f5468eeb4a099a8/propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052", size = 227361, upload-time = "2024-12-01T18:28:36.777Z" }, + { url = "https://files.pythonhosted.org/packages/f1/20/3c6d696cd6fd70b29445960cc803b1851a1131e7a2e4ee261ee48e002bcd/propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce", size = 213904, upload-time = "2024-12-01T18:28:38.041Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cb/1593bfc5ac6d40c010fa823f128056d6bc25b667f5393781e37d62f12005/propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d", size = 212632, upload-time = "2024-12-01T18:28:39.401Z" }, + { url = "https://files.pythonhosted.org/packages/6d/5c/e95617e222be14a34c709442a0ec179f3207f8a2b900273720501a70ec5e/propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce", size = 207897, upload-time = "2024-12-01T18:28:40.996Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3b/56c5ab3dc00f6375fbcdeefdede5adf9bee94f1fab04adc8db118f0f9e25/propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95", size = 208118, upload-time = "2024-12-01T18:28:42.38Z" }, + { url = "https://files.pythonhosted.org/packages/86/25/d7ef738323fbc6ebcbce33eb2a19c5e07a89a3df2fded206065bd5e868a9/propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf", size = 217851, upload-time = "2024-12-01T18:28:43.655Z" }, + { url = "https://files.pythonhosted.org/packages/b3/77/763e6cef1852cf1ba740590364ec50309b89d1c818e3256d3929eb92fabf/propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f", size = 222630, upload-time = "2024-12-01T18:28:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e9/0f86be33602089c701696fbed8d8c4c07b6ee9605c5b7536fd27ed540c5b/propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30", size = 216269, upload-time = "2024-12-01T18:28:47.602Z" }, + { url = "https://files.pythonhosted.org/packages/cc/02/5ac83217d522394b6a2e81a2e888167e7ca629ef6569a3f09852d6dcb01a/propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6", size = 39472, upload-time = "2024-12-01T18:28:48.983Z" }, + { url = "https://files.pythonhosted.org/packages/f4/33/d6f5420252a36034bc8a3a01171bc55b4bff5df50d1c63d9caa50693662f/propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1", size = 43363, upload-time = "2024-12-01T18:28:50.025Z" }, + { url = "https://files.pythonhosted.org/packages/41/b6/c5319caea262f4821995dca2107483b94a3345d4607ad797c76cb9c36bcc/propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54", size = 11818, upload-time = "2024-12-01T18:29:14.716Z" }, +] + +[[package]] +name = "propcache" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/92/76/f941e63d55c0293ff7829dd21e7cf1147e90a526756869a9070f287a68c9/propcache-0.3.0.tar.gz", hash = "sha256:a8fd93de4e1d278046345f49e2238cdb298589325849b2645d4a94c53faeffc5", size = 42722, upload-time = "2025-02-20T19:03:29.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/2c/921f15dc365796ec23975b322b0078eae72995c7b4d49eba554c6a308d70/propcache-0.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e53d19c2bf7d0d1e6998a7e693c7e87300dd971808e6618964621ccd0e01fe4e", size = 79867, upload-time = "2025-02-20T19:00:59.948Z" }, + { url = "https://files.pythonhosted.org/packages/11/a5/4a6cc1a559d1f2fb57ea22edc4245158cdffae92f7f92afcee2913f84417/propcache-0.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a61a68d630e812b67b5bf097ab84e2cd79b48c792857dc10ba8a223f5b06a2af", size = 46109, upload-time = "2025-02-20T19:01:04.447Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6d/28bfd3af3a567ad7d667348e7f46a520bda958229c4d545ba138a044232f/propcache-0.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb91d20fa2d3b13deea98a690534697742029f4fb83673a3501ae6e3746508b5", size = 45635, upload-time = "2025-02-20T19:01:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/73/20/d75b42eaffe5075eac2f4e168f6393d21c664c91225288811d85451b2578/propcache-0.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67054e47c01b7b349b94ed0840ccae075449503cf1fdd0a1fdd98ab5ddc2667b", size = 242159, upload-time = "2025-02-20T19:01:10.047Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fb/4b537dd92f9fd4be68042ec51c9d23885ca5fafe51ec24c58d9401034e5f/propcache-0.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:997e7b8f173a391987df40f3b52c423e5850be6f6df0dcfb5376365440b56667", size = 248163, upload-time = "2025-02-20T19:01:12.883Z" }, + { url = "https://files.pythonhosted.org/packages/e7/af/8a9db04ac596d531ca0ef7dde518feaadfcdabef7b17d6a5ec59ee3effc2/propcache-0.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d663fd71491dde7dfdfc899d13a067a94198e90695b4321084c6e450743b8c7", size = 248794, upload-time = "2025-02-20T19:01:15.291Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c4/ecfc988879c0fd9db03228725b662d76cf484b6b46f7e92fee94e4b52490/propcache-0.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8884ba1a0fe7210b775106b25850f5e5a9dc3c840d1ae9924ee6ea2eb3acbfe7", size = 243912, upload-time = "2025-02-20T19:01:16.95Z" }, + { url = "https://files.pythonhosted.org/packages/04/a2/298dd27184faa8b7d91cc43488b578db218b3cc85b54d912ed27b8c5597a/propcache-0.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa806bbc13eac1ab6291ed21ecd2dd426063ca5417dd507e6be58de20e58dfcf", size = 229402, upload-time = "2025-02-20T19:01:20.913Z" }, + { url = "https://files.pythonhosted.org/packages/be/0d/efe7fec316ca92dbf4bc4a9ba49ca889c43ca6d48ab1d6fa99fc94e5bb98/propcache-0.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f4d7a7c0aff92e8354cceca6fe223973ddf08401047920df0fcb24be2bd5138", size = 226896, upload-time = "2025-02-20T19:01:23.57Z" }, + { url = "https://files.pythonhosted.org/packages/60/63/72404380ae1d9c96d96e165aa02c66c2aae6072d067fc4713da5cde96762/propcache-0.3.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9be90eebc9842a93ef8335291f57b3b7488ac24f70df96a6034a13cb58e6ff86", size = 221447, upload-time = "2025-02-20T19:01:26.142Z" }, + { url = "https://files.pythonhosted.org/packages/9d/18/b8392cab6e0964b67a30a8f4dadeaff64dc7022b5a34bb1d004ea99646f4/propcache-0.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bf15fc0b45914d9d1b706f7c9c4f66f2b7b053e9517e40123e137e8ca8958b3d", size = 222440, upload-time = "2025-02-20T19:01:28.438Z" }, + { url = "https://files.pythonhosted.org/packages/6f/be/105d9ceda0f97eff8c06bac1673448b2db2a497444de3646464d3f5dc881/propcache-0.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a16167118677d94bb48bfcd91e420088854eb0737b76ec374b91498fb77a70e", size = 234104, upload-time = "2025-02-20T19:01:31.256Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c9/f09a4ec394cfcce4053d8b2a04d622b5f22d21ba9bb70edd0cad061fa77b/propcache-0.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:41de3da5458edd5678b0f6ff66691507f9885f5fe6a0fb99a5d10d10c0fd2d64", size = 239086, upload-time = "2025-02-20T19:01:33.753Z" }, + { url = "https://files.pythonhosted.org/packages/ea/aa/96f7f9ed6def82db67c972bdb7bd9f28b95d7d98f7e2abaf144c284bf609/propcache-0.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:728af36011bb5d344c4fe4af79cfe186729efb649d2f8b395d1572fb088a996c", size = 230991, upload-time = "2025-02-20T19:01:35.433Z" }, + { url = "https://files.pythonhosted.org/packages/5a/11/bee5439de1307d06fad176f7143fec906e499c33d7aff863ea8428b8e98b/propcache-0.3.0-cp312-cp312-win32.whl", hash = "sha256:6b5b7fd6ee7b54e01759f2044f936dcf7dea6e7585f35490f7ca0420fe723c0d", size = 40337, upload-time = "2025-02-20T19:01:37.655Z" }, + { url = "https://files.pythonhosted.org/packages/e4/17/e5789a54a0455a61cb9efc4ca6071829d992220c2998a27c59aeba749f6f/propcache-0.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:2d15bc27163cd4df433e75f546b9ac31c1ba7b0b128bfb1b90df19082466ff57", size = 44404, upload-time = "2025-02-20T19:01:38.946Z" }, + { url = "https://files.pythonhosted.org/packages/3a/0f/a79dd23a0efd6ee01ab0dc9750d8479b343bfd0c73560d59d271eb6a99d4/propcache-0.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a2b9bf8c79b660d0ca1ad95e587818c30ccdb11f787657458d6f26a1ea18c568", size = 77287, upload-time = "2025-02-20T19:01:40.897Z" }, + { url = "https://files.pythonhosted.org/packages/b8/51/76675703c90de38ac75adb8deceb3f3ad99b67ff02a0fa5d067757971ab8/propcache-0.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0c1a133d42c6fc1f5fbcf5c91331657a1ff822e87989bf4a6e2e39b818d0ee9", size = 44923, upload-time = "2025-02-20T19:01:42.397Z" }, + { url = "https://files.pythonhosted.org/packages/01/9b/fd5ddbee66cf7686e73c516227c2fd9bf471dbfed0f48329d095ea1228d3/propcache-0.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bb2f144c6d98bb5cbc94adeb0447cfd4c0f991341baa68eee3f3b0c9c0e83767", size = 44325, upload-time = "2025-02-20T19:01:43.976Z" }, + { url = "https://files.pythonhosted.org/packages/13/1c/6961f11eb215a683b34b903b82bde486c606516c1466bf1fa67f26906d51/propcache-0.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1323cd04d6e92150bcc79d0174ce347ed4b349d748b9358fd2e497b121e03c8", size = 225116, upload-time = "2025-02-20T19:01:45.488Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ea/f8410c40abcb2e40dffe9adeed017898c930974650a63e5c79b886aa9f73/propcache-0.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b812b3cb6caacd072276ac0492d249f210006c57726b6484a1e1805b3cfeea0", size = 229905, upload-time = "2025-02-20T19:01:49.454Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5a/a9bf90894001468bf8e6ea293bb00626cc9ef10f8eb7996e9ec29345c7ed/propcache-0.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:742840d1d0438eb7ea4280f3347598f507a199a35a08294afdcc560c3739989d", size = 233221, upload-time = "2025-02-20T19:01:51.142Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ce/fffdddd9725b690b01d345c1156b4c2cc6dca09ab5c23a6d07b8f37d6e2f/propcache-0.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c6e7e4f9167fddc438cd653d826f2222222564daed4116a02a184b464d3ef05", size = 227627, upload-time = "2025-02-20T19:01:53.695Z" }, + { url = "https://files.pythonhosted.org/packages/58/ae/45c89a5994a334735a3032b48e8e4a98c05d9536ddee0719913dc27da548/propcache-0.3.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a94ffc66738da99232ddffcf7910e0f69e2bbe3a0802e54426dbf0714e1c2ffe", size = 214217, upload-time = "2025-02-20T19:01:55.309Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/bc60188c3290ff8f5f4a92b9ca2d93a62e449c8daf6fd11ad517ad136926/propcache-0.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c6ec957025bf32b15cbc6b67afe233c65b30005e4c55fe5768e4bb518d712f1", size = 212921, upload-time = "2025-02-20T19:01:57.893Z" }, + { url = "https://files.pythonhosted.org/packages/14/b3/39d60224048feef7a96edabb8217dc3f75415457e5ebbef6814f8b2a27b5/propcache-0.3.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:549722908de62aa0b47a78b90531c022fa6e139f9166be634f667ff45632cc92", size = 208200, upload-time = "2025-02-20T19:02:00.026Z" }, + { url = "https://files.pythonhosted.org/packages/9d/b3/0a6720b86791251273fff8a01bc8e628bc70903513bd456f86cde1e1ef84/propcache-0.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5d62c4f6706bff5d8a52fd51fec6069bef69e7202ed481486c0bc3874912c787", size = 208400, upload-time = "2025-02-20T19:02:03.997Z" }, + { url = "https://files.pythonhosted.org/packages/e9/4f/bb470f3e687790547e2e78105fb411f54e0cdde0d74106ccadd2521c6572/propcache-0.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:24c04f8fbf60094c531667b8207acbae54146661657a1b1be6d3ca7773b7a545", size = 218116, upload-time = "2025-02-20T19:02:06.042Z" }, + { url = "https://files.pythonhosted.org/packages/34/71/277f7f9add469698ac9724c199bfe06f85b199542121a71f65a80423d62a/propcache-0.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7c5f5290799a3f6539cc5e6f474c3e5c5fbeba74a5e1e5be75587746a940d51e", size = 222911, upload-time = "2025-02-20T19:02:08.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/e3/a7b9782aef5a2fc765b1d97da9ec7aed2f25a4e985703608e73232205e3f/propcache-0.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0e7c9c3cf7c276d4f6ab9af8adddc127d04e0fcabede315904d2ff76db626", size = 216563, upload-time = "2025-02-20T19:02:11.322Z" }, + { url = "https://files.pythonhosted.org/packages/ab/76/0583ca2c551aa08ffcff87b2c6849c8f01c1f6fb815a5226f0c5c202173e/propcache-0.3.0-cp313-cp313-win32.whl", hash = "sha256:ee0bd3a7b2e184e88d25c9baa6a9dc609ba25b76daae942edfb14499ac7ec374", size = 39763, upload-time = "2025-02-20T19:02:12.977Z" }, + { url = "https://files.pythonhosted.org/packages/80/ec/c6a84f9a36f608379b95f0e786c111d5465926f8c62f12be8cdadb02b15c/propcache-0.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c8f7d896a16da9455f882870a507567d4f58c53504dc2d4b1e1d386dfe4588a", size = 43650, upload-time = "2025-02-20T19:02:15.041Z" }, + { url = "https://files.pythonhosted.org/packages/ee/95/7d32e3560f5bf83fc2f2a4c1b0c181d327d53d5f85ebd045ab89d4d97763/propcache-0.3.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e560fd75aaf3e5693b91bcaddd8b314f4d57e99aef8a6c6dc692f935cc1e6bbf", size = 82140, upload-time = "2025-02-20T19:02:16.562Z" }, + { url = "https://files.pythonhosted.org/packages/86/89/752388f12e6027a5e63f5d075f15291ded48e2d8311314fff039da5a9b11/propcache-0.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65a37714b8ad9aba5780325228598a5b16c47ba0f8aeb3dc0514701e4413d7c0", size = 47296, upload-time = "2025-02-20T19:02:17.974Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4c/b55c98d586c69180d3048984a57a5ea238bdeeccf82dbfcd598e935e10bb/propcache-0.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:07700939b2cbd67bfb3b76a12e1412405d71019df00ca5697ce75e5ef789d829", size = 46724, upload-time = "2025-02-20T19:02:19.588Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b6/67451a437aed90c4e951e320b5b3d7eb584ade1d5592f6e5e8f678030989/propcache-0.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c0fdbdf6983526e269e5a8d53b7ae3622dd6998468821d660d0daf72779aefa", size = 291499, upload-time = "2025-02-20T19:02:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ff/e4179facd21515b24737e1e26e02615dfb5ed29416eed4cf5bc6ac5ce5fb/propcache-0.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:794c3dd744fad478b6232289c866c25406ecdfc47e294618bdf1697e69bd64a6", size = 293911, upload-time = "2025-02-20T19:02:24.248Z" }, + { url = "https://files.pythonhosted.org/packages/76/8d/94a8585992a064a23bd54f56c5e58c3b8bf0c0a06ae10e56f2353ae16c3d/propcache-0.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4544699674faf66fb6b4473a1518ae4999c1b614f0b8297b1cef96bac25381db", size = 293301, upload-time = "2025-02-20T19:02:26.034Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b8/2c860c92b4134f68c7716c6f30a0d723973f881c32a6d7a24c4ddca05fdf/propcache-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddb8870bdb83456a489ab67c6b3040a8d5a55069aa6f72f9d872235fbc52f54", size = 281947, upload-time = "2025-02-20T19:02:27.838Z" }, + { url = "https://files.pythonhosted.org/packages/cd/72/b564be7411b525d11757b713c757c21cd4dc13b6569c3b2b8f6d3c96fd5e/propcache-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f857034dc68d5ceb30fb60afb6ff2103087aea10a01b613985610e007053a121", size = 268072, upload-time = "2025-02-20T19:02:29.594Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/d94649e399e8d7fc051e5a4f2334efc567993525af083db145a70690a121/propcache-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:02df07041e0820cacc8f739510078f2aadcfd3fc57eaeeb16d5ded85c872c89e", size = 275190, upload-time = "2025-02-20T19:02:32.255Z" }, + { url = "https://files.pythonhosted.org/packages/d8/3c/446e125f5bbbc1922964dd67cb541c01cdb678d811297b79a4ff6accc843/propcache-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f47d52fd9b2ac418c4890aad2f6d21a6b96183c98021f0a48497a904199f006e", size = 254145, upload-time = "2025-02-20T19:02:33.932Z" }, + { url = "https://files.pythonhosted.org/packages/f4/80/fd3f741483dc8e59f7ba7e05eaa0f4e11677d7db2077522b92ff80117a2a/propcache-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9ff4e9ecb6e4b363430edf2c6e50173a63e0820e549918adef70515f87ced19a", size = 257163, upload-time = "2025-02-20T19:02:35.675Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cf/6292b5ce6ed0017e6a89024a827292122cc41b6259b30ada0c6732288513/propcache-0.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ecc2920630283e0783c22e2ac94427f8cca29a04cfdf331467d4f661f4072dac", size = 280249, upload-time = "2025-02-20T19:02:38.406Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f0/fd9b8247b449fe02a4f96538b979997e229af516d7462b006392badc59a1/propcache-0.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c441c841e82c5ba7a85ad25986014be8d7849c3cfbdb6004541873505929a74e", size = 288741, upload-time = "2025-02-20T19:02:40.149Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/cf831fdc2617f86cfd7f414cfc487d018e722dac8acc098366ce9bba0941/propcache-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c929916cbdb540d3407c66f19f73387f43e7c12fa318a66f64ac99da601bcdf", size = 277061, upload-time = "2025-02-20T19:02:42.309Z" }, + { url = "https://files.pythonhosted.org/packages/42/78/9432542a35d944abeca9e02927a0de38cd7a298466d8ffa171536e2381c3/propcache-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:0c3e893c4464ebd751b44ae76c12c5f5c1e4f6cbd6fbf67e3783cd93ad221863", size = 42252, upload-time = "2025-02-20T19:02:44.447Z" }, + { url = "https://files.pythonhosted.org/packages/6f/45/960365f4f8978f48ebb56b1127adf33a49f2e69ecd46ac1f46d6cf78a79d/propcache-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:75e872573220d1ee2305b35c9813626e620768248425f58798413e9c39741f46", size = 46425, upload-time = "2025-02-20T19:02:48.071Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/6c4c6fc8774a9e3629cd750dc24a7a4fb090a25ccd5c3246d127b70f9e22/propcache-0.3.0-py3-none-any.whl", hash = "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043", size = 12101, upload-time = "2025-02-20T19:03:27.202Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/cb/09e5184fb5fc0358d110fc3ca7f6b1d033800734d34cac10f4136cfac10e/psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3", size = 490253, upload-time = "2025-12-29T08:26:00.169Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/8e/f0c242053a368c2aa89584ecd1b054a18683f13d6e5a318fc9ec36582c94/psutil-7.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d", size = 129624, upload-time = "2025-12-29T08:26:04.255Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/a58a4968f8990617decee234258a2b4fc7cd9e35668387646c1963e69f26/psutil-7.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49", size = 130132, upload-time = "2025-12-29T08:26:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/db/6d/ed44901e830739af5f72a85fa7ec5ff1edea7f81bfbf4875e409007149bd/psutil-7.2.1-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc", size = 180612, upload-time = "2025-12-29T08:26:08.276Z" }, + { url = "https://files.pythonhosted.org/packages/c7/65/b628f8459bca4efbfae50d4bf3feaab803de9a160b9d5f3bd9295a33f0c2/psutil-7.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf", size = 183201, upload-time = "2025-12-29T08:26:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/fb/23/851cadc9764edcc18f0effe7d0bf69f727d4cf2442deb4a9f78d4e4f30f2/psutil-7.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f", size = 139081, upload-time = "2025-12-29T08:26:12.483Z" }, + { url = "https://files.pythonhosted.org/packages/59/82/d63e8494ec5758029f31c6cb06d7d161175d8281e91d011a4a441c8a43b5/psutil-7.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672", size = 134767, upload-time = "2025-12-29T08:26:14.528Z" }, + { url = "https://files.pythonhosted.org/packages/05/c2/5fb764bd61e40e1fe756a44bd4c21827228394c17414ade348e28f83cd79/psutil-7.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679", size = 129716, upload-time = "2025-12-29T08:26:16.017Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d2/935039c20e06f615d9ca6ca0ab756cf8408a19d298ffaa08666bc18dc805/psutil-7.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f", size = 130133, upload-time = "2025-12-29T08:26:18.009Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/19f1eb0e01d24c2b3eacbc2f78d3b5add8a89bf0bb69465bc8d563cc33de/psutil-7.2.1-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129", size = 181518, upload-time = "2025-12-29T08:26:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6d/7e18b1b4fa13ad370787626c95887b027656ad4829c156bb6569d02f3262/psutil-7.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a", size = 184348, upload-time = "2025-12-29T08:26:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/60/1672114392dd879586d60dd97896325df47d9a130ac7401318005aab28ec/psutil-7.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79", size = 140400, upload-time = "2025-12-29T08:26:23.993Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7b/d0e9d4513c46e46897b46bcfc410d51fc65735837ea57a25170f298326e6/psutil-7.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266", size = 135430, upload-time = "2025-12-29T08:26:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42", size = 128137, upload-time = "2025-12-29T08:26:27.759Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1", size = 128947, upload-time = "2025-12-29T08:26:29.548Z" }, + { url = "https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8", size = 154694, upload-time = "2025-12-29T08:26:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/06/e4/b751cdf839c011a9714a783f120e6a86b7494eb70044d7d81a25a5cd295f/psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6", size = 156136, upload-time = "2025-12-29T08:26:34.079Z" }, + { url = "https://files.pythonhosted.org/packages/44/ad/bbf6595a8134ee1e94a4487af3f132cef7fce43aef4a93b49912a48c3af7/psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8", size = 148108, upload-time = "2025-12-29T08:26:36.225Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/dd6fd869753ce82ff64dcbc18356093471a5a5adf4f77ed1f805d473d859/psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67", size = 147402, upload-time = "2025-12-29T08:26:39.21Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17", size = 136938, upload-time = "2025-12-29T08:26:41.036Z" }, + { url = "https://files.pythonhosted.org/packages/3e/73/2ce007f4198c80fcf2cb24c169884f833fe93fbc03d55d302627b094ee91/psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442", size = 133836, upload-time = "2025-12-29T08:26:43.086Z" }, +] + +[[package]] +name = "psutil-home-assistant" +version = "0.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "psutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/4f/32a51f53d645044740d0513a6a029d782b35bdc51a55ea171ce85034f5b7/psutil-home-assistant-0.0.1.tar.gz", hash = "sha256:ebe4f3a98d76d93a3140da2823e9ef59ca50a59761fdc453b30b4407c4c1bdb8", size = 6045, upload-time = "2022-08-25T14:28:39.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/48/8a0acb683d1fee78b966b15e78143b673154abb921061515254fb573aacd/psutil_home_assistant-0.0.1-py3-none-any.whl", hash = "sha256:35a782e93e23db845fc4a57b05df9c52c2d5c24f5b233bd63b01bae4efae3c41", size = 6300, upload-time = "2022-08-25T14:28:38.083Z" }, +] + +[[package]] +name = "pycares" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/ad/9d1e96486d2eb5a2672c4d9a2dd372d015b8d7a332c6ac2722c4c8e6bbbf/pycares-4.11.0.tar.gz", hash = "sha256:c863d9003ca0ce7df26429007859afd2a621d3276ed9fef154a9123db9252557", size = 654473, upload-time = "2025-09-09T15:18:21.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/4e/4821b66feefaaa8ec03494c1a11614c430983572e54ff062b4589441e199/pycares-4.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b93d624560ba52287873bacff70b42c99943821ecbc810b959b0953560f53c36", size = 145906, upload-time = "2025-09-09T15:16:53.204Z" }, + { url = "https://files.pythonhosted.org/packages/e8/81/93a505dcbb7533254b0ce1da519591dcda889d6a66dcdfa5737e3280e18a/pycares-4.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:775d99966e28c8abd9910ddef2de0f1e173afc5a11cea9f184613c747373ab80", size = 141972, upload-time = "2025-09-09T15:16:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d6/76994c8b21316e48ea6c3ce3298574c28f90c9c41428a3349a57104621c9/pycares-4.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:84fde689557361764f052850a2d68916050adbfd9321f6105aca1d8f1a9bd49b", size = 637832, upload-time = "2025-09-09T15:16:55.523Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/5ca7e316d0edb714d78974cb34f4883f63fe9f580644c2db99fb62b05f56/pycares-4.11.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:30ceed06f3bf5eff865a34d21562c25a7f3dad0ed336b9dd415330e03a6c50c4", size = 687751, upload-time = "2025-09-09T15:16:57.55Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8d/c5c578fdd335d7b1dcaea88fae3497390095b5b05a1ba34a29f62d037abb/pycares-4.11.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:97d971b3a88a803bb95ff8a40ea4d68da59319eb8b59e924e318e2560af8c16d", size = 678362, upload-time = "2025-09-09T15:16:58.859Z" }, + { url = "https://files.pythonhosted.org/packages/b9/96/9be4d838a9348dd2e72a90c34d186b918b66d499af5be79afa18a6ba2808/pycares-4.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:2d5cac829da91ade70ce1af97dad448c6cd4778b48facbce1b015e16ced93642", size = 641069, upload-time = "2025-09-09T15:17:00.046Z" }, + { url = "https://files.pythonhosted.org/packages/39/d6/8ea9b5dcef6b566cde034aa2b68743f7b0a19fa0fba9ea01a4f98b8a57fb/pycares-4.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee1ea367835eb441d246164c09d1f9703197af4425fc6865cefcde9e2ca81f85", size = 622357, upload-time = "2025-09-09T15:17:01.205Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/3401e89b5d2970e30e02f9beb29ad59e2a8f19ef2c68c978de2b764cacb0/pycares-4.11.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3139ec1f4450a4b253386035c5ecd2722582ae3320a456df5021ffe3f174260a", size = 670290, upload-time = "2025-09-09T15:17:02.413Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c4/ff6a166e1d1d1987339548a19d0b1d52ec3ead8b3a8a2247a0d96e56013c/pycares-4.11.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5d70324ca1d82c6c4b00aa678347f7560d1ef2ce1d181978903459a97751543a", size = 652958, upload-time = "2025-09-09T15:17:04.203Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/fc084b395921c9b862d31a83f809fe649c24314b51b527ad0ab0df33edd4/pycares-4.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e2f8d9cfe0eb3a2997fde5df99b1aaea5a46dabfcfcac97b2d05f027c2cd5e28", size = 629239, upload-time = "2025-09-09T15:17:05.477Z" }, + { url = "https://files.pythonhosted.org/packages/b0/7f/2f26062bea95ab657f979217d50df563dc9fd9cc4c5dd21a6e7323e9efe7/pycares-4.11.0-cp312-cp312-win32.whl", hash = "sha256:1571a7055c03a95d5270c914034eac7f8bfa1b432fc1de53d871b821752191a4", size = 118918, upload-time = "2025-09-09T15:17:06.882Z" }, + { url = "https://files.pythonhosted.org/packages/a5/86/277473d20f3df4e00fa7e0ebb21955b2830b15247462aaf8f3fc8c4950be/pycares-4.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:7570e0b50db619b2ee370461c462617225dc3a3f63f975c6f117e2f0c94f82ca", size = 144560, upload-time = "2025-09-09T15:17:07.891Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f9/d65ad17ec921d8b7eb42161dec2024ee2f5c9f1c44cabf0dd1b7f4fac6c5/pycares-4.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:f199702740f3b766ed8c70efb885538be76cb48cd0cb596b948626f0b825e07a", size = 115695, upload-time = "2025-09-09T15:17:09.333Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a9/62fea7ad72ac1fed2ac9dd8e9a7379b7eb0288bf2b3ea5731642c3a6f7de/pycares-4.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c296ab94d1974f8d2f76c499755a9ce31ffd4986e8898ef19b90e32525f7d84", size = 145909, upload-time = "2025-09-09T15:17:10.491Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/0317d6d0d3bd7599c53b8f1db09ad04260647d2f6842018e322584791fd5/pycares-4.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0fcd3a8bac57a0987d9b09953ba0f8703eb9dca7c77f7051d8c2ed001185be8", size = 141974, upload-time = "2025-09-09T15:17:11.634Z" }, + { url = "https://files.pythonhosted.org/packages/63/11/731b565ae1e81c43dac247a248ee204628186f6df97c9927bd06c62237f8/pycares-4.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bac55842047567ddae177fb8189b89a60633ac956d5d37260f7f71b517fd8b87", size = 637796, upload-time = "2025-09-09T15:17:12.815Z" }, + { url = "https://files.pythonhosted.org/packages/f5/30/a2631fe2ffaa85475cdbff7df1d9376bc0b2a6ae77ca55d53233c937a5da/pycares-4.11.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:4da2e805ed8c789b9444ef4053f6ef8040cd13b0c1ca6d3c4fe6f9369c458cb4", size = 687734, upload-time = "2025-09-09T15:17:14.015Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b7/b3a5f99d4ab776662e71d5a56e8f6ea10741230ff988d1f502a8d429236b/pycares-4.11.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:ea785d1f232b42b325578f0c8a2fa348192e182cc84a1e862896076a4a2ba2a7", size = 678320, upload-time = "2025-09-09T15:17:15.442Z" }, + { url = "https://files.pythonhosted.org/packages/ea/77/a00d962b90432993afbf3bd05da8fe42117e0d9037cd7fd428dc41094d7b/pycares-4.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:aa160dc9e785212c49c12bb891e242c949758b99542946cc8e2098ef391f93b0", size = 641012, upload-time = "2025-09-09T15:17:16.728Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fb/9266979ba59d37deee1fd74452b2ae32a7395acafe1bee510ac023c6c9a5/pycares-4.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7830709c23bbc43fbaefbb3dde57bdd295dc86732504b9d2e65044df8fd5e9fb", size = 622363, upload-time = "2025-09-09T15:17:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/91/c2/16dbc3dc33781a3c79cbdd76dd1cda808d98ba078d9a63a725d6a1fad181/pycares-4.11.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ef1ab7abbd238bb2dbbe871c3ea39f5a7fc63547c015820c1e24d0d494a1689", size = 670294, upload-time = "2025-09-09T15:17:19.214Z" }, + { url = "https://files.pythonhosted.org/packages/ff/75/f003905e55298a6dd5e0673a2dc11e31518a5141393b925dc05fcaba9fb4/pycares-4.11.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a4060d8556c908660512d42df1f4a874e4e91b81f79e3a9090afedc7690ea5ba", size = 652973, upload-time = "2025-09-09T15:17:20.388Z" }, + { url = "https://files.pythonhosted.org/packages/55/2a/eafb235c371979e11f8998d686cbaa91df6a84a34ffe4d997dfe57c45445/pycares-4.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a98fac4a3d4f780817016b6f00a8a2c2f41df5d25dfa8e5b1aa0d783645a6566", size = 629235, upload-time = "2025-09-09T15:17:21.92Z" }, + { url = "https://files.pythonhosted.org/packages/05/99/60f19eb1c8eb898882dd8875ea51ad0aac3aff5780b27247969e637cc26a/pycares-4.11.0-cp313-cp313-win32.whl", hash = "sha256:faa8321bc2a366189dcf87b3823e030edf5ac97a6b9a7fc99f1926c4bf8ef28e", size = 118918, upload-time = "2025-09-09T15:17:23.327Z" }, + { url = "https://files.pythonhosted.org/packages/2a/14/bc89ad7225cba73068688397de09d7cad657d67b93641c14e5e18b88e685/pycares-4.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:6f74b1d944a50fa12c5006fd10b45e1a45da0c5d15570919ce48be88e428264c", size = 144556, upload-time = "2025-09-09T15:17:24.341Z" }, + { url = "https://files.pythonhosted.org/packages/af/88/4309576bd74b5e6fc1f39b9bc5e4b578df2cadb16bdc026ac0cc15663763/pycares-4.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f7581793d8bb3014028b8397f6f80b99db8842da58f4409839c29b16397ad", size = 115692, upload-time = "2025-09-09T15:17:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/2a/70/a723bc79bdcac60361b40184b649282ac0ab433b90e9cc0975370c2ff9c9/pycares-4.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:df0a17f4e677d57bca3624752bbb515316522ad1ce0de07ed9d920e6c4ee5d35", size = 145910, upload-time = "2025-09-09T15:17:26.774Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/46311ef5a384b5f0bb206851135dde8f86b3def38fdbee9e3c03475d35ae/pycares-4.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b44e54cad31d3c3be5e8149ac36bc1c163ec86e0664293402f6f846fb22ad00", size = 142053, upload-time = "2025-09-09T15:17:27.956Z" }, + { url = "https://files.pythonhosted.org/packages/74/23/d236fc4f134d6311e4ad6445571e8285e84a3e155be36422ff20c0fbe471/pycares-4.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:80752133442dc7e6dd9410cec227c49f69283c038c316a8585cca05ec32c2766", size = 637878, upload-time = "2025-09-09T15:17:29.173Z" }, + { url = "https://files.pythonhosted.org/packages/f7/92/6edd41282b3f0e3d9defaba7b05c39730d51c37c165d9d3b319349c975aa/pycares-4.11.0-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:84b0b402dd333403fdce0e204aef1ef834d839c439c0c1aa143dc7d1237bb197", size = 687865, upload-time = "2025-09-09T15:17:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a9/4d7cf4d72600fd47d9518f9ce99703a3e8711fb08d2ef63d198056cdc9a9/pycares-4.11.0-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:c0eec184df42fc82e43197e073f9cc8f93b25ad2f11f230c64c2dc1c80dbc078", size = 678396, upload-time = "2025-09-09T15:17:32.304Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4b/e546eeb1d8ff6559e2e3bef31a6ea0c6e57ec826191941f83a3ce900ca89/pycares-4.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ee751409322ff10709ee867d5aea1dc8431eec7f34835f0f67afd016178da134", size = 640786, upload-time = "2025-09-09T15:17:33.602Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f5/b4572d9ee9c26de1f8d1dc80730df756276b9243a6794fa3101bbe56613d/pycares-4.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1732db81e348bfce19c9bf9448ba660aea03042eeeea282824da1604a5bd4dcf", size = 621857, upload-time = "2025-09-09T15:17:34.74Z" }, + { url = "https://files.pythonhosted.org/packages/17/f2/639090376198bcaeff86562b25e1bce05a481cfb1e605f82ce62285230cd/pycares-4.11.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:702d21823996f139874aba5aa9bb786d69e93bde6e3915b99832eb4e335d31ae", size = 670130, upload-time = "2025-09-09T15:17:35.982Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c4/cf40773cd9c36a12cebbe1e9b6fb120f9160dc9bfe0398d81a20b6c69972/pycares-4.11.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:218619b912cef7c64a339ab0e231daea10c994a05699740714dff8c428b9694a", size = 653133, upload-time = "2025-09-09T15:17:37.179Z" }, + { url = "https://files.pythonhosted.org/packages/32/6b/06054d977b0a9643821043b59f523f3db5e7684c4b1b4f5821994d5fa780/pycares-4.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:719f7ddff024fdacde97b926b4b26d0cc25901d5ef68bb994a581c420069936d", size = 629344, upload-time = "2025-09-09T15:17:38.308Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6f/14bb0c2171a286d512e3f02d6168e608ffe5f6eceab78bf63e3073091ae3/pycares-4.11.0-cp314-cp314-win32.whl", hash = "sha256:d552fb2cb513ce910d1dc22dbba6420758a991a356f3cd1b7ec73a9e31f94d01", size = 121804, upload-time = "2025-09-09T15:17:39.388Z" }, + { url = "https://files.pythonhosted.org/packages/24/dc/6822f9ad6941027f70e1cf161d8631456531a87061588ed3b1dcad07d49d/pycares-4.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:23d50a0842e8dbdddf870a7218a7ab5053b68892706b3a391ecb3d657424d266", size = 148005, upload-time = "2025-09-09T15:17:40.44Z" }, + { url = "https://files.pythonhosted.org/packages/ea/24/24ff3a80aa8471fbb62785c821a8e90f397ca842e0489f83ebf7ee274397/pycares-4.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:836725754c32363d2c5d15b931b3ebd46b20185c02e850672cb6c5f0452c1e80", size = 119239, upload-time = "2025-09-09T15:17:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/fe/2f3558d298ff8db31d5c83369001ab72af3b86a0374d9b0d40dc63314187/pycares-4.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c9d839b5700542b27c1a0d359cbfad6496341e7c819c7fea63db9588857065ed", size = 146408, upload-time = "2025-09-09T15:17:43.74Z" }, + { url = "https://files.pythonhosted.org/packages/3c/c8/516901e46a1a73b3a75e87a35f3a3a4fe085f1214f37d954c9d7e782bd6d/pycares-4.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:31b85ad00422b38f426e5733a71dfb7ee7eb65a99ea328c508d4f552b1760dc8", size = 142371, upload-time = "2025-09-09T15:17:45.186Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/c3fba0aa575f331ebed91f87ba960ffbe0849211cdf103ab275bc0107ac6/pycares-4.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cdac992206756b024b371760c55719eb5cd9d6b2cb25a8d5a04ae1b0ff426232", size = 647504, upload-time = "2025-09-09T15:17:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e4/1cdc3ec9c92f8069ec18c58b016b2df7c44a088e2849f37ed457554961aa/pycares-4.11.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:ffb22cee640bc12ee0e654eba74ecfb59e2e0aebc5bccc3cc7ef92f487008af7", size = 697122, upload-time = "2025-09-09T15:17:47.772Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d5/bd8f370b97bb73e5bdd55dc2a78e18d6f49181cf77e88af0599d16f5c073/pycares-4.11.0-cp314-cp314t-manylinux_2_28_s390x.whl", hash = "sha256:00538826d2eaf4a0e4becb0753b0ac8d652334603c445c9566c9eb273657eb4c", size = 687543, upload-time = "2025-09-09T15:17:49.183Z" }, + { url = "https://files.pythonhosted.org/packages/33/38/49b77b9cf5dffc0b1fdd86656975c3bc1a58b79bdc883a9ef749b17a013c/pycares-4.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:29daa36548c04cdcd1a78ae187a4b7b003f0b357a2f4f1f98f9863373eedc759", size = 649565, upload-time = "2025-09-09T15:17:51.03Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/f6d57bfb99d00a6a7363f95c8d3a930fe82a868d9de24c64c8048d66f16a/pycares-4.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cf306f3951740d7bed36149a6d8d656a7d5432dd4bbc6af3bb6554361fc87401", size = 631242, upload-time = "2025-09-09T15:17:52.298Z" }, + { url = "https://files.pythonhosted.org/packages/33/a2/7b9121c71cfe06a8474e221593f83a78176fae3b79e5853d2dfd13ab01cc/pycares-4.11.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:386da2581db4ea2832629e275c061103b0be32f9391c5dfaea7f6040951950ad", size = 680304, upload-time = "2025-09-09T15:17:53.638Z" }, + { url = "https://files.pythonhosted.org/packages/5b/07/dfe76807f637d8b80e1a59dfc4a1bceabdd0205a45b2ebf78b415ae72af3/pycares-4.11.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:45d3254a694459fdb0640ef08724ca9d4b4f6ff6d7161c9b526d7d2e2111379e", size = 661039, upload-time = "2025-09-09T15:17:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9b/55d50c5acd46cbe95d0da27740a83e721d89c0ce7e42bff9891a9f29a855/pycares-4.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eddf5e520bb88b23b04ac1f28f5e9a7c77c718b8b4af3a4a7a2cc4a600f34502", size = 637560, upload-time = "2025-09-09T15:17:56.492Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/2b2e723d1b929dbe7f99e80a56abb29a4f86988c1f73195d960d706b1629/pycares-4.11.0-cp314-cp314t-win32.whl", hash = "sha256:8a75a406432ce39ce0ca41edff7486df6c970eb0fe5cfbe292f195a6b8654461", size = 122235, upload-time = "2025-09-09T15:17:57.576Z" }, + { url = "https://files.pythonhosted.org/packages/93/fe/bf3b3ed9345a38092e72cd9890a5df5c2349fc27846a714d823a41f0ee27/pycares-4.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3784b80d797bcc2ff2bf3d4b27f46d8516fe1707ff3b82c2580dc977537387f9", size = 148575, upload-time = "2025-09-09T15:17:58.699Z" }, + { url = "https://files.pythonhosted.org/packages/ce/20/c0c5cfcf89725fe533b27bc5f714dc4efa8e782bf697c36f9ddf04ba975d/pycares-4.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:afc6503adf8b35c21183b9387be64ca6810644ef54c9ef6c99d1d5635c01601b", size = 119690, upload-time = "2025-09-09T15:17:59.809Z" }, +] + +[[package]] +name = "pycognito" +version = "2024.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boto3" }, + { name = "envs" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests", version = "2.32.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13.2'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/67/3975cf257fcc04903686ef87d39be386d894a0d8182f43d37e9cbfc9609f/pycognito-2024.5.1.tar.gz", hash = "sha256:e211c66698c2c3dc8680e95107c2b4a922f504c3f7c179c27b8ee1ab0fc23ae4", size = 31182, upload-time = "2024-05-16T10:02:28.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/7a/f38dd351f47596b22ddbde1b8906e7f43d14be391dcdbd0c2daba886f26c/pycognito-2024.5.1-py3-none-any.whl", hash = "sha256:c821895dc62b7aea410fdccae4f96d8be7cab374182339f50a03de0fcb93f9ea", size = 26607, upload-time = "2024-05-16T10:02:27.3Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types", marker = "python_full_version >= '3.13.2'" }, + { name = "pydantic-core", marker = "python_full_version >= '3.13.2'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/35/d319ed522433215526689bad428a94058b6dd12190ce7ddd78618ac14b28/pydantic-2.12.2.tar.gz", hash = "sha256:7b8fa15b831a4bbde9d5b84028641ac3080a4ca2cbd4a621a661687e741624fd", size = 816358, upload-time = "2025-10-14T15:02:21.842Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/98/468cb649f208a6f1279448e6e5247b37ae79cf5e4041186f1e2ef3d16345/pydantic-2.12.2-py3-none-any.whl", hash = "sha256:25ff718ee909acd82f1ff9b1a4acfd781bb23ab3739adaa7144f19a6a4e231ae", size = 460628, upload-time = "2025-10-14T15:02:19.623Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043, upload-time = "2025-10-14T10:20:28.561Z" }, + { url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699, upload-time = "2025-10-14T10:20:30.217Z" }, + { url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121, upload-time = "2025-10-14T10:20:32.246Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590, upload-time = "2025-10-14T10:20:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869, upload-time = "2025-10-14T10:20:35.965Z" }, + { url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169, upload-time = "2025-10-14T10:20:37.627Z" }, + { url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165, upload-time = "2025-10-14T10:20:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067, upload-time = "2025-10-14T10:20:41.015Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997, upload-time = "2025-10-14T10:20:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187, upload-time = "2025-10-14T10:20:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204, upload-time = "2025-10-14T10:20:46.781Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536, upload-time = "2025-10-14T10:20:48.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132, upload-time = "2025-10-14T10:20:50.421Z" }, + { url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483, upload-time = "2025-10-14T10:20:52.35Z" }, + { url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" }, + { url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" }, + { url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" }, + { url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" }, + { url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" }, + { url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" }, + { url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" }, + { url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" }, + { url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" }, + { url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" }, + { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" }, + { url = "https://files.pythonhosted.org/packages/c4/48/ae937e5a831b7c0dc646b2ef788c27cd003894882415300ed21927c21efa/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537", size = 2112087, upload-time = "2025-10-14T10:22:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/5e/db/6db8073e3d32dae017da7e0d16a9ecb897d0a4d92e00634916e486097961/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94", size = 1920387, upload-time = "2025-10-14T10:22:59.342Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c1/dd3542d072fcc336030d66834872f0328727e3b8de289c662faa04aa270e/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c", size = 1951495, upload-time = "2025-10-14T10:23:02.089Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c6/db8d13a1f8ab3f1eb08c88bd00fd62d44311e3456d1e85c0e59e0a0376e7/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335", size = 2139008, upload-time = "2025-10-14T10:23:04.539Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13.2'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/35/e3814a5b7df295df69d035cfb8aab78b2967cdf11fcfae7faed726b66664/pymdown_extensions-10.20.tar.gz", hash = "sha256:5c73566ab0cf38c6ba084cb7c5ea64a119ae0500cce754ccb682761dfea13a52", size = 852774, upload-time = "2025-12-31T19:59:42.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl", hash = "sha256:ea9e62add865da80a271d00bfa1c0fa085b20d133fb3fc97afdc88e682f60b2f", size = 268733, upload-time = "2025-12-31T19:59:40.652Z" }, +] + +[[package]] +name = "pymicro-vad" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/0f/a92acea368e2b37fbc706f6d049f04557497d981316a2f428b26f14666a9/pymicro_vad-1.0.1.tar.gz", hash = "sha256:60e0508b338b694c7ad71c633c0da6fcd2678a88abb8e948b80fa68934965111", size = 135575, upload-time = "2024-07-31T20:04:04.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/75/3804b5838f575daa913070667413c806e17cfb95ce4c2fae5ff595f3bc72/pymicro_vad-1.0.1-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:c4917f2862f8bfc862ce0cc46a63a5505f7d960d3987fe06b7b5e420446c098e", size = 274697, upload-time = "2024-07-31T20:04:37.348Z" }, + { url = "https://files.pythonhosted.org/packages/0c/47/ad10e2f9d40d40db185401cc486395138eacfe120de24272cb662e9b808f/pymicro_vad-1.0.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:499bf2799efde7f1135b215f52d5fab25718504deab919f5d3b0423a70d258ef", size = 137545, upload-time = "2024-07-31T20:04:35.774Z" }, + { url = "https://files.pythonhosted.org/packages/ff/18/8278176bc4e5f8d2f72bf1c6f6ff5c0fa46b665dd1b9d559810a8472ffef/pymicro_vad-1.0.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ca40fd192807fa4e537e247747e70cc5d630ba13f035e2a60b4d00670c698f1", size = 147940, upload-time = "2024-07-31T20:04:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/c69ac5c5be879845946467701863cdfe6625be0ee63f11f843686fb9eebb/pymicro_vad-1.0.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c78d72d7f02d8d33caaf176ed13e097b08b81051920bb22a0be905f4bf1e17f", size = 153423, upload-time = "2024-07-31T20:04:51.702Z" }, + { url = "https://files.pythonhosted.org/packages/9b/08/5c4fc50cd6156287028f1df96e2cc6b753308c6de46c51a8353853d47c05/pymicro_vad-1.0.1-cp312-cp312-manylinux_2_34_armv7l.whl", hash = "sha256:29c0ccb5b8bd77d564c90526828260e94ea132c08098198f82ac1c5d6df767d6", size = 133665, upload-time = "2024-07-31T21:56:39.644Z" }, +] + +[[package]] +name = "pyobjc-core" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335, upload-time = "2025-11-14T09:32:20.107Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" }, + { url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" }, + { url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590, upload-time = "2025-11-14T09:41:17.336Z" }, + { url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" }, + { url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" }, +] + +[[package]] +name = "pyobjc-framework-corebluetooth" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/25/d21d6cb3fd249c2c2aa96ee54279f40876a0c93e7161b3304bf21cbd0bfe/pyobjc_framework_corebluetooth-12.1.tar.gz", hash = "sha256:8060c1466d90bbb9100741a1091bb79975d9ba43911c9841599879fc45c2bbe0", size = 33157, upload-time = "2025-11-14T10:13:28.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/56/01fef62a479cdd6ff9ee40b6e062a205408ff386ce5ba56d7e14a71fcf73/pyobjc_framework_corebluetooth-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe72c9732ee6c5c793b9543f08c1f5bdd98cd95dfc9d96efd5708ec9d6eeb213", size = 13209, upload-time = "2025-11-14T09:44:08.203Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6c/831139ebf6a811aed36abfdfad846bc380dcdf4e6fb751a310ce719ddcfd/pyobjc_framework_corebluetooth-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a894f695e6c672f0260327103a31ad8b98f8d4fb9516a0383db79a82a7e58dc", size = 13229, upload-time = "2025-11-14T09:44:10.463Z" }, + { url = "https://files.pythonhosted.org/packages/09/3c/3a6fe259a9e0745aa4612dee86b61b4fd7041c44b62642814e146b654463/pyobjc_framework_corebluetooth-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1daf07a0047c3ed89fab84ad5f6769537306733b6a6e92e631581a0f419e3f32", size = 13409, upload-time = "2025-11-14T09:44:12.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/41/90640a4db62f0bf0611cf8a161129c798242116e2a6a44995668b017b106/pyobjc_framework_corebluetooth-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:15ba5207ca626dffe57ccb7c1beaf01f93930159564211cb97d744eaf0d812aa", size = 13222, upload-time = "2025-11-14T09:44:14.345Z" }, + { url = "https://files.pythonhosted.org/packages/86/99/8ed2f0ca02b9abe204966142bd8c4501cf6da94234cc320c4c0562c467e8/pyobjc_framework_corebluetooth-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e5385195bd365a49ce70e2fb29953681eefbe68a7b15ecc2493981d2fb4a02b1", size = 13408, upload-time = "2025-11-14T09:44:16.558Z" }, +] + +[[package]] +name = "pyobjc-framework-libdispatch" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/e8/75b6b9b3c88b37723c237e5a7600384ea2d84874548671139db02e76652b/pyobjc_framework_libdispatch-12.1.tar.gz", hash = "sha256:4035535b4fae1b5e976f3e0e38b6e3442ffea1b8aa178d0ca89faa9b8ecdea41", size = 38277, upload-time = "2025-11-14T10:16:46.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/6f/96e15c7b2f7b51fc53252216cd0bed0c3541bc0f0aeb32756fefd31bed7d/pyobjc_framework_libdispatch-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0e9570d7a9a3136f54b0b834683bf3f206acd5df0e421c30f8fd4f8b9b556789", size = 15650, upload-time = "2025-11-14T09:52:59.284Z" }, + { url = "https://files.pythonhosted.org/packages/38/3a/d85a74606c89b6b293782adfb18711026ff79159db20fc543740f2ac0bc7/pyobjc_framework_libdispatch-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:58ffce5e6bcd7456b4311009480b195b9f22107b7682fb0835d4908af5a68ad0", size = 15668, upload-time = "2025-11-14T09:53:01.354Z" }, + { url = "https://files.pythonhosted.org/packages/cc/40/49b1c1702114ee972678597393320d7b33f477e9d24f2a62f93d77f23dfb/pyobjc_framework_libdispatch-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e9f49517e253716e40a0009412151f527005eec0b9a2311ac63ecac1bdf02332", size = 15938, upload-time = "2025-11-14T09:53:03.461Z" }, + { url = "https://files.pythonhosted.org/packages/59/d8/7d60a70fc1a546c6cb482fe0595cb4bd1368d75c48d49e76d0bc6c0a2d0f/pyobjc_framework_libdispatch-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0ebfd9e4446ab6528126bff25cfb09e4213ddf992b3208978911cfd3152e45f5", size = 15693, upload-time = "2025-11-14T09:53:05.531Z" }, + { url = "https://files.pythonhosted.org/packages/99/32/15e08a0c4bb536303e1568e2ba5cae1ce39a2e026a03aea46173af4c7a2d/pyobjc_framework_libdispatch-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:23fc9915cba328216b6a736c7a48438a16213f16dfb467f69506300b95938cc7", size = 15976, upload-time = "2025-11-14T09:53:07.936Z" }, +] + +[[package]] +name = "pyopenssl" +version = "24.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/70/ff56a63248562e77c0c8ee4aefc3224258f1856977e0c1472672b62dadb8/pyopenssl-24.2.1.tar.gz", hash = "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95", size = 184323, upload-time = "2024-07-20T17:26:31.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/dd/e0aa7ebef5168c75b772eda64978c597a9129b46be17779054652a7999e4/pyOpenSSL-24.2.1-py3-none-any.whl", hash = "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d", size = 58390, upload-time = "2024-07-20T17:26:29.057Z" }, +] + +[[package]] +name = "pyopenssl" +version = "25.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/26/e25b4a374b4639e0c235527bbe31c0524f26eda701d79456a7e1877f4cc5/pyopenssl-25.0.0.tar.gz", hash = "sha256:cd2cef799efa3936bb08e8ccb9433a575722b9dd986023f1cabc4ae64e9dac16", size = 179573, upload-time = "2025-01-12T17:22:48.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/d7/eb76863d2060dcbe7c7e6cccfd95ac02ea0b9acc37745a0d99ff6457aefb/pyOpenSSL-25.0.0-py3-none-any.whl", hash = "sha256:424c247065e46e76a37411b9ab1782541c23bb658bf003772c3405fbaa128e90", size = 56453, upload-time = "2025-01-12T17:22:43.44Z" }, +] + +[[package]] +name = "pyopenssl" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/be/97b83a464498a79103036bc74d1038df4a7ef0e402cfaf4d5e113fb14759/pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329", size = 184073, upload-time = "2025-09-17T00:32:21.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size = 57268, upload-time = "2025-09-17T00:32:19.474Z" }, +] + +[[package]] +name = "pyrfc3339" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/7f/3c194647ecb80ada6937c38a162ab3edba85a8b6a58fa2919405f4de2509/pyrfc3339-2.1.0.tar.gz", hash = "sha256:c569a9714faf115cdb20b51e830e798c1f4de8dabb07f6ff25d221b5d09d8d7f", size = 12589, upload-time = "2025-08-23T16:40:31.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/90/0200184d2124484f918054751ef997ed6409cb05b7e8dcbf5a22da4c4748/pyrfc3339-2.1.0-py3-none-any.whl", hash = "sha256:560f3f972e339f579513fe1396974352fd575ef27caff160a38b312252fcddf3", size = 6758, upload-time = "2025-08-23T16:40:30.49Z" }, +] + +[[package]] +name = "pyric" +version = "0.1.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/64/a99f27d3b4347486c7bfc0aa516016c46dc4c0f380ffccbd742a61af1eda/PyRIC-0.1.6.3.tar.gz", hash = "sha256:b539b01cafebd2406c00097f94525ea0f8ecd1dd92f7731f43eac0ef16c2ccc9", size = 870401, upload-time = "2016-12-04T07:54:48.374Z" } + +[[package]] +name = "pyspeex-noise" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/1d/7d2ebb8f73c2b2e929b4ba5370b35dbc91f37268ea53f4b6acd9afa532cb/pyspeex_noise-1.0.2.tar.gz", hash = "sha256:56a888ca2ef7fdea2316aa7fad3636d2fcf5f4450f3a0db58caa7c10a614b254", size = 49882, upload-time = "2024-08-27T17:00:34.859Z" } + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, +] + +[[package]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, +] + +[[package]] +name = "pyturbojpeg" +version = "1.7.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/ba/37c075c7cc86b89a22db4ac46c2e4f444666f9a43975a512b7cf70ced2fd/PyTurboJPEG-1.7.5.tar.gz", hash = "sha256:5dd5f40dbf4159f41b6abaa123733910e8b1182df562b6ddb768991868b487d3", size = 12065, upload-time = "2024-07-28T08:34:03.778Z" } + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "questionary" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", version = "25.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "rpds-py", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2025.11.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312, upload-time = "2025-11-03T21:31:34.343Z" }, + { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256, upload-time = "2025-11-03T21:31:35.675Z" }, + { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921, upload-time = "2025-11-03T21:31:37.07Z" }, + { url = "https://files.pythonhosted.org/packages/ea/98/6a8dff667d1af907150432cf5abc05a17ccd32c72a3615410d5365ac167a/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568, upload-time = "2025-11-03T21:31:38.784Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/92c1db4fa4e12733dd5a526c2dd2b6edcbfe13257e135fc0f6c57f34c173/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165, upload-time = "2025-11-03T21:31:40.559Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e7/3ad7da8cdee1ce66c7cd37ab5ab05c463a86ffeb52b1a25fe7bd9293b36c/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182, upload-time = "2025-11-03T21:31:42.002Z" }, + { url = "https://files.pythonhosted.org/packages/84/bd/9ce9f629fcb714ffc2c3faf62b6766ecb7a585e1e885eb699bcf130a5209/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501, upload-time = "2025-11-03T21:31:43.815Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0f/8dc2e4349d8e877283e6edd6c12bdcebc20f03744e86f197ab6e4492bf08/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842, upload-time = "2025-11-03T21:31:45.353Z" }, + { url = "https://files.pythonhosted.org/packages/f9/73/cff02702960bc185164d5619c0c62a2f598a6abff6695d391b096237d4ab/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519, upload-time = "2025-11-03T21:31:46.814Z" }, + { url = "https://files.pythonhosted.org/packages/61/83/0e8d1ae71e15bc1dc36231c90b46ee35f9d52fab2e226b0e039e7ea9c10a/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611, upload-time = "2025-11-03T21:31:48.289Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f5/70a5cdd781dcfaa12556f2955bf170cd603cb1c96a1827479f8faea2df97/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759, upload-time = "2025-11-03T21:31:49.759Z" }, + { url = "https://files.pythonhosted.org/packages/59/9b/7c29be7903c318488983e7d97abcf8ebd3830e4c956c4c540005fcfb0462/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194, upload-time = "2025-11-03T21:31:51.53Z" }, + { url = "https://files.pythonhosted.org/packages/1a/67/3b92df89f179d7c367be654ab5626ae311cb28f7d5c237b6bb976cd5fbbb/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069, upload-time = "2025-11-03T21:31:53.151Z" }, + { url = "https://files.pythonhosted.org/packages/d7/55/85ba4c066fe5094d35b249c3ce8df0ba623cfd35afb22d6764f23a52a1c5/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330, upload-time = "2025-11-03T21:31:54.514Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081, upload-time = "2025-11-03T21:31:55.9Z" }, + { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123, upload-time = "2025-11-03T21:31:57.758Z" }, + { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814, upload-time = "2025-11-03T21:32:01.12Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592, upload-time = "2025-11-03T21:32:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122, upload-time = "2025-11-03T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272, upload-time = "2025-11-03T21:32:06.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497, upload-time = "2025-11-03T21:32:08.162Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892, upload-time = "2025-11-03T21:32:09.769Z" }, + { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462, upload-time = "2025-11-03T21:32:11.769Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528, upload-time = "2025-11-03T21:32:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866, upload-time = "2025-11-03T21:32:15.748Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" }, + { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" }, + { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984, upload-time = "2025-11-03T21:32:23.466Z" }, + { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673, upload-time = "2025-11-03T21:32:25.034Z" }, + { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029, upload-time = "2025-11-03T21:32:26.528Z" }, + { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437, upload-time = "2025-11-03T21:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368, upload-time = "2025-11-03T21:32:30.4Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921, upload-time = "2025-11-03T21:32:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708, upload-time = "2025-11-03T21:32:34.305Z" }, + { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472, upload-time = "2025-11-03T21:32:36.364Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341, upload-time = "2025-11-03T21:32:38.042Z" }, + { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666, upload-time = "2025-11-03T21:32:40.079Z" }, + { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473, upload-time = "2025-11-03T21:32:42.148Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" }, + { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" }, + { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" }, + { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" }, + { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" }, + { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" }, + { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" }, + { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" }, + { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" }, + { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version < '3.13.2'" }, + { name = "charset-normalizer", marker = "python_full_version < '3.13.2'" }, + { name = "idna", marker = "python_full_version < '3.13.2'" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.13.2'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.13.2'" }, + { name = "idna", marker = "python_full_version >= '3.13.2'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" }, + { url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" }, + { url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" }, + { url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" }, + { url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" }, + { url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, +] + +[[package]] +name = "securetar" +version = "2024.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/c5/46fc614b4d23823d90fd34b9f41dd649ebf639fc9581c7828eb05f0bd2df/securetar-2024.11.0.tar.gz", hash = "sha256:2191d8c8234777bba287a9b3e8a16cd3ec78fb52d092d1ef1b57d14c81d6838d", size = 11100, upload-time = "2024-11-21T17:25:48.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/cc/82943abb46b97c1b476d3c4c2b86b9601862f31c79b468fafa2fc76a4f25/securetar-2024.11.0-py3-none-any.whl", hash = "sha256:e538dc403b1773f33a58d3ef5fa71ab14c51f060b784924b3745eb6b0b27bfaa", size = 9379, upload-time = "2024-11-21T17:25:46.826Z" }, +] + +[[package]] +name = "securetar" +version = "2025.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/5b/da5f56ad39cbb1ca49bd0d4cccde7e97ea7d01fa724fa953746fa2b32ee6/securetar-2025.2.1.tar.gz", hash = "sha256:59536a73fe5cecbc1f00b1838c8b1052464a024e2adcf6c9ce1d200d91990fb1", size = 16124, upload-time = "2025-02-25T14:17:51.784Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/e0/b93a18e9bb7f7d2573a9c6819d42d996851edde0b0406d017067d7d23a0a/securetar-2025.2.1-py3-none-any.whl", hash = "sha256:760ad9d93579d5923f3d0da86e0f185d0f844cf01795a8754539827bb6a1bab4", size = 11545, upload-time = "2025-02-25T14:17:50.832Z" }, +] + +[[package]] +name = "sentence-stream" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/69/f3d048692aac843f41102507f6257138392ec841c16718f0618d27051caf/sentence_stream-1.3.0.tar.gz", hash = "sha256:b06261d35729de97df9002a1cc708f9a888f662b80d5d6d008ee69c51f36041b", size = 10049, upload-time = "2026-01-08T16:25:06.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/b6/48339c109bab6f54ff608800773b9425464c6cbf7fd3f2ba01294d78be3d/sentence_stream-1.3.0-py3-none-any.whl", hash = "sha256:7448d131315b85eefdf238e5edd9caa62899acf609145d5e0e10c09812eb8a1d", size = 8707, upload-time = "2026-01-08T16:25:05.918Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "shiny" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "click", marker = "sys_platform != 'emscripten'" }, + { name = "htmltools" }, + { name = "linkify-it-py" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "narwhals" }, + { name = "orjson", version = "3.10.12", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "orjson", version = "3.10.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "orjson", version = "3.11.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "prompt-toolkit", marker = "sys_platform != 'emscripten'" }, + { name = "python-multipart", marker = "sys_platform != 'emscripten'" }, + { name = "questionary", marker = "sys_platform != 'emscripten'" }, + { name = "setuptools" }, + { name = "shinychat" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, + { name = "watchfiles", marker = "sys_platform != 'emscripten'" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/8c/570fb0c8aa3b3f2b30e2f401bbe4048e7c39fb5ffb70994520fdff32a8da/shiny-1.5.1.tar.gz", hash = "sha256:482fa54635a6dc7e914cdbaa9099fe445790af3053d849b4e9a81e0f61677d37", size = 4941243, upload-time = "2025-12-08T18:18:23.763Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/2d/c955cbc6e66aff92645a7af35fa017001dffecb849b764f609fdc68da088/shiny-1.5.1-py3-none-any.whl", hash = "sha256:5ed60168e94fce0b7fc65bc04b92398e2611a2410acbcb850f8ae3001b99de40", size = 3943136, upload-time = "2025-12-08T18:18:21.654Z" }, +] + +[[package]] +name = "shinychat" +version = "0.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "htmltools" }, + { name = "shiny" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/c6/6887a3618842e65794e5dcfb345825ccd43d58c56fefa9d31286d8e14d16/shinychat-0.2.8.tar.gz", hash = "sha256:d27f28ddf1d512a05ef2cc2f0cffbb75b5b6e3157d5e3690ce4e63d18bfa7492", size = 547318, upload-time = "2025-09-11T20:13:20.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/01/8658eaffa920f4c621f05acc42ea98248147706079287b3a7ab4e47e1ece/shinychat-0.2.8-py3-none-any.whl", hash = "sha256:6742a2354257280458269a8b5675f1254e5583be34b6eb1a626de44c0323286a", size = 561057, upload-time = "2025-09-11T20:13:18.683Z" }, +] + +[[package]] +name = "shinylive" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appdirs" }, + { name = "chevron" }, + { name = "click" }, + { name = "lzstring" }, + { name = "setuptools" }, + { name = "shiny" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/b4/a741e01b756d76bf5839d47c54562563353fd5c5849e0cbb8b5b8ea23d8b/shinylive-0.8.5.tar.gz", hash = "sha256:d485efa14a4053be0e6ec19a21a49e5d0b6bd8b99cd2f5cb81b0b04f57a1c249", size = 29519, upload-time = "2025-12-08T21:08:27.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/bb/8dc375e7fb90737fe64a3aa150441a667225672d5d4b735880e4a41f1686/shinylive-0.8.5-py3-none-any.whl", hash = "sha256:85ac83b5b86adf181f79937a50c997315d30cd944a565a31d1a99abc8d715dcd", size = 29432, upload-time = "2025-12-08T21:08:25.868Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "snitun" +version = "0.39.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "aiohttp", version = "3.11.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "async-timeout", marker = "python_full_version < '3.13'" }, + { name = "attrs", version = "24.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "cryptography", version = "43.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/ff/2b7499dbfea2fa748620f8181aebdff26e24b6f78026516760e73d11a319/snitun-0.39.1.tar.gz", hash = "sha256:fadbe447eea786291d5c52e67eae0658f53a1f68c4b97425e17a9579df503d7e", size = 33073, upload-time = "2024-05-13T07:21:26.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/c3/1fd863e859a91d9087613195012d96e03c09063718b53deac610b4fd86b4/snitun-0.39.1-py3-none-any.whl", hash = "sha256:6ff55f6ba21d463877f9872de7d632fc18e400c8b42f8115c42d17e51075f674", size = 39089, upload-time = "2024-05-13T07:21:24.598Z" }, +] + +[[package]] +name = "snitun" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "aiohttp", version = "3.11.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "async-timeout", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "attrs", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "cryptography", version = "44.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/5d/c39d5dee7119017efa571e7ce09fcb4f098734cb367adab59bed497ae0e9/snitun-0.40.0.tar.gz", hash = "sha256:f5a70b3aab07524f196d27baf7a8f8774b3b00c442e91392539dd11dbd033c9c", size = 33111, upload-time = "2024-12-18T12:43:16.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/07/9982bd349e7a1aef3f8077ccfcf7ee9b447bd70ccab8121ad786334a882a/snitun-0.40.0-py3-none-any.whl", hash = "sha256:dedb58d3042d13311142b55337ad6ce6ed339e43da9dca4c4c2c83df77c64ac0", size = 39122, upload-time = "2024-12-18T12:43:12.756Z" }, +] + +[[package]] +name = "snitun" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "aiohttp", version = "3.13.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, + { name = "cryptography", version = "46.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/e2/b5bbf04971d1c3e07a3e16a706ea3c1a4b711c6d8c9566e8012772d3351a/snitun-0.45.1.tar.gz", hash = "sha256:d76d48cf4190ea59e8f63892da9c18499bfc6ca796220a463c6f3b32099d661c", size = 43335, upload-time = "2025-09-25T05:24:07.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/1b/83ff83003994bc8b56483c75a710de588896c167c7c42d66d059a2eb48dc/snitun-0.45.1-py3-none-any.whl", hash = "sha256:c1fa4536320ec3126926ade775c429e20664db1bc61d8fec0e181dc393d36ab4", size = 51236, upload-time = "2025-09-25T05:24:06.412Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.36" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'WIN32') or (python_full_version < '3.13' and platform_machine == 'aarch64') or (python_full_version < '3.13' and platform_machine == 'amd64') or (python_full_version < '3.13' and platform_machine == 'ppc64le') or (python_full_version < '3.13' and platform_machine == 'win32') or (python_full_version < '3.13' and platform_machine == 'x86_64')" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/65/9cbc9c4c3287bed2499e05033e207473504dc4df999ce49385fb1f8b058a/sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5", size = 9574485, upload-time = "2024-10-15T19:41:44.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/bf/005dc47f0e57556e14512d5542f3f183b94fde46e15ff1588ec58ca89555/SQLAlchemy-2.0.36-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4", size = 2092378, upload-time = "2024-10-16T00:43:55.469Z" }, + { url = "https://files.pythonhosted.org/packages/94/65/f109d5720779a08e6e324ec89a744f5f92c48bd8005edc814bf72fbb24e5/SQLAlchemy-2.0.36-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855", size = 2082778, upload-time = "2024-10-16T00:43:57.304Z" }, + { url = "https://files.pythonhosted.org/packages/60/f6/d9aa8c49c44f9b8c9b9dada1f12fa78df3d4c42aa2de437164b83ee1123c/SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53", size = 3232191, upload-time = "2024-10-15T21:31:12.896Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ab/81d4514527c068670cb1d7ab62a81a185df53a7c379bd2a5636e83d09ede/SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a", size = 3243044, upload-time = "2024-10-15T20:16:28.954Z" }, + { url = "https://files.pythonhosted.org/packages/35/b4/f87c014ecf5167dc669199cafdb20a7358ff4b1d49ce3622cc48571f811c/SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686", size = 3178511, upload-time = "2024-10-15T21:31:16.792Z" }, + { url = "https://files.pythonhosted.org/packages/ea/09/badfc9293bc3ccba6ede05e5f2b44a760aa47d84da1fc5a326e963e3d4d9/SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588", size = 3205147, upload-time = "2024-10-15T20:16:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/c8/60/70e681de02a13c4b27979b7b78da3058c49bacc9858c89ba672e030f03f2/SQLAlchemy-2.0.36-cp312-cp312-win32.whl", hash = "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e", size = 2062709, upload-time = "2024-10-15T20:16:29.946Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ed/f6cd9395e41bfe47dd253d74d2dfc3cab34980d4e20c8878cb1117306085/SQLAlchemy-2.0.36-cp312-cp312-win_amd64.whl", hash = "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5", size = 2088433, upload-time = "2024-10-15T20:16:33.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/5c/236398ae3678b3237726819b484f15f5c038a9549da01703a771f05a00d6/SQLAlchemy-2.0.36-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef", size = 2087651, upload-time = "2024-10-16T00:43:59.168Z" }, + { url = "https://files.pythonhosted.org/packages/a8/14/55c47420c0d23fb67a35af8be4719199b81c59f3084c28d131a7767b0b0b/SQLAlchemy-2.0.36-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8", size = 2078132, upload-time = "2024-10-16T00:44:01.279Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/1e843b36abff8c4a7aa2e37f9bea364f90d021754c2de94d792c2d91405b/SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b", size = 3164559, upload-time = "2024-10-15T21:31:18.961Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c5/07f18a897b997f6d6b234fab2bf31dccf66d5d16a79fe329aefc95cd7461/SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2", size = 3177897, upload-time = "2024-10-15T20:16:35.048Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cd/e16f3cbefd82b5c40b33732da634ec67a5f33b587744c7ab41699789d492/SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf", size = 3111289, upload-time = "2024-10-15T21:31:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/15/85/5b8a3b0bc29c9928aa62b5c91fcc8335f57c1de0a6343873b5f372e3672b/SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c", size = 3139491, upload-time = "2024-10-15T20:16:38.048Z" }, + { url = "https://files.pythonhosted.org/packages/a1/95/81babb6089938680dfe2cd3f88cd3fd39cccd1543b7cb603b21ad881bff1/SQLAlchemy-2.0.36-cp313-cp313-win32.whl", hash = "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436", size = 2060439, upload-time = "2024-10-15T20:16:36.182Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/5f7428df55660d6879d0522adc73a3364970b5ef33ec17fa125c5dbcac1d/SQLAlchemy-2.0.36-cp313-cp313-win_amd64.whl", hash = "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88", size = 2084574, upload-time = "2024-10-15T20:16:38.686Z" }, + { url = "https://files.pythonhosted.org/packages/b8/49/21633706dd6feb14cd3f7935fc00b60870ea057686035e1a99ae6d9d9d53/SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", size = 1883787, upload-time = "2024-10-15T20:04:30.265Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.39" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "greenlet", marker = "(python_full_version >= '3.13' and python_full_version < '3.13.2' and platform_machine == 'AMD64') or (python_full_version >= '3.13' and python_full_version < '3.13.2' and platform_machine == 'WIN32') or (python_full_version >= '3.13' and python_full_version < '3.13.2' and platform_machine == 'aarch64') or (python_full_version >= '3.13' and python_full_version < '3.13.2' and platform_machine == 'amd64') or (python_full_version >= '3.13' and python_full_version < '3.13.2' and platform_machine == 'ppc64le') or (python_full_version >= '3.13' and python_full_version < '3.13.2' and platform_machine == 'win32') or (python_full_version >= '3.13' and python_full_version < '3.13.2' and platform_machine == 'x86_64')" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/8e/e77fcaa67f8b9f504b4764570191e291524575ddbfe78a90fc656d671fdc/sqlalchemy-2.0.39.tar.gz", hash = "sha256:5d2d1fe548def3267b4c70a8568f108d1fed7cbbeccb9cc166e05af2abc25c22", size = 9644602, upload-time = "2025-03-11T18:27:09.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/86/b2cb432aeb00a1eda7ed33ce86d943c2452dc1642f3ec51bfe9eaae9604b/sqlalchemy-2.0.39-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c457a38351fb6234781d054260c60e531047e4d07beca1889b558ff73dc2014b", size = 2107210, upload-time = "2025-03-11T19:21:50.748Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b0/b2479edb3419ca763ba1b587161c292d181351a33642985506a530f9162b/sqlalchemy-2.0.39-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:018ee97c558b499b58935c5a152aeabf6d36b3d55d91656abeb6d93d663c0c4c", size = 2097599, upload-time = "2025-03-11T19:21:52.273Z" }, + { url = "https://files.pythonhosted.org/packages/58/5e/c5b792a4abcc71e68d44cb531c4845ac539d558975cc61db1afbc8a73c96/sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5493a8120d6fc185f60e7254fc056a6742f1db68c0f849cfc9ab46163c21df47", size = 3247012, upload-time = "2025-03-11T19:09:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a8/055fa8a7c5f85e6123b7e40ec2e9e87d63c566011d599b4a5ab75e033017/sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2cf5b5ddb69142511d5559c427ff00ec8c0919a1e6c09486e9c32636ea2b9dd", size = 3257851, upload-time = "2025-03-11T19:32:43.917Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/aec16681e91a22ddf03dbaeb3c659bce96107c5f47d2a7c665eb7f24a014/sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f03143f8f851dd8de6b0c10784363712058f38209e926723c80654c1b40327a", size = 3193155, upload-time = "2025-03-11T19:09:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/21/9d/cef697b137b9eb0b66ab8e9cf193a7c7c048da3b4bb667e5fcea4d90c7a2/sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06205eb98cb3dd52133ca6818bf5542397f1dd1b69f7ea28aa84413897380b06", size = 3219770, upload-time = "2025-03-11T19:32:48.237Z" }, + { url = "https://files.pythonhosted.org/packages/57/05/e109ca7dde837d8f2f1b235357e4e607f8af81ad8bc29c230fed8245687d/sqlalchemy-2.0.39-cp312-cp312-win32.whl", hash = "sha256:7f5243357e6da9a90c56282f64b50d29cba2ee1f745381174caacc50d501b109", size = 2077567, upload-time = "2025-03-11T18:43:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/97/c6/25ca068e38c29ed6be0fde2521888f19da923dbd58f5ff16af1b73ec9b58/sqlalchemy-2.0.39-cp312-cp312-win_amd64.whl", hash = "sha256:2ed107331d188a286611cea9022de0afc437dd2d3c168e368169f27aa0f61338", size = 2103136, upload-time = "2025-03-11T18:43:15.316Z" }, + { url = "https://files.pythonhosted.org/packages/32/47/55778362642344324a900b6b2b1b26f7f02225b374eb93adc4a363a2d8ae/sqlalchemy-2.0.39-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fe193d3ae297c423e0e567e240b4324d6b6c280a048e64c77a3ea6886cc2aa87", size = 2102484, upload-time = "2025-03-11T19:21:54.018Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e1/f5f26f67d095f408138f0fb2c37f827f3d458f2ae51881546045e7e55566/sqlalchemy-2.0.39-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:79f4f502125a41b1b3b34449e747a6abfd52a709d539ea7769101696bdca6716", size = 2092955, upload-time = "2025-03-11T19:21:55.658Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c2/0db0022fc729a54fc7aef90a3457bf20144a681baef82f7357832b44c566/sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a10ca7f8a1ea0fd5630f02feb055b0f5cdfcd07bb3715fc1b6f8cb72bf114e4", size = 3179367, upload-time = "2025-03-11T19:09:31.059Z" }, + { url = "https://files.pythonhosted.org/packages/33/b7/f33743d87d0b4e7a1f12e1631a4b9a29a8d0d7c0ff9b8c896d0bf897fb60/sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6b0a1c7ed54a5361aaebb910c1fa864bae34273662bb4ff788a527eafd6e14d", size = 3192705, upload-time = "2025-03-11T19:32:50.795Z" }, + { url = "https://files.pythonhosted.org/packages/c9/74/6814f31719109c973ddccc87bdfc2c2a9bc013bec64a375599dc5269a310/sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52607d0ebea43cf214e2ee84a6a76bc774176f97c5a774ce33277514875a718e", size = 3125927, upload-time = "2025-03-11T19:09:32.678Z" }, + { url = "https://files.pythonhosted.org/packages/e8/6b/18f476f4baaa9a0e2fbc6808d8f958a5268b637c8eccff497bf96908d528/sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c08a972cbac2a14810463aec3a47ff218bb00c1a607e6689b531a7c589c50723", size = 3154055, upload-time = "2025-03-11T19:32:53.344Z" }, + { url = "https://files.pythonhosted.org/packages/b4/60/76714cecb528da46bc53a0dd36d1ccef2f74ef25448b630a0a760ad07bdb/sqlalchemy-2.0.39-cp313-cp313-win32.whl", hash = "sha256:23c5aa33c01bd898f879db158537d7e7568b503b15aad60ea0c8da8109adf3e7", size = 2075315, upload-time = "2025-03-11T18:43:16.946Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7c/76828886d913700548bac5851eefa5b2c0251ebc37921fe476b93ce81b50/sqlalchemy-2.0.39-cp313-cp313-win_amd64.whl", hash = "sha256:4dabd775fd66cf17f31f8625fc0e4cfc5765f7982f94dc09b9e5868182cb71c0", size = 2099175, upload-time = "2025-03-11T18:43:18.141Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0f/d69904cb7d17e65c65713303a244ec91fd3c96677baf1d6331457fd47e16/sqlalchemy-2.0.39-py3-none-any.whl", hash = "sha256:a1c6b0a5e3e326a466d809b651c63f278b1256146a377a528b6938a279da334f", size = 1898621, upload-time = "2025-03-11T19:20:33.027Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.41" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "greenlet", marker = "(python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version >= '3.13.2' and python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload-time = "2025-05-14T17:55:24.854Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload-time = "2025-05-14T17:55:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload-time = "2025-05-14T17:50:38.227Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload-time = "2025-05-14T17:51:49.829Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload-time = "2025-05-14T17:50:39.774Z" }, + { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload-time = "2025-05-14T17:55:49.915Z" }, + { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload-time = "2025-05-14T17:55:51.349Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" }, + { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" }, + { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" }, +] + +[[package]] +name = "standard-aifc" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, + { name = "standard-chunk", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload-time = "2024-10-30T16:01:31.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/52/5fbb203394cc852334d1575cc020f6bcec768d2265355984dfd361968f36/standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66", size = 10492, upload-time = "2024-10-30T16:01:07.071Z" }, +] + +[[package]] +name = "standard-chunk" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/06/ce1bb165c1f111c7d23a1ad17204d67224baa69725bb6857a264db61beaf/standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654", size = 4672, upload-time = "2024-10-30T16:18:28.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/90/a5c1084d87767d787a6caba615aa50dc587229646308d9420c960cb5e4c0/standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c", size = 4944, upload-time = "2024-10-30T16:18:26.694Z" }, +] + +[[package]] +name = "standard-telnetlib" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/06/7bf7c0ec16574aeb1f6602d6a7bdb020084362fb4a9b177c5465b0aae0b6/standard_telnetlib-3.13.0.tar.gz", hash = "sha256:243333696bf1659a558eb999c23add82c41ffc2f2d04a56fae13b61b536fb173", size = 12636, upload-time = "2024-10-30T16:01:42.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/85/a1808451ac0b36c61dffe8aea21e45c64ba7da28f6cb0d269171298c6281/standard_telnetlib-3.13.0-py3-none-any.whl", hash = "sha256:b268060a3220c80c7887f2ad9df91cd81e865f0c5052332b81d80ffda8677691", size = 9995, upload-time = "2024-10-30T16:01:29.289Z" }, +] + +[[package]] +name = "starlette" +version = "0.51.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/65/5a1fadcc40c5fdc7df421a7506b79633af8f5d5e3a95c3e72acacec644b9/starlette-0.51.0.tar.gz", hash = "sha256:4c4fda9b1bc67f84037d3d14a5112e523509c369d9d47b111b2f984b0cc5ba6c", size = 2647658, upload-time = "2026-01-10T20:23:15.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/c4/09985a03dba389d4fe16a9014147a7b02fa76ef3519bf5846462a485876d/starlette-0.51.0-py3-none-any.whl", hash = "sha256:fb460a3d6fd3c958d729fdd96aee297f89a51b0181f16401fe8fd4cb6129165d", size = 74133, upload-time = "2026-01-10T20:23:13.445Z" }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex", marker = "python_full_version >= '3.13.2'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.13.2' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "typer-slim" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "python_full_version >= '3.13.2'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/d4/064570dec6358aa9049d4708e4a10407d74c99258f8b2136bb8702303f1a/typer_slim-0.21.1.tar.gz", hash = "sha256:73495dd08c2d0940d611c5a8c04e91c2a0a98600cbd4ee19192255a233b6dbfd", size = 110478, upload-time = "2026-01-06T11:21:11.176Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/0a/4aca634faf693e33004796b6cee0ae2e1dba375a800c16ab8d3eff4bb800/typer_slim-0.21.1-py3-none-any.whl", hash = "sha256:6e6c31047f171ac93cc5a973c9e617dbc5ab2bddc4d0a3135dc161b4e2020e0d", size = 47444, upload-time = "2026-01-06T11:21:12.441Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "uart-devices" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/08/a8fd6b3dd2cb92344fb4239d4e81ee121767430d7ce71f3f41282f7334e0/uart_devices-0.1.1.tar.gz", hash = "sha256:3a52c4ae0f5f7400ebe1ae5f6e2a2d40cc0b7f18a50e895236535c4e53c6ed34", size = 5167, upload-time = "2025-02-22T16:47:05.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/64/edf33c2d7fba7d6bf057c9dc4235bfc699517ea4c996240a1a9c2bf51c29/uart_devices-0.1.1-py3-none-any.whl", hash = "sha256:55bc8cce66465e90b298f0910e5c496bc7be021341c5455954cf61c6253dc123", size = 4827, upload-time = "2025-02-22T16:47:04.286Z" }, +] + +[[package]] +name = "uc-micro-py" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" }, +] + +[[package]] +name = "ulid-transform" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/64/5e/d48740b5aa3f1135c3c76f4bffaa7bddb209807d6ac11ad79666d451641b/ulid_transform-1.0.2.tar.gz", hash = "sha256:9b710f6adb93a7620910bce385c7e977a234ab321443ec3bc1e48ae931f1e5d4", size = 15743, upload-time = "2024-08-24T23:23:22.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/61/63d342c76937a4422ec0c581bf976717e243ae0f84d53592a00a78d18aaa/ulid_transform-1.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:eb88626b68fa34883722ade34df0fd3b51f55ab6730e8bda6532a087568bbb54", size = 41179, upload-time = "2024-08-24T23:28:52.585Z" }, + { url = "https://files.pythonhosted.org/packages/74/2e/b5e0afc1f9a19eefb293c55d8c6444c9132b8c026006e2b3f1e3848ce2a9/ulid_transform-1.0.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b39b0188c788dac1338e3e217fc83728189f1e4a91ff75afd5152ddb7a41fe9", size = 164907, upload-time = "2024-08-24T23:28:53.845Z" }, + { url = "https://files.pythonhosted.org/packages/b7/08/17391871f8585a0d3321a96f57ce27225d1c15d8497881935f180e668ab7/ulid_transform-1.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc58deae5c2a3868824f3ce8e103845a600b6420fab88c4b6f1cab8a45c657f4", size = 172564, upload-time = "2024-08-24T23:28:55.301Z" }, + { url = "https://files.pythonhosted.org/packages/05/5e/2e326e349ce3da7667817e12de333a5aaa18d904d8ee75122bb17dbdf61c/ulid_transform-1.0.2-cp312-cp312-manylinux_2_36_x86_64.whl", hash = "sha256:55812ff17d265a1318c16b2af3349bdd893f0c5ca4352ea429d82ea8a7ab36ac", size = 170072, upload-time = "2024-08-24T23:23:20.946Z" }, + { url = "https://files.pythonhosted.org/packages/1e/36/736752356192f19d0b65fc1da68b616ac185a6ee960b8c9f69df4d5d8622/ulid_transform-1.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a95449d9343ee6cfae49ace10b7ed094acbfcb19c4d56a5ba06874507e1e550f", size = 1243697, upload-time = "2024-08-24T23:28:56.403Z" }, + { url = "https://files.pythonhosted.org/packages/80/78/768b512c3870d512692ec108b430debdcdaa5fa1cbb5d9411253f37747e8/ulid_transform-1.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e8b7ac6d9273bac1c1a242bbc6c3d9c3018bff8e00501293a8cf7493bff7190", size = 1155626, upload-time = "2024-08-24T23:28:57.898Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/ca24bafa285d37fef4edda165c9893cc23977ba7309cf1cbbf9347dd5de7/ulid_transform-1.0.2-cp312-cp312-win32.whl", hash = "sha256:72f719c10b3e9135a09c8d38182e4afacb2c8908ea566257c037b3a4a62f9b91", size = 38715, upload-time = "2024-08-24T23:28:59.036Z" }, + { url = "https://files.pythonhosted.org/packages/02/be/dcb545a55247636bbbf642d0a61c3db3106aab9fccd59f799630e3203478/ulid_transform-1.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:943aaa6c6888f93e4718bcd1cc852db5ee0b2694a1107dcf411bfa2b5e3eb3bd", size = 41041, upload-time = "2024-08-24T23:29:00.645Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/3dba6de60c854b9baa56aaff114f1a8f34327ea6bdceb7a76647dbf4b408/ulid_transform-1.0.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:76563b84d0852a2861cdd22fb082ae1f7a88f038c078be4ad716610eeea421a6", size = 40524, upload-time = "2024-08-24T23:29:01.873Z" }, + { url = "https://files.pythonhosted.org/packages/e8/48/323ed2ae0c80a875793115ae97fab331b9785b96d56064096011f31758e3/ulid_transform-1.0.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96c3c2423972cae3f26ef564c79507da49558553fb13e605dcf9395fab2924e4", size = 158855, upload-time = "2024-08-24T23:29:02.938Z" }, + { url = "https://files.pythonhosted.org/packages/be/cc/5b06e4bce8b0b6b1d4334ad83e6224684eac1744fcff0e9e4d215165af33/ulid_transform-1.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da3887088b3a3bc4a98bcd65274a0324c50dfd6d56305434ddcc25024a1ed98b", size = 166633, upload-time = "2024-08-24T23:29:04.435Z" }, + { url = "https://files.pythonhosted.org/packages/56/a0/78b2f37777ef89c335a86f6803dd68d60fd0f700aa2c30990a4cb064aa4e/ulid_transform-1.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bd972e8c1695ad3e24b5d9eaa5ad352ff9237f58186d182f4ef48a1f7d352b1d", size = 1237875, upload-time = "2024-08-24T23:29:05.876Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/398dd416d45b0f7d497486c67d080bc113d1b0aefdeab85d16a12d2f852f/ulid_transform-1.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71c71a436f5e2c0a1580ad5269b1e7ad8193c1cbb69463342261e46f0f722f4d", size = 1150251, upload-time = "2024-08-24T23:29:07.016Z" }, + { url = "https://files.pythonhosted.org/packages/e0/92/47c5c4f09da84e6f73bf4b984a757a53086a8216b0c0ee4ccdb5a3f3c0e9/ulid_transform-1.0.2-cp313-cp313-win32.whl", hash = "sha256:2231ca1d83f5964a7cdf353f9d7cbc16a2e51eb8c9d5a9c743fe3aa0d17c6b3e", size = 38270, upload-time = "2024-08-24T23:29:08.192Z" }, + { url = "https://files.pythonhosted.org/packages/58/88/bdb72f143c76aabe2f5169a441f8d28973ed4e6fb598c4b3acdd77b379a0/ulid_transform-1.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:0d5bc5f3392b78ba9225dbb919b05fed7d62cff77f8674cc1389c01d3ae9a947", size = 40309, upload-time = "2024-08-24T23:29:09.395Z" }, +] + +[[package]] +name = "ulid-transform" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/f2/16c8e6f3d82debedeb1b09bec889ad4a1ca8a71d2d269c156dd80d049c2e/ulid_transform-1.4.0.tar.gz", hash = "sha256:5914a3c4277b0d25ebb67f47bfee2167ac858d970249ea275221fb3e5d91c9a0", size = 16023, upload-time = "2025-03-07T10:44:02.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/1d/c43d3e1bda52a321f6cde3526b3634602958dc8ccf1f20fd6616767fd1a1/ulid_transform-1.4.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:9b1429ca7403696b290e4e97ffadbf8ed0b7470a97ad7e273372c3deae5bfb2f", size = 51566, upload-time = "2025-03-07T10:44:00.79Z" }, +] + +[[package]] +name = "ulid-transform" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/25/44/2ef5e7218ad021fda3fedcb6c1347dd3bf972e9cbdea94644aaa7e4884bb/ulid_transform-1.5.2.tar.gz", hash = "sha256:9a5caf279ec21789ddc2f36b9008ce33a3197d7d66fdd7628fbebec9ba778829", size = 14247, upload-time = "2025-10-04T20:57:22.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/3a/7b9e8cfa2739fd31bdfc0e0e9065a237c2b4a1164331eb0a859e46b3dee8/ulid_transform-1.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2fbadf7fc2b72b1ab0ae176d42492e2b8f69817db32e653fe8d2817ca5b1a714", size = 41637, upload-time = "2025-10-04T21:03:49.467Z" }, + { url = "https://files.pythonhosted.org/packages/12/98/f9c05b0c2b341db1adbffa2507665557cd1cce403150174322537e4d5532/ulid_transform-1.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b5766373871bdf874e3468c1e1e9c291e628223fbc085ca10a961f26f720fe9d", size = 42186, upload-time = "2025-10-04T21:03:50.736Z" }, + { url = "https://files.pythonhosted.org/packages/25/c3/6903a4d068b2e93727ee5c590aa8025da6a7927b10718e624129ad5daf22/ulid_transform-1.5.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5074c362d7bcf53b4a5acaeb0416542ba3838ebabe54d60c3b6ac920aa612d6c", size = 49292, upload-time = "2025-10-04T21:03:52.615Z" }, + { url = "https://files.pythonhosted.org/packages/7f/aa/36cf33b3a514f295ec379908597872ae5c3d334bec942777d6afe6449c60/ulid_transform-1.5.2-cp312-cp312-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e986b5079764350a586006b6bfda04f9ae87170f03efe377a3a6ec3e0ac086e6", size = 46576, upload-time = "2025-10-04T21:03:53.574Z" }, + { url = "https://files.pythonhosted.org/packages/85/7f/f39c7b8c7987c82f01384c615a75bcf6bc3b37fab8a64029d40c71752fa5/ulid_transform-1.5.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba5f9ed304f956e83c29642704a0f814c05c23c68cc028a27bd234acc2f04782", size = 49702, upload-time = "2025-10-04T21:03:54.611Z" }, + { url = "https://files.pythonhosted.org/packages/1e/45/68cb9b8a86bdabc3ba06a00451a6c8b720213cc7eb56adc1b3c741bbe421/ulid_transform-1.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2303395992e6a3d95f589173a78952a8690bf3b8bd97d248eee580ad9c1898eb", size = 1028866, upload-time = "2025-10-04T21:03:55.708Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b1/17a9fbddf4371ec04f958ec157e10f4b2361829b9f9c7263b88a2cf8906a/ulid_transform-1.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:07cd1b250293d731e3fcf1ad3a4eb260e78f03c51676bf96d4e126c77eca48c4", size = 894658, upload-time = "2025-10-04T21:03:56.933Z" }, + { url = "https://files.pythonhosted.org/packages/8c/43/421cbc0f751d09b09b563f69ea4b99a95c8a269417b6ff3266e27a5a8f3b/ulid_transform-1.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:547a824a7db7436bc68afd39ccdec0ce5f7e0b235897cc7f01ab148257db9ab9", size = 1080539, upload-time = "2025-10-04T21:03:59.092Z" }, + { url = "https://files.pythonhosted.org/packages/1f/39/39e85470bd409d05502b552946d1a852704bee92e2f38f43d576e551f300/ulid_transform-1.5.2-cp312-cp312-win32.whl", hash = "sha256:8b0650b56ee6bde9de7c7f247584c2be931834c8a49b306ed905d2c8a6653eaa", size = 39356, upload-time = "2025-10-04T21:04:00.308Z" }, + { url = "https://files.pythonhosted.org/packages/43/55/c8dedcde6ec1c75ab8c0b9e937f2d5032391ac5f95496fd80cb97dc018f4/ulid_transform-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:301aa542de87a792cc4e82dc7678cc9e3be61e60c16bbdf24f12bc4883e9e8e8", size = 41929, upload-time = "2025-10-04T21:04:01.235Z" }, + { url = "https://files.pythonhosted.org/packages/32/e5/f0d51b4b67e91dae04416623d81017863ea576dfa3ead2a8639761564423/ulid_transform-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:74ba0045e2ab94be1fa6a7901f9958cef6d35cda58546cdfbadc7129ebcdc88b", size = 41138, upload-time = "2025-10-04T21:04:02.198Z" }, + { url = "https://files.pythonhosted.org/packages/43/07/a80882cbc9557996996aae583c2d98bd90e54573390ae1332fed1a7e0124/ulid_transform-1.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6354467dab6aa922cdd7e4a8a2da31222d07609616df167e656dac7244d0f658", size = 41495, upload-time = "2025-10-04T21:04:03.173Z" }, + { url = "https://files.pythonhosted.org/packages/23/81/768b26d31ba4e7002abfd507e2ed42731e8bc4de5c38d10b5530ec19d52f/ulid_transform-1.5.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4faec817e9e5a031d4887c69e1254c428683c62e6f26ae9fd2f0a330a7c4c85", size = 48781, upload-time = "2025-10-04T21:04:04.112Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a3/13f10f9cf258f4faf8340f9e7dbb9c001340c27b3b68a4baa9688af2b428/ulid_transform-1.5.2-cp313-cp313-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:40897a0189cef7cce7c0b26bcff9daafa9df2ce68249e7e6095090a6372ed6ac", size = 45918, upload-time = "2025-10-04T21:04:05.106Z" }, + { url = "https://files.pythonhosted.org/packages/38/9d/aed53563a544556a2c547e33b950a861d006feeda1ded38e79ade6212672/ulid_transform-1.5.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:397a2d6c2030a3c3d572dfa35c27c647911219daba93056a80091f76888f597e", size = 49232, upload-time = "2025-10-04T21:04:06.123Z" }, + { url = "https://files.pythonhosted.org/packages/c5/76/47c2a1940c732a3842163668a7ebe11796fc02c1fa170c4abe9868a78193/ulid_transform-1.5.2-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:6794612dbc085abdac5ee3c4e3ec141ab8eb0e7b31f94f56111cacbe36137339", size = 49269, upload-time = "2025-10-04T20:57:20.283Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/d15aa9cfe960ce9fe8e1a14a4b7222c928862ae14128e7480bcee142b546/ulid_transform-1.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497e2dbca8b53c0d072da2c662ac8779faf698ccd52827932a8dcbc5d15960d4", size = 1028355, upload-time = "2025-10-04T21:04:07.25Z" }, + { url = "https://files.pythonhosted.org/packages/d0/37/d259be5021e95d443d8659e1bc2dd90b2248d6ef5ce8e4c97c434fe688ae/ulid_transform-1.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a51bab261fc5a50e1eb81f022129ffc98e64b917a69b29ca8411a00464c09d47", size = 894259, upload-time = "2025-10-04T21:04:08.663Z" }, + { url = "https://files.pythonhosted.org/packages/15/1d/568e1abf4089d4f7b8bcef09abe99fd4b2a56c9ca1ea81fc94e8d7a3cdff/ulid_transform-1.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:52f728769c822a52c05310598d949541d1e929dd1a481b2e73c971beea089a2a", size = 1080159, upload-time = "2025-10-04T21:04:10.321Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d1/ecbd9ab6a1b4c9c6fc57a974ad0ccd960d9adaed4c04794dc18063a80c57/ulid_transform-1.5.2-cp313-cp313-win32.whl", hash = "sha256:0b90b0b7ed937f8cf245f8c71adbd73cf93fcc4915cac9150255595a016b53d0", size = 38989, upload-time = "2025-10-04T21:04:11.562Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3f/180a40f60252cd195d00f1a5328684a0c14cce2201014db1a72ce4a92c0c/ulid_transform-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:3c1354706ac87ecf3b941c836c04b96f83e5d50ba7b2c3e2f746da838405bc09", size = 41200, upload-time = "2025-10-04T21:04:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c5/8eb0aa7bcd5cfb772ae8535b63d8d5fe7503c3d0adda931e7ee7e5e9af39/ulid_transform-1.5.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bae1b0f6041bd8c7d49019c14fb94e42ccab2b59083e7b0cb9f4d13483d7435a", size = 41085, upload-time = "2025-10-04T21:04:13.468Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ff/45cfb8ceaa67eea28c7a1a90242c1ade01b2a1b714feec7af47c086c6945/ulid_transform-1.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9c4a61fd13ced6b0b96f5983ef4e57ad8adefed4361b6d0f55a2bbfbb18b17d8", size = 41575, upload-time = "2025-10-04T21:04:14.379Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d9/ab80688863e7228732736ec39764910891b0978ae0d1953395ce2d505cdc/ulid_transform-1.5.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8065ddfd43827b1299a64da4437161a4f3fa1f03c05d838a3a8c82bc1d014518", size = 48948, upload-time = "2025-10-04T21:04:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/9f/dd/9bd352aac0fddf167a70dcab386cc4d8d099676531a89afa5019c6f1dbe7/ulid_transform-1.5.2-cp314-cp314-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b60965067d13942b0eaaaf986da5eff4ba8f1261b379ca1dac78afe47d940f1a", size = 45789, upload-time = "2025-10-04T21:04:16.861Z" }, + { url = "https://files.pythonhosted.org/packages/cb/90/0b4b4e0ac6061ea90cbdc526e17a75aad0fefafadbe43c56bfd7a77b8a85/ulid_transform-1.5.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e97a11b56b9e5537ef4521a97fc095b49d849c0ac0ec8d30a2974bd69e5948d", size = 49351, upload-time = "2025-10-04T21:04:18.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/7b/57da5afcd538306c44c093ef314bef4b04768f04b3c509144ed201b7d374/ulid_transform-1.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6b4915062eee740eefa937459ef468f7f1e35bd2ad5bffdf4245051d656df2c4", size = 1028528, upload-time = "2025-10-04T21:04:19.278Z" }, + { url = "https://files.pythonhosted.org/packages/a1/96/5d3c3464bb64b4fd6b6605787b3a4fef982ba207ba8a8ecc432543fe954e/ulid_transform-1.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7d4cf4bb26fe102dfd1bd10c5b18712fe7640433839c8d9dd20e2d8ccefa972d", size = 894080, upload-time = "2025-10-04T21:04:20.548Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/05dc2a2b2f981617a3ba1cdd8277b86504c4293feefc3a3ba342bac7cbec/ulid_transform-1.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad4953675e6dec400de633087f61cbb38d0ad978d57b60cc3539f7b821d9559", size = 1080292, upload-time = "2025-10-04T21:04:22.092Z" }, + { url = "https://files.pythonhosted.org/packages/f3/78/06032df3d6cc211a4d3edad92ed9433fa84e654c42936c1e268d53feab31/ulid_transform-1.5.2-cp314-cp314-win32.whl", hash = "sha256:d6793d4c477b30d95ed84123cc73d515ba4dac58cd01e7584637421b377349d3", size = 39903, upload-time = "2025-10-04T21:04:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ba/c0b0f757e9d2b5c565624227336fcc353c5e3160667d451ac361d342b11d/ulid_transform-1.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:dbbe98fd8b46431e3a15268e0dceeb80291ebfa7741d1ee692006928c0900d0c", size = 41987, upload-time = "2025-10-04T21:04:24.576Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/4f74de4fe96bb85fcc1e7fd572527aafd0999d48de3f6d1a41c66cdebc41/ulid_transform-1.5.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:08286ccc6bac0107e1bd5415a28e730d88089293ba5ce51dc5883175eccc31e2", size = 68442, upload-time = "2025-10-04T21:04:25.66Z" }, + { url = "https://files.pythonhosted.org/packages/03/58/854e4bd4539be70e5714002198e0565f897cf5b65203d9c230b362cc41df/ulid_transform-1.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ed0b533c936cb120312cd98ca1c8ec1f8af66bac6bc08426c030b48291d5505e", size = 69113, upload-time = "2025-10-04T21:04:27.039Z" }, + { url = "https://files.pythonhosted.org/packages/6e/45/73d6aa7c63e1e472bbe8e54a0892cdec78dc8438798e9ea518f1c371f640/ulid_transform-1.5.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58617bae6fc21507f5151328faf7b77c6ba6a615b42efd18f494564354a3ce68", size = 85341, upload-time = "2025-10-04T21:04:28.046Z" }, + { url = "https://files.pythonhosted.org/packages/dd/fb/263774b2249d683addd0de5746d4c4debbb33966277d4d33390150944ba3/ulid_transform-1.5.2-cp314-cp314t-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e24e68971a64a04af2d0b3df98bfe0087c85d35a1b02fa0bbf43a3a0a99dccf6", size = 78616, upload-time = "2025-10-04T21:04:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/88/cf/2eda3645a002a9fd141c19fd7416de87adaa12b25f8916b42b78644dbddd/ulid_transform-1.5.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb5da66ec5e7d97f695dd16637d5a8816bb9661df43ff1f2de0d46071d96a7a8", size = 85582, upload-time = "2025-10-04T21:04:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/09/37/e08e975e4ed61fb2ae7014591fcc7e5f1a62966c7ed53fc1c95c3da78923/ulid_transform-1.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:397646cf156aa46456cd8504075d117d2983ebf2cff01955c3add6280d0fb3c8", size = 1066091, upload-time = "2025-10-04T21:04:32.066Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e7/43474bf4ec56eadf2509939585894ef094dc364143596f62639bebd6d42e/ulid_transform-1.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:dc5ac2ffa704a21df2a36cea47ac1022fb6f8ab03abe31a5f7b81312f972e2c2", size = 926160, upload-time = "2025-10-04T21:04:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/ca/48/36df80548d96ffdaa3ae124854bbd5a0a0b07da22a8d22b301e5ec17de6e/ulid_transform-1.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6f29b8004fba0da7061b5eecf6101c6283377b6cd04f3626089cc67d9342c8fd", size = 1116228, upload-time = "2025-10-04T21:04:35.325Z" }, + { url = "https://files.pythonhosted.org/packages/9f/85/9cd506a4e0fe06946c3fd75b89c7d1e9a175a5ac11dfd8e4cc56658ff389/ulid_transform-1.5.2-cp314-cp314t-win32.whl", hash = "sha256:c09f58aff7a4974f560dd5fb19dd5144e8964371fcb1971bffa817c9abcb2232", size = 67476, upload-time = "2025-10-04T21:04:36.907Z" }, + { url = "https://files.pythonhosted.org/packages/41/a9/161ab974510c78a28155de33091a0063ed84b110e6d4fb866d5110112ce9/ulid_transform-1.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:445e14170301229a486e8815c2f9cec4a10b3e3cd4c9aa509689443d05e4f020", size = 72206, upload-time = "2025-10-04T21:04:38.286Z" }, +] + +[[package]] +name = "unicode-rbnf" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/1f/d952ba97832647e608700c36b22d1c4476016076c9ed1ce74ae814bea55a/unicode_rbnf-2.4.0.tar.gz", hash = "sha256:6d2f12a7581c69ea6218ee61fafcd2da46e1f9986bdcd0964c5151f7c2a938ac", size = 89069, upload-time = "2025-10-07T20:59:41.3Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/21/82f5d435808cba330668a8b69efb180e3ef9739d4998e8cd0381e8c9cb23/unicode_rbnf-2.4.0-py3-none-any.whl", hash = "sha256:0176b30ac9b7b84008d7dc0f23078055dc10d2671fdadfab5747943243e20e2d", size = 141691, upload-time = "2025-10-07T20:59:40.139Z" }, +] + +[[package]] +name = "urllib3" +version = "1.26.20" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380, upload-time = "2024-08-29T15:43:11.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "usb-devices" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/48/dbe6c4c559950ebebd413e8c40a8a60bfd47ddd79cb61b598a5987e03aad/usb_devices-0.4.5.tar.gz", hash = "sha256:9b5c7606df2bc791c6c45b7f76244a0cbed83cb6fa4c68791a143c03345e195d", size = 5421, upload-time = "2023-12-16T19:59:53.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/c9/26171ae5b78d72dd006bbc51ca9baa2cbb889ae8e91608910207482108fd/usb_devices-0.4.5-py3-none-any.whl", hash = "sha256:8a415219ef1395e25aa0bddcad484c88edf9673acdeae8a07223ca7222a01dcf", size = 5349, upload-time = "2023-12-16T19:59:51.604Z" }, +] + +[[package]] +name = "uv" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/18/ad/66cc8e00c217e7fcf76598c880632b480aa38d4cad311596b78e99737498/uv-0.5.4.tar.gz", hash = "sha256:cd7a5a3a36f975a7678f27849a2d49bafe7272143d938e9b6f3bf28392a3ba00", size = 2315678, upload-time = "2024-11-20T21:21:52.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/3e/6bf24d7bb0d11715ea783ecabcacdecdc8c51fca0144fcdad2090d65bae5/uv-0.5.4-py3-none-linux_armv6l.whl", hash = "sha256:2118bb99cbc9787cb5e5cc4a507201e25a3fe88a9f389e8ffb84f242d96038c2", size = 13853445, upload-time = "2024-11-20T21:20:54.968Z" }, + { url = "https://files.pythonhosted.org/packages/b8/be/c3acbe2944cd694a5d61a7a461468fa886512c84014545bb8f3244092eaa/uv-0.5.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4432215deb8d5c1ccab17ee51cb80f5de1a20865ee02df47532f87442a3d6a58", size = 13969300, upload-time = "2024-11-20T21:20:59.441Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c5/06e3b93045179b92d75cf94e6e224baec3226070f1cbc0e11d4898300b54/uv-0.5.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f40c6c6c3a1b398b56d3a8b28f7b455ac1ce4cbb1469f8d35d3bbc804d83daa4", size = 12932325, upload-time = "2024-11-20T21:21:02.986Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/06ab86e9f0c270c495077ef2b588458172ed84f9c337de725c8b08872354/uv-0.5.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:df3cb58b7da91f4fc647d09c3e96006cd6c7bd424a81ce2308a58593c6887c39", size = 13183356, upload-time = "2024-11-20T21:21:05.847Z" }, + { url = "https://files.pythonhosted.org/packages/c1/cb/bee01ef23e5020dc1f12d86ca8f82e95a723585db3ec64bfab4016e5616c/uv-0.5.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd2df2ba823e6684230ab4c581f2320be38d7f46de11ce21d2dbba631470d7b6", size = 13622310, upload-time = "2024-11-20T21:21:09.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/128fd874151919c71af51f528db28964e6d8e509fff12210ec9ba99b13fb/uv-0.5.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:928ed95fefe4e1338d0a7ad2f6b635de59e2ec92adaed4a267f7501a3b252263", size = 14207832, upload-time = "2024-11-20T21:21:12.992Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2b/0fed8a49440494f6806dcb67021ca8f14d46f45a665235fc153791e19574/uv-0.5.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:05b45c7eefb178dcdab0d49cd642fb7487377d00727102a8d6d306cc034c0d83", size = 14878796, upload-time = "2024-11-20T21:21:16.014Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/a6dc404d4d8884e26ad7bda004c101972fe7d81f86546a8628272812b897/uv-0.5.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed5659cde099f39995f4cb793fd939d2260b4a26e4e29412c91e7537f53d8d25", size = 14687838, upload-time = "2024-11-20T21:21:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/74/9e/c2ebf66b90d48def06cda29626bb38068418ed135ca903beb293825ef66d/uv-0.5.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f07e5e0df40a09154007da41b76932671333f9fecb0735c698b19da25aa08927", size = 18960541, upload-time = "2024-11-20T21:21:22.391Z" }, + { url = "https://files.pythonhosted.org/packages/3d/67/28a8b4c23920ae1b1b0103ebae2fa176bd5677c4353b5e814a51bd183285/uv-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30ce031e36c54d4ba791d743d992d0a4fd8d70480db781d30a2f6f5125f39194", size = 14471756, upload-time = "2024-11-20T21:21:25.445Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1c/9698818f4c5493dfd5ab0899a90eee789cac214de2f171220bcdfaefc93a/uv-0.5.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ca72e6a4c3c6b8b5605867e16a7f767f5c99b7f526de6bbb903c60eb44fd1e01", size = 13389089, upload-time = "2024-11-20T21:21:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/30/31a9985d84ffb63fb9212fa2b565497e0ceb581be055e5cc760afbe26b11/uv-0.5.4-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:69079e900bd26b0f65069ac6fa684c74662ed87121c076f2b1cbcf042539034c", size = 13612748, upload-time = "2024-11-20T21:21:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/26/8d/bae613187ba88d74f0268246ce140f23d399bab96d2cbc055d6e4adafd09/uv-0.5.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:8d7a4a3df943a7c16cd032ccbaab8ed21ff64f4cb090b3a0a15a8b7502ccd876", size = 13946421, upload-time = "2024-11-20T21:21:35.633Z" }, + { url = "https://files.pythonhosted.org/packages/0e/22/efd1eec81a566139bced68f4bd140c275edac3dac1bd6236cf8d756423db/uv-0.5.4-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:f511faf719b797ef0f14688f1abe20b3fd126209cf58512354d1813249745119", size = 15752913, upload-time = "2024-11-20T21:21:39.413Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/0cc4ae143b9605c25e75772aea22876b5875db79982ba62bb6f8d3099fab/uv-0.5.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f806af0ee451a81099c449c4cff0e813056fdf7dd264f3d3a8fd321b17ff9efc", size = 14599503, upload-time = "2024-11-20T21:21:43.966Z" }, + { url = "https://files.pythonhosted.org/packages/51/9a/33d40a5068fd37c4f7b4fa82396e3ee90a691cd256f364ff398612c1d5d4/uv-0.5.4-py3-none-win32.whl", hash = "sha256:a79a0885df364b897da44aae308e6ed9cca3a189d455cf1c205bd6f7b03daafa", size = 13749570, upload-time = "2024-11-20T21:21:46.99Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c8/827e4da65cbdab2c1619767a68ab99a31de078e511b71ca9f24777df33f9/uv-0.5.4-py3-none-win_amd64.whl", hash = "sha256:493aedc3c758bbaede83ecc8d5f7e6a9279ebec151c7f756aa9ea898c73f8ddb", size = 15573613, upload-time = "2024-11-20T21:21:50.328Z" }, +] + +[[package]] +name = "uv" +version = "0.6.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/46/32/ffa984c2ecbcf48d0ae813adf1aad79b3ecb5ffc743362088755d64ae3be/uv-0.6.10.tar.gz", hash = "sha256:cbbb03deb30af457cd93ad299ee5c3258ade3d900b4dee1af936c8a6d87d5bcb", size = 3109190, upload-time = "2025-03-26T01:08:35.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/5a/5ef9324c333478608eaca8c97a374a869b861a9a614c1e6695045e06d90c/uv-0.6.10-py3-none-linux_armv6l.whl", hash = "sha256:06932d36f1afaf611522a6a7ec361dac48dc67a1147d24e9eadee9703b15faaf", size = 15825875, upload-time = "2025-03-26T01:07:49.96Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2f/001f6bb4342ba50cf921bd4a338ea40e5228ea6a817bd3101fbabaf010dd/uv-0.6.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e5c2ba1922c47a245d7393465fcee942df5a8bd8b80489a7b8860ba9d60102f9", size = 15967139, upload-time = "2025-03-26T01:07:53.521Z" }, + { url = "https://files.pythonhosted.org/packages/36/6b/f66dcd28508bceed7cff48efb9dfe62a50a40a0685c41fb5e6ecd45f33cd/uv-0.6.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd8a4bcfd33a0dcae3fc0936bff8602f74e5719cf839e3df233059a0b8c8330d", size = 14796758, upload-time = "2025-03-26T01:07:56.269Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a2/13eb03e8691b098f9ee63c4d3fa3a054c48bfa05a0a52aec3df33ab52376/uv-0.6.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:4dd20c47898c15ebd4b5f48101062ea248e32513bfc61fc04bc822abfe39ce8a", size = 15252527, upload-time = "2025-03-26T01:07:58.728Z" }, + { url = "https://files.pythonhosted.org/packages/58/90/053bde333fbf9030dff1354797bd74ce3624235bcf59d7558397749a88c1/uv-0.6.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:950c9cd7b75f67e25760d2f43ad4b0ee3f8c6724fe0a9cf9eff948b3044b6a6d", size = 15560957, upload-time = "2025-03-26T01:08:01.188Z" }, + { url = "https://files.pythonhosted.org/packages/34/80/feb9ecc8ab8f9e1968d6783dd47e7ebd1dfcd0231c8b7b0efd7204625cec/uv-0.6.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acca1dca7be342b2b8e26e509aa07c3144cb009788140eee045da2aad6a0c6fe", size = 16249302, upload-time = "2025-03-26T01:08:03.734Z" }, + { url = "https://files.pythonhosted.org/packages/0f/14/9a2e40e25fba7b550cb57cce62a07ddf28350cb53e9e8bd2e70c0fbacdbb/uv-0.6.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:13ac09945976dc0df0edde7e4ba3a46107036a114117c8ff84916e55216c2e32", size = 17196146, upload-time = "2025-03-26T01:08:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/5e/05/5c9cd846243aca204f96c2da13da0fb38b6143eb3827dedea0e1dc1bcf1c/uv-0.6.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:145e75b99d6b7bdce8e454a851cfcd5605ff0491d568244c66fa75ca6b071bd6", size = 16944298, upload-time = "2025-03-26T01:08:08.827Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/63233a3143535a6df34ee6dc8246ef09ee79d99b902a6cc1ee179c1898f9/uv-0.6.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666d9fe312c810bba77633dbd463dc85f5a6a0d07905726a014dc53d07c774d9", size = 21226376, upload-time = "2025-03-26T01:08:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/0a/d3/7e881e2a391203a7567cf03c72213701e63923591d2072c4e7fe694c919f/uv-0.6.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b98e8884093cbfb1a1cc3f855aa22f97ec8da1a87e0e761800e165d4f9224a45", size = 16621313, upload-time = "2025-03-26T01:08:14.435Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a9/124aa76690a04cf30344386358b772cdede17d84660ae1dce8643bf64939/uv-0.6.10-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e8a8a75cf34c0814c1eabdbe651741d44fb125a6dcbe159b2da02871bbfdec7e", size = 15461518, upload-time = "2025-03-26T01:08:17.002Z" }, + { url = "https://files.pythonhosted.org/packages/ef/97/19813f2ec2faac77da5548c35d6ae039d044b973ecbb0732e3f07662fd36/uv-0.6.10-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:5260f52386e217615553f2f42740ce2f64ba439ff0fd502dc5b06250eb8ae613", size = 15524130, upload-time = "2025-03-26T01:08:19.392Z" }, + { url = "https://files.pythonhosted.org/packages/09/3f/5637bbf27ac145a09ea8eba8e0c926f7a3fe8fc4b3b1c91131c4558f4ec2/uv-0.6.10-py3-none-musllinux_1_1_i686.whl", hash = "sha256:603aebbaf6be938120c73fd36e9fd85f5e1b671d3d4638b3086f478e2bb423d9", size = 15901256, upload-time = "2025-03-26T01:08:22.141Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/2c688a897efad60d6e0587027968c1fdb0a63f70a8bef33d0b8154cc0fcd/uv-0.6.10-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d1f1bc7d94a4a7fdd75142be71b6bf2d7e01282f322721da185d711f065d7b80", size = 16746279, upload-time = "2025-03-26T01:08:24.916Z" }, + { url = "https://files.pythonhosted.org/packages/75/d4/df57d3f40c93c21fceed94156da41183daabd15422506e0fe73236c458a1/uv-0.6.10-py3-none-win32.whl", hash = "sha256:df6560256b93441c70ea2c062975bce2307a32de280f103cedb8db4a0f542348", size = 15953827, upload-time = "2025-03-26T01:08:27.395Z" }, + { url = "https://files.pythonhosted.org/packages/8a/21/a71c95c85624544c56695ae2469745bbda834e77dfc1e29d76711409eda5/uv-0.6.10-py3-none-win_amd64.whl", hash = "sha256:d795721fdd32e0471c952b7cb02a030657b6e67625fe836f4df14a3ae4aa4921", size = 17425178, upload-time = "2025-03-26T01:08:29.898Z" }, + { url = "https://files.pythonhosted.org/packages/ce/07/e6ffe467e1e365f7dd7863c4d505b1941af8cf69c494d0dbda08ba907043/uv-0.6.10-py3-none-win_arm64.whl", hash = "sha256:5188dc7041f4166bf64182d76c32c873f750259b6e4621a1400c26ebeea8c8dd", size = 16169219, upload-time = "2025-03-26T01:08:32.812Z" }, +] + +[[package]] +name = "uv" +version = "0.9.17" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/52/1a/cb0c37ae8513b253bcbc13d42392feb7d95ea696eb398b37535a28df9040/uv-0.9.17.tar.gz", hash = "sha256:6d93ab9012673e82039cfa7f9f66f69b388bc3f910f9e8a2ebee211353f620aa", size = 3815957, upload-time = "2025-12-09T23:01:21.756Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/e2/b6e2d473bdc37f4d86307151b53c0776e9925de7376ce297e92eab2e8894/uv-0.9.17-py3-none-linux_armv6l.whl", hash = "sha256:c708e6560ae5bc3cda1ba93f0094148ce773b6764240ced433acf88879e57a67", size = 21254511, upload-time = "2025-12-09T23:00:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/d5/40/75f1529a8bf33cc5c885048e64a014c3096db5ac7826c71e20f2b731b588/uv-0.9.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:233b3d90f104c59d602abf434898057876b87f64df67a37129877d6dab6e5e10", size = 20384366, upload-time = "2025-12-09T23:01:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/de/30/b3a343893681a569cbb74f8747a1c24e5f18ca9e07de0430aceaf9389ef4/uv-0.9.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4b8e5513d48a267bfa180ca7fefaf6f27b1267e191573b3dba059981143e88ef", size = 18924624, upload-time = "2025-12-09T23:01:10.291Z" }, + { url = "https://files.pythonhosted.org/packages/21/56/9daf8bbe4a9a36eb0b9257cf5e1e20f9433d0ce996778ccf1929cbe071a4/uv-0.9.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:8f283488bbcf19754910cc1ae7349c567918d6367c596e5a75d4751e0080eee0", size = 20671687, upload-time = "2025-12-09T23:00:51.927Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c8/4050ff7dc692770092042fcef57223b8852662544f5981a7f6cac8fc488d/uv-0.9.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9cf8052ba669dc17bdba75dae655094d820f4044990ea95c01ec9688c182f1da", size = 20861866, upload-time = "2025-12-09T23:01:12.555Z" }, + { url = "https://files.pythonhosted.org/packages/84/d4/208e62b7db7a65cb3390a11604c59937e387d07ed9f8b63b54edb55e2292/uv-0.9.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06749461b11175a884be193120044e7f632a55e2624d9203398808907d346aad", size = 21858420, upload-time = "2025-12-09T23:01:00.009Z" }, + { url = "https://files.pythonhosted.org/packages/86/2c/91288cd5a04db37dfc1e0dad26ead84787db5832d9836b4cc8e0fa7f3c53/uv-0.9.17-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:35eb1a519688209160e48e1bb8032d36d285948a13b4dd21afe7ec36dc2a9787", size = 23471658, upload-time = "2025-12-09T23:00:49.503Z" }, + { url = "https://files.pythonhosted.org/packages/44/ba/493eba650ffad1df9e04fd8eabfc2d0aebc23e8f378acaaee9d95ca43518/uv-0.9.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2bfb60a533e82690ab17dfe619ff7f294d053415645800d38d13062170230714", size = 23062950, upload-time = "2025-12-09T23:00:39.055Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9e/f7f679503c06843ba59451e3193f35fb7c782ff0afc697020d4718a7de46/uv-0.9.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd0f3e380ff148aff3d769e95a9743cb29c7f040d7ef2896cafe8063279a6bc1", size = 22080299, upload-time = "2025-12-09T23:00:44.026Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/76ba33c7d9efe9f17480db1b94d3393025062005e346bb8b3660554526da/uv-0.9.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2c3d25fbd8f91b30d0fac69a13b8e2c2cd8e606d7e6e924c1423e4ff84e616", size = 22087554, upload-time = "2025-12-09T23:00:41.715Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/ef4aae4a6c49076db2acd2a7b0278ddf3dbf785d5172b3165018b96ba2fb/uv-0.9.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:330e7085857e4205c5196a417aca81cfbfa936a97dd2a0871f6560a88424ebf2", size = 20823225, upload-time = "2025-12-09T23:00:57.041Z" }, + { url = "https://files.pythonhosted.org/packages/11/73/e0f816cacd802a1cb25e71de9d60e57fa1f6c659eb5599cef708668618cc/uv-0.9.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:45880faa9f6cf91e3cda4e5f947da6a1004238fdc0ed4ebc18783a12ce197312", size = 22004893, upload-time = "2025-12-09T23:01:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/15/6b/700f6256ee191136eb06e40d16970a4fc687efdccf5e67c553a258063019/uv-0.9.17-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:8e775a1b94c6f248e22f0ce2f86ed37c24e10ae31fb98b7e1b9f9a3189d25991", size = 20853850, upload-time = "2025-12-09T23:01:02.694Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6a/13f02e2ed6510223c40f74804586b09e5151d9319f93aab1e49d91db13bb/uv-0.9.17-py3-none-musllinux_1_1_i686.whl", hash = "sha256:8650c894401ec96488a6fd84a5b4675e09be102f5525c902a12ba1c8ef8ff230", size = 21322623, upload-time = "2025-12-09T23:00:46.806Z" }, + { url = "https://files.pythonhosted.org/packages/d0/18/2d19780cebfbec877ea645463410c17859f8070f79c1a34568b153d78e1d/uv-0.9.17-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:673066b72d8b6c86be0dae6d5f73926bcee8e4810f1690d7b8ce5429d919cde3", size = 22290123, upload-time = "2025-12-09T23:00:54.394Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/ab79bde3f7b6d2ac89f839ea40411a9cf3e67abede2278806305b6ba797e/uv-0.9.17-py3-none-win32.whl", hash = "sha256:7407d45afeae12399de048f7c8c2256546899c94bd7892dbddfae6766616f5a3", size = 20070709, upload-time = "2025-12-09T23:01:05.105Z" }, + { url = "https://files.pythonhosted.org/packages/08/a0/ab5b1850197bf407d095361b214352e40805441791fed35b891621cb1562/uv-0.9.17-py3-none-win_amd64.whl", hash = "sha256:22fcc26755abebdf366becc529b2872a831ce8bb14b36b6a80d443a1d7f84d3b", size = 22122852, upload-time = "2025-12-09T23:01:07.783Z" }, + { url = "https://files.pythonhosted.org/packages/37/ef/813cfedda3c8e49d8b59a41c14fcc652174facfd7a1caf9fee162b40ccbd/uv-0.9.17-py3-none-win_arm64.whl", hash = "sha256:6761076b27a763d0ede2f5e72455d2a46968ff334badf8312bb35988c5254831", size = 20435751, upload-time = "2025-12-09T23:01:19.732Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.36.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, +] + +[[package]] +name = "voluptuous" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/91/af/a54ce0fb6f1d867e0b9f0efe5f082a691f51ccf705188fca67a3ecefd7f4/voluptuous-0.15.2.tar.gz", hash = "sha256:6ffcab32c4d3230b4d2af3a577c87e1908a714a11f6f95570456b1849b0279aa", size = 51651, upload-time = "2024-07-02T19:10:00.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/a8/8f9cc6749331186e6a513bfe3745454f81d25f6e34c6024f88f80c71ed28/voluptuous-0.15.2-py3-none-any.whl", hash = "sha256:016348bc7788a9af9520b1764ebd4de0df41fe2138ebe9e06fa036bf86a65566", size = 31349, upload-time = "2024-07-02T19:09:58.125Z" }, +] + +[[package]] +name = "voluptuous" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/92/f4/0738e6849858deae22218be3bbb8207ba83a96e9d0ec7e8e8cd67b30e5ca/voluptuous-0.16.0.tar.gz", hash = "sha256:006535e22fed944aec17bef6e8725472476194743c87bd233e912eb463f8ff05", size = 54238, upload-time = "2025-12-18T23:18:46.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/00/0e0da784245c93cf346150ab67634177bf277f93b7a162bb56c928c39c04/voluptuous-0.16.0-py3-none-any.whl", hash = "sha256:ee342095263e1b5afbd4d418cb5adc92810eebfd07696bb033a261210df33db4", size = 31931, upload-time = "2025-12-18T23:18:44.694Z" }, +] + +[[package]] +name = "voluptuous-openapi" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.13'", +] +dependencies = [ + { name = "voluptuous", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/3a/0dda3268377c890561bdce1c445bd36a9c79fa77be7f456d9db45b664a7f/voluptuous_openapi-0.0.5.tar.gz", hash = "sha256:1619cd298da0024fa01338ac5a9ce3b3b7059205ce3c69230c24803b11308fb0", size = 10621, upload-time = "2024-07-30T06:18:46.459Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4e/b20c825f6cc78ed857f44afe32b7408c1d1c2c08cab4bcb5bb9b5e8fcb00/voluptuous_openapi-0.0.5-py3-none-any.whl", hash = "sha256:d51509503b3080b54a746ef357534f124ef7ae4f0ccecd3c3f261660b193c19a", size = 8636, upload-time = "2024-07-30T06:18:45.134Z" }, +] + +[[package]] +name = "voluptuous-openapi" +version = "0.0.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "voluptuous", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/6f/1075651d387c1570e4603080bdf0aa15aa254c21efb2688fdb18544cf4b9/voluptuous_openapi-0.0.6.tar.gz", hash = "sha256:4078c2acef23e04ceeab1ba58252590fcdc3ba6e3ed34521e8595374ab4de884", size = 13190, upload-time = "2025-01-07T07:19:07.266Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5c/331c21122901d4f5f4f6869683ab9859a08498074cee6075ff3eac3027a6/voluptuous_openapi-0.0.6-py3-none-any.whl", hash = "sha256:3561bbe5f46483f4cd9f631a0bd4a3ac3d7d74bab24f41bcd09b52501f712d5e", size = 9249, upload-time = "2025-01-07T07:19:05.948Z" }, +] + +[[package]] +name = "voluptuous-openapi" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "voluptuous", version = "0.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/5f/e77f088f4eff2e0f572f651bb9c2259561ec32d0bff20f59dd107e392ad8/voluptuous_openapi-0.3.0.tar.gz", hash = "sha256:1ecd2386e63b59b791e0f64715e317c7018a2e5598333138bd1f28adfd348d5c", size = 17467, upload-time = "2025-12-31T00:40:24.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/2d/3a8728e638bac8fd5857bb7775e39a9a520ae90b1d6dffd8badfa9852ccc/voluptuous_openapi-0.3.0-py3-none-any.whl", hash = "sha256:db35598e9fc711cbaf7d7732de160de3edf89cc7411b6febe5a132f1f54ea931", size = 10896, upload-time = "2025-12-31T00:40:23.067Z" }, +] + +[[package]] +name = "voluptuous-serialize" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +dependencies = [ + { name = "voluptuous", version = "0.15.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/09/c26b38ab35d9f61e9bf5c3e805215db1316dd73c77569b47ab36a40d19b1/voluptuous-serialize-2.6.0.tar.gz", hash = "sha256:79acdc58239582a393144402d827fa8efd6df0f5350cdc606d9242f6f9bca7c4", size = 7562, upload-time = "2023-02-15T21:09:08.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/86/355e1c65934760e2fb037219f1f360562567cf6731d281440c1d57d36856/voluptuous_serialize-2.6.0-py3-none-any.whl", hash = "sha256:85a5c8d4d829cb49186c1b5396a8a517413cc5938e1bb0e374350190cd139616", size = 6819, upload-time = "2023-02-15T21:09:06.512Z" }, +] + +[[package]] +name = "voluptuous-serialize" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "voluptuous", version = "0.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/70/03a9b61324e1bb8b16682455b8b953bccd1001a28e43478c86f539e26285/voluptuous_serialize-2.7.0.tar.gz", hash = "sha256:d0da959f2fd93c8f1eb779c5d116231940493b51020c2c1026bab76eb56cd09e", size = 9202, upload-time = "2025-08-17T10:43:04.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/41/d536d9cf39821c35cc13aff403728e60e32b2fd711c240b6b9980af1c03f/voluptuous_serialize-2.7.0-py3-none-any.whl", hash = "sha256:ee3ebecace6136f38d0bf8c20ee97155db2486c6b2d0795563fafd04a519e76f", size = 7850, upload-time = "2025-08-17T10:43:03.498Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[package]] +name = "webrtc-models" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mashumaro" }, + { name = "orjson", version = "3.10.12", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "orjson", version = "3.10.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, + { name = "orjson", version = "3.11.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/e8/050ffe3b71ff44d3885eee2bed763ca937e2a30bc950d866f22ba657776b/webrtc_models-0.3.0.tar.gz", hash = "sha256:559c743e5cc3bcc8133be1b6fb5e8492a9ddb17151129c21cbb2e3f2a1166526", size = 9411, upload-time = "2024-11-18T17:43:45.682Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/e7/62f29980c9e8d75af93b642a0c37aa8e201fd5268ba3a7179c172549bac3/webrtc_models-0.3.0-py3-none-any.whl", hash = "sha256:8fddded3ffd7ca837de878033501927580799a2c1b7829f7ae8a0f43b49004ea", size = 7476, upload-time = "2024-11-18T17:43:44.165Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "winrt-runtime" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/dd/acdd527c1d890c8f852cc2af644aa6c160974e66631289420aa871b05e65/winrt_runtime-3.2.1.tar.gz", hash = "sha256:c8dca19e12b234ae6c3dadf1a4d0761b51e708457492c13beb666556958801ea", size = 21721, upload-time = "2025-06-06T14:40:27.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/54/3dd06f2341fab6abb06588a16b30e0b213b0125be7b79dafc3bdba3b334a/winrt_runtime-3.2.1-cp312-cp312-win32.whl", hash = "sha256:762b3d972a2f7037f7db3acbaf379dd6d8f6cda505f71f66c6b425d1a1eae2f1", size = 210090, upload-time = "2025-06-06T06:44:08.151Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a1/1d7248d5c62ccbea5f3e0da64ca4529ce99c639c3be2485b6ed709f5c740/winrt_runtime-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:06510db215d4f0dc45c00fbb1251c6544e91742a0ad928011db33b30677e1576", size = 241391, upload-time = "2025-06-06T06:44:09.442Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ae/6a205d8dafc79f7c242be7f940b1e0c1971fd64ab3079bda4b514aa3d714/winrt_runtime-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:14562c29a087ccad38e379e585fef333e5c94166c807bdde67b508a6261aa195", size = 415242, upload-time = "2025-06-06T06:44:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/79/d4/1a555d8bdcb8b920f8e896232c82901cc0cda6d3e4f92842199ae7dff70a/winrt_runtime-3.2.1-cp313-cp313-win32.whl", hash = "sha256:44e2733bc709b76c554aee6c7fe079443b8306b2e661e82eecfebe8b9d71e4d1", size = 210022, upload-time = "2025-06-06T06:44:11.767Z" }, + { url = "https://files.pythonhosted.org/packages/aa/24/2b6e536ca7745d788dfd17a2ec376fa03a8c7116dc638bb39b035635484f/winrt_runtime-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:3c1fdcaeedeb2920dc3b9039db64089a6093cad2be56a3e64acc938849245a6d", size = 241349, upload-time = "2025-06-06T06:44:12.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7f/6d72973279e2929b2a71ed94198ad4a5d63ee2936e91a11860bf7b431410/winrt_runtime-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:28f3dab083412625ff4d2b46e81246932e6bebddf67bea7f05e01712f54e6159", size = 415126, upload-time = "2025-06-06T06:44:13.702Z" }, + { url = "https://files.pythonhosted.org/packages/c8/87/88bd98419a9da77a68e030593fee41702925a7ad8a8aec366945258cbb31/winrt_runtime-3.2.1-cp314-cp314-win32.whl", hash = "sha256:9b6298375468ac2f6815d0c008a059fc16508c8f587e824c7936ed9216480dad", size = 210257, upload-time = "2025-09-20T07:06:41.054Z" }, + { url = "https://files.pythonhosted.org/packages/87/85/e5c2a10d287edd9d3ee8dc24bf7d7f335636b92bf47119768b7dd2fd1669/winrt_runtime-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:e36e587ab5fd681ee472cd9a5995743f75107a1a84d749c64f7e490bc86bc814", size = 241873, upload-time = "2025-09-20T07:06:42.059Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/eb9e78397132175f70dd51dfa4f93e489c17d6b313ae9dce60369b8d84a7/winrt_runtime-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:35d6241a2ebd5598e4788e69768b8890ee1eee401a819865767a1fbdd3e9a650", size = 416222, upload-time = "2025-09-20T07:06:43.376Z" }, +] + +[[package]] +name = "winrt-windows-devices-bluetooth" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/a0/1c8a0c469abba7112265c6cb52f0090d08a67c103639aee71fc690e614b8/winrt_windows_devices_bluetooth-3.2.1.tar.gz", hash = "sha256:db496d2d92742006d5a052468fc355bf7bb49e795341d695c374746113d74505", size = 23732, upload-time = "2025-06-06T14:41:20.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/ff/c4a3de909a875b46fad5e9f4fd412bba48571405bfa802b878954abf128c/winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win32.whl", hash = "sha256:18c833ec49e7076127463679e85efc59f61785ade0dc185c852586b21be1f31c", size = 105752, upload-time = "2025-06-06T07:00:10.684Z" }, + { url = "https://files.pythonhosted.org/packages/e7/78/bfee1f0c8d188c561c5b946ab21f6a0037e60dea110e80b1d6a1d529639f/winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:9b6702c462b216c91e32388023a74d0f87210cef6fd5d93b7191e9427ce2faca", size = 113356, upload-time = "2025-06-06T07:00:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1b/d9da9c29d36cabadef4e19c3e9ba6d2692f6a28224c81fcff757132ea0da/winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:419fd1078c7749119f6b4bbf6be4e586e03a0ed544c03b83178f1d85f1b3d148", size = 104724, upload-time = "2025-06-06T07:00:12.406Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/797516c5c0f8d7f5b680862e0ed7c1087c58aec0bcf57a417fa90f7eb983/winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win32.whl", hash = "sha256:12b0a16fb36ce0b42243ca81f22a6b53fbb344ed7ea07a6eeec294604f0505e4", size = 105757, upload-time = "2025-06-06T07:00:13.269Z" }, + { url = "https://files.pythonhosted.org/packages/05/6d/f60588846a065e69a2ec5e67c5f85eb45cb7edef2ee8974cd52fa8504de6/winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6703dfbe444ee22426738830fb305c96a728ea9ccce905acfdf811d81045fdb3", size = 113363, upload-time = "2025-06-06T07:00:14.135Z" }, + { url = "https://files.pythonhosted.org/packages/2c/13/2d3c4762018b26a9f66879676ea15d7551cdbf339c8e8e0c56ea05ea31ef/winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2cf8a0bfc9103e32dc7237af15f84be06c791f37711984abdca761f6318bbdb2", size = 104722, upload-time = "2025-06-06T07:00:14.999Z" }, + { url = "https://files.pythonhosted.org/packages/b7/95/91cfdf941a1ba791708ab3477fc4e46793c8fe9117fc3e0a8c5ac5d7a09c/winrt_windows_devices_bluetooth-3.2.1-cp314-cp314-win32.whl", hash = "sha256:de36ded53ca3ba12fc6dd4deb14b779acc391447726543815df4800348aad63a", size = 109015, upload-time = "2025-09-20T07:09:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/7460655628d0f340a93524f5236bb9f8514eb0e1d334b38cba8a89f6c1a6/winrt_windows_devices_bluetooth-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3295d932cc93259d5ccb23a41e3a3af4c78ce5d6a6223b2b7638985f604fa34c", size = 115931, upload-time = "2025-09-20T07:09:51.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/70/e1248dea2ab881eb76b61ff1ad6cb9c07ac005faf99349e4af0b29bc3f1b/winrt_windows_devices_bluetooth-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:1f61c178766a1bbce0669f44790c6161ff4669404c477b4aedaa576348f9e102", size = 109561, upload-time = "2025-09-20T07:09:52.733Z" }, +] + +[[package]] +name = "winrt-windows-devices-bluetooth-advertisement" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/fc/7ffe66ca4109b9e994b27c00f3d2d506e6e549e268791f755287ad9106d8/winrt_windows_devices_bluetooth_advertisement-3.2.1.tar.gz", hash = "sha256:0223852a7b7fa5c8dea3c6a93473bd783df4439b1ed938d9871f947933e574cc", size = 16906, upload-time = "2025-06-06T14:41:21.448Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/15/ad05c28e049208c97011728e2debdb45439175f75efe357b6faa4c9ba099/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win32.whl", hash = "sha256:901933cc40de5eb7e5f4188897c899dd0b0f577cb2c13eab1a63c7dfe89b08c4", size = 90033, upload-time = "2025-06-06T07:00:23.421Z" }, + { url = "https://files.pythonhosted.org/packages/26/48/074779081841f6eba4987930c4e7adcec38a5985b7dffd9fecc41f39a89c/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:e6c66e7d4f4ca86d2c801d30efd2b9673247b59a2b4c365d9e11650303d68d89", size = 95824, upload-time = "2025-06-06T07:00:24.238Z" }, + { url = "https://files.pythonhosted.org/packages/aa/25/e01966033a02b2d0718710bb47ef4f6b9b5a619ca2c857e06eb5c8e3ed13/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:447d19defd8982d39944642eb7ebe89e4e20259ec9734116cf88879fb2c514ff", size = 89311, upload-time = "2025-06-06T07:00:25.029Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/8fc8e57605ea08dd0723c035ed0c2d0435dace2bc80a66d33aecfea49a56/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win32.whl", hash = "sha256:4122348ea525a914e85615647a0b54ae8b2f42f92cdbf89c5a12eea53ef6ed90", size = 90037, upload-time = "2025-06-06T07:00:25.818Z" }, + { url = "https://files.pythonhosted.org/packages/86/83/503cf815d84c5ba8c8bc61480f32e55579ebf76630163405f7df39aa297b/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:b66410c04b8dae634a7e4b615c3b7f8adda9c7d4d6902bcad5b253da1a684943", size = 95822, upload-time = "2025-06-06T07:00:26.666Z" }, + { url = "https://files.pythonhosted.org/packages/32/13/052be8b6642e6f509b30c194312b37bfee8b6b60ac3bd5ca2968c3ea5b80/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:07af19b1d252ddb9dd3eb2965118bc2b7cabff4dda6e499341b765e5038ca61d", size = 89326, upload-time = "2025-06-06T07:00:27.477Z" }, + { url = "https://files.pythonhosted.org/packages/27/3d/421d04a20037370baf13de929bc1dc5438b306a76fe17275ec5d893aae6c/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp314-cp314-win32.whl", hash = "sha256:2985565c265b3f9eab625361b0e40e88c94b03d89f5171f36146f2e88b3ee214", size = 92264, upload-time = "2025-09-20T07:09:53.563Z" }, + { url = "https://files.pythonhosted.org/packages/07/c7/43601ab82fe42bcff430b8466d84d92b31be06cc45c7fd64e9aac40f7851/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:d102f3fac64fde32332e370969dfbc6f37b405d8cc055d9da30d14d07449a3c2", size = 97517, upload-time = "2025-09-20T07:09:54.411Z" }, + { url = "https://files.pythonhosted.org/packages/91/17/e3303f6a25a2d98e424b06580fc85bbfd068f383424c67fa47cb1b357a46/winrt_windows_devices_bluetooth_advertisement-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:ffeb5e946cd42c32c6999a62e240d6730c653cdfb7b49c7839afba375e20a62a", size = 94122, upload-time = "2025-09-20T07:09:55.187Z" }, +] + +[[package]] +name = "winrt-windows-devices-bluetooth-genericattributeprofile" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/21/aeeddc0eccdfbd25e543360b5cc093233e2eab3cdfb53ad3cabae1b5d04d/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1.tar.gz", hash = "sha256:cdf6ddc375e9150d040aca67f5a17c41ceaf13a63f3668f96608bc1d045dde71", size = 38896, upload-time = "2025-06-06T14:41:22.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/a1/75ac783a5faee9b455fef2f53b7fef97b21ed60d52401b44c690202141e4/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win32.whl", hash = "sha256:ef894d21e0a805f3e114940254636a8045335fa9de766c7022af5d127dfad557", size = 183326, upload-time = "2025-06-06T07:00:52.662Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d9/a9dcc15322d2f5c7dfd491bd7ab121e36437caf78ebfa92bc0dd0546e2ca/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:db05de95cd1b24a51abb69cb936a8b17e9214e015757d0b37e3a5e207ddceb3d", size = 187810, upload-time = "2025-06-06T07:00:53.594Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fc/47d00af076f558267097af3050910beda6bf8a21ceaa5830bbd26fcaf85e/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d4e131cf3d15fc5ad81c1bcde3509ac171298217381abed6bdf687f29871984", size = 184516, upload-time = "2025-06-06T07:00:55.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/93/30b45ce473d1a604908221a1fa035fe8d5e4bb9008e820ae671a21dab94c/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win32.whl", hash = "sha256:b1879c8dcf46bd2110b9ad4b0b185f4e2a5f95170d014539203a5fee2b2115f0", size = 183342, upload-time = "2025-06-06T07:00:56.16Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3b/eb9d99b82a36002d7885206d00ea34f4a23db69c16c94816434ded728fa3/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d8d89f01e9b6931fb48217847caac3227a0aeb38a5b7782af71c2e7b262ec30", size = 187844, upload-time = "2025-06-06T07:00:57.134Z" }, + { url = "https://files.pythonhosted.org/packages/84/9b/ebbbe9be9a3e640dcfc5f166eb48f2f9d8ce42553f83aa9f4c5dcd9eb5f5/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:4e71207bb89798016b1795bb15daf78afe45529f2939b3b9e78894cfe650b383", size = 184540, upload-time = "2025-06-06T07:00:58.081Z" }, + { url = "https://files.pythonhosted.org/packages/b7/32/cb447ca7730a1e05730272309b074da6a04af29a8c0f5121014db8a2fc02/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp314-cp314-win32.whl", hash = "sha256:d5f83739ca370f0baf52b0400aebd6240ab80150081fbfba60fd6e7b2e7b4c5f", size = 185249, upload-time = "2025-09-20T07:09:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/bb/fa/f465d5d44dda166bf7ec64b7a950f57eca61f165bfe18345e9a5ea542def/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:13786a5853a933de140d456cd818696e1121c7c296ae7b7af262fc5d2cffb851", size = 193739, upload-time = "2025-09-20T07:09:59.893Z" }, + { url = "https://files.pythonhosted.org/packages/78/08/51c53ac3c704cd92da5ed7e7b9b57159052f6e46744e4f7e447ed708aa22/winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:5140682da2860f6a55eb6faf9e980724dc457c2e4b4b35a10e1cebd8fc97d892", size = 194836, upload-time = "2025-09-20T07:10:00.87Z" }, +] + +[[package]] +name = "winrt-windows-devices-enumeration" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/dd/75835bfbd063dffa152109727dedbd80f6e92ea284855f7855d48cdf31c9/winrt_windows_devices_enumeration-3.2.1.tar.gz", hash = "sha256:df316899e39bfc0ffc1f3cb0f5ee54d04e1d167fbbcc1484d2d5121449a935cf", size = 23538, upload-time = "2025-06-06T14:41:26.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/3e/81642208ecd6c6c936f35a39a433c54e3f68e09d316546b8f953581ae334/winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win32.whl", hash = "sha256:1db22b0292b93b0688d11ad932ad1f3629d4f471310281a2fbfe187530c2c1f3", size = 130249, upload-time = "2025-06-06T07:02:02.237Z" }, + { url = "https://files.pythonhosted.org/packages/00/f4/a9ede5f3f0d86abfc7590726cf711133d97419b49ced372fca532e4f0696/winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:a73bc88d7f510af454f2b392985501c96f39b89fd987140708ccaec1588ceebc", size = 141512, upload-time = "2025-06-06T07:02:03.424Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/4fad07c03124bdc3acd64f80f3bd3cc4417ea641e07bb16a9503afd3e554/winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:2853d687803f0dd76ae1afe3648abc0453e09dff0e7eddbb84b792eddb0473ca", size = 135383, upload-time = "2025-06-06T07:02:04.312Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7d/ebd712ab8ccd599c593796fbcd606abe22b5a8e20db134aa87987d67ac0e/winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win32.whl", hash = "sha256:14a71cdcc84f624c209cbb846ed6bd9767a9a9437b2bf26b48ac9a91599da6e9", size = 130276, upload-time = "2025-06-06T07:02:05.178Z" }, + { url = "https://files.pythonhosted.org/packages/70/de/f30daaaa0e6f4edb6bd7ddb3e058bd453c9ad90c032a4545c4d4639338aa/winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6ca40d334734829e178ad46375275c4f7b5d6d2d4fc2e8879690452cbfb36015", size = 141536, upload-time = "2025-06-06T07:02:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/75/4b/9a6aafdc74a085c550641a325be463bf4b811f6f605766c9cd4f4b5c19d2/winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2d14d187f43e4409c7814b7d1693c03a270e77489b710d92fcbbaeca5de260d4", size = 135362, upload-time = "2025-06-06T07:02:06.997Z" }, + { url = "https://files.pythonhosted.org/packages/41/31/5785cd1ec54dc0f0e6f3e6a466d07a62b8014a6e2b782e80444ef87e83ab/winrt_windows_devices_enumeration-3.2.1-cp314-cp314-win32.whl", hash = "sha256:e087364273ed7c717cd0191fed4be9def6fdf229fe9b536a4b8d0228f7814106", size = 134252, upload-time = "2025-09-20T07:10:12.935Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f6/68d91068048410f49794c0b19c45759c63ca559607068cfe5affba2f211b/winrt_windows_devices_enumeration-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:0da1ddb8285d97a6775c36265d7157acf1bbcb88bcc9a7ce9a4549906c822472", size = 145509, upload-time = "2025-09-20T07:10:13.797Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a4/898951d5bfc474aa9c7d133fe30870f0f2184f4ba3027eafb779d30eb7bc/winrt_windows_devices_enumeration-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:09bf07e74e897e97a49a9275d0a647819254ddb74142806bbbcf4777ed240a22", size = 141334, upload-time = "2025-09-20T07:10:14.637Z" }, +] + +[[package]] +name = "winrt-windows-devices-radios" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/02/9704ea359ad8b0d6faa1011f98fb477e8fb6eac5201f39d19e73c2407e7b/winrt_windows_devices_radios-3.2.1.tar.gz", hash = "sha256:4dc9b9d1501846049eb79428d64ec698d6476c27a357999b78a8331072e18a0b", size = 5908, upload-time = "2025-06-06T14:41:44.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/e0/4731a3c412318b2c5e74a8803a32e2fb9afc2c98368c6b61a422eb359e7e/winrt_windows_devices_radios-3.2.1-cp312-cp312-win32.whl", hash = "sha256:c3e683ce682338a5a5ed465f735e223ba7a22f16d0bbea2d070962bc7657edbb", size = 38606, upload-time = "2025-06-06T07:08:01.477Z" }, + { url = "https://files.pythonhosted.org/packages/37/8e/91464854dfc9e0be9ce8dcbe2bd6a67c19b68ab91584fc5de0f4f13e78f8/winrt_windows_devices_radios-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:a116e552a3f38607b9be558fb2e7de9b4450d1f9080069944d74d80cdda1873e", size = 40172, upload-time = "2025-06-06T07:08:02.214Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0d/1bd62f606b6c4dfa936fccc4712be5506a40fc5d1b7177c3d3cbcaf30972/winrt_windows_devices_radios-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4c28822f9251c9d547324f596b5c2581f050254ded05e5b786c650a3502744c1", size = 36989, upload-time = "2025-06-06T07:08:03.295Z" }, + { url = "https://files.pythonhosted.org/packages/d1/94/c22a14fd424632f3f3c0b25672218db9e8f4ae9e1355e0b148f2fe6015b5/winrt_windows_devices_radios-3.2.1-cp313-cp313-win32.whl", hash = "sha256:ae4a0065927fcd2d10215223f8a46be6fb89bad71cb4edd25dae3d01c137b3a8", size = 38613, upload-time = "2025-06-06T07:08:04.077Z" }, + { url = "https://files.pythonhosted.org/packages/39/c1/24cec0cc228642554b48d436a7617d7162fb952919c55fc26e2d99c310bd/winrt_windows_devices_radios-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:bf1a975f46a2aa271ffea1340be0c7e64985050d07433e701343dddc22a72290", size = 40180, upload-time = "2025-06-06T07:08:04.849Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d3/776453af26e78c0d0c0e1bfa89f86fd81322872f31a3e5dafb344dd47bf2/winrt_windows_devices_radios-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:10b298ed154c5824cea2de174afce1694ed2aabfb58826de814074027ffef96f", size = 36989, upload-time = "2025-06-06T07:08:05.576Z" }, + { url = "https://files.pythonhosted.org/packages/76/79/4627afae6b389ddd1e5f1d691663c6b14d6c8f98959082aed1217cc57ef9/winrt_windows_devices_radios-3.2.1-cp314-cp314-win32.whl", hash = "sha256:21452e1cae50e44cd1d5e78159e1b9986ac3389b66458ad89caa196ce5eca2d6", size = 39521, upload-time = "2025-09-20T07:11:17.992Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7c/c6aea91908ee7279ed51d12157bc8aeecb8850af2441073c3c91b261ad31/winrt_windows_devices_radios-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:6a8413e586fe597c6849607885cca7e0549da33ae5699165d11f7911534c6eaf", size = 41121, upload-time = "2025-09-20T07:11:18.747Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/652f14e3c501452ad8e0723518d9bbd729219b47f4a4dbe2966c2f82dca8/winrt_windows_devices_radios-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:39129fd9d09103adb003575f59881c1a5a70a43310547850150b46c6f4020312", size = 38114, upload-time = "2025-09-20T07:11:19.599Z" }, +] + +[[package]] +name = "winrt-windows-foundation" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/55/098ce7ea0679efcc1298b269c48768f010b6c68f90c588f654ec874c8a74/winrt_windows_foundation-3.2.1.tar.gz", hash = "sha256:ad2f1fcaa6c34672df45527d7c533731fdf65b67c4638c2b4aca949f6eec0656", size = 30485, upload-time = "2025-06-06T14:41:53.344Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/f8/495e304ddedd5ff2f196efbde906265cb75ade4d79e2937837f72ef654a0/winrt_windows_foundation-3.2.1-cp312-cp312-win32.whl", hash = "sha256:867642ccf629611733db482c4288e17b7919f743a5873450efb6d69ae09fdc2b", size = 112169, upload-time = "2025-06-06T07:11:01.438Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5e/b5059e4ece095351c496c9499783130c302d25e353c18031d5231b1b3b3c/winrt_windows_foundation-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:45550c5b6c2125cde495c409633e6b1ea5aa1677724e3b95eb8140bfccbe30c9", size = 118668, upload-time = "2025-06-06T07:11:02.475Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/acbcb3ef07b1b67e2de4afab9176a5282cfd775afd073efe6828dfc65ace/winrt_windows_foundation-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:94f4661d71cb35ebc52be7af112f2eeabdfa02cb05e0243bf9d6bd2cafaa6f37", size = 109671, upload-time = "2025-06-06T07:11:03.538Z" }, + { url = "https://files.pythonhosted.org/packages/7b/71/5e87131e4aecc8546c76b9e190bfe4e1292d028bda3f9dd03b005d19c76c/winrt_windows_foundation-3.2.1-cp313-cp313-win32.whl", hash = "sha256:3998dc58ed50ecbdbabace1cdef3a12920b725e32a5806d648ad3f4829d5ba46", size = 112184, upload-time = "2025-06-06T07:11:04.459Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7f/8d5108461351d4f6017f550af8874e90c14007f9122fa2eab9f9e0e9b4e1/winrt_windows_foundation-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6e98617c1e46665c7a56ce3f5d28e252798416d1ebfee3201267a644a4e3c479", size = 118672, upload-time = "2025-06-06T07:11:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/44/f5/2edf70922a3d03500dab17121b90d368979bd30016f6dbca0d043f0c71f1/winrt_windows_foundation-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2a8c1204db5c352f6a563130a5a41d25b887aff7897bb677d4ff0b660315aad4", size = 109673, upload-time = "2025-06-06T07:11:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/e3/0a/d77346e39fe0c81f718cde49f83fe77c368c0e14c6418f72dfa1e7ef22d0/winrt_windows_foundation-3.2.1-cp314-cp314-win32.whl", hash = "sha256:35e973ab3c77c2a943e139302256c040e017fd6ff1a75911c102964603bba1da", size = 114590, upload-time = "2025-09-20T07:11:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/a1/56/4d2b545bea0f34f68df6d4d4ca22950ff8a935497811dccdc0ca58737a05/winrt_windows_foundation-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:a22a7ebcec0d262e60119cff728f32962a02df60471ded8b2735a655eccc0ef5", size = 122148, upload-time = "2025-09-20T07:11:50.826Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ed/b9d3a11cac73444c0a3703200161cd7267dab5ab85fd00e1f965526e74a8/winrt_windows_foundation-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:3be7fbae829b98a6a946db4fbaf356b11db1fbcbb5d4f37e7a73ac6b25de8b87", size = 114360, upload-time = "2025-09-20T07:11:51.626Z" }, +] + +[[package]] +name = "winrt-windows-foundation-collections" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/62/d21e3f1eeb8d47077887bbf0c3882c49277a84d8f98f7c12bda64d498a07/winrt_windows_foundation_collections-3.2.1.tar.gz", hash = "sha256:0eff1ad0d8d763ad17e9e7bbd0c26a62b27215016393c05b09b046d6503ae6d5", size = 16043, upload-time = "2025-06-06T14:41:53.983Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/0b/7802349391466d3f7e8f62f588f36a1a0b6560abfcdbdaa426fe21d322b4/winrt_windows_foundation_collections-3.2.1-cp312-cp312-win32.whl", hash = "sha256:15704eef3125788f846f269cf54a3d89656fa09a1dc8428b70871f717d595ad6", size = 60060, upload-time = "2025-06-06T07:11:16.173Z" }, + { url = "https://files.pythonhosted.org/packages/37/94/5b888713e472746635a382e523513ab1b8200af55c5b56bc70e1e4369115/winrt_windows_foundation_collections-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:550dfb8c82fe74d9e0728a2a16a9175cc9e34ca2b8ef758d69b2a398894b698b", size = 69058, upload-time = "2025-06-06T07:11:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3c/829273622c9b37c67b97f187b92be318404f7d33db045e31d72b7d50f54c/winrt_windows_foundation_collections-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:810ad4bd11ab4a74fdbcd3ed33b597ef7c0b03af73fc9d7986c22bcf3bd24f84", size = 58793, upload-time = "2025-06-06T07:11:17.837Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cd/99ef050d80bea2922fa1ded93e5c250732634095d8bd3595dd808083e5ca/winrt_windows_foundation_collections-3.2.1-cp313-cp313-win32.whl", hash = "sha256:4267a711b63476d36d39227883aeb3fb19ac92b88a9fc9973e66fbce1fd4aed9", size = 60063, upload-time = "2025-06-06T07:11:18.65Z" }, + { url = "https://files.pythonhosted.org/packages/94/93/4f75fd6a4c96f1e9bee198c5dc9a9b57e87a9c38117e1b5e423401886353/winrt_windows_foundation_collections-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:5e12a6e75036ee90484c33e204b85fb6785fcc9e7c8066ad65097301f48cdd10", size = 69057, upload-time = "2025-06-06T07:11:19.446Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/de47ccc390017ec5575e7e7fd9f659ee3747c52049cdb2969b1b538ce947/winrt_windows_foundation_collections-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:34b556255562f1b36d07fba933c2bcd9f0db167fa96727a6cbb4717b152ad7a2", size = 58792, upload-time = "2025-06-06T07:11:20.24Z" }, + { url = "https://files.pythonhosted.org/packages/e1/47/b3301d964422d4611c181348149a7c5956a2a76e6339de451a000d4ae8e7/winrt_windows_foundation_collections-3.2.1-cp314-cp314-win32.whl", hash = "sha256:33188ed2d63e844c8adfbb82d1d3d461d64aaf78d225ce9c5930421b413c45ab", size = 62211, upload-time = "2025-09-20T07:11:52.411Z" }, + { url = "https://files.pythonhosted.org/packages/20/59/5f2c940ff606297129e93ebd6030c813e6a43a786de7fc33ccb268e0b06b/winrt_windows_foundation_collections-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:d4cfece7e9c0ead2941e55a1da82f20d2b9c8003bb7a8853bb7f999b539f80a4", size = 70399, upload-time = "2025-09-20T07:11:53.254Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2d/2c8eb89062c71d4be73d618457ed68e7e2ba29a660ac26349d44fc121cbf/winrt_windows_foundation_collections-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:3884146fea13727510458f6a14040b7632d5d90127028b9bfd503c6c655d0c01", size = 61392, upload-time = "2025-09-20T07:11:53.993Z" }, +] + +[[package]] +name = "winrt-windows-storage-streams" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "winrt-runtime" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/50/f4488b07281566e3850fcae1021f0285c9653992f60a915e15567047db63/winrt_windows_storage_streams-3.2.1.tar.gz", hash = "sha256:476f522722751eb0b571bc7802d85a82a3cae8b1cce66061e6e758f525e7b80f", size = 34335, upload-time = "2025-06-06T14:43:23.905Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/e7/7d3f2a4a442f264e05cab2bdf20ed1b95cb3f753bd1b0f277f2b49fb8335/winrt_windows_storage_streams-3.2.1-cp312-cp312-win32.whl", hash = "sha256:77c1f0e004b84347b5bd705e8f0fc63be8cd29a6093be13f1d0869d0d97b7d78", size = 127787, upload-time = "2025-06-06T14:02:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2f/cc36f475f8af293f40e2c2a5d6c2e75a189c2c2d4d01ecb3551578518c79/winrt_windows_storage_streams-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4508ee135af53e4fc142876abbf4bc7c2a95edfc7d19f52b291a8499cacd6dc", size = 131849, upload-time = "2025-06-06T14:02:03.09Z" }, + { url = "https://files.pythonhosted.org/packages/94/84/896fb734f7456910ec412f3f3adfdc3f0dc3134864a496d5b120592f3bfd/winrt_windows_storage_streams-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:040cb94e6fb26b0d00a00e8b88b06fadf29dfe18cf24ed6cb3e69709c3613307", size = 128144, upload-time = "2025-06-06T14:02:03.946Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d2/24d9f59bdc05e741261d5bec3bcea9a848d57714126a263df840e2b515a8/winrt_windows_storage_streams-3.2.1-cp313-cp313-win32.whl", hash = "sha256:401bb44371720dc43bd1e78662615a2124372e7d5d9d65dfa8f77877bbcb8163", size = 127774, upload-time = "2025-06-06T14:02:04.752Z" }, + { url = "https://files.pythonhosted.org/packages/15/59/601724453b885265c7779d5f8025b043a68447cbc64ceb9149d674d5b724/winrt_windows_storage_streams-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:202c5875606398b8bfaa2a290831458bb55f2196a39c1d4e5fa88a03d65ef915", size = 131827, upload-time = "2025-06-06T14:02:05.601Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c2/a419675a6087c9ea496968c9b7805ef234afa585b7483e2269608a12b044/winrt_windows_storage_streams-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ca3c5ec0aab60895006bf61053a1aca6418bc7f9a27a34791ba3443b789d230d", size = 128180, upload-time = "2025-06-06T14:02:06.759Z" }, + { url = "https://files.pythonhosted.org/packages/55/70/2869ea2112c565caace73c9301afd1d7afcc49bdd37fac058f0178ba95d4/winrt_windows_storage_streams-3.2.1-cp314-cp314-win32.whl", hash = "sha256:5cd0dbad86fcc860366f6515fce97177b7eaa7069da261057be4813819ba37ee", size = 131701, upload-time = "2025-09-20T07:17:16.849Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/aae50b1d0e37b5a61055759aedd42c6c99d7c17ab8c3e568ab33c0288938/winrt_windows_storage_streams-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3c5bf41d725369b9986e6d64bad7079372b95c329897d684f955d7028c7f27a0", size = 135566, upload-time = "2025-09-20T07:17:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c3/6d3ce7a58e6c828e0795c9db8790d0593dd7fdf296e513c999150deb98d4/winrt_windows_storage_streams-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:293e09825559d0929bbe5de01e1e115f7a6283d8996ab55652e5af365f032987", size = 134393, upload-time = "2025-09-20T07:17:18.802Z" }, +] + +[[package]] +name = "yarl" +version = "1.18.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", + "python_full_version < '3.13'", +] +dependencies = [ + { name = "idna", marker = "python_full_version < '3.13.2'" }, + { name = "multidict", marker = "python_full_version < '3.13.2'" }, + { name = "propcache", version = "0.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "propcache", version = "0.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062, upload-time = "2024-12-01T20:35:23.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/85/bd2e2729752ff4c77338e0102914897512e92496375e079ce0150a6dc306/yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", size = 142644, upload-time = "2024-12-01T20:33:39.204Z" }, + { url = "https://files.pythonhosted.org/packages/ff/74/1178322cc0f10288d7eefa6e4a85d8d2e28187ccab13d5b844e8b5d7c88d/yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", size = 94962, upload-time = "2024-12-01T20:33:40.808Z" }, + { url = "https://files.pythonhosted.org/packages/be/75/79c6acc0261e2c2ae8a1c41cf12265e91628c8c58ae91f5ff59e29c0787f/yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", size = 92795, upload-time = "2024-12-01T20:33:42.322Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/927b2d67a412c31199e83fefdce6e645247b4fb164aa1ecb35a0f9eb2058/yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", size = 332368, upload-time = "2024-12-01T20:33:43.956Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/859fca07169d6eceeaa4fde1997c91d8abde4e9a7c018e371640c2da2b71/yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", size = 342314, upload-time = "2024-12-01T20:33:46.046Z" }, + { url = "https://files.pythonhosted.org/packages/08/75/76b63ccd91c9e03ab213ef27ae6add2e3400e77e5cdddf8ed2dbc36e3f21/yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", size = 341987, upload-time = "2024-12-01T20:33:48.352Z" }, + { url = "https://files.pythonhosted.org/packages/1a/e1/a097d5755d3ea8479a42856f51d97eeff7a3a7160593332d98f2709b3580/yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", size = 336914, upload-time = "2024-12-01T20:33:50.875Z" }, + { url = "https://files.pythonhosted.org/packages/0b/42/e1b4d0e396b7987feceebe565286c27bc085bf07d61a59508cdaf2d45e63/yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", size = 325765, upload-time = "2024-12-01T20:33:52.641Z" }, + { url = "https://files.pythonhosted.org/packages/7e/18/03a5834ccc9177f97ca1bbb245b93c13e58e8225276f01eedc4cc98ab820/yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", size = 344444, upload-time = "2024-12-01T20:33:54.395Z" }, + { url = "https://files.pythonhosted.org/packages/c8/03/a713633bdde0640b0472aa197b5b86e90fbc4c5bc05b727b714cd8a40e6d/yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", size = 340760, upload-time = "2024-12-01T20:33:56.286Z" }, + { url = "https://files.pythonhosted.org/packages/eb/99/f6567e3f3bbad8fd101886ea0276c68ecb86a2b58be0f64077396cd4b95e/yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", size = 346484, upload-time = "2024-12-01T20:33:58.375Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a9/84717c896b2fc6cb15bd4eecd64e34a2f0a9fd6669e69170c73a8b46795a/yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", size = 359864, upload-time = "2024-12-01T20:34:00.22Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2e/d0f5f1bef7ee93ed17e739ec8dbcb47794af891f7d165fa6014517b48169/yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", size = 364537, upload-time = "2024-12-01T20:34:03.54Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/568d07c5d4964da5b02621a517532adb8ec5ba181ad1687191fffeda0ab6/yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", size = 357861, upload-time = "2024-12-01T20:34:05.73Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e3/924c3f64b6b3077889df9a1ece1ed8947e7b61b0a933f2ec93041990a677/yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", size = 84097, upload-time = "2024-12-01T20:34:07.664Z" }, + { url = "https://files.pythonhosted.org/packages/34/45/0e055320daaabfc169b21ff6174567b2c910c45617b0d79c68d7ab349b02/yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", size = 90399, upload-time = "2024-12-01T20:34:09.61Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789, upload-time = "2024-12-01T20:34:11.414Z" }, + { url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144, upload-time = "2024-12-01T20:34:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974, upload-time = "2024-12-01T20:34:15.234Z" }, + { url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587, upload-time = "2024-12-01T20:34:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386, upload-time = "2024-12-01T20:34:19.842Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421, upload-time = "2024-12-01T20:34:21.975Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384, upload-time = "2024-12-01T20:34:24.717Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689, upload-time = "2024-12-01T20:34:26.886Z" }, + { url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453, upload-time = "2024-12-01T20:34:29.605Z" }, + { url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872, upload-time = "2024-12-01T20:34:31.454Z" }, + { url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497, upload-time = "2024-12-01T20:34:34.004Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981, upload-time = "2024-12-01T20:34:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229, upload-time = "2024-12-01T20:34:38.657Z" }, + { url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383, upload-time = "2024-12-01T20:34:40.501Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152, upload-time = "2024-12-01T20:34:42.814Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723, upload-time = "2024-12-01T20:34:44.699Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109, upload-time = "2024-12-01T20:35:20.834Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "idna", marker = "python_full_version >= '3.13.2'" }, + { name = "multidict", marker = "python_full_version >= '3.13.2'" }, + { name = "propcache", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + +[[package]] +name = "zensical" +version = "0.0.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "deepmerge" }, + { name = "markdown" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "pyyaml", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13.2'" }, + { name = "pyyaml", version = "6.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/ad/87b7b591551c74de67b08dcf172532fbb6df6f0e626dce9f220aae293052/zensical-0.0.15.tar.gz", hash = "sha256:b3200c91b30370671c50b8b4aa41c20e55ff2814b9003ee23c9b6f923a0c19be", size = 3816831, upload-time = "2025-12-24T11:15:49.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/07/ede00d39ff6cff7ab4971d15caa04a6710b126cbf0c8342add0337f4db89/zensical-0.0.15-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:13f205d24baeb4a77d096c3d385a8496850567b45c397cd54dde7c22dcbf0da8", size = 11934661, upload-time = "2025-12-24T11:15:09.019Z" }, + { url = "https://files.pythonhosted.org/packages/d6/32/24449c59f90a6a17dd0d9740ee44f245887d0c177c9c1dc452b9eb812024/zensical-0.0.15-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:99702504d38d8f7da4bf9a69513d2f65a0371dcb8f0e9f180c861cb285adb61f", size = 11820384, upload-time = "2025-12-24T11:15:12.265Z" }, + { url = "https://files.pythonhosted.org/packages/ac/db/64fed914788f2f5a8880dcc9a08ce25bcf1a7f2586a09e5d7d61003fd652/zensical-0.0.15-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:051ea368d268ffebf7ed7b6422f211a57680b2810fcde7f251816eb5c8d2f488", size = 12128961, upload-time = "2025-12-24T11:15:16.178Z" }, + { url = "https://files.pythonhosted.org/packages/59/ca/04fd676880acad9571736e470e1f7bd8e6a2391f09952e69800f1b77fc75/zensical-0.0.15-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31a333afaf54d042bb51ac7dc623bd1b3bbe173c0ce4c7b72764cdef143ff4e2", size = 12098586, upload-time = "2025-12-24T11:15:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/8b/02/6a6ecad3b4cb07e11c8d160882cc937f8e3e96868f598c371892f1e594d7/zensical-0.0.15-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:072f80b7346ad4657a4c23008ae5cd3b3511e3bfac8d6bb277cbb6b3c11b6387", size = 12418552, upload-time = "2025-12-24T11:15:23.096Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b7/2a7205dfbeeb8612fbb1e22ad9f770804a07e440b276dcf23cbce3592da4/zensical-0.0.15-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec1b146adb3ee4146a3f800e0a4e6c8e681092d87ac51c60e35e99b68a724b31", size = 12193618, upload-time = "2025-12-24T11:15:26.359Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f4/5f38022d09e668622e49e8b5606bbfb5177d42ab1b4e91706ef0d38e2422/zensical-0.0.15-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:07a24e11a0e00d14f37d209cc37df446f91908109ca86ab3fe41c0b981e32668", size = 12308805, upload-time = "2025-12-24T11:15:29.602Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/6dd7657d6e1c1cd4b01ad531cea6a95eb660a2f960b03415d2184720a283/zensical-0.0.15-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:cc2bc9b4f89863d8b3443b0b9446ccaf5181085c3fc7a7e1bbc4708afe864f7d", size = 12367060, upload-time = "2025-12-24T11:15:32.621Z" }, + { url = "https://files.pythonhosted.org/packages/d3/83/781bdfbf459ec84b085085b142f433541de70d642d665227442a4deb9360/zensical-0.0.15-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:a3809be6d5f25bbd69dbe43715a60bf9d0186c51a1ec821e02f106e660e632b4", size = 12493255, upload-time = "2025-12-24T11:15:36.126Z" }, + { url = "https://files.pythonhosted.org/packages/54/10/0440e848658b97467c15c356f9038906fa90ac52b7f969ed1cce5deaf017/zensical-0.0.15-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4fd9382db77eff79eb347d4d0b6194c1c285e4151ad104bc9c8f65a27cb71d7f", size = 12430315, upload-time = "2025-12-24T11:15:39.697Z" }, + { url = "https://files.pythonhosted.org/packages/19/d9/50f3e833075e678047cd6e302f731b4b992762d366949124c2eeed6bd96c/zensical-0.0.15-cp310-abi3-win32.whl", hash = "sha256:1d29712dd4659e26b351417534e2ad6364f506514e168ac4c0ed42093b3a9469", size = 11559368, upload-time = "2025-12-24T11:15:42.893Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/cf06fc620dd94b2ffaa3e9df47b174603e0234096fd857b25dcda6c4a538/zensical-0.0.15-cp310-abi3-win_amd64.whl", hash = "sha256:0d0b303ec8a7aec2e733239f21d3602ce29e0eaabe4e4d60c9bcccb5c2d162dc", size = 11740088, upload-time = "2025-12-24T11:15:45.631Z" }, +] + +[[package]] +name = "zeroconf" +version = "0.146.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and python_full_version < '3.13.2'", +] +dependencies = [ + { name = "ifaddr", marker = "python_full_version >= '3.13' and python_full_version < '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/ac/d4c67df0649c77f343e4976ef0a00e19a6e5c3342a6eaa6e64d7b853224f/zeroconf-0.146.0.tar.gz", hash = "sha256:a48010a1931acdba5b26e99326464788daeef96dcb7b9a44d1832352f76da49c", size = 161804, upload-time = "2025-03-05T01:47:18.095Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/db/4efc6ef84737933e4b88852d4f79f028aa593e2d10981538c3ff7fd71d99/zeroconf-0.146.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9fbd7f6bfd421d28561657463795a8a4487fa2adecb872b5f19f04edcb9c3b3", size = 1862910, upload-time = "2025-03-05T02:20:37.359Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/4c2d673f8cc3aa5f51568c9df08643a26c5c74c8cd2ee78e375c3dca01e2/zeroconf-0.146.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2972be542d6ae32ef715f899ad2bb3fdee877c42fd4a9bb148033b4ccaca6a08", size = 1716714, upload-time = "2025-03-05T02:20:38.82Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/68edc28d172799ef75d22aecad9478199c7e73223dcd1f15d316d084cca6/zeroconf-0.146.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:459542edd64b276296da5776cf86de5a7d1529f25038fc8a93a8a70b014f2671", size = 2154698, upload-time = "2025-03-05T02:20:40.249Z" }, + { url = "https://files.pythonhosted.org/packages/45/14/1918a965c33a86701d598cacee4e01a9aa5d5142998eb3f3b5b637c2de22/zeroconf-0.146.0-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:8517c68576a3fece27882ff5743e69620ffc5a973b3d8ed21a28210b52da95d5", size = 2329442, upload-time = "2025-03-05T02:20:41.702Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ee/30da2f51619dc04a5c8a0d392ae0b56965e7918560b7c6ecc76955196909/zeroconf-0.146.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a63b901281619bab0106c993c80ed1db534e38d1c57d5553806f3430467d493", size = 2270277, upload-time = "2025-03-05T02:20:43.103Z" }, + { url = "https://files.pythonhosted.org/packages/85/cb/ab152a4a52632f5919f2d223344df4c7fd1611ce661abeabe39956e28a91/zeroconf-0.146.0-cp312-cp312-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:85291bab82fe11df5c410152f410ab86ca3dae27327222530027ec7edf878dea", size = 2109845, upload-time = "2025-03-05T02:20:44.527Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b6/8487c734f6de0ea462ba77203c9630d2a06d38fa04340fa5d5fbeb406ea7/zeroconf-0.146.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:14443655df3b4fe7510eefe49f5b1385db57ac1fd7ab2aabb36e18cc614e0158", size = 2296624, upload-time = "2025-03-05T02:20:46.078Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/781833c5e0f7c9f0542c2795b9c56fa43d9755868da54ed9c2b2be7d1655/zeroconf-0.146.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7f794cd616ffb2c0588286bc33e3cb2df9f9bd277cd93b714050639b3d7564e5", size = 2158011, upload-time = "2025-03-05T02:20:47.581Z" }, + { url = "https://files.pythonhosted.org/packages/28/d7/52af0de3fa26e3df976cf0fdaf395d7c228c157734450a9612e20ddbd4be/zeroconf-0.146.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e3ab0015afb0189cb80f115ffd5985f9bfab29e73bb073d4a9cb0a21ff5c9dc9", size = 2502891, upload-time = "2025-03-05T02:20:49.227Z" }, + { url = "https://files.pythonhosted.org/packages/ee/c2/83774e9a1a4cd40b426fe2eefaba2ff0b130105114d1a2c1814fd7e64060/zeroconf-0.146.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6534b6acf6db2dd1888053dbece9ce330d951c3bd98fbe27f28d6bc29311b64f", size = 2466408, upload-time = "2025-03-05T02:20:51.055Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e1/89b49a92c63f1a63a511cbfd0557afd49a82cc9649b1f73ba814768eb7cf/zeroconf-0.146.0-cp312-cp312-win32.whl", hash = "sha256:3361cd796898b244e638439e155576a8551b85c15ae1332eaefcd1b8ec447393", size = 1431606, upload-time = "2025-03-05T02:20:52.574Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f7/2e18ca60637aee55d13cb846c275b7f8dc23bd8aee5090858e4b16d1c55f/zeroconf-0.146.0-cp312-cp312-win_amd64.whl", hash = "sha256:eac992020f1b0a10fb9facd18e9e65baf4c0dd5a0a9c56f95c8868f5befa61e5", size = 1661168, upload-time = "2025-03-05T02:20:54.086Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/25e258fbd064e7b8f1497b9e345f29d6e44dd250a0fc5afd91aa04aafa57/zeroconf-0.146.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:faec552163f3007247ef9713fc817627d89844a782da8c1479b11ea3d3370684", size = 1840888, upload-time = "2025-03-05T02:20:56.799Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1c/1fd373e225e7282c244003683740ca58bc39270cb79fa13b13435c6dc88a/zeroconf-0.146.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0fc7b22e33b9d5d81b5aee58af6447fc1cf9b11e04dc2a04e1a5d00b3ae4d23a", size = 1697122, upload-time = "2025-03-05T02:20:58.687Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/2a42f1f88f69b11db2524469e5dc6752dc819e4fe9b985e293be74b38dee/zeroconf-0.146.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6e24daeeb926f8c7d426c46082224786e26aa5b4ce9fb573133e52ff8bae81", size = 2143632, upload-time = "2025-03-05T02:21:00.927Z" }, + { url = "https://files.pythonhosted.org/packages/81/8c/6caf3a48575c2bcf7ba2739b2cda6f117a305c03e87b53d08f19c859fae3/zeroconf-0.146.0-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:9a798ea22c24a4148364f85b46ab33541072715bf8abccae2e3fd0c069f5808f", size = 2315076, upload-time = "2025-03-05T02:21:02.419Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b3/b86fa34f8d682b1bd3e144896b2c2cfb8a6c3308c1f773a7dcdb733d677b/zeroconf-0.146.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97a536e5b3d694e342bc908149342db2aae08a6f56466db63b6dffc26d2399ae", size = 2260655, upload-time = "2025-03-05T02:21:04.68Z" }, + { url = "https://files.pythonhosted.org/packages/06/2a/9b509a9d70c9f98b1b60f8d0002ac457df8f401325be6545ecb1f8071e8a/zeroconf-0.146.0-cp313-cp313-manylinux_2_31_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97a09c01424e2356c41b39f7c2fb7a743623c2d413a082e861030e28090aebb", size = 2097673, upload-time = "2025-03-05T02:21:06.287Z" }, + { url = "https://files.pythonhosted.org/packages/90/70/2fb1c0470fb4a230d9cd63e245b5d4bd349a19454d363499eb4cdee3b54a/zeroconf-0.146.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:55c2a0087847d5c8bc00cc1e85cb1d048e8b70b09b4e949a2b763f33389819bb", size = 2307311, upload-time = "2025-03-05T01:47:15.987Z" }, + { url = "https://files.pythonhosted.org/packages/64/c3/351b7c1d07c9bf43d75bbbf18f9d07f8081373e85865b5faa78b661ae882/zeroconf-0.146.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b87d01dc8d7d10b929cc63330cf2e0f726f105a57e8d86df5d946b93a0e6280f", size = 2297954, upload-time = "2025-03-05T02:21:08.129Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b17dda38b9c5b916b1491acb6f32044d198c89ee74074dec0a277f0d8f49/zeroconf-0.146.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:f844214eed7b66c1db3ea4ab2dddf0d84b91c340d83b2721656f70efb8588ae4", size = 2152669, upload-time = "2025-03-05T02:21:09.702Z" }, + { url = "https://files.pythonhosted.org/packages/75/6c/ef97dcd5abdcfb6c4ea8a52d2cb08982541c43a9ec64dff1335ecc45a901/zeroconf-0.146.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cf9e85463a4fdeed8c5ea3b13e4a6c6de924d90b8b0982021e7331632f80192e", size = 2495779, upload-time = "2025-03-05T02:21:11.629Z" }, + { url = "https://files.pythonhosted.org/packages/90/f5/755bd701c69da699b6f0bd939972cd0978af6a7399174048a337de610f87/zeroconf-0.146.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fc1dd03301d370c21a8c5fbbe0a6a54a068a08384fa673d596c3f2424153aeca", size = 2459359, upload-time = "2025-03-05T02:21:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/0c/24/12f936d8ec82e3ca14291e3c6f45f2a2b3d1139f5ae19670fd4624a66f86/zeroconf-0.146.0-cp313-cp313-win32.whl", hash = "sha256:b4e70e77a67b3f39e91b5c02df82ab49a54bfc4edb1aa5779e404a711938c5af", size = 1427510, upload-time = "2025-03-05T02:21:16.184Z" }, + { url = "https://files.pythonhosted.org/packages/49/bb/9ccf706c4f3dad7b72956d5123e2b228d0411a6f977f4db410ff6b8963c0/zeroconf-0.146.0-cp313-cp313-win_amd64.whl", hash = "sha256:5274ba298d2edd5d02bb3937181a1e82deef773075b04374eac149bd40fccd96", size = 1655847, upload-time = "2025-03-05T02:21:18.359Z" }, +] + +[[package]] +name = "zeroconf" +version = "0.148.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.13.2' and python_full_version < '3.14'", +] +dependencies = [ + { name = "ifaddr", marker = "python_full_version >= '3.13.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/46/10db987799629d01930176ae523f70879b63577060d63e05ebf9214aba4b/zeroconf-0.148.0.tar.gz", hash = "sha256:03fcca123df3652e23d945112d683d2f605f313637611b7d4adf31056f681702", size = 164447, upload-time = "2025-10-05T00:21:19.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b3/6c08ccbda1e78c8f538d8add49fac2fe49ef85ee34b62877df4154715583/zeroconf-0.148.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aef8699ea47cd47c9219e3f110a35ad50c13c34c7c6db992f3c9f75feec6ef8f", size = 1735431, upload-time = "2025-10-05T01:08:09.375Z" }, + { url = "https://files.pythonhosted.org/packages/cb/37/6b91c4a4258863e485602e6b1eb098fe406142a653112e8719c49b69afc4/zeroconf-0.148.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9097e7010b9f9a64e5f2084493e9973d446bd85c7a7cbef5032b2b0a2ecc5a12", size = 1701594, upload-time = "2025-10-05T01:08:11.448Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/5eaaf66d39b3bccc17b52187eebb2dde93f761f4ee8b6c83b8fe764273f5/zeroconf-0.148.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdc566c387260fb7bf89f91d00460d0c9b9373dfddcf1fcc980ab3f7270154f9", size = 2134103, upload-time = "2025-10-05T01:08:13.061Z" }, + { url = "https://files.pythonhosted.org/packages/19/a5/e4ebe7b5fbea512fe13efb466d855124126d2f531a18216c7cb509b8a4dd/zeroconf-0.148.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10cbd4134cacc22c3b3b169d7f782472a1dd36895e1421afa4f681caf181c07b", size = 1930109, upload-time = "2025-10-05T01:08:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/e1/16/7f7c5cee5279afe2a6a8b9657de9a587ccb34168d7c99acc6d2b40b9d87e/zeroconf-0.148.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dde01541e6a45c4d1b6e6d97b532ea241abc32c183745a74021b134d867388d8", size = 2230425, upload-time = "2025-10-05T01:08:16.296Z" }, + { url = "https://files.pythonhosted.org/packages/cd/41/0e1999db76e390fca9eef8257455955445a0386b94ce0ef6ce74896d7e2a/zeroconf-0.148.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8ceab8f10ab6fc0847a2de74377663793a974fdba77e7e6ba1ff47679f4bb845", size = 2161052, upload-time = "2025-10-05T01:08:17.976Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/6585fe6308b8f1ac0ac4d37ac69064ec2a36b81cf9080813cb666229694c/zeroconf-0.148.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0a8c36c37d8835420fc337be4aaa03c3a34272028919de575124c10d31a7e304", size = 2015005, upload-time = "2025-10-05T01:08:20.318Z" }, + { url = "https://files.pythonhosted.org/packages/74/ec/a9d0a577be157170f513e6ad6ebb3cd8dd9602c670d74911e9c5534e1c1d/zeroconf-0.148.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:848d57df1bb3b48279ba9b66e6c1f727570e2c8e7e0c4518c2daffaf23419d03", size = 2253785, upload-time = "2025-10-05T01:08:21.971Z" }, + { url = "https://files.pythonhosted.org/packages/ae/43/6679c16d4e6897c9aa502ee35c122bb605eee855612fad2ef6e0e13722c4/zeroconf-0.148.0-cp312-cp312-win32.whl", hash = "sha256:ba6eaa6b769924391c213dc391f36bd1c7e3ebe45fa3fa0cd97451b4f9ccef5c", size = 1295810, upload-time = "2025-10-05T01:08:23.575Z" }, + { url = "https://files.pythonhosted.org/packages/8e/42/a2d61df82086ddd32b9a5870ac683e8e5038cae38e2433c4fa03fe044235/zeroconf-0.148.0-cp312-cp312-win_amd64.whl", hash = "sha256:cec84ae7028db4a3addcc18628d12456cf39a9e973abee4a41e3b94d0db7df4c", size = 1533317, upload-time = "2025-10-05T01:08:26.973Z" }, + { url = "https://files.pythonhosted.org/packages/46/09/394a24a633645063557c5144c9abb694699df76155dcab5e1e3078dd1323/zeroconf-0.148.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ad889929bdc3953530546a4a2486d8c07f5a18d4ef494a98446bf17414897a7", size = 1714465, upload-time = "2025-10-05T01:08:28.692Z" }, + { url = "https://files.pythonhosted.org/packages/3d/db/f57c4bfcceb67fe474705cbadba3f8f7a88bdc95892e74ba6d85e24d28c3/zeroconf-0.148.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:29fb10be743650eb40863f1a1ee868df1869357a0c2ab75140ee3d7079540c1e", size = 1683877, upload-time = "2025-10-05T01:08:30.42Z" }, + { url = "https://files.pythonhosted.org/packages/54/6c/b3e2d39c40802a8cc9415357acdb76ff01bc29e25ffaa811771b6fffc428/zeroconf-0.148.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2995e74969c577461060539164c47e1ba674470585cb0f954ebeb77f032f3c2", size = 2122874, upload-time = "2025-10-05T01:08:32.11Z" }, + { url = "https://files.pythonhosted.org/packages/66/eb/0ac2bf51d58d47cfa854628036a7ad95544a1802bc890f3d69649dc35e46/zeroconf-0.148.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5be50346efdc20823f9d68d8757612767d11ceb8da7637d46080977b87912551", size = 1922164, upload-time = "2025-10-05T01:08:33.78Z" }, + { url = "https://files.pythonhosted.org/packages/59/ff/c7372507c7e25ad3499fe08d4678deb1ed41c57f78ff5df43bd2d4d98cfc/zeroconf-0.148.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc88fd01b5552ffb4d5bc551d027ac28a1852c03ceab754d02bd0d5f04c54e85", size = 2214119, upload-time = "2025-10-05T01:08:35.478Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/57f0889f47923b4fa4364b62b7b3ffc347f6bad09a25ce4e578b8991a86d/zeroconf-0.148.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:5af260c74187751c0df6a40f38d6fd17cb8658a734b0e1148a86084b71c1977c", size = 2137609, upload-time = "2025-10-05T00:21:15.953Z" }, + { url = "https://files.pythonhosted.org/packages/3b/33/9cb5558695c1377941dbb10a5591f88a787f9e1fba130642693d5c80663b/zeroconf-0.148.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6078c73a76d49ba969ca2bb7067e4d58ebd2b79a5f956e45c4c989b11d36e03", size = 2154314, upload-time = "2025-10-05T01:08:37.523Z" }, + { url = "https://files.pythonhosted.org/packages/38/06/cf4e17a86922b4561d85d36f50f1adada1328723e882d95aa42baefa5479/zeroconf-0.148.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3e686bf741158f4253d5e0aa6a8f9d34b3140bf5826c0aca9b906273b9c77a5f", size = 2004973, upload-time = "2025-10-05T01:08:39.825Z" }, + { url = "https://files.pythonhosted.org/packages/a4/61/937a405783317639cd11e7bfab3879669896297b6ca2edfb0d2d9c8dbb30/zeroconf-0.148.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:52d6ac06efe05a1e46089cfde066985782824f64b64c6982e8678e70b4b49453", size = 2237775, upload-time = "2025-10-05T01:08:41.535Z" }, + { url = "https://files.pythonhosted.org/packages/03/43/a1751c4b63e108a2318c2266e5afdd9d62292250aa8b1a8ed1674090885c/zeroconf-0.148.0-cp313-cp313-win32.whl", hash = "sha256:b9ba58e2bbb0cff020b54330916eaeb8ee8f4b0dde852e84f670f4ca3a0dd059", size = 1291073, upload-time = "2025-10-05T01:08:43.757Z" }, + { url = "https://files.pythonhosted.org/packages/5e/69/5f4f9eb14506e2afd2d423472e566d5455334d0c8740b933914d642bdbb5/zeroconf-0.148.0-cp313-cp313-win_amd64.whl", hash = "sha256:ee3fcc2edcc04635cf673c400abac2f0c22c9786490fbfb971e0a860a872bf26", size = 1528568, upload-time = "2025-10-05T01:08:45.505Z" }, + { url = "https://files.pythonhosted.org/packages/a5/46/ac86e3a3ff355058cd0818b01a3a97ca3f2abc0a034f1edb8eea27cea65c/zeroconf-0.148.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:2158d8bfefcdb90237937df65b2235870ccef04644497e4e29d3ab5a4b3199b6", size = 1714870, upload-time = "2025-10-05T01:08:47.624Z" }, + { url = "https://files.pythonhosted.org/packages/de/02/c5e8cd8dfda0ca16c7309c8d12c09a3114e5b50054bce3c93da65db8b8e4/zeroconf-0.148.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:695f6663bf8df30fe1826a2c4d5acd8213d9cbd9111f59d375bf1ad635790e98", size = 1697756, upload-time = "2025-10-05T01:08:49.472Z" }, + { url = "https://files.pythonhosted.org/packages/63/04/a66c1011d05d7bb8ae6a847d41ac818271a942390f3d8c83c776389ca094/zeroconf-0.148.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa65a24ec055be0a1cba2b986ac3e1c5d97a40abe164991aabc6a6416cc9df02", size = 2146784, upload-time = "2025-10-05T01:08:51.766Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d4/2239d87c3f60f886bd2dd299e9c63b811efd58b8b6fc659d8fd0900db3bc/zeroconf-0.148.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79890df4ff696a5cdc4a59152957be568bea1423ed13632fc09e2a196c6721d5", size = 1899394, upload-time = "2025-10-05T01:08:53.457Z" }, + { url = "https://files.pythonhosted.org/packages/fb/60/534a4b576a8f9f5edff648ac9a5417323bef3086a77397f2f2058125a3c8/zeroconf-0.148.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c0ca6e8e063eb5a385469bb8d8dec12381368031cb3a82c446225511863ede3", size = 2221319, upload-time = "2025-10-05T01:08:55.271Z" }, + { url = "https://files.pythonhosted.org/packages/b5/8c/1c8e9b7d604910830243ceb533d796dae98ed0c72902624a642487edfd61/zeroconf-0.148.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ece6f030cc7a771199760963c11ce4e77ed95011eedffb1ca5186247abfec24a", size = 2178586, upload-time = "2025-10-05T01:08:56.966Z" }, + { url = "https://files.pythonhosted.org/packages/16/55/178c4b95840dc687d45e413a74d2236a25395ab036f4813628271306ab9d/zeroconf-0.148.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c3f860ad0003a8999736fa2ae4c2051dd3c2e5df1bc1eaea2f872f5fcbd1f1c1", size = 1972371, upload-time = "2025-10-05T01:08:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/fb/86/b599421fe634d9f3a2799f69e6e7db9f13f77d326331fa2bb5982e936665/zeroconf-0.148.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ab8e687255cf54ebeae7ede6a8be0566aec752c570e16dbea84b3f9b149ba829", size = 2244286, upload-time = "2025-10-05T01:09:01.029Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cb/a30c42057be5da6bb4cbe1ab53bc3a7d9a29cd59caae097d3072a9375c14/zeroconf-0.148.0-cp314-cp314-win32.whl", hash = "sha256:6b1a6ddba3328d741798c895cecff21481863eb945c3e5d30a679461f4435684", size = 1321693, upload-time = "2025-10-05T01:09:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/2c/38/06873cdf769130af463ef5acadbaf4a50826a7274374bc3b9a4ec5d32678/zeroconf-0.148.0-cp314-cp314-win_amd64.whl", hash = "sha256:2588f1ca889f57cdc09b3da0e51175f1b6153ce0f060bf5eb2a8804c5953b135", size = 1563980, upload-time = "2025-10-05T01:09:04.857Z" }, + { url = "https://files.pythonhosted.org/packages/36/fb/53d749793689279bc9657d818615176577233ad556d62f76f719e86ead1d/zeroconf-0.148.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:40fe100381365c983a89e4b219a7ececcc2a789ac179cd26d4a6bbe00ae3e8fe", size = 3418152, upload-time = "2025-10-05T01:09:06.71Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/5eb647f7277378cbfdb6943dc8e60c3b17cdd1556f5082ccfdd6813e1ce8/zeroconf-0.148.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0b9c7bcae8af8e27593bad76ee0f0c21d43c6a2324cd1e34d06e6e08cb3fd922", size = 3389671, upload-time = "2025-10-05T01:09:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/3134aa54d30a9ae2e2473212eab586fe1779f845bf241e68729eca63d2ab/zeroconf-0.148.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf8ba75dacd58558769afb5da24d83da4fdc2a5c43a52f619aaa107fa55d3fdc", size = 4123125, upload-time = "2025-10-05T01:09:11.064Z" }, + { url = "https://files.pythonhosted.org/packages/12/23/4a0284254ebce373ff1aee7240932a0599ecf47e3c711f93242a861aa382/zeroconf-0.148.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75f9a8212c541a4447c064433862fd4b23d75d47413912a28204d2f9c4929a59", size = 3651426, upload-time = "2025-10-05T01:09:13.725Z" }, + { url = "https://files.pythonhosted.org/packages/76/9a/7b79ef986b5467bb8f17b9a9e6eea887b0b56ecafc00515c81d118e681b4/zeroconf-0.148.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be64c0eb48efa1972c13f7f17a7ac0ed7932ebb9672e57f55b17536412146206", size = 4263151, upload-time = "2025-10-05T01:09:15.732Z" }, + { url = "https://files.pythonhosted.org/packages/dd/0a/caa6d05548ca7cf28a0b8aa20a9dbb0f8176172f28799e53ea11f78692a3/zeroconf-0.148.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac1d4ee1d5bac71c27aea6d1dc1e1485423a1631a81be1ea65fb45ac280ade96", size = 4191717, upload-time = "2025-10-05T01:09:18.071Z" }, + { url = "https://files.pythonhosted.org/packages/46/f6/dbafa3b0f2d7a09315ed3ad588d36de79776ce49e00ec945c6195cad3f18/zeroconf-0.148.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8da9bdb39ead9d5971136046146cd5e11413cb979c011e19f717b098788b5c37", size = 3793490, upload-time = "2025-10-05T01:09:20.045Z" }, + { url = "https://files.pythonhosted.org/packages/c4/05/f8b88937659075116c122355bdd9ce52376cc46e2269d91d7d4f10c9a658/zeroconf-0.148.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f6e3dd22732df47a126aefb5ca4b267e828b47098a945d4468d38c72843dd6df", size = 4311455, upload-time = "2025-10-05T01:09:22.042Z" }, + { url = "https://files.pythonhosted.org/packages/58/c0/359bdb3b435d9c573aec1f877f8a63d5e81145deb6c160de89647b237363/zeroconf-0.148.0-cp314-cp314t-win32.whl", hash = "sha256:cdc8083f0b5efa908ab6c8e41687bcb75fd3d23f49ee0f34cbc58422437a456f", size = 2755961, upload-time = "2025-10-05T01:09:24.041Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ab/7b487afd5d1fd053c5a018565be734ac6d5e554bce938c7cc126154adcfc/zeroconf-0.148.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f72c1f77a89638e87f243a63979f0fd921ce391f83e18e17ec88f9f453717701", size = 3309977, upload-time = "2025-10-05T01:09:26.039Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] diff --git a/zensical.toml b/zensical.toml new file mode 100644 index 00000000..5b47fca1 --- /dev/null +++ b/zensical.toml @@ -0,0 +1,117 @@ +# Adaptive Lighting Documentation Site Configuration +# Built with Zensical - https://zensical.org + +[project] +site_name = "Adaptive Lighting" +site_description = "Automatically adjust brightness and color of lights based on the sun position" +site_author = "Bas Nijholt" +site_url = "https://adaptive-lighting.nijho.lt/" +copyright = "Copyright © 2020-2026 Bas Nijholt" + +repo_url = "https://github.com/basnijholt/adaptive-lighting" +repo_name = "GitHub" +edit_uri = "edit/main/docs" + +nav = [ + { "Home" = "index.md" }, + { "Getting Started" = "getting-started.md" }, + { "Configuration" = "configuration.md" }, + { "Services" = "services.md" }, + { "Automation Examples" = "automation-examples.md" }, + { "Troubleshooting" = "troubleshooting.md" }, + { "Advanced" = [ + { "Brightness Modes" = "advanced/brightness-modes.md" }, + { "Manual Control" = "advanced/manual-control.md" }, + { "Sleep Mode" = "advanced/sleep-mode.md" }, + ] }, + { "See Also" = "see-also.md" }, +] + +[project.theme] +custom_dir = "docs/overrides" +language = "en" +logo = "assets/logo.png" + +features = [ + "announce.dismiss", + "content.action.edit", + "content.action.view", + "content.code.annotate", + "content.code.copy", + "content.code.select", + "content.footnote.tooltips", + "content.tabs.link", + "content.tooltips", + "navigation.footer", + "navigation.indexes", + "navigation.instant", + "navigation.instant.prefetch", + "navigation.path", + "navigation.top", + "navigation.tracking", + "search.highlight", +] + +# Three-way toggle: system preference -> light -> dark -> system preference +[[project.theme.palette]] +media = "(prefers-color-scheme)" +toggle.icon = "lucide/sun-moon" +toggle.name = "Switch to light mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: light)" +scheme = "default" +primary = "amber" +accent = "orange" +toggle.icon = "lucide/sun" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: dark)" +scheme = "slate" +primary = "amber" +accent = "orange" +toggle.icon = "lucide/moon-star" +toggle.name = "Switch to system preference" + +[project.theme.font] +text = "Inter" +code = "JetBrains Mono" + +[project.theme.icon] +repo = "lucide/github" + +[project.extra] +generator = false + +[[project.extra.social]] +icon = "fontawesome/brands/github" +link = "https://github.com/basnijholt/adaptive-lighting" + +[[project.extra.social]] +icon = "fontawesome/brands/discord" +link = "https://discord.gg/home-assistant" + +# Enable GitHub-style admonitions (> [!NOTE], > [!TIP], etc.) +[project.markdown_extensions.gfm_admonition] + +# Enable markdown inside HTML blocks (e.g.,
) +[project.markdown_extensions.md_in_html] + +# Enable attribute lists for adding CSS classes (e.g., {.md-button}) +[project.markdown_extensions.attr_list] + +# Enable syntax highlighting for fenced code blocks +[project.markdown_extensions.pymdownx.highlight] +anchor_linenums = true + +[project.markdown_extensions.pymdownx.superfences] + +# Enable tabs (=== "Tab Name" syntax) +[project.markdown_extensions.pymdownx.tabbed] +alternate_style = true + +# Enable emoji/icon support +[project.markdown_extensions.pymdownx.emoji] +emoji_index = "zensical.extensions.emoji.twemoji" +emoji_generator = "zensical.extensions.emoji.to_svg" From a1ddbc000ff16a966a86c1d580966edc01f53b69 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:40:32 +0000 Subject: [PATCH 0942/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Pin=20dependenci?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docs.yml | 2 +- .github/workflows/markdown-code-runner.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7b62b15c..433b7526 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.12.12' - name: Install uv uses: astral-sh/setup-uv@v5 diff --git a/.github/workflows/markdown-code-runner.yml b/.github/workflows/markdown-code-runner.yml index 884cfa71..2ac333d6 100644 --- a/.github/workflows/markdown-code-runner.yml +++ b/.github/workflows/markdown-code-runner.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.13" + python-version: "3.13.11" - name: Install uv uses: astral-sh/setup-uv@v5 From 9480211e1bca06ef9ccc165e0b281ee11422be92 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:46:32 +0000 Subject: [PATCH 0943/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20python?= =?UTF-8?q?=20to=20v3.14.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docs.yml | 2 +- .github/workflows/markdown-code-runner.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 433b7526..832b5d71 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12.12' + python-version: '3.14.2' - name: Install uv uses: astral-sh/setup-uv@v5 diff --git a/.github/workflows/markdown-code-runner.yml b/.github/workflows/markdown-code-runner.yml index 2ac333d6..b4a5a795 100644 --- a/.github/workflows/markdown-code-runner.yml +++ b/.github/workflows/markdown-code-runner.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.13.11" + python-version: "3.14.2" - name: Install uv uses: astral-sh/setup-uv@v5 From eb25df01d5bf53ed8e11db36c9e73020f723a68d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 12 Jan 2026 23:50:55 +0100 Subject: [PATCH 0944/1077] Enable custom analytics provider for Plausible (#1388) --- zensical.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/zensical.toml b/zensical.toml index 5b47fca1..dfa1b309 100644 --- a/zensical.toml +++ b/zensical.toml @@ -84,6 +84,9 @@ repo = "lucide/github" [project.extra] generator = false +[project.extra.analytics] +provider = "custom" + [[project.extra.social]] icon = "fontawesome/brands/github" link = "https://github.com/basnijholt/adaptive-lighting" From 455040e532c59f15b9466d2de87cf931843bba46 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 09:06:42 +0100 Subject: [PATCH 0945/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/checkout=20action=20to=20v6=20(#1389)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docs.yml | 2 +- .github/workflows/markdown-code-runner.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 832b5d71..c1c40cfc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v5 diff --git a/.github/workflows/markdown-code-runner.yml b/.github/workflows/markdown-code-runner.yml index b4a5a795..550de3b2 100644 --- a/.github/workflows/markdown-code-runner.yml +++ b/.github/workflows/markdown-code-runner.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code from GitHub - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.head_ref }} fetch-depth: 0 From 093376dbed2acdd290e711238cb209221da9e655 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 09:06:58 +0100 Subject: [PATCH 0946/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/setup-python=20action=20to=20v6=20(#1390)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docs.yml | 2 +- .github/workflows/markdown-code-runner.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c1c40cfc..a8c8be8a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.14.2' diff --git a/.github/workflows/markdown-code-runner.yml b/.github/workflows/markdown-code-runner.yml index 550de3b2..8d9ddcad 100644 --- a/.github/workflows/markdown-code-runner.yml +++ b/.github/workflows/markdown-code-runner.yml @@ -17,7 +17,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.14.2" From fcff6d48ecfba8d30e2b3631ec2b4a07b569e9c3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 09:07:28 +0100 Subject: [PATCH 0947/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20astral-?= =?UTF-8?q?sh/setup-uv=20action=20to=20v7=20(#1392)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docs.yml | 2 +- .github/workflows/markdown-code-runner.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a8c8be8a..7eb1e082 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,7 +28,7 @@ jobs: python-version: '3.14.2' - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 - name: Install dependencies run: uv sync --group docs diff --git a/.github/workflows/markdown-code-runner.yml b/.github/workflows/markdown-code-runner.yml index 8d9ddcad..589976cf 100644 --- a/.github/workflows/markdown-code-runner.yml +++ b/.github/workflows/markdown-code-runner.yml @@ -22,7 +22,7 @@ jobs: python-version: "3.14.2" - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 - name: Run markdown-code-runner run: | From 844afc081a75f683106656e275373eff76e6eb6b Mon Sep 17 00:00:00 2001 From: Mario Guggenberger Date: Tue, 13 Jan 2026 11:32:17 +0100 Subject: [PATCH 0948/1077] build: fix tasks not executing in dev container (#1394) --- scripts/develop | 2 +- scripts/lint | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/develop b/scripts/develop index 5382e0fc..ea36af03 100755 --- a/scripts/develop +++ b/scripts/develop @@ -17,4 +17,4 @@ fi export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" # Start Home Assistant -hass --config "${PWD}/config" --debug +uv run hass --config "${PWD}/config" --debug diff --git a/scripts/lint b/scripts/lint index 55a1f485..eadfe510 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,4 +4,4 @@ set -e cd "$(dirname "$0")/.." -pre-commit run --all-files +uv run pre-commit run --all-files From da732ae1a1d009b5b8a6a977712f7b67dd5f4c50 Mon Sep 17 00:00:00 2001 From: Andrei LAZAROV Date: Wed, 14 Jan 2026 20:27:19 +0200 Subject: [PATCH 0949/1077] =?UTF-8?q?=F0=9F=93=9D=20Update=20readme:=20Mod?= =?UTF-8?q?ern=20lights=20are=20routers=20(#1396)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 23bb93f1..1e82f684 100644 --- a/README.md +++ b/README.md @@ -394,7 +394,7 @@ Ensure your light bulbs have a strong WiFi connection. If the signal strength is #### :spider_web: Zigbee, Z-Wave, and Other Mesh Networks Mesh networks typically require powered devices to act as routers, relaying messages back to the central coordinator (the radio connected to Home Assistant). -Philips lights usually function as routers, while Ikea, Sengled, and generic Tuya bulbs often do not. +Most modern lights function as routers, very early models may not. If devices become unresponsive or fail to respond to commands, Adaptive Lighting can exacerbate the issue. Use network maps (available in ZHA, zigbee2mqtt, deCONZ, and ZWaveJS UI) to evaluate your network health. Smart plugs can be an affordable way to add more routers to your network. From d60ba750143aa9d7ce5673fa01f8cf55984a3fe1 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:28:08 +0100 Subject: [PATCH 0950/1077] docs: add andrei-lazarov as a contributor for doc (#1397) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 9a04d160..091aba91 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1179,6 +1179,15 @@ "contributions": [ "code" ] + }, + { + "login": "andrei-lazarov", + "name": "Andrei LAZAROV", + "avatar_url": "https://avatars.githubusercontent.com/u/51081857?v=4", + "profile": "https://github.com/andrei-lazarov", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 1e82f684..36ad135c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-129-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-130-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -656,6 +656,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
+ From ae719a676d32b5c6efd7ed8ae86dd3bd27f5750b Mon Sep 17 00:00:00 2001 From: Adam DeMuri Date: Fri, 16 Jan 2026 23:30:06 -0700 Subject: [PATCH 0951/1077] Add core/ to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index eeef9022..f40a1605 100644 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,6 @@ dmypy.json # Home Assistant configuration config/* !config/configuration.yaml + +# Home Assistant core +core/ From e656dd31b09df7c15cf4c30019d7452881b4b980 Mon Sep 17 00:00:00 2001 From: Adam DeMuri Date: Mon, 19 Jan 2026 10:03:20 -0700 Subject: [PATCH 0952/1077] Fix coverage. (#1404) --- .github/workflows/pytest.yaml | 2 +- Dockerfile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index b8a59b40..597d04f2 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -61,7 +61,7 @@ jobs: -qq \ --timeout=9 \ --durations=10 \ - --cov="homeassistant" \ + --cov=homeassistant.components.adaptive_lighting \ --cov-report=xml \ -o console_output_style=count \ -p no:sugar \ diff --git a/Dockerfile b/Dockerfile index 353a0748..eb8fed44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,8 +48,8 @@ ENTRYPOINT ["python3", \ "--timeout=9", \ # Print the 10 slowest tests "--durations=10", \ - # Measure code coverage for the 'homeassistant' package - "--cov='homeassistant'", \ + # Measure code coverage for the 'homeassistant.components.adaptive_lighting' component + "--cov=homeassistant.components.adaptive_lighting", \ # Generate an XML report of the code coverage "--cov-report=xml", \ # Generate an HTML report of the code coverage From f7a7226fdb2d3d8e58428d8a570448449fd39c35 Mon Sep 17 00:00:00 2001 From: Adam DeMuri Date: Mon, 19 Jan 2026 10:03:40 -0700 Subject: [PATCH 0953/1077] Fix Docker warning by removing unbound variable (#1400) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index eb8fed44..b2c54bab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,7 +35,7 @@ RUN /app/scripts/setup-dependencies WORKDIR /app/core # Make 'custom_components/adaptive_lighting' imports available to tests -ENV PYTHONPATH="${PYTHONPATH}:/app" +ENV PYTHONPATH="/app" ENTRYPOINT ["python3", \ # Enable Python development mode From d964c861d578c8253b52afa48f46a249d1c003a1 Mon Sep 17 00:00:00 2001 From: Adam DeMuri Date: Sun, 18 Jan 2026 23:52:14 -0700 Subject: [PATCH 0954/1077] Fix the markdown-code-runner workflow. This fixes the workflow to work for pull requests in addition to pushes: - Correctly determines the repository and branch - For pull requests, if there are changes, fails with a message to make the changes locally - Extracts the commands to update generated files into a script --- .github/workflows/markdown-code-runner.yml | 49 +++++++++------------- scripts/update-generated-content | 11 +++++ 2 files changed, 31 insertions(+), 29 deletions(-) create mode 100755 scripts/update-generated-content diff --git a/.github/workflows/markdown-code-runner.yml b/.github/workflows/markdown-code-runner.yml index 589976cf..efd76831 100644 --- a/.github/workflows/markdown-code-runner.yml +++ b/.github/workflows/markdown-code-runner.yml @@ -13,7 +13,8 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v6 with: - ref: ${{ github.head_ref }} + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + ref: ${{ github.head_ref || github.ref }} fetch-depth: 0 - name: Set up Python @@ -24,34 +25,24 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v7 - - name: Run markdown-code-runner + - name: Update generated content + run: ./scripts/update-generated-content + + - name: Check for changes run: | - uv sync --group docs - uv pip install -e . - uv run python docs/run_markdown_code_runner.py - - - name: Run update services.yaml - run: uv run python .github/update-services.py - - - name: Run update strings.json - run: uv run python .github/update-strings.py - - - name: Commit updated files - id: commit - run: | - git add -u . - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - if git diff --quiet && git diff --staged --quiet; then - echo "No changes, skipping commit." - echo "commit_status=skipped" >> $GITHUB_ENV + if [ -n "$(git status --porcelain)" ]; then + if [ "${{ github.event_name }}" == "pull_request" ]; then + echo "::error::Auto-generated files are not up to date. Please run './scripts/update-generated-content' locally and push the changes." + exit 1 + else + echo "Changes detected, committing and pushing..." + git add -u . + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git commit -m "Update auto-generated content" + git pull --rebase + git push + fi else - git commit -m "Update auto-generated content" - echo "commit_status=committed" >> $GITHUB_ENV + echo "No changes detected." fi - - - name: Push changes - if: env.commit_status == 'committed' - run: | - git pull --rebase - git push diff --git a/scripts/update-generated-content b/scripts/update-generated-content new file mode 100755 index 00000000..1fed3178 --- /dev/null +++ b/scripts/update-generated-content @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -ex + +cd "$(dirname "$0")/.." + +uv sync --group docs +uv pip install -e . +uv run python docs/run_markdown_code_runner.py +uv run python .github/update-services.py +uv run python .github/update-strings.py From 5b7153a9a1b29786548e44b01f6efca6969abed7 Mon Sep 17 00:00:00 2001 From: Adam DeMuri Date: Mon, 19 Jan 2026 15:13:38 -0700 Subject: [PATCH 0955/1077] Automated update of docs/troubleshooting.md --- docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index e644f385..4e33e24c 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -56,7 +56,7 @@ Ensure your light bulbs have a strong WiFi connection. If the signal strength is #### :spider_web: Zigbee, Z-Wave, and Other Mesh Networks Mesh networks typically require powered devices to act as routers, relaying messages back to the central coordinator (the radio connected to Home Assistant). -Philips lights usually function as routers, while Ikea, Sengled, and generic Tuya bulbs often do not. +Most modern lights function as routers, very early models may not. If devices become unresponsive or fail to respond to commands, Adaptive Lighting can exacerbate the issue. Use network maps (available in ZHA, zigbee2mqtt, deCONZ, and ZWaveJS UI) to evaluate your network health. Smart plugs can be an affordable way to add more routers to your network. From 7317c2966de199ceb47301d8ddca7a1399b3f217 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 07:36:36 +0000 Subject: [PATCH 0956/1077] docs: add ademuri as a contributor for code (#1406) --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 091aba91..acb9bd2f 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1188,6 +1188,15 @@ "contributions": [ "doc" ] + }, + { + "login": "ademuri", + "name": "Adam DeMuri", + "avatar_url": "https://avatars.githubusercontent.com/u/3051618?v=4", + "profile": "https://github.com/ademuri", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 36ad135c..10c78eed 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-130-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-131-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -657,6 +657,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From 2fdc2a1636c97d51a7a1d80546483af4362aa693 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 03:18:04 -0800 Subject: [PATCH 0957/1077] docs: add NatanDosAnjos as a contributor for translation (#1407) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index acb9bd2f..e799cfc5 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1197,6 +1197,15 @@ "contributions": [ "code" ] + }, + { + "login": "NatanDosAnjos", + "name": "Natanael", + "avatar_url": "https://avatars.githubusercontent.com/u/45629905?v=4", + "profile": "https://github.com/NatanDosAnjos", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 10c78eed..e521e7b0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-131-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-132-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -658,6 +658,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From ec33670b3f089b4cea8e0f79dc9f7c70c028490f Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 03:19:00 -0800 Subject: [PATCH 0958/1077] docs: add Yllelder as a contributor for translation (#1408) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index e799cfc5..8ce71790 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1206,6 +1206,15 @@ "contributions": [ "translation" ] + }, + { + "login": "Yllelder", + "name": "Yllelder Bamir", + "avatar_url": "https://avatars.githubusercontent.com/u/6941502?v=4", + "profile": "https://github.com/Yllelder", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index e521e7b0..ba0fdb74 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-132-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-133-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -659,6 +659,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From e9f7d4925fcce418a6e0adf97b821112b3bc6a4b Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 03:20:18 -0800 Subject: [PATCH 0959/1077] docs: add Esspel as a contributor for translation (#1409) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 8ce71790..e2a901fd 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1215,6 +1215,15 @@ "contributions": [ "translation" ] + }, + { + "login": "Esspel", + "name": "Esspel", + "avatar_url": "https://avatars.githubusercontent.com/u/47383506?v=4", + "profile": "https://github.com/Esspel", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index ba0fdb74..3a26df24 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-133-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-134-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -661,6 +661,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + + + From 74a795d907a56e17f0025e62d93bd42b68df6bee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 23:02:25 +0000 Subject: [PATCH 0960/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/checkout=20action=20to=20v6.0.2=20(#1411)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/hassfest.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml index 58a152e4..b063de53 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/hassfest.yaml @@ -11,5 +11,5 @@ jobs: validate_hassfest: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v6.0.1" + - uses: "actions/checkout@v6.0.2" - uses: home-assistant/actions/hassfest@master From f9ccc946ee57c94294e504c8d2ce7905cfb536fe Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 26 Jan 2026 09:01:50 +0100 Subject: [PATCH 0961/1077] Migrate to markdown-code-runner's built-in include_section() (#1417) --- .../adaptive_lighting/docs_gen.py | 53 +------------------ docs/advanced/brightness-modes.md | 6 +-- docs/advanced/manual-control.md | 3 +- docs/automation-examples.md | 4 +- docs/configuration.md | 4 +- docs/index.md | 4 +- docs/see-also.md | 4 +- docs/services.md | 6 +-- docs/troubleshooting.md | 8 +-- pyproject.toml | 2 +- uv.lock | 8 +-- 11 files changed, 24 insertions(+), 78 deletions(-) diff --git a/custom_components/adaptive_lighting/docs_gen.py b/custom_components/adaptive_lighting/docs_gen.py index 8681da4b..9a10a68b 100644 --- a/custom_components/adaptive_lighting/docs_gen.py +++ b/custom_components/adaptive_lighting/docs_gen.py @@ -1,61 +1,12 @@ """Documentation generation utilities for Adaptive Lighting. -Provides functions to extract sections from README.md and transform -content for the documentation site. Used by markdown-code-runner -to generate documentation pages from README content. +Provides functions to transform content for the documentation site. +Used by markdown-code-runner to generate documentation pages from README content. """ from __future__ import annotations import re -from pathlib import Path - -# Path to README relative to this module -_MODULE_DIR = Path(__file__).parent -README_PATH = _MODULE_DIR.parent.parent / "README.md" - - -def readme_section(section_name: str, *, strip_heading: bool = True) -> str: - """Extract a marked section from README.md. - - Sections are marked with HTML comments: - - content - - - Args: - section_name: The name of the section to extract - strip_heading: If True, remove the first heading from the section - - Returns: - The content between the section markers - - Raises: - ValueError: If the section is not found in README.md - - """ - content = README_PATH.read_text() - - start_marker = f"" - end_marker = f"" - - start_idx = content.find(start_marker) - if start_idx == -1: - msg = f"Section '{section_name}' not found in README.md" - raise ValueError(msg) - - end_idx = content.find(end_marker, start_idx) - if end_idx == -1: - msg = f"End marker for section '{section_name}' not found" - raise ValueError(msg) - - section = content[start_idx + len(start_marker) : end_idx].strip() - - if strip_heading: - # Remove first heading (# or ## or ###) - section = re.sub(r"^#{1,3}\s+[^\n]+\n+", "", section, count=1) - - return _transform_readme_links(section) def _transform_readme_links(content: str) -> str: diff --git a/docs/advanced/brightness-modes.md b/docs/advanced/brightness-modes.md index b735a583..10408fb2 100644 --- a/docs/advanced/brightness-modes.md +++ b/docs/advanced/brightness-modes.md @@ -19,8 +19,7 @@ Adaptive Lighting supports three brightness modes: ## Detailed Explanation - - + @@ -90,8 +89,7 @@ adaptive_lighting: These graphs show how brightness changes throughout the day based on calculated values: - - + diff --git a/docs/advanced/manual-control.md b/docs/advanced/manual-control.md index 157e081e..36720f3c 100644 --- a/docs/advanced/manual-control.md +++ b/docs/advanced/manual-control.md @@ -9,8 +9,7 @@ Adaptive Lighting is designed to work seamlessly with manual adjustments, detect ## How It Works - - + diff --git a/docs/automation-examples.md b/docs/automation-examples.md index e8ed0e7d..e2d136f6 100644 --- a/docs/automation-examples.md +++ b/docs/automation-examples.md @@ -7,8 +7,8 @@ icon: lucide/bot Real-world automation examples showing how to integrate Adaptive Lighting with your Home Assistant setup. - - + + diff --git a/docs/configuration.md b/docs/configuration.md index 622c1416..21c7028e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -85,8 +85,8 @@ All configuration options are listed below with their default values. These opti ## Full Configuration Example - - + + diff --git a/docs/index.md b/docs/index.md index 28820274..bc3acebe 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,8 +26,8 @@ By automatically adapting the settings of your lights throughout the day, Adapti ## Features - - + + diff --git a/docs/see-also.md b/docs/see-also.md index 8c1a2613..5b06fcae 100644 --- a/docs/see-also.md +++ b/docs/see-also.md @@ -9,8 +9,8 @@ Resources, tutorials, and related projects for Adaptive Lighting. ## Tutorials & Articles - - + + diff --git a/docs/services.md b/docs/services.md index 5b0ccf58..306aa7c3 100644 --- a/docs/services.md +++ b/docs/services.md @@ -112,13 +112,11 @@ data: ## adaptive_lighting.change_switch_settings - - + + -#### `adaptive_lighting.change_switch_settings` - `adaptive_lighting.change_switch_settings` (new in 1.7.0) Change any of the above configuration options of Adaptive Lighting (such as `sunrise_time` or `prefer_rgb_color`) with a service call directly from your script/automation. > [!WARNING] diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 4e33e24c..22374949 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -9,8 +9,8 @@ This guide covers common issues and their solutions when using Adaptive Lighting ## Enable Debug Logging - - + + @@ -30,8 +30,8 @@ After the issue occurs, create a new issue report with the log (`/config/home-as ## Common Problems & Solutions - - + + diff --git a/pyproject.toml b/pyproject.toml index 7021ac48..bdaf9acf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ requires-python = ">=3.12" docs = [ "astral", "homeassistant", - "markdown-code-runner", + "markdown-code-runner>=2.7.0", "markdown-gfm-admonition", "pandas", "shinylive", diff --git a/uv.lock b/uv.lock index 5dd33777..57564ac4 100644 --- a/uv.lock +++ b/uv.lock @@ -109,7 +109,7 @@ dev = [ docs = [ { name = "astral" }, { name = "homeassistant" }, - { name = "markdown-code-runner" }, + { name = "markdown-code-runner", specifier = ">=2.7.0" }, { name = "markdown-gfm-admonition" }, { name = "pandas" }, { name = "shinylive" }, @@ -2790,11 +2790,11 @@ wheels = [ [[package]] name = "markdown-code-runner" -version = "2.4.0" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/91/6b7030f873b0c49f38131c7a5ac9432238cf1e1cd47104c05b07cf5c32ed/markdown_code_runner-2.4.0.tar.gz", hash = "sha256:7d6229c437f0c71e5c0442585663af4bfe4beacddf884a64ea8c09fd5dbc31dd", size = 19072, upload-time = "2025-08-23T19:07:41.78Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/d1/2a7753e05dcc711552721048a34341b63a6a2b09e70ac738c7a4c81ca414/markdown_code_runner-2.7.0.tar.gz", hash = "sha256:b36f9314839c0db3ee5e5d644e1b4471454af8f624eb15bd5a5d68dbc6430afe", size = 99832, upload-time = "2026-01-24T21:20:04.772Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/3c/8857763070aa0f39591b012f92ab40bd5fe906914085e925eb117e28f306/markdown_code_runner-2.4.0-py3-none-any.whl", hash = "sha256:5be62e75ab35188b37f2d440c19b6683df68615ea24bed4955d95a745e56b483", size = 12292, upload-time = "2025-08-23T19:07:40.972Z" }, + { url = "https://files.pythonhosted.org/packages/cf/92/9d7fa0567cf3d38b551bb1a30c177285e0e42ae2b10c16946af7e8a38e73/markdown_code_runner-2.7.0-py3-none-any.whl", hash = "sha256:fad5d5fbad9c49687141ab9c39ed7e054f857b97aa79d11f031de157afd21c20", size = 14692, upload-time = "2026-01-24T21:20:03.382Z" }, ] [[package]] From 6cebe14a69bf0ea19efeaf3ed5c20e5396087216 Mon Sep 17 00:00:00 2001 From: Roee Hendel Date: Tue, 17 Mar 2026 00:54:53 +0200 Subject: [PATCH 0962/1077] fix: merge last_service_data across split calls to fix detect_non_ha_changes with separate_turn_on_commands (#1426) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: regression test — AL must not override manual brightness with separate_turn_on_commands End-to-end scenario: user adjusts brightness via a directly-bound Zigbee switch (e.g. IKEA RODRET). No HA service call is made; ZHA reports the new brightness via async_update_entity. On the next adaptation interval AL must detect the change and stop overriding the user's brightness. The test verifies the user-visible symptom: after two adaptation cycles following a simulated direct-Zigbee brightness change, the light's brightness must still be the manually set value — not AL's own target. NOTE: this test FAILS on the current code. It is committed here to document the bug before the fix is applied in the next commit. * fix: merge last_service_data across split calls to fix detect_non_ha_changes with separate_turn_on_commands When separate_turn_on_commands=True, each adaptation cycle makes two light.turn_on calls (brightness, then color_temp). Previously each call overwrote last_service_data[light], so after the cycle only the color_temp key remained. _attributes_have_changed() then saw old_brightness=None and silently skipped the brightness comparison, so a manually-set brightness was never detected and AL kept overriding it. Fix: merge instead of overwrite so all split-call attributes accumulate: self.manager.last_service_data[light] = { **self.manager.last_service_data.get(light, {}), **service_data, } * test: add intermediate assertions to regression test Two assertions were promised in the PR description but missing: 1. After the force-adapt, assert that last_service_data contains BOTH brightness AND color — directly proving the merge fix works. 2. After the first non-forced update, assert that BRIGHTNESS is in manual_control — proving detection fired, not just that the final state is right. Co-Authored-By: Claude Sonnet 4.6 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * refactor: remove spurious comments, trim test docstring and assertions Co-Authored-By: Claude Sonnet 4.6 * refactor: strip verbose comments from test, trim assert messages Co-Authored-By: Claude Sonnet 4.6 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * test: add message to bare assert --- custom_components/adaptive_lighting/switch.py | 5 +- tests/test_switch.py | 76 ++++++++++++++++++- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 3923e056..91049970 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1356,7 +1356,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): data.context.id, ) light = service_data[ATTR_ENTITY_ID] - self.manager.last_service_data[light] = service_data + self.manager.last_service_data[light] = { + **self.manager.last_service_data.get(light, {}), + **service_data, + } await self.hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, diff --git a/tests/test_switch.py b/tests/test_switch.py index 5ba3cb60..3da5c967 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -9,7 +9,7 @@ from collections import OrderedDict from copy import deepcopy from random import randint from typing import Any -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch import homeassistant.util.dt as dt_util import pytest @@ -1696,7 +1696,7 @@ async def test_change_switch_settings_service(hass): async def test_cancellable_service_calls_task(hass): """Test the creation and execution of the task that wraps adaptation service calls.""" - (light, *_) = await setup_lights(hass) + light, *_ = await setup_lights(hass) _, switch = await setup_switch(hass, {CONF_SEPARATE_TURN_ON_COMMANDS: True}) context = switch.create_context("test") @@ -2914,3 +2914,75 @@ async def test_adapt_only_on_bare_turn_on_respects_pause_changed_mode(hass, inte f"With take_over_control_mode=PAUSE_CHANGED and only brightness marked " f"as manually controlled, color_temp should still be adapted." ) + + +async def test_detect_non_ha_changes_with_separate_turn_on_commands(hass): + """Regression test for detect_non_ha_changes with separate_turn_on_commands. + + With separate_turn_on_commands=True, each adaptation cycle makes two sequential + light.turn_on calls (brightness, then color). If the second call overwrites + last_service_data instead of merging, brightness is dropped — and + _attributes_have_changed silently skips the brightness comparison, so a direct + Zigbee brightness change is never detected as manual control. + """ + switch, (light, *_) = await setup_lights_and_switch( + hass, + { + CONF_SEPARATE_TURN_ON_COMMANDS: True, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_TAKE_OVER_CONTROL: True, + }, + ) + + context = switch.create_context("test") + + async def update(force: bool = False): + await switch._update_attrs_and_maybe_adapt_lights( + context=context, + force=force, + transition=0, + ) + await hass.async_block_till_done() + + await update(force=True) + + last_sd = switch.manager.last_service_data.get(ENTITY_LIGHT_1) + assert last_sd is not None, "last_service_data not set after force adapt" + assert ( + ATTR_BRIGHTNESS in last_sd + ), f"brightness missing from last_service_data after split calls: {last_sd}" + assert ( + ATTR_COLOR_TEMP_KELVIN in last_sd or ATTR_RGB_COLOR in last_sd + ), f"color missing from last_service_data after split calls: {last_sd}" + + al_brightness = light._brightness + switch.manager.manual_control[ENTITY_LIGHT_1] = LightControlAttributes.NONE + + manual_brightness = ( + al_brightness - 120 if al_brightness >= 120 else al_brightness + 120 + ) + light._brightness = manual_brightness + + async def _flush_attr_state(hass, entity_id): + """Mimic a ZHA attribute report: write current hardware state to HA.""" + light.async_write_ha_state() + + with patch( + "homeassistant.components.adaptive_lighting.switch.async_update_entity", + new=AsyncMock(side_effect=_flush_attr_state), + ): + await update(force=False) + + assert LightControlAttributes.BRIGHTNESS in switch.manager.manual_control.get( + ENTITY_LIGHT_1, + LightControlAttributes.NONE, + ), ( + f"manual_control={switch.manager.manual_control.get(ENTITY_LIGHT_1)}, " + f"last_service_data={switch.manager.last_service_data.get(ENTITY_LIGHT_1)}" + ) + + await update(force=False) + + assert ( + light._brightness == manual_brightness + ), f"AL overrode manual brightness {manual_brightness} with {al_brightness}" From c4b8e28ea845488a73e956e289f6c6839007af51 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 24 Apr 2026 11:31:53 -0700 Subject: [PATCH 0963/1077] fix: restore hassfest and Home Assistant CI ## Summary - fix hassfest validation by replacing raw options-description URLs with Home Assistant translation placeholders - extend the pytest CI matrix to cover the latest patch release for each Home Assistant month, plus dev - align CI/dev container Python versions and tests with newer Home Assistant behavior ## Validation - GitHub Actions pytest matrix passed for 2024.12.5 through 2026.4.3 plus dev - Docker passed for linux/amd64 and linux/arm64 - hassfest, HACS validation, pre-commit, pre-commit.ci, markdown-code-runner, docs build, and Release Drafter passed --- .github/update-strings.py | 3 ++ .github/workflows/pytest.yaml | 12 +++++- Dockerfile | 2 +- .../adaptive_lighting/config_flow.py | 12 +++++- .../adaptive_lighting/strings.json | 2 +- .../adaptive_lighting/translations/bg.json | 2 +- .../adaptive_lighting/translations/ca.json | 2 +- .../adaptive_lighting/translations/de.json | 2 +- .../adaptive_lighting/translations/en.json | 2 +- .../adaptive_lighting/translations/es.json | 2 +- .../adaptive_lighting/translations/fi.json | 2 +- .../adaptive_lighting/translations/fr.json | 2 +- .../adaptive_lighting/translations/hu.json | 2 +- .../adaptive_lighting/translations/id.json | 2 +- .../adaptive_lighting/translations/ko.json | 2 +- .../adaptive_lighting/translations/nl.json | 2 +- .../adaptive_lighting/translations/pl.json | 2 +- .../adaptive_lighting/translations/pt.json | 2 +- .../adaptive_lighting/translations/sk.json | 2 +- .../adaptive_lighting/translations/sl.json | 2 +- .../adaptive_lighting/translations/ta.json | 2 +- .../adaptive_lighting/translations/tr.json | 2 +- .../adaptive_lighting/translations/ur.json | 2 +- .../translations/zh-Hans.json | 2 +- scripts/setup-dependencies | 24 +++++++++++ scripts/setup-devcontainer | 2 +- scripts/update-test-matrix.py | 8 +++- tests/test_switch.py | 43 ++++++++++++------- 28 files changed, 104 insertions(+), 42 deletions(-) diff --git a/.github/update-strings.py b/.github/update-strings.py index 94fce459..bfb329a0 100644 --- a/.github/update-strings.py +++ b/.github/update-strings.py @@ -60,6 +60,9 @@ with en_fname.open() as f: en["config"]["step"]["user"] = strings["config"]["step"]["user"] en["options"]["step"]["init"]["data"] = data en["options"]["step"]["init"]["data_description"] = data_description +en["options"]["step"]["init"]["description"] = strings["options"]["step"]["init"][ + "description" +] en["services"] = services_json with en_fname.open("w") as f: diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 597d04f2..dd0e4559 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -38,8 +38,18 @@ jobs: python-version: "3.13" - core-version: "2025.11.3" python-version: "3.13" - - core-version: "dev" + - core-version: "2025.12.5" python-version: "3.13" + - core-version: "2026.1.3" + python-version: "3.13" + - core-version: "2026.2.3" + python-version: "3.13" + - core-version: "2026.3.4" + python-version: "3.14.2" + - core-version: "2026.4.3" + python-version: "3.14.2" + - core-version: "dev" + python-version: "3.14.2" steps: - name: Check out code from GitHub uses: actions/checkout@v6 diff --git a/Dockerfile b/Dockerfile index b2c54bab..db75963d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ RUN ln -s /core /app/core && /app/scripts/setup-symlinks # Install home-assistant/core dependencies RUN mkdir -p /.venv -ENV UV_PROJECT_ENVIRONMENT=/.venv UV_PYTHON=3.13 PATH="/.venv/bin:$PATH" +ENV UV_PROJECT_ENVIRONMENT=/.venv UV_PYTHON=3.14.2 PATH="/.venv/bin:$PATH" RUN uv venv RUN /app/scripts/setup-dependencies diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index 5b0dd13c..eb7b4a0a 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -20,6 +20,11 @@ from .switch import validate _LOGGER = logging.getLogger(__name__) +OPTIONS_FLOW_DESCRIPTION_PLACEHOLDERS = { + "webapp_url": "https://basnijholt.github.io/adaptive-lighting", + "docs_url": "https://github.com/basnijholt/adaptive-lighting#readme", +} + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Adaptive Lighting.""" @@ -127,7 +132,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow): conf = self.config_entry data = validate(conf) if conf.source == config_entries.SOURCE_IMPORT: - return self.async_show_form(step_id="init", data_schema=None) + return self.async_show_form( + step_id="init", + data_schema=None, + description_placeholders=OPTIONS_FLOW_DESCRIPTION_PLACEHOLDERS, + ) errors: dict[str, str] = {} if user_input is not None: validate_options(user_input, errors) @@ -164,4 +173,5 @@ class OptionsFlowHandler(config_entries.OptionsFlow): step_id="init", data_schema=vol.Schema(options_schema), errors=errors, + description_placeholders=OPTIONS_FLOW_DESCRIPTION_PLACEHOLDERS, ) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 4b18ea06..cf816d4f 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -24,7 +24,7 @@ "step": { "init": { "title": "Adaptive Lighting options", - "description": "Configure an Adaptive Lighting component. Option names align with the YAML settings. If you've defined this entry in YAML, no options will appear here. For interactive graphs that demonstrate parameter effects, visit [this web app](https://basnijholt.github.io/adaptive-lighting). For further details, see the [official documentation](https://github.com/basnijholt/adaptive-lighting#readme).", + "description": "Configure an Adaptive Lighting component. Option names align with the YAML settings. If you've defined this entry in YAML, no options will appear here. For interactive graphs that demonstrate parameter effects, visit [this web app]({webapp_url}). For further details, see the [official documentation]({docs_url}).", "data": { "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟", "interval": "interval", diff --git a/custom_components/adaptive_lighting/translations/bg.json b/custom_components/adaptive_lighting/translations/bg.json index b88bbaae..f8f6f7e9 100644 --- a/custom_components/adaptive_lighting/translations/bg.json +++ b/custom_components/adaptive_lighting/translations/bg.json @@ -18,7 +18,7 @@ "step": { "init": { "title": "Настройки на Адаптивно осветление", - "description": "Конфигурирайте компонент за Адаптивно осветление. Имената на опциите съвпадат с настройките на YAML. Ако сте дефинирали този запис в YAML, няма да се появят опции тук. За интерактивни графики, които демонстрират ефектите на параметрите, посетете [това уеб приложение](https://basnijholt.github.io/adaptive-lighting). За повече подробности, вижте [официалната документация](https://github.com/basnijholt/adaptive-lighting#readme).", + "description": "Конфигурирайте компонент за Адаптивно осветление. Имената на опциите съвпадат с настройките на YAML. Ако сте дефинирали този запис в YAML, няма да се появят опции тук. За интерактивни графики, които демонстрират ефектите на параметрите, посетете [това уеб приложение]({webapp_url}). За повече подробности, вижте [официалната документация]({docs_url}).", "data": { "lights": "lights: Списък от entity_ids на лампи за контрол (може да е празен). 🌟", "interval": "интервал", diff --git a/custom_components/adaptive_lighting/translations/ca.json b/custom_components/adaptive_lighting/translations/ca.json index 48f409d2..4291d8c2 100644 --- a/custom_components/adaptive_lighting/translations/ca.json +++ b/custom_components/adaptive_lighting/translations/ca.json @@ -46,7 +46,7 @@ "intercept": "intercept: Intercepta i adapta les crides `light.turn_on` per permetre canvis instantanis de color i brillantor. 🏎️ Inhabilita-ho per a llums que no admeten `light.turn_on` amb color i brillantor.", "skip_redundant_commands": "skip_redundant_commands: Evita l'enviament de d'ordres d'adaptació als objectius on el seu estat ja és el conegut del llum. Minimitza el trànsit de la xarxa i millora la resposta de l'adaptació en alguns casos. 📉 Inhabilita-ho si l'estat físic del llum queda desincronitzat amb l'estat registrat a Home Assistant." }, - "description": "Configura un component d'Adaptive Lighting. Els noms de les opcions s'alineen amb la configuració de YAML. Si has definit aquesta entrada a YAML, aquí no apareixerà cap opció. Per veure gràfics interactius que demostren efectes de paràmetres, visita [aquesta aplicació web] (https://basnijholt.github.io/adaptive-lighting). Per a més detalls, pots veure la [documentació oficial] (https://github.com/basnijholt/adaptive-lighting#readme)." + "description": "Configura un component d'Adaptive Lighting. Els noms de les opcions s'alineen amb la configuració de YAML. Si has definit aquesta entrada a YAML, aquí no apareixerà cap opció. Per veure gràfics interactius que demostren efectes de paràmetres, visita [aquesta aplicació web]({webapp_url}). Per a més detalls, pots veure la [documentació oficial]({docs_url})." } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json index c5a0f0e2..c7592dc5 100644 --- a/custom_components/adaptive_lighting/translations/de.json +++ b/custom_components/adaptive_lighting/translations/de.json @@ -25,7 +25,7 @@ "step": { "init": { "title": "Optionen für Adaptive Beleuchtung", - "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde. Interaktive Diagramme zur Veranschaulichung der Auswirkungen der Parameter finden Sie unter [dieser Webanwendung](https://basnijholt.github.io/adaptive-lighting). Weitere Details finden Sie in der [offiziellen Dokumentation](https://github.com/basnijholt/adaptive-lighting#readme).", + "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde. Interaktive Diagramme zur Veranschaulichung der Auswirkungen der Parameter finden Sie unter [dieser Webanwendung]({webapp_url}). Weitere Details finden Sie in der [offiziellen Dokumentation]({docs_url}).", "data": { "lights": "Lichter", "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt", diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index e39898fd..43bb2105 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -25,7 +25,7 @@ "step": { "init": { "title": "Adaptive Lighting options", - "description": "Configure an Adaptive Lighting component. Option names align with the YAML settings. If you've defined this entry in YAML, no options will appear here. For interactive graphs that demonstrate parameter effects, visit [this web app](https://basnijholt.github.io/adaptive-lighting). For further details, see the [official documentation](https://github.com/basnijholt/adaptive-lighting#readme).", + "description": "Configure an Adaptive Lighting component. Option names align with the YAML settings. If you've defined this entry in YAML, no options will appear here. For interactive graphs that demonstrate parameter effects, visit [this web app]({webapp_url}). For further details, see the [official documentation]({docs_url}).", "data": { "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟", "interval": "interval", diff --git a/custom_components/adaptive_lighting/translations/es.json b/custom_components/adaptive_lighting/translations/es.json index 66d6a7a8..eff8172d 100644 --- a/custom_components/adaptive_lighting/translations/es.json +++ b/custom_components/adaptive_lighting/translations/es.json @@ -46,7 +46,7 @@ "skip_redundant_commands": "skip_redundant_commands: Evitar mandar comandos de adaptación a luces cuyo estado ya sea el esperado. Reduce tráfico en la red y mejora la respuesta de la adaptación en ciertas situaciones. 📉Deshabilitar si el estado real de las luces se desincroniza con el estado registrado en Home Assistant.", "take_over_control": "take_over_control: Deshabilita Adaptive Lighting si otra fuente llama`light.turn_on` mientras las luces están encendidas y adaptándose. Cuidado, esto llama`homeassistant.update_entity` cada `interval`! 🔒" }, - "description": "Configura un componente Adaptive Lighting. Los nombres de las opciones se asemejan a las disponibles en la configuración YAML. Si has definido esta entrada en YAML, no aparecerá ninguna opción aquí. Para gráficos interactivos que demuestran los efectos de los parámetros, visita [esta web app](https://basnijholt.github.io/adaptive-lighting). Para más detalles, ver la [documentación oficial](https://github.com/basnijholt/adaptive-lighting#readme)." + "description": "Configura un componente Adaptive Lighting. Los nombres de las opciones se asemejan a las disponibles en la configuración YAML. Si has definido esta entrada en YAML, no aparecerá ninguna opción aquí. Para gráficos interactivos que demuestran los efectos de los parámetros, visita [esta web app]({webapp_url}). Para más detalles, ver la [documentación oficial]({docs_url})." } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/fi.json b/custom_components/adaptive_lighting/translations/fi.json index 6fd5f528..65233d1b 100644 --- a/custom_components/adaptive_lighting/translations/fi.json +++ b/custom_components/adaptive_lighting/translations/fi.json @@ -161,7 +161,7 @@ "sleep_rgb_or_color_temp": "Käytä joko `\"rgb_color\"` tai `\"color_temp\"` lepotilassa. 🌙", "max_sunset_time": "Aseta viimeisin virtuaalinen auringonlaskuaika (TT:MM:SS), jotta aikaisemmat auringonlaskut ovat mahdollisia. 🌇" }, - "description": "Määritä Adaptive Lighting -komponentti. Vaihtoehtojen nimet vastaavat YAML-asetuksia. Jos olet määrittänyt tämän merkinnän YAML:ssa, tässä ei näy vaihtoehtoja. Interaktiiviset kaaviot, jotka esittelevät parametrien vaikutuksia, on [tässä verkkosovelluksessa](https://basnijholt.github.io/adaptive-lighting). Lisätietoja löytyy [virallisesta dokumentaatiosta](https://github.com/basnijholt/adaptive-lighting#readme).", + "description": "Määritä Adaptive Lighting -komponentti. Vaihtoehtojen nimet vastaavat YAML-asetuksia. Jos olet määrittänyt tämän merkinnän YAML:ssa, tässä ei näy vaihtoehtoja. Interaktiiviset kaaviot, jotka esittelevät parametrien vaikutuksia, on [tässä verkkosovelluksessa]({webapp_url}). Lisätietoja löytyy [virallisesta dokumentaatiosta]({docs_url}).", "data": { "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Kun valot sytytetään ensimmäisen kerran. Jos asetuksena on \"true\", Adaptive Lighting mukautuu vain, jos \"light.turn_on\" kutsutaan määrittelemättä väriä tai kirkkautta. ❌🌈 Tämä esimerkiksi estää mukautumisen näkymää aktivoitaessa. Jos \"false\", Adaptive Lighting mukautuu riippumatta siitä, onko alkuperäisessä \"service_data\"-arvossa väri tai kirkkaus. Vaatii \"take_over_control\":n käyttöönoton. 🕵️", "multi_light_intercept": "multi_light_intercept: sieppaa ja mukauta light.turn_on-kutsut, jotka kohdistuvat useisiin valoihin. ➗⚠️ Tämä saattaa johtaa yksittäisen light.turn_on-kutsun jakamiseen useiksi kutsuiksi, esimerkiksi kun valot ovat eri kytkimissä. Vaadi `intercept`:n käyttöönotto.", diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json index c749ba6b..b816a008 100644 --- a/custom_components/adaptive_lighting/translations/fr.json +++ b/custom_components/adaptive_lighting/translations/fr.json @@ -18,7 +18,7 @@ "step": { "init": { "title": "Options d'éclairage adaptatif", - "description": "Configurer un composant d'éclairage adaptatif. Les noms correspondent aux paramètres YAML. Si vous avez défini cette entrée en YAML, aucune option n'apparaît ici. Pour les graphiques interactifs qui montrent les effets des paramètres, visiter [cette application web](https://basnijholt.github.io/adaptive-lighting). Pour plus de détail, voir la [documentation](https://github.com/basnijholt/adaptive-lighting#readme)", + "description": "Configurer un composant d'éclairage adaptatif. Les noms correspondent aux paramètres YAML. Si vous avez défini cette entrée en YAML, aucune option n'apparaît ici. Pour les graphiques interactifs qui montrent les effets des paramètres, visiter [cette application web]({webapp_url}). Pour plus de détail, voir la [documentation]({docs_url})", "data": { "lights": "lights : Liste d'\"entity_ids\" de lumières à controller (peu être vide). 🌟", "initial_transition": "initial_transition : Transition (en secondes) lorsque l'état d'une lampe passe d'« éteinte » à « allumée ».", diff --git a/custom_components/adaptive_lighting/translations/hu.json b/custom_components/adaptive_lighting/translations/hu.json index d349699e..d6f49af9 100644 --- a/custom_components/adaptive_lighting/translations/hu.json +++ b/custom_components/adaptive_lighting/translations/hu.json @@ -45,7 +45,7 @@ "include_config_in_attributes": "include_config_in_attributes: A kapcsoló összes opciójának attribútumként való megjelenítése a Home Assistantben, ha a beállítás értéke `igaz`. 📝" }, "title": "Adaptív világítás beállításai", - "description": "Egy Adaptív világítás komponens konfigurálása. Az opciók nevei a YAML-beállításokhoz igazodnak. Ha ezt a bejegyzést YAML-ben definiálta, itt nem jelennek meg beállítások. A paraméterek hatásait bemutató interaktív grafikonokért látogasson el [erre a webes alkalmazásra](https://basnijholt.github.io/adaptive-lighting). További részletekért olvasd el a [hivatalos dokumentációt](https://github.com/basnijholt/adaptive-lighting#readme)." + "description": "Egy Adaptív világítás komponens konfigurálása. Az opciók nevei a YAML-beállításokhoz igazodnak. Ha ezt a bejegyzést YAML-ben definiálta, itt nem jelennek meg beállítások. A paraméterek hatásait bemutató interaktív grafikonokért látogasson el [erre a webes alkalmazásra]({webapp_url}). További részletekért olvasd el a [hivatalos dokumentációt]({docs_url})." } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/id.json b/custom_components/adaptive_lighting/translations/id.json index 00876915..6e670c1a 100644 --- a/custom_components/adaptive_lighting/translations/id.json +++ b/custom_components/adaptive_lighting/translations/id.json @@ -179,7 +179,7 @@ "include_config_in_attributes": "include_config_in_attributes: Tampilkan semua opsi sebagai atribut pada sakelar di Home Assistant ketika diatur ke `true`. 📝" }, "title": "Opsi Pencahayaan Adaptif", - "description": "Konfigurasikan komponen Pencahayaan Adaptif. Nama opsi selaras dengan pengaturan YAML. Jika Anda telah menentukan entri ini di YAML, tidak ada opsi yang akan muncul di sini. Untuk grafik interaktif yang menunjukkan efek parameter, kunjungi [aplikasi web ini](https://basnijholt.github.io/adaptive-lighting). Untuk detail lebih lanjut, lihat [dokumentasi resmi](https://github.com/basnijholt/adaptive-lighting#readme)." + "description": "Konfigurasikan komponen Pencahayaan Adaptif. Nama opsi selaras dengan pengaturan YAML. Jika Anda telah menentukan entri ini di YAML, tidak ada opsi yang akan muncul di sini. Untuk grafik interaktif yang menunjukkan efek parameter, kunjungi [aplikasi web ini]({webapp_url}). Untuk detail lebih lanjut, lihat [dokumentasi resmi]({docs_url})." } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/ko.json b/custom_components/adaptive_lighting/translations/ko.json index 8a628f70..41807efb 100644 --- a/custom_components/adaptive_lighting/translations/ko.json +++ b/custom_components/adaptive_lighting/translations/ko.json @@ -18,7 +18,7 @@ "step": { "init": { "title": "적응형 조명 옵션", - "description": "적응형 조명 구성요소를 구성합니다. 옵션 이름은 YAML 설정과 일치합니다. 이 항목을 YAML에서 정의한 경우 여기에 옵션이 표시되지 않습니다. 매개변수 효과를 시연하는 인터랙티브 그래프는 [이 웹 앱](https://basnijholt.github.io/adaptive-lighting)에서 확인할 수 있습니다. 자세한 내용은 [공식 문서](https://github.com/basnijholt/adaptive-lighting#readme)를 참조하세요.", + "description": "적응형 조명 구성요소를 구성합니다. 옵션 이름은 YAML 설정과 일치합니다. 이 항목을 YAML에서 정의한 경우 여기에 옵션이 표시되지 않습니다. 매개변수 효과를 시연하는 인터랙티브 그래프는 [이 웹 앱]({webapp_url})에서 확인할 수 있습니다. 자세한 내용은 [공식 문서]({docs_url})를 참조하세요.", "data": { "lights": "조명: 제어될 조명 entity_ids의 목록 (비어 있을 수 있음). 🌟", "interval": "간격", diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json index d287ef0c..98a1b51b 100644 --- a/custom_components/adaptive_lighting/translations/nl.json +++ b/custom_components/adaptive_lighting/translations/nl.json @@ -18,7 +18,7 @@ "step": { "init": { "title": "Adaptieve verlichting instellingen", - "description": "Alle instellingen voor een Adaptieve verlichting component. De optienamen komen overeen met de YAML-instellingen. Er worden geen opties weergegeven als u het item `adaptive_lighting` hebt gedefinieerd in uw YAML-configuratie.\nVoor een demonstratie met interactieve grafieken, parameters en effecten, bezoek [deze web applicatie](https://basnijholt.github.io/adaptive-lighting). Voor verdere details, bekijk de [officiële documentatie](https://github.com/basnijholt/adaptive-lighting#readme).", + "description": "Alle instellingen voor een Adaptieve verlichting component. De optienamen komen overeen met de YAML-instellingen. Er worden geen opties weergegeven als u het item `adaptive_lighting` hebt gedefinieerd in uw YAML-configuratie.\nVoor een demonstratie met interactieve grafieken, parameters en effecten, bezoek [deze web applicatie]({webapp_url}). Voor verdere details, bekijk de [officiële documentatie]({docs_url}).", "data": { "lights": "Lampen: lijst van `light` entiteiten om te bedienen (kan leeg zijn). 🌟", "initial_transition": "initial_transition: Wanneer lichten van 'uit' naar 'aan' gaan. (seconden)", diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json index bebb4b18..28b22d3a 100644 --- a/custom_components/adaptive_lighting/translations/pl.json +++ b/custom_components/adaptive_lighting/translations/pl.json @@ -18,7 +18,7 @@ "step": { "init": { "title": "Opcje adaptacyjnego oświetlenia", - "description": "Konfiguracja komponentu Adaptacyjnego oświetlenia. Nazwy opcji odpowiadają ustawieniom YAML. Żadne opcje nie są wyświetlane, jeśli są zdefiniowane w konfiguracji YAML. Aby zobaczyć interaktywne wykresy demonstrujące działanie parametrów odwiedź [tą aplikację webową](https://basnijholt.github.io/adaptive-lighting). Aby zobaczyć więcej szczegółów odwiedź [oficjalną dokumentację](https://github.com/basnijholt/adaptive-lighting#readme).", + "description": "Konfiguracja komponentu Adaptacyjnego oświetlenia. Nazwy opcji odpowiadają ustawieniom YAML. Żadne opcje nie są wyświetlane, jeśli są zdefiniowane w konfiguracji YAML. Aby zobaczyć interaktywne wykresy demonstrujące działanie parametrów odwiedź [tą aplikację webową]({webapp_url}). Aby zobaczyć więcej szczegółów odwiedź [oficjalną dokumentację]({docs_url}).", "data": { "lights": "lights: Lista `entity_id`, które mają być kontrolowane (może być pusta). 🌟", "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (sekund)", diff --git a/custom_components/adaptive_lighting/translations/pt.json b/custom_components/adaptive_lighting/translations/pt.json index a491cc34..cc94ff24 100644 --- a/custom_components/adaptive_lighting/translations/pt.json +++ b/custom_components/adaptive_lighting/translations/pt.json @@ -72,7 +72,7 @@ "brightness_mode": "Brilho que irá ser usado. Possíveis valores são `default`, `linear` e `tanh`(usa `brightness_mode_time_dark` e `brightness_mode_time_light`). 📈" }, "title": "Opções da Iluminação Adaptativa", - "description": "Configure um componente da Iluminação Adaptativa. O nome das opções são as mesmas que as do YML. Se você já definiu essa configuração no YAML, nenhuma opção vai aparecer aqui. Para acessar um gráfico que demonstra o efeito dos parâmetros, acesse [esse app](https://basnijholt.github.io/adaptive-lighting). Para mais detalhes, veja a [documentação oficial](https://github.com/basnijholt/adaptive-lighting#readme).", + "description": "Configure um componente da Iluminação Adaptativa. O nome das opções são as mesmas que as do YML. Se você já definiu essa configuração no YAML, nenhuma opção vai aparecer aqui. Para acessar um gráfico que demonstra o efeito dos parâmetros, acesse [esse app]({webapp_url}). Para mais detalhes, veja a [documentação oficial]({docs_url}).", "data": { "lights": "lights: Lista das entity_ids das luzes para serem controladas (pode ser vazia). 🌟", "min_brightness": "min_brightness: Percentagem minima de brilho. 💡", diff --git a/custom_components/adaptive_lighting/translations/sk.json b/custom_components/adaptive_lighting/translations/sk.json index e1cf499f..0dbc8d7b 100644 --- a/custom_components/adaptive_lighting/translations/sk.json +++ b/custom_components/adaptive_lighting/translations/sk.json @@ -45,7 +45,7 @@ "send_split_delay": "Pauza (v ms) medzi príkazmi pri zapnutom `separate_turn_on_commands` pre svetlá, ktoré nepodporujú súčasné nastavenie jasu a teploty svetla. ⏲️" }, "title": "Nastavenia Adaptívneho osvetlenia", - "description": "Nastavte komponentu Adaptívneho osvetlenia. Názvy nastavení sú zhodné s názvami v súbore YAML. Ak ste túto položku nastavili už v YAML, tak tu sa nezobrazia žiadne možnosti nastavenia. Interaktívne grafy, ktoré zobrazujú vplyv nastavení, nájdete na [tejto webovej aplikácii](https://basnijholt.github.io/adaptive-lighting). Ďalšie informácie nájdete v [oficiálnej dokumentácii](https://github.com/basnijholt/adaptive-lighting#readme)." + "description": "Nastavte komponentu Adaptívneho osvetlenia. Názvy nastavení sú zhodné s názvami v súbore YAML. Ak ste túto položku nastavili už v YAML, tak tu sa nezobrazia žiadne možnosti nastavenia. Interaktívne grafy, ktoré zobrazujú vplyv nastavení, nájdete na [tejto webovej aplikácii]({webapp_url}). Ďalšie informácie nájdete v [oficiálnej dokumentácii]({docs_url})." } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/sl.json b/custom_components/adaptive_lighting/translations/sl.json index dc78acaa..011f1d89 100644 --- a/custom_components/adaptive_lighting/translations/sl.json +++ b/custom_components/adaptive_lighting/translations/sl.json @@ -44,7 +44,7 @@ "sleep_rgb_color": "RGB barva v načinu spanja (uporabljena, ko je \"sleep_rgb_or_color_temp\" nastavljeno na \"rgb_color\"). 🌈" }, "title": "Nastavitve prilagodljive osvetlitve", - "description": "Konfigurirajte komponento Adaptive Lighting. Imena možnosti so usklajena z nastavitvami v YAML. Če ste ta vnos definirali v YAML, tukaj ne bodo prikazane nobene možnosti. Za interaktivne grafe, ki ponazarjajo učinke parametrov, obiščite to [spletno aplikacijo](https://basnijholt.github.io/adaptive-lighting). Za dodatne podrobnosti glejte [uradno dokumentacijo](https://github.com/basnijholt/adaptive-lighting#readme)." + "description": "Konfigurirajte komponento Adaptive Lighting. Imena možnosti so usklajena z nastavitvami v YAML. Če ste ta vnos definirali v YAML, tukaj ne bodo prikazane nobene možnosti. Za interaktivne grafe, ki ponazarjajo učinke parametrov, obiščite to [spletno aplikacijo]({webapp_url}). Za dodatne podrobnosti glejte [uradno dokumentacijo]({docs_url})." } } }, diff --git a/custom_components/adaptive_lighting/translations/ta.json b/custom_components/adaptive_lighting/translations/ta.json index 92c552e5..50d5ecbc 100644 --- a/custom_components/adaptive_lighting/translations/ta.json +++ b/custom_components/adaptive_lighting/translations/ta.json @@ -149,7 +149,7 @@ "step": { "init": { "title": "தகவமைப்பு விளக்கு விருப்பங்கள்", - "description": "தகவமைப்பு விளக்கு கூறுகளை உள்ளமைக்கவும். விருப்பப் பெயர்கள் YAML அமைப்புகளுடன் சீரமைக்கப்படுகின்றன. இந்த உள்ளீட்டை நீங்கள் YAML இல் வரையறுத்திருந்தால், இங்கே எந்த விருப்பங்களும் தோன்றாது. அளவுரு விளைவுகளை நிரூபிக்கும் ஊடாடும் வரைபடங்களுக்கு, [இந்த வலை பயன்பாடு] (https://basnijholt.github.io/adaptive-lighting) ஐப் பார்வையிடவும். மேலும் விவரங்களுக்கு, [அதிகாரப்பூர்வ ஆவணங்கள்] (https://github.com/basnijholt/adaptive-lighting#readme) ஐப் பார்க்கவும்.", + "description": "தகவமைப்பு விளக்கு கூறுகளை உள்ளமைக்கவும். விருப்பப் பெயர்கள் YAML அமைப்புகளுடன் சீரமைக்கப்படுகின்றன. இந்த உள்ளீட்டை நீங்கள் YAML இல் வரையறுத்திருந்தால், இங்கே எந்த விருப்பங்களும் தோன்றாது. அளவுரு விளைவுகளை நிரூபிக்கும் ஊடாடும் வரைபடங்களுக்கு, [இந்த வலை பயன்பாடு]({webapp_url}) ஐப் பார்வையிடவும். மேலும் விவரங்களுக்கு, [அதிகாரப்பூர்வ ஆவணங்கள்]({docs_url}) ஐப் பார்க்கவும்.", "data": { "lights": "விளக்குகள்: கட்டுப்படுத்தப்பட வேண்டிய ஒளி நிறுவனம்_டுகளின் பட்டியல் (காலியாக இருக்கலாம்). .", "min_brightness": "min_brightness: குறைந்தபட்ச ஒளி விழுக்காடு. .", diff --git a/custom_components/adaptive_lighting/translations/tr.json b/custom_components/adaptive_lighting/translations/tr.json index 3b5c2d6e..00f1459f 100644 --- a/custom_components/adaptive_lighting/translations/tr.json +++ b/custom_components/adaptive_lighting/translations/tr.json @@ -46,7 +46,7 @@ "sleep_rgb_or_color_temp": "Uyku modunda `\"rgb_color\"` veya `\"color_temp\"` kullanın. 🌙", "adapt_delay": "Işık açıldıktan sonra Adaptive Lighting’in değişiklikleri uygulamasına kadar bekleme süresi (saniye cinsinden). Titremeyi önlemeye yardımcı olabilir. ⏲️" }, - "description": "Bir Adaptive Lighting bileşenini yapılandırın. Seçenek adları YAML ayarlarıyla uyumludur. Eğer bu girdiyi YAML’da tanımladıysanız, burada seçenekler görünmez. \nParametrelerin etkilerini gösteren etkileşimli grafikler için [bu web uygulamasını](https://basnijholt.github.io/adaptive-lighting) ziyaret edebilirsiniz. Daha fazla bilgi için [resmi dokümantasyona](https://github.com/basnijholt/adaptive-lighting#readme) bakın." + "description": "Bir Adaptive Lighting bileşenini yapılandırın. Seçenek adları YAML ayarlarıyla uyumludur. Eğer bu girdiyi YAML’da tanımladıysanız, burada seçenekler görünmez. \nParametrelerin etkilerini gösteren etkileşimli grafikler için [bu web uygulamasını]({webapp_url}) ziyaret edebilirsiniz. Daha fazla bilgi için [resmi dokümantasyona]({docs_url}) bakın." } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/ur.json b/custom_components/adaptive_lighting/translations/ur.json index 67f3d3e0..1f04e5f6 100644 --- a/custom_components/adaptive_lighting/translations/ur.json +++ b/custom_components/adaptive_lighting/translations/ur.json @@ -179,7 +179,7 @@ "include_config_in_attributes": "include_config_in_attributes: 'سچ' پر سیٹ ہونے پر ہوم اسسٹنٹ میں سوئچ پر خصوصیات کے طور پر تمام اختیارات دکھائیں۔ 📝" }, "title": "مطابقت پذیر روشنی کے اختیارات", - "description": "ایک مطابقت پذیر لائٹنگ جزو تشکیل دیں۔ آپشن کے نام YAML کی ترتیبات کے ساتھ مطابقت رکھتے ہیں۔ اگر آپ نے YAML میں اس اندراج کی وضاحت کی ہے تو ، یہاں کوئی آپشن ظاہر نہیں ہوگا۔ انٹرایکٹو گراف کے لئے جو پیرامیٹر کے اثرات کو ظاہر کرتے ہیں ، ملاحظہ کریں [اس ویب ایپ] (https://basnijholt.github.io/adaptive-lighting)۔ مزید تفصیلات کے لئے ، [سرکاری دستاویزات] (https://github.com/basnijholt/adaptive-lighting#readme) ملاحظہ کریں۔" + "description": "ایک مطابقت پذیر لائٹنگ جزو تشکیل دیں۔ آپشن کے نام YAML کی ترتیبات کے ساتھ مطابقت رکھتے ہیں۔ اگر آپ نے YAML میں اس اندراج کی وضاحت کی ہے تو ، یہاں کوئی آپشن ظاہر نہیں ہوگا۔ انٹرایکٹو گراف کے لئے جو پیرامیٹر کے اثرات کو ظاہر کرتے ہیں ، ملاحظہ کریں [اس ویب ایپ]({webapp_url})۔ مزید تفصیلات کے لئے ، [سرکاری دستاویزات]({docs_url}) ملاحظہ کریں۔" } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/zh-Hans.json b/custom_components/adaptive_lighting/translations/zh-Hans.json index 0d501731..1fd9d3da 100644 --- a/custom_components/adaptive_lighting/translations/zh-Hans.json +++ b/custom_components/adaptive_lighting/translations/zh-Hans.json @@ -18,7 +18,7 @@ "step": { "init": { "title": "自适应照明选项", - "description": "配置自适应照明组件。选项名称与YAML设置对齐。如果在YAML中定义了此条目,则此处不会显示任何选项。有关演示参数影响的交互式图表,请访问[此Web应用程序](https://basnijholt.github.io/adaptive-lighting)。有关更多详细信息,请参阅[官方文档](https://github.com/basnijholt/adaptive-lighting#readme)。", + "description": "配置自适应照明组件。选项名称与YAML设置对齐。如果在YAML中定义了此条目,则此处不会显示任何选项。有关演示参数影响的交互式图表,请访问[此Web应用程序]({webapp_url})。有关更多详细信息,请参阅[官方文档]({docs_url})。", "data": { "lights": "lights:要控制的灯光实体ID列表(可以为空)。🌟", "interval": "频率(interval)", diff --git a/scripts/setup-dependencies b/scripts/setup-dependencies index 7722c58d..d69bce3c 100755 --- a/scripts/setup-dependencies +++ b/scripts/setup-dependencies @@ -9,6 +9,30 @@ sed -i '/^mypy-dev/d' core/requirements_test.txt uv pip install -r core/requirements.txt uv pip install -r core/requirements_test.txt + +# HA 2026.4+ imports aiohasupervisor from tests/components/conftest.py +# but pins it in requirements_test_all.txt instead of requirements_test.txt. +aiohasupervisor_req="" +if [[ -f core/requirements_test_all.txt ]]; then + aiohasupervisor_req="$(grep -m1 '^aiohasupervisor' core/requirements_test_all.txt || true)" +fi +if [[ -n "${aiohasupervisor_req}" ]]; then + uv pip install "${aiohasupervisor_req}" +fi + +# HA 2026.4+ validates service translations through translations/en.json. +# The core checkout keeps English source strings in strings.json, so seed en.json +# in the temporary CI checkout before tests load integrations. +for strings_file in core/homeassistant/components/*/strings.json; do + [[ -f "${strings_file}" ]] || continue + translations_dir="$(dirname "${strings_file}")/translations" + en_translation="${translations_dir}/en.json" + if [[ ! -f "${en_translation}" ]]; then + mkdir -p "${translations_dir}" + cp "${strings_file}" "${en_translation}" + fi +done + uv pip install -e core/ uv pip install ulid-transform # this is in Adaptive-lighting's manifest.json uv pip install $(python test_dependencies.py) diff --git a/scripts/setup-devcontainer b/scripts/setup-devcontainer index fb5bf472..31ad774e 100755 --- a/scripts/setup-devcontainer +++ b/scripts/setup-devcontainer @@ -15,7 +15,7 @@ pip install \ pip cache purge -uv venv --clear --python 3.13 +uv venv --clear --python 3.14.2 ./scripts/setup-dependencies ./scripts/setup-symlinks uv run pre-commit install-hooks diff --git a/scripts/update-test-matrix.py b/scripts/update-test-matrix.py index 1d06d992..c37b7f25 100755 --- a/scripts/update-test-matrix.py +++ b/scripts/update-test-matrix.py @@ -82,9 +82,13 @@ def get_python_version(ha_version: str) -> str: """Determine Python version based on HA Core version.""" parts = ha_version.split(".") year, month = int(parts[0]), int(parts[1]) - # 2024.x and 2025.1 use Python 3.12, 2025.2+ use Python 3.13 + # 2024.x and 2025.1 use Python 3.12. + # 2025.2 through 2026.2 use Python 3.13. + # 2026.3+ uses Python 3.14. if year == 2024 or (year == 2025 and month == 1): return "3.12" + if year > 2026 or (year == 2026 and month >= 3): + return "3.14.2" return "3.13" @@ -97,7 +101,7 @@ def generate_matrix_yaml(versions: list[str]) -> str: lines.append(f' python-version: "{python_ver}"') # Add dev version lines.append(' - core-version: "dev"') - lines.append(' python-version: "3.13"') + lines.append(' python-version: "3.14.2"') return "\n".join(lines) diff --git a/tests/test_switch.py b/tests/test_switch.py index 3da5c967..470e0e25 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -242,15 +242,25 @@ async def setup_lights(hass: HomeAssistant, with_group: bool = False): await lights[0].async_turn_on() await lights[1].async_turn_on() + for light in lights[2:]: + await light.async_turn_off() for light in lights: - light._attr_brightness = 255 + set_light_brightness(light, 255) light._attr_color_temp = 250 + light.async_write_ha_state() assert all(hass.states.get(light.entity_id) is not None for light in lights) return lights +def set_light_brightness(light: LightTemplate, brightness: int) -> None: + """Set brightness across Home Assistant template light internals.""" + if hasattr(light, "_brightness"): + light._brightness = brightness + light._attr_brightness = brightness + + async def setup_lights_and_switch( hass, extra_conf=None, @@ -352,14 +362,14 @@ def create_transition_events( async def test_adaptive_lighting_switches(hass): """Test switches created for adaptive_lighting integration.""" - entry, _ = await setup_switch(hass, {}) + entry, switch = await setup_switch(hass, {}) assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 4 assert set(hass.states.async_entity_ids(SWITCH_DOMAIN)) == { - ENTITY_SWITCH, - ENTITY_SLEEP_MODE_SWITCH, - ENTITY_ADAPT_COLOR_SWITCH, - ENTITY_ADAPT_BRIGHTNESS_SWITCH, + switch.entity_id, + switch.sleep_mode_switch.entity_id, + switch.adapt_color_switch.entity_id, + switch.adapt_brightness_switch.entity_id, } assert ATTR_ADAPTIVE_LIGHTING_MANAGER in hass.data[DOMAIN] assert entry.entry_id in hass.data[DOMAIN] @@ -500,7 +510,7 @@ async def test_light_settings(hass): await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ENTITY_SLEEP_MODE_SWITCH}, + {ATTR_ENTITY_ID: switch.sleep_mode_switch.entity_id}, blocking=True, ) await hass.async_block_till_done() @@ -520,7 +530,7 @@ async def test_light_settings(hass): await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: ENTITY_SLEEP_MODE_SWITCH}, + {ATTR_ENTITY_ID: switch.sleep_mode_switch.entity_id}, blocking=True, ) await hass.async_block_till_done() @@ -726,7 +736,7 @@ async def test_manual_control( ), manual_control # Check that toggling (sleep mode) switch resets manual control - for entity_id in [ENTITY_SWITCH, ENTITY_SLEEP_MODE_SWITCH]: + for entity_id in [switch.entity_id, switch.sleep_mode_switch.entity_id]: await change_manual_control(True) assert manual_control[ENTITY_LIGHT_1] await turn_switch(False, entity_id) @@ -806,7 +816,7 @@ async def test_manual_control( DOMAIN, SERVICE_APPLY, { - ATTR_ENTITY_ID: ENTITY_SWITCH, + ATTR_ENTITY_ID: switch.entity_id, CONF_LIGHTS: [ENTITY_LIGHT_1], CONF_TURN_ON_LIGHTS: True, }, @@ -1058,7 +1068,7 @@ async def test_apply_service(hass): DOMAIN, SERVICE_APPLY, { - ATTR_ENTITY_ID: ENTITY_SWITCH, + ATTR_ENTITY_ID: switch.entity_id, CONF_LIGHTS: [entity_id], CONF_TURN_ON_LIGHTS: True, **kwargs, @@ -1275,7 +1285,7 @@ async def test_state_change_handlers(hass): await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ENTITY_SLEEP_MODE_SWITCH}, + {ATTR_ENTITY_ID: switch.sleep_mode_switch.entity_id}, blocking=True, ) await hass.async_block_till_done() @@ -1647,7 +1657,7 @@ async def test_change_switch_settings_service(hass): DOMAIN, SERVICE_CHANGE_SWITCH_SETTINGS, { - ATTR_ENTITY_ID: ENTITY_SWITCH, + ATTR_ENTITY_ID: switch.entity_id, **kwargs, }, blocking=True, @@ -2955,13 +2965,14 @@ async def test_detect_non_ha_changes_with_separate_turn_on_commands(hass): ATTR_COLOR_TEMP_KELVIN in last_sd or ATTR_RGB_COLOR in last_sd ), f"color missing from last_service_data after split calls: {last_sd}" - al_brightness = light._brightness + al_brightness = light.brightness + assert al_brightness is not None switch.manager.manual_control[ENTITY_LIGHT_1] = LightControlAttributes.NONE manual_brightness = ( al_brightness - 120 if al_brightness >= 120 else al_brightness + 120 ) - light._brightness = manual_brightness + set_light_brightness(light, manual_brightness) async def _flush_attr_state(hass, entity_id): """Mimic a ZHA attribute report: write current hardware state to HA.""" @@ -2984,5 +2995,5 @@ async def test_detect_non_ha_changes_with_separate_turn_on_commands(hass): await update(force=False) assert ( - light._brightness == manual_brightness + light.brightness == manual_brightness ), f"AL overrode manual brightness {manual_brightness} with {al_brightness}" From 0a305a7a4c96d94edaf60641a0659eed906a1326 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:59:38 -0700 Subject: [PATCH 0964/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20docker/?= =?UTF-8?q?build-push-action=20action=20to=20v7=20(#1441)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index edea4bb5..190a87dc 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -39,7 +39,7 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest,enable={{is_default_branch}} - - uses: docker/build-push-action@v6 + - uses: docker/build-push-action@v7 with: context: . platforms: ${{ matrix.platform }} From 31762ba78d35c216908763306b032362448713ae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:00:10 -0700 Subject: [PATCH 0965/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20docker/?= =?UTF-8?q?metadata-action=20action=20to=20v6=20(#1440)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 190a87dc..92c94a03 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -30,7 +30,7 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | From 86091de0e7aae6b8ecca4707a8d3f148161bfad4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:00:21 -0700 Subject: [PATCH 0966/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20docker/?= =?UTF-8?q?login-action=20action=20to=20v4=20(#1437)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 92c94a03..d5c92c85 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v6 - uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} From c8c31be573d122a3ec6c30cae1299e4d14e7041f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:12:01 -0700 Subject: [PATCH 0967/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/upload-pages-artifact=20action=20to=20v5=20(#1467)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7eb1e082..64a24264 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -47,7 +47,7 @@ jobs: echo "Webapp integrated at site/simulator/" - name: Upload artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@v5 with: path: ./site From b787c533bfdfd5fc1dad224a72ae63cc165dcde1 Mon Sep 17 00:00:00 2001 From: Florian Date: Fri, 24 Apr 2026 22:12:20 +0200 Subject: [PATCH 0968/1077] fix: make setup-dependencies portable on macOS (#1463) Two issues prevented the script from running on macOS without manual intervention: 1. `sed -i` requires a backup suffix on macOS BSD sed. Replace with a portable `grep -v | mv` idiom that works on both macOS and Linux. 2. `python` is not in PATH on macOS by default. Replace with `python3`. Co-authored-by: Florian Horner Co-authored-by: Bas Nijholt --- scripts/setup-dependencies | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/setup-dependencies b/scripts/setup-dependencies index d69bce3c..f3948dd5 100755 --- a/scripts/setup-dependencies +++ b/scripts/setup-dependencies @@ -5,7 +5,7 @@ cd "$(dirname "$0")/.." # Remove mypy-dev from requirements_test.txt since the maintainer deletes old versions from PyPI. # We'll install the latest version separately below. # See: https://github.com/cdce8p/mypy-dev/issues/62 -sed -i '/^mypy-dev/d' core/requirements_test.txt +grep -v '^mypy-dev' core/requirements_test.txt > core/requirements_test.txt.tmp && mv core/requirements_test.txt.tmp core/requirements_test.txt uv pip install -r core/requirements.txt uv pip install -r core/requirements_test.txt @@ -35,7 +35,7 @@ done uv pip install -e core/ uv pip install ulid-transform # this is in Adaptive-lighting's manifest.json -uv pip install $(python test_dependencies.py) +uv pip install $(python3 test_dependencies.py) # Install the latest mypy-dev (not pinned since old versions get deleted from PyPI) uv pip install --upgrade mypy-dev From 17e43e042746e42f011c22a0480eaf809c5ec477 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:13:15 -0700 Subject: [PATCH 0969/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20astral-?= =?UTF-8?q?sh/setup-uv=20action=20to=20v8=20(#1468)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- .github/workflows/install_dependencies/action.yml | 2 +- .github/workflows/markdown-code-runner.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 64a24264..86367ad7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,7 +28,7 @@ jobs: python-version: '3.14.2' - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@v8.1.0 - name: Install dependencies run: uv sync --group docs diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index 4ab58169..910e5b35 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -32,7 +32,7 @@ runs: with: python-version: ${{ inputs.python-version }} - name: Set up UV - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@v8.1.0 - name: Install dependencies shell: bash run: | diff --git a/.github/workflows/markdown-code-runner.yml b/.github/workflows/markdown-code-runner.yml index efd76831..4ea1bb81 100644 --- a/.github/workflows/markdown-code-runner.yml +++ b/.github/workflows/markdown-code-runner.yml @@ -23,7 +23,7 @@ jobs: python-version: "3.14.2" - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@v8.1.0 - name: Update generated content run: ./scripts/update-generated-content From cb4eb7ef491cb895f4f7297c583e6cdd76696289 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:13:34 -0700 Subject: [PATCH 0970/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/deploy-pages=20action=20to=20v5=20(#1453)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 86367ad7..d359b54a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -61,4 +61,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 From 7dcad99d81082a573894ff715f637fc74884c2db Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:13:53 -0700 Subject: [PATCH 0971/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20docker/?= =?UTF-8?q?setup-buildx-action=20action=20to=20v4=20(#1439)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index d5c92c85..da5c74ef 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@v6 - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-buildx-action@v4 - uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} From 830c5589f7076ef5ef094a0fcc2996bbf36ba5cc Mon Sep 17 00:00:00 2001 From: Florian Date: Fri, 24 Apr 2026 22:30:18 +0200 Subject: [PATCH 0972/1077] fix: reduce log verbosity for self-triggered off-to-on warning (#1433) (#1434) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: reduce log verbosity for self-triggered off-to-on warning (#1433) Move full event object dump from warning to debug level in _off_to_on_state_event_is_from_turn_on() (switch.py:2717). For lights with large effect_list attributes (e.g. Govee lights with 130+ effects), the warning dumped the entire Event object including both old_state and new_state, creating log entries thousands of characters long. The warning now logs only entity_id and context.id, while the full event remains available at debug level for troubleshooting. Fixes #1433 * fix: correct indentation for _LOGGER.debug block * fix: correct indentation for _LOGGER.warning and _LOGGER.debug * fix: remove unintended encoding corruption, keep only log level change Reset switch.py to main and re-apply only the intended change: move the full event object from warning to debug level in _off_to_on_state_event_is_from_turn_on(). Addresses reviewer feedback about unintended Unicode corruption (→ and ≈ characters were corrupted to mojibake). Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Florian Horner Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/switch.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 91049970..b57aeaa7 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2718,12 +2718,16 @@ class AdaptiveLightingManager: "service", # adaptive_lighting.apply is allowed to turn on lights ): _LOGGER.warning( - "Detected an 'off' → 'on' event for '%s' with context.id='%s' and" - " event='%s', triggered by the adaptive_lighting integration itself," + "Detected an 'off' → 'on' event for '%s' with context.id='%s'," + " triggered by the adaptive_lighting integration itself," " which *should* not happen. If you see this please submit an issue with" " your full logs at https://github.com/basnijholt/adaptive-lighting", entity_id, off_to_on_event.context.id, + ) + _LOGGER.debug( + "Full 'off' → 'on' event for '%s': %s", + entity_id, off_to_on_event, ) turn_on_event: Event | None = self.turn_on_event.get(entity_id) From 89b5e9a14fc26c9cfdc5f56dadaa9deb964cacdc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:23:26 -0700 Subject: [PATCH 0973/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20docker/?= =?UTF-8?q?setup-qemu-action=20action=20to=20v4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squash merge Renovate GitHub Actions update. Validation: - GitHub Actions checks for PR #1438 passed before merge. --- .github/workflows/docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index da5c74ef..00e8d89a 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -22,7 +22,7 @@ jobs: platform: [linux/amd64, linux/arm64] steps: - uses: actions/checkout@v6 - - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-qemu-action@v4 - uses: docker/setup-buildx-action@v4 - uses: docker/login-action@v4 with: From ddaf851be3d47320762a85affb9996d40f1ff9a3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:55:04 -0700 Subject: [PATCH 0974/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20release?= =?UTF-8?q?-drafter/release-drafter=20action=20to=20v7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squash merge Renovate GitHub Actions update. Validation: - Fixed PR-time Release Drafter v7 execution by using dry-run for pull_request events. - GitHub Actions checks for PR #1445 passed before merge. --- .github/workflows/release-drafter.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index e3badf0f..973577d1 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -17,6 +17,8 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: release-drafter/release-drafter@v6 + - uses: release-drafter/release-drafter@v7 + with: + dry-run: ${{ github.event_name == 'pull_request' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From d4d3d50ada813d60b61d240c882f7bdab935755d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 1 Jul 2026 23:00:35 -0700 Subject: [PATCH 0975/1077] fix: replace deprecated `get_astral_location` with `get_astral_observer` (#1482) * fix: replace deprecated get_astral_location with get_astral_observer (#1481) HA 2026.7 deprecates homeassistant.helpers.sun.get_astral_location (removal planned for 2027.7) in favor of get_astral_observer, causing a deprecation warning in the HA logs. - Switch SunEvents/SunLightSettings from astral.location.Location to astral.Observer, using the astral.sun module functions (which return UTC times by default, matching the previous local=False calls). - Use get_astral_observer in switch.py, with a fallback for HA < 2026.7 that constructs the Observer directly from the HA config. - Update tests and the webapp simulator accordingly. * ci: handle removal of requirements_test_all.txt in HA 2026.8 dev HA core removed requirements_test_all.txt (home-assistant/core#171530), which made test_dependencies.py crash with FileNotFoundError and broke the dev pytest job and the Docker builds. Fall back to requirements_all.txt, which carries the same per-integration '# homeassistant.components.x' annotations. Also extend the aiohasupervisor pin lookup in scripts/setup-dependencies accordingly. * test: support modern template light config for HA 2026.6+ HA 2026.6 removed the legacy `light: platform: template` YAML format (home-assistant/core#169615), so setup_lights found no template platform on HA dev and every test using it failed with IndexError. Detect legacy support at runtime (PLATFORM_SCHEMA presence) and fall back to the modern `template:` config format. The group platform is set up before the template integration in the modern path, because setting up `template` also sets up the `light` domain, which would make a later async_setup_component(hass, LIGHT_DOMAIN, ...) a no-op. --- .../adaptive_lighting/color_and_brightness.py | 20 ++-- custom_components/adaptive_lighting/switch.py | 18 +++- scripts/setup-dependencies | 4 + test_dependencies.py | 4 + tests/test_color_and_brightness.py | 18 ++-- tests/test_switch.py | 89 ++++++++++++------ webapp/app.py | 8 +- webapp/color_and_brightness.py | 94 +++++++++++-------- 8 files changed, 160 insertions(+), 95 deletions(-) diff --git a/custom_components/adaptive_lighting/color_and_brightness.py b/custom_components/adaptive_lighting/color_and_brightness.py index f3ae3efe..c91278fc 100644 --- a/custom_components/adaptive_lighting/color_and_brightness.py +++ b/custom_components/adaptive_lighting/color_and_brightness.py @@ -11,17 +11,15 @@ from dataclasses import dataclass from datetime import UTC, timedelta from enum import Enum from functools import cached_property, partial -from typing import TYPE_CHECKING, Any, Literal, cast +from typing import Any, Literal, cast +import astral.sun from homeassistant.util.color import ( color_RGB_to_xy, color_temperature_to_rgb, color_xy_to_hs, ) -if TYPE_CHECKING: - import astral.location - class SunEvent(str, Enum): """A set of sun events that happen during a day.""" @@ -48,7 +46,7 @@ class SunEvents: """Track the state of the sun and associated light settings.""" name: str - astral_location: astral.location.Location + astral_observer: astral.Observer sunrise_time: datetime.time | None min_sunrise_time: datetime.time | None max_sunrise_time: datetime.time | None @@ -62,7 +60,7 @@ class SunEvents: def sunrise(self, dt: datetime.date) -> datetime.datetime: """Return the (adjusted) sunrise time for the given datetime.""" sunrise = ( - self.astral_location.sunrise(dt, local=False) + astral.sun.sunrise(self.astral_observer, dt) if self.sunrise_time is None else self._replace_time(dt, self.sunrise_time) ) + self.sunrise_offset @@ -77,7 +75,7 @@ class SunEvents: def sunset(self, dt: datetime.date) -> datetime.datetime: """Return the (adjusted) sunset time for the given datetime.""" sunset = ( - self.astral_location.sunset(dt, local=False) + astral.sun.sunset(self.astral_observer, dt) if self.sunset_time is None else self._replace_time(dt, self.sunset_time) ) + self.sunset_offset @@ -113,8 +111,8 @@ class SunEvents: and self.min_sunset_time is None and self.max_sunset_time is None ): - solar_noon = self.astral_location.noon(dt, local=False) - solar_midnight = self.astral_location.midnight(dt, local=False) + solar_noon = astral.sun.noon(self.astral_observer, dt) + solar_midnight = astral.sun.midnight(self.astral_observer, dt) return solar_noon, solar_midnight if sunset is None: @@ -208,7 +206,7 @@ class SunLightSettings: """Track the state of the sun and associated light settings.""" name: str - astral_location: astral.location.Location + astral_observer: astral.Observer adapt_until_sleep: bool max_brightness: int max_color_temp: int @@ -236,7 +234,7 @@ class SunLightSettings: """Return the SunEvents object.""" return SunEvents( name=self.name, - astral_location=self.astral_location, + astral_observer=self.astral_observer, sunrise_time=self.sunrise_time, sunrise_offset=self.sunrise_offset, min_sunrise_time=self.min_sunrise_time, diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index b57aeaa7..e80e6089 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -66,7 +66,6 @@ from homeassistant.helpers.event import ( async_track_time_interval, ) from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.sun import get_astral_location from homeassistant.util import slugify from homeassistant.util.color import ( color_temperature_to_rgb, @@ -164,6 +163,19 @@ if TYPE_CHECKING: from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import NoEventData, VolDictType +try: + from homeassistant.helpers.sun import get_astral_observer +except ImportError: # `get_astral_observer` was added in HA 2026.7 + from astral import Observer + + def get_astral_observer(hass: HomeAssistant) -> Observer: + """Get an astral observer for the current HA configuration.""" + return Observer( + hass.config.latitude, + hass.config.longitude, + hass.config.elevation, + ) + _LOGGER = logging.getLogger(__name__) @@ -944,11 +956,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) self._multi_light_intercept = False self._expand_light_groups() # updates manual control timers - location, _ = get_astral_location(self.hass) + observer = get_astral_observer(self.hass) self._sun_light_settings = SunLightSettings( name=self._name, - astral_location=location, + astral_observer=observer, adapt_until_sleep=data[CONF_ADAPT_UNTIL_SLEEP], max_brightness=data[CONF_MAX_BRIGHTNESS], max_color_temp=data[CONF_MAX_COLOR_TEMP], diff --git a/scripts/setup-dependencies b/scripts/setup-dependencies index f3948dd5..0a96dd77 100755 --- a/scripts/setup-dependencies +++ b/scripts/setup-dependencies @@ -12,9 +12,13 @@ uv pip install -r core/requirements_test.txt # HA 2026.4+ imports aiohasupervisor from tests/components/conftest.py # but pins it in requirements_test_all.txt instead of requirements_test.txt. +# HA 2026.8+ removed requirements_test_all.txt (home-assistant/core#171530); +# the pin lives in requirements_all.txt there. aiohasupervisor_req="" if [[ -f core/requirements_test_all.txt ]]; then aiohasupervisor_req="$(grep -m1 '^aiohasupervisor' core/requirements_test_all.txt || true)" +elif [[ -f core/requirements_all.txt ]]; then + aiohasupervisor_req="$(grep -m1 '^aiohasupervisor' core/requirements_all.txt || true)" fi if [[ -n "${aiohasupervisor_req}" ]]; then uv pip install "${aiohasupervisor_req}" diff --git a/test_dependencies.py b/test_dependencies.py index b92ff1fb..029e1beb 100644 --- a/test_dependencies.py +++ b/test_dependencies.py @@ -7,6 +7,10 @@ deps = defaultdict(list) components, packages = [], [] requirements = Path("core") / "requirements_test_all.txt" +if not requirements.exists(): + # Removed from HA core in 2026.8 (home-assistant/core#171530); the same + # per-integration annotations live in requirements_all.txt. + requirements = Path("core") / "requirements_all.txt" with requirements.open() as f: lines = f.readlines() diff --git a/tests/test_color_and_brightness.py b/tests/test_color_and_brightness.py index cc3e8c4d..32425055 100644 --- a/tests/test_color_and_brightness.py +++ b/tests/test_color_and_brightness.py @@ -9,7 +9,7 @@ from homeassistant.components.adaptive_lighting.color_and_brightness import ( SunEvents, ) -# Create a mock astral_location object +# Create a mock astral location object (its `.observer` is passed to `SunEvents`) location = Location(LocationInfo()) LAT_LONG_TZS = [ @@ -40,7 +40,7 @@ def test_replace_time(tzinfo_and_location): tzinfo, location = tzinfo_and_location sun_events = SunEvents( name="test", - astral_location=location, + astral_observer=location.observer, sunrise_time=None, min_sunrise_time=None, max_sunrise_time=None, @@ -61,7 +61,7 @@ def test_sunrise_without_offset(tzinfo_and_location): sun_events = SunEvents( name="test", - astral_location=location, + astral_observer=location.observer, sunrise_time=None, min_sunrise_time=None, max_sunrise_time=None, @@ -79,7 +79,7 @@ def test_sun_position_no_fixed_sunset_and_sunrise(tzinfo_and_location): tzinfo, location = tzinfo_and_location sun_events = SunEvents( name="test", - astral_location=location, + astral_observer=location.observer, sunrise_time=None, min_sunrise_time=None, max_sunrise_time=None, @@ -107,7 +107,7 @@ def test_sun_position_fixed_sunset_and_sunrise(tzinfo_and_location): tzinfo, location = tzinfo_and_location sun_events = SunEvents( name="test", - astral_location=location, + astral_observer=location.observer, sunrise_time=dt.time(6, 0), min_sunrise_time=None, max_sunrise_time=None, @@ -134,7 +134,7 @@ def test_noon_and_midnight(tzinfo_and_location): tzinfo, location = tzinfo_and_location sun_events = SunEvents( name="test", - astral_location=location, + astral_observer=location.observer, sunrise_time=None, min_sunrise_time=None, max_sunrise_time=None, @@ -153,7 +153,7 @@ def test_sun_events(tzinfo_and_location): tzinfo, location = tzinfo_and_location sun_events = SunEvents( name="test", - astral_location=location, + astral_observer=location.observer, sunrise_time=None, min_sunrise_time=None, max_sunrise_time=None, @@ -173,7 +173,7 @@ def test_prev_and_next_events(tzinfo_and_location): tzinfo, location = tzinfo_and_location sun_events = SunEvents( name="test", - astral_location=location, + astral_observer=location.observer, sunrise_time=None, min_sunrise_time=None, max_sunrise_time=None, @@ -193,7 +193,7 @@ def test_closest_event(tzinfo_and_location): tzinfo, location = tzinfo_and_location sun_events = SunEvents( name="test", - astral_location=location, + astral_observer=location.observer, sunrise_time=None, min_sunrise_time=None, max_sunrise_time=None, diff --git a/tests/test_switch.py b/tests/test_switch.py index 470e0e25..46957466 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -97,6 +97,7 @@ except ImportError: # HA < 2025.8 from homeassistant.components.template.light import LightTemplate +from homeassistant.components.template import light as template_light from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_AREA_ID, @@ -121,6 +122,10 @@ from homeassistant.util.color import color_temperature_mired_to_kelvin from tests.common import MockConfigEntry +# HA 2026.6 removed the legacy `light: platform: template` YAML format +# (home-assistant/core#169615); use the modern `template:` format there. +LEGACY_TEMPLATE_LIGHTS = hasattr(template_light, "PLATFORM_SCHEMA") + _LOGGER = logging.getLogger(__name__) SUNRISE = datetime.datetime( @@ -200,37 +205,65 @@ async def setup_switch(hass, extra_data) -> tuple[MockConfigEntry, AdaptiveSwitc async def setup_lights(hass: HomeAssistant, with_group: bool = False): """Set up 3 light entities using the 'template' platform.""" n = 3 if not with_group else 5 # last 2 will be put in a group - template_lights = { - f"light_{i}": { - "unique_id": f"light_{i}", - "friendly_name": f"light_{i}", - "turn_on": None, - "turn_off": None, - "set_level": None, - "set_temperature": None, - "set_color": None, - } - for i in range(1, n + 1) + + group_platform = { + "platform": "group", + "entities": ["light.light_4", "light.light_5"], + "name": "Light Group", + "unique_id": "light_group", + "all": "false", } - template_lights["light_3"]["supports_transition_template"] = True - platforms = [{"platform": "template", "lights": template_lights}] - if with_group: - platforms.append( - { - "platform": "group", - "entities": ["light.light_4", "light.light_5"], - "name": "Light Group", - "unique_id": "light_group", - "all": "false", - }, + if LEGACY_TEMPLATE_LIGHTS: + template_lights = { + f"light_{i}": { + "unique_id": f"light_{i}", + "friendly_name": f"light_{i}", + "turn_on": None, + "turn_off": None, + "set_level": None, + "set_temperature": None, + "set_color": None, + } + for i in range(1, n + 1) + } + template_lights["light_3"]["supports_transition_template"] = True + platforms = [{"platform": "template", "lights": template_lights}] + if with_group: + platforms.append(group_platform) + await async_setup_component( + hass, + LIGHT_DOMAIN, + {LIGHT_DOMAIN: platforms}, + ) + else: + if with_group: + # Setting up `template` below also sets up the `light` domain, + # after which `async_setup_component(hass, LIGHT_DOMAIN, ...)` + # would be a no-op, so the group platform must be set up first. + await async_setup_component( + hass, + LIGHT_DOMAIN, + {LIGHT_DOMAIN: [group_platform]}, + ) + modern_lights = [ + { + "name": f"light_{i}", + "unique_id": f"light_{i}", + "turn_on": None, + "turn_off": None, + "set_level": None, + "set_temperature": None, + "set_hs": None, + } + for i in range(1, n + 1) + ] + modern_lights[2]["supports_transition"] = "{{ true }}" + await async_setup_component( + hass, + "template", + {"template": {"light": modern_lights}}, ) - - await async_setup_component( - hass, - LIGHT_DOMAIN, - {LIGHT_DOMAIN: platforms}, - ) await hass.async_block_till_done() if with_group: diff --git a/webapp/app.py b/webapp/app.py index 68dbb2f9..d46debe1 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -8,8 +8,7 @@ from typing import Any import matplotlib.pyplot as plt import numpy as np import shinyswatch -from astral import LocationInfo -from astral.location import Location +from astral import Observer from homeassistant_util_color import color_temperature_to_rgb from shiny import App, render, ui @@ -298,7 +297,6 @@ def time_to_float(time: dt.time | dt.datetime) -> float: def _kw(input): - location = Location(LocationInfo(timezone=dt.timezone.utc)) return { "name": "Adaptive Lighting Simulator", "adapt_until_sleep": input.adapt_until_sleep(), @@ -324,8 +322,8 @@ def _kw(input): "max_sunrise_time": None, "min_sunset_time": None, "max_sunset_time": None, - "astral_location": location, - "timezone": location.timezone, + "astral_observer": Observer(), + "timezone": dt.timezone.utc, } diff --git a/webapp/color_and_brightness.py b/webapp/color_and_brightness.py index d636c674..85e351c5 100644 --- a/webapp/color_and_brightness.py +++ b/webapp/color_and_brightness.py @@ -9,27 +9,30 @@ import logging import math from dataclasses import dataclass from datetime import UTC, timedelta +from enum import Enum from functools import cached_property, partial -from typing import TYPE_CHECKING, Any, Literal, cast +from typing import Any, Literal, cast +import astral.sun from homeassistant_util_color import ( color_RGB_to_xy, color_temperature_to_rgb, color_xy_to_hs, ) -if TYPE_CHECKING: - import astral.location -# Same as homeassistant.const.SUN_EVENT_SUNRISE and homeassistant.const.SUN_EVENT_SUNSET -# We re-define them here to not depend on homeassistant in this file. -SUN_EVENT_SUNRISE = "sunrise" -SUN_EVENT_SUNSET = "sunset" +class SunEvent(str, Enum): + """A set of sun events that happen during a day.""" -SUN_EVENT_NOON = "solar_noon" -SUN_EVENT_MIDNIGHT = "solar_midnight" + # Same as homeassistant.const.SUN_EVENT_SUNRISE and homeassistant.const.SUN_EVENT_SUNSET + # We re-define them here to not depend on homeassistant in this file. + SUNRISE = "sunrise" + SUNSET = "sunset" + NOON = "solar_noon" + MIDNIGHT = "solar_midnight" -_ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT) + +_ORDER = (SunEvent.SUNRISE, SunEvent.NOON, SunEvent.SUNSET, SunEvent.MIDNIGHT) _ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))} utcnow: partial[datetime.datetime] = partial(datetime.datetime.now, UTC) @@ -43,7 +46,7 @@ class SunEvents: """Track the state of the sun and associated light settings.""" name: str - astral_location: astral.location.Location + astral_observer: astral.Observer sunrise_time: datetime.time | None min_sunrise_time: datetime.time | None max_sunrise_time: datetime.time | None @@ -57,7 +60,7 @@ class SunEvents: def sunrise(self, dt: datetime.date) -> datetime.datetime: """Return the (adjusted) sunrise time for the given datetime.""" sunrise = ( - self.astral_location.sunrise(dt, local=False) + astral.sun.sunrise(self.astral_observer, dt) if self.sunrise_time is None else self._replace_time(dt, self.sunrise_time) ) + self.sunrise_offset @@ -72,7 +75,7 @@ class SunEvents: def sunset(self, dt: datetime.date) -> datetime.datetime: """Return the (adjusted) sunset time for the given datetime.""" sunset = ( - self.astral_location.sunset(dt, local=False) + astral.sun.sunset(self.astral_observer, dt) if self.sunset_time is None else self._replace_time(dt, self.sunset_time) ) + self.sunset_offset @@ -108,8 +111,8 @@ class SunEvents: and self.min_sunset_time is None and self.max_sunset_time is None ): - solar_noon = self.astral_location.noon(dt, local=False) - solar_midnight = self.astral_location.midnight(dt, local=False) + solar_noon = astral.sun.noon(self.astral_observer, dt) + solar_midnight = astral.sun.midnight(self.astral_observer, dt) return solar_noon, solar_midnight if sunset is None: @@ -126,21 +129,21 @@ class SunEvents: noon = midnight + timedelta(hours=12) * (1 if midnight.hour < 12 else -1) return noon, midnight - def sun_events(self, dt: datetime.datetime) -> list[tuple[str, float]]: + def sun_events(self, dt: datetime.datetime) -> list[tuple[SunEvent, float]]: """Get the four sun event's timestamps at 'dt'.""" sunrise = self.sunrise(dt) sunset = self.sunset(dt) solar_noon, solar_midnight = self.noon_and_midnight(dt, sunset, sunrise) - events = [ - (SUN_EVENT_SUNRISE, sunrise.timestamp()), - (SUN_EVENT_SUNSET, sunset.timestamp()), - (SUN_EVENT_NOON, solar_noon.timestamp()), - (SUN_EVENT_MIDNIGHT, solar_midnight.timestamp()), + events: list[tuple[SunEvent, float]] = [ + (SunEvent.SUNRISE, sunrise.timestamp()), + (SunEvent.SUNSET, sunset.timestamp()), + (SunEvent.NOON, solar_noon.timestamp()), + (SunEvent.MIDNIGHT, solar_midnight.timestamp()), ] self._validate_sun_event_order(events) return events - def _validate_sun_event_order(self, events: list[tuple[str, float]]) -> None: + def _validate_sun_event_order(self, events: list[tuple[SunEvent, float]]) -> None: """Check if the sun events are in the expected order.""" events = sorted(events, key=lambda x: x[1]) events_names, _ = zip(*events, strict=True) @@ -154,7 +157,10 @@ class SunEvents: _LOGGER.error(msg) raise ValueError(msg) - def prev_and_next_events(self, dt: datetime.datetime) -> list[tuple[str, float]]: + def prev_and_next_events( + self, + dt: datetime.datetime, + ) -> list[tuple[SunEvent, float]]: """Get the previous and next sun event.""" events = [ event @@ -171,23 +177,26 @@ class SunEvents: (_, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt) h, x = ( (prev_ts, next_ts) - if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE) + if next_event in (SunEvent.SUNSET, SunEvent.SUNRISE) else (next_ts, prev_ts) ) # k = -1 between sunset and sunrise (sun below horizon) # k = 1 between sunrise and sunset (sun above horizon) - k = 1 if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_NOON) else -1 + k = 1 if next_event in (SunEvent.SUNSET, SunEvent.NOON) else -1 return k * (1 - ((target_ts - h) / (h - x)) ** 2) - def closest_event(self, dt: datetime.datetime) -> tuple[str, float]: + def closest_event( + self, + dt: datetime.datetime, + ) -> tuple[Literal[SunEvent.SUNRISE, SunEvent.SUNSET], float]: """Get the closest sunset or sunrise event.""" (prev_event, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt) - if SUN_EVENT_SUNRISE in (prev_event, next_event): - ts_event = prev_ts if prev_event == SUN_EVENT_SUNRISE else next_ts - return SUN_EVENT_SUNRISE, ts_event - if SUN_EVENT_SUNSET in (prev_event, next_event): - ts_event = prev_ts if prev_event == SUN_EVENT_SUNSET else next_ts - return SUN_EVENT_SUNSET, ts_event + if SunEvent.SUNRISE in (prev_event, next_event): + ts_event = prev_ts if prev_event == SunEvent.SUNRISE else next_ts + return SunEvent.SUNRISE, ts_event + if SunEvent.SUNSET in (prev_event, next_event): + ts_event = prev_ts if prev_event == SunEvent.SUNSET else next_ts + return SunEvent.SUNSET, ts_event msg = "No sunrise or sunset event found." raise ValueError(msg) @@ -197,7 +206,7 @@ class SunLightSettings: """Track the state of the sun and associated light settings.""" name: str - astral_location: astral.location.Location + astral_observer: astral.Observer adapt_until_sleep: bool max_brightness: int max_color_temp: int @@ -225,7 +234,7 @@ class SunLightSettings: """Return the SunEvents object.""" return SunEvents( name=self.name, - astral_location=self.astral_location, + astral_observer=self.astral_observer, sunrise_time=self.sunrise_time, sunrise_offset=self.sunrise_offset, min_sunrise_time=self.min_sunrise_time, @@ -249,7 +258,7 @@ class SunLightSettings: event, ts_event = self.sun.closest_event(dt) dark = self.brightness_mode_time_dark.total_seconds() light = self.brightness_mode_time_light.total_seconds() - if event == SUN_EVENT_SUNRISE: + if event == SunEvent.SUNRISE: brightness = scaled_tanh( dt.timestamp() - ts_event, x1=-dark, @@ -259,7 +268,7 @@ class SunLightSettings: y_min=self.min_brightness, y_max=self.max_brightness, ) - elif event == SUN_EVENT_SUNSET: + elif event == SunEvent.SUNSET: brightness = scaled_tanh( dt.timestamp() - ts_event, x1=-light, # shifted timestamp for the start of sunset @@ -269,6 +278,9 @@ class SunLightSettings: y_min=self.min_brightness, y_max=self.max_brightness, ) + else: + msg = "Unsupported sun event" + raise ValueError(msg) return clamp(brightness, self.min_brightness, self.max_brightness) def _brightness_pct_linear(self, dt: datetime.datetime) -> float: @@ -277,7 +289,7 @@ class SunLightSettings: # at ts_event + dt_end, brightness == end_brightness dark = self.brightness_mode_time_dark.total_seconds() light = self.brightness_mode_time_light.total_seconds() - if event == SUN_EVENT_SUNRISE: + if event == SunEvent.SUNRISE: brightness = lerp( dt.timestamp() - ts_event, x1=-dark, @@ -285,7 +297,7 @@ class SunLightSettings: y1=self.min_brightness, y2=self.max_brightness, ) - elif event == SUN_EVENT_SUNSET: + elif event == SunEvent.SUNSET: brightness = lerp( dt.timestamp() - ts_event, x1=-light, @@ -293,6 +305,9 @@ class SunLightSettings: y1=self.max_brightness, y2=self.min_brightness, ) + else: + msg = "Unsupported sun event" + raise ValueError(msg) return clamp(brightness, self.min_brightness, self.max_brightness) def brightness_pct(self, dt: datetime.datetime, is_sleep: bool) -> float | None: @@ -356,7 +371,8 @@ class SunLightSettings: force_rgb_color = True else: color_temp_kelvin = self.color_temp_kelvin(sun_position) - rgb_color = color_temperature_to_rgb(color_temp_kelvin) + r, g, b = color_temperature_to_rgb(color_temp_kelvin) + rgb_color = (round(r), round(g), round(b)) # backwards compatibility for versions < 1.3.1 - see #403 color_temp_mired: float = math.floor(1000000 / color_temp_kelvin) xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color) From 3638fb30138494aeb6cbb2f9f7dabf370eeed859 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 2 Jul 2026 08:08:14 -0700 Subject: [PATCH 0976/1077] fix: don't cancel adaptation when a light group turns on via a member with a reused context (#1483) * fix: don't cancel adaptation when a light group turns on via a member with a reused context (#1378) When a member of a light group is turned on (e.g., by a motion sensor automation) while the group is off, the group turns on as a side effect, but Home Assistant may reuse the context of the earlier turn_off call for the group's state change. just_turned_off() saw matching context IDs and treated the state change as a polling artifact, cancelling adaptation. - Check whether the off->on state change comes from a light.turn_on call before the matching-context polling-artifact check, so automations that turn a light off and back on with a single (automation) context adapt correctly. - For light groups, allow adaptation when a member's turn_on event falls between the group's on->off and off->on state changes, bounded on both sides so stale member events are never treated as explanatory. - Document that integration-level groups (e.g., Zigbee2MQTT groups) should not be nested inside HA Light Groups managed by Adaptive Lighting. * fix: time-bound the same-context turn_on check instead of reordering Address review findings: - Reordering the turn_on-service check above the matching-context check reintroduced stale-event false negatives: turn_on_event entries are never cleaned up, so a 'turn_on -> delay -> turn_off(transition)' automation (one shared context) would defeat the polling-artifact guard and AL could turn a light back on right after it was turned off. Restore main's check order and instead add a time-bounded own-turn_on check inside the matching-context branch, symmetric with the group-member check. This also avoids emitting the 'should not happen' warning for self-context polling artifacts. - Add a regression test for the stale same-context turn_on case. - Add an end-to-end test driving the event-bus listeners for the #1378 scenario (group kept in manager.lights, as in the reported setups). - Docs: drop the inaccurate 'expands only one level deep' claim; explain that integration-level groups cannot be expanded and nested groups make tracking unpredictable. --- README.md | 5 + custom_components/adaptive_lighting/switch.py | 68 +++++- docs/troubleshooting.md | 5 + tests/test_switch.py | 205 ++++++++++++++++++ 4 files changed, 282 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a26df24..a819f788 100644 --- a/README.md +++ b/README.md @@ -409,6 +409,11 @@ Expose only the group (not individual bulbs) in Home Assistant Dashboards and ex > :warning: **If you control lights individually, `manual_control` cannot behave correctly! If you need to control lights individually as well, use a [Home Assistant Light Group](https://www.home-assistant.io/integrations/group/).** +When mixing group types, avoid nesting: do not add integration-level groups (e.g., Zigbee2MQTT groups) to a [Home Assistant Light Group](https://www.home-assistant.io/integrations/group/) that is managed by Adaptive Lighting, and do not nest Home Assistant Light Groups inside each other. +Adaptive Lighting cannot expand an integration-level group into its member lights, and nested groups make it unpredictable which entity Adaptive Lighting tracks and adapts, which can prevent lights from being adapted at all (see [#1378](https://github.com/basnijholt/adaptive-lighting/issues/1378)). +Instead, add the individual light entities or a single Zigbee group directly to the Adaptive Lighting configuration. +Also note that bulbs turned on via a Zigbee group broadcast may briefly flash their last (cached) brightness and color before the adapted values arrive; this happens inside the bulbs and cannot be prevented by Home Assistant or Adaptive Lighting. + #### :rainbow: Light Colors Not Matching Bulbs from different manufacturers or models may have varying color temperature specifications. For instance, if you have two Adaptive Lighting configurations—one with only Philips Hue White Ambiance bulbs and another with a mix of Philips Hue White Ambiance and Sengled bulbs—the Philips Hue bulbs may appear to have different color temperatures despite having identical settings. diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index e80e6089..3b042702 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2746,7 +2746,45 @@ class AdaptiveLightingManager: id_off_to_on = off_to_on_event.context.id return turn_on_event is not None and id_off_to_on == turn_on_event.context.id - async def just_turned_off( # noqa: PLR0911 + def _member_turn_on_explains_group_turn_on( + self, + entity_id: str, + on_to_off_event: Event[EventStateChangedData], + off_to_on_event: Event[EventStateChangedData], + ) -> bool: + """Check if a light group's 'off' → 'on' is caused by a member's 'light.turn_on'. + + When a member of a light group is turned on while the group is off, the + group turns on as a side effect. Home Assistant may reuse the context of + an earlier 'light.turn_off' call for the group's state change (entities + keep their context for a few seconds), which makes the group's turn-on + look like a polling artifact of the turn-off. + See https://github.com/basnijholt/adaptive-lighting/issues/1378 + """ + state = self.hass.states.get(entity_id) + if state is None or not _is_light_group(state): + return False + members: list[str] = state.attributes[ATTR_ENTITY_ID] + for member in members: + member_turn_on = self.turn_on_event.get(member) + if ( + member_turn_on is not None + and on_to_off_event.time_fired + < member_turn_on.time_fired + <= off_to_on_event.time_fired + ): + _LOGGER.debug( + "just_turned_off: Light group '%s' turned on because its member" + " '%s' was turned on (context.id='%s'), so this is a legitimate" + " turn-on, not a polling artifact.", + entity_id, + member, + member_turn_on.context.id, + ) + return True + return False + + async def just_turned_off( # noqa: PLR0911, PLR0912 self, entity_id: str, ) -> bool: @@ -2774,6 +2812,34 @@ class AdaptiveLightingManager: return False if off_to_on_event.context.id == on_to_off_event.context.id: + # Matching context IDs usually mean a polling artifact (HA briefly + # reports 'on' while the light is still turning off). However, the + # context is also reused when e.g. one automation turns the light + # off and later back on, or when an integration writes the state + # with the entity's cached context. Only treat the state change as + # a legitimate turn-on if a 'light.turn_on' call for this light (or + # for a member of this light group) fired between the two state + # changes. + turn_on_event = self.turn_on_event.get(entity_id) + if ( + turn_on_event is not None + and on_to_off_event.time_fired + < turn_on_event.time_fired + <= off_to_on_event.time_fired + ): + _LOGGER.debug( + "just_turned_off: 'light.turn_on' was called for '%s' between its" + " 'on' → 'off' and 'off' → 'on' state changes, so this is a" + " legitimate turn-on, not a polling artifact.", + entity_id, + ) + return False + if self._member_turn_on_explains_group_turn_on( + entity_id, + on_to_off_event, + off_to_on_event, + ): + return False _LOGGER.debug( "just_turned_off: 'on' → 'off' state change has the same context.id as the" " 'off' → 'on' state change for '%s'. This is probably a false positive.", diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 22374949..cdff4487 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -71,6 +71,11 @@ Expose only the group (not individual bulbs) in Home Assistant Dashboards and ex > :warning: **If you control lights individually, `manual_control` cannot behave correctly! If you need to control lights individually as well, use a [Home Assistant Light Group](https://www.home-assistant.io/integrations/group/).** +When mixing group types, avoid nesting: do not add integration-level groups (e.g., Zigbee2MQTT groups) to a [Home Assistant Light Group](https://www.home-assistant.io/integrations/group/) that is managed by Adaptive Lighting, and do not nest Home Assistant Light Groups inside each other. +Adaptive Lighting cannot expand an integration-level group into its member lights, and nested groups make it unpredictable which entity Adaptive Lighting tracks and adapts, which can prevent lights from being adapted at all (see [#1378](https://github.com/basnijholt/adaptive-lighting/issues/1378)). +Instead, add the individual light entities or a single Zigbee group directly to the Adaptive Lighting configuration. +Also note that bulbs turned on via a Zigbee group broadcast may briefly flash their last (cached) brightness and color before the adapted values arrive; this happens inside the bulbs and cannot be prevented by Home Assistant or Adaptive Lighting. + #### :rainbow: Light Colors Not Matching Bulbs from different manufacturers or models may have varying color temperature specifications. For instance, if you have two Adaptive Lighting configurations—one with only Philips Hue White Ambiance bulbs and another with a mix of Philips Hue White Ambiance and Sengled bulbs—the Philips Hue bulbs may appear to have different color temperatures despite having identical settings. diff --git a/tests/test_switch.py b/tests/test_switch.py index 46957466..eabe226a 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -2429,6 +2429,211 @@ async def test_light_group( assert len(events) == 3 +def _state_changed_event(entity_id: str, ts: float, context: Context) -> Event: + return Event( + EVENT_STATE_CHANGED, + {"entity_id": entity_id}, + time_fired_timestamp=ts, + context=context, + ) + + +def _turn_on_service_event(entity_ids: list[str], ts: float, context: Context) -> Event: + return Event( + EVENT_CALL_SERVICE, + { + "domain": LIGHT_DOMAIN, + "service": SERVICE_TURN_ON, + "service_data": {ATTR_ENTITY_ID: entity_ids}, + }, + time_fired_timestamp=ts, + context=context, + ) + + +async def test_just_turned_off_group_context_reuse(hass, cleanup): + """Group 'off' → 'on' with a reused 'turn_off' context must still adapt. + + When a member of a light group is turned on (e.g., by a motion sensor + automation) while the group is off, the group turns on as a side effect, + but Home Assistant may reuse the context of the earlier 'turn_off' call + for the group's state change. `just_turned_off` used to treat this as a + polling artifact and cancel adaptation. + + Regression test for https://github.com/basnijholt/adaptive-lighting/issues/1378 + """ + await setup_lights(hass, with_group=True) + _, switch = await setup_switch(hass, {CONF_LIGHTS: ["light.light_group"]}) + await hass.async_block_till_done() + manager = switch.manager + + group = "light.light_group" + member = "light.light_4" + now = dt_util.utcnow().timestamp() + turn_off_context = Context() + + # The group was turned off 2 seconds ago... + manager.on_to_off_event[group] = _state_changed_event( + group, + now - 2, + turn_off_context, + ) + # ...then an automation turned on a member light with a fresh context... + manager.turn_on_event[member] = _turn_on_service_event( + [member], + now - 0.5, + Context(), + ) + # ...which turned the group back on, but HA reused the old turn_off context. + manager.off_to_on_event[group] = _state_changed_event( + group, + now, + turn_off_context, + ) + + # The member's turn_on explains the group's turn-on: adaptation must proceed. + assert not await manager.just_turned_off(group) + + # A member turn_on from *before* the group was turned off does not explain + # the group's turn-on: this must still be treated as a polling artifact. + manager.turn_on_event[member] = _turn_on_service_event( + [member], + now - 10, + Context(), + ) + assert await manager.just_turned_off(group) + + # Without any member turn_on event, the matching context IDs must still be + # treated as a polling artifact. + del manager.turn_on_event[member] + assert await manager.just_turned_off(group) + + +async def test_just_turned_off_same_automation_context(hass, cleanup): + """'turn_off' and 'turn_on' from one automation share a context. + + An automation calling 'light.turn_off' and later 'light.turn_on' reuses + its own context for both service calls, so the 'on' → 'off' and + 'off' → 'on' state changes have matching context IDs. The turn_on service + call must take precedence over the matching-context polling-artifact check. + """ + await setup_lights(hass) + _, switch = await setup_switch(hass, {CONF_LIGHTS: [ENTITY_LIGHT_1]}) + await hass.async_block_till_done() + manager = switch.manager + + now = dt_util.utcnow().timestamp() + automation_context = Context() + + manager.on_to_off_event[ENTITY_LIGHT_1] = _state_changed_event( + ENTITY_LIGHT_1, + now - 2, + automation_context, + ) + manager.turn_on_event[ENTITY_LIGHT_1] = _turn_on_service_event( + [ENTITY_LIGHT_1], + now - 0.5, + automation_context, + ) + manager.off_to_on_event[ENTITY_LIGHT_1] = _state_changed_event( + ENTITY_LIGHT_1, + now, + automation_context, + ) + assert not await manager.just_turned_off(ENTITY_LIGHT_1) + + # A stale turn_on with an unrelated context does not explain the + # 'off' → 'on' state change: still a polling artifact. + manager.turn_on_event[ENTITY_LIGHT_1] = _turn_on_service_event( + [ENTITY_LIGHT_1], + now - 10, + Context(), + ) + assert await manager.just_turned_off(ENTITY_LIGHT_1) + + # A stale turn_on *sharing the automation's context* but fired before the + # 'on' → 'off' state change (i.e., 'turn_on' → delay → 'turn_off' in one + # automation run) does not explain the 'off' → 'on' state change either: + # `turn_on_event` entries are never cleaned up, so without the time bounds + # this would defeat the polling-artifact detection. + manager.turn_on_event[ENTITY_LIGHT_1] = _turn_on_service_event( + [ENTITY_LIGHT_1], + now - 10, + automation_context, + ) + assert await manager.just_turned_off(ENTITY_LIGHT_1) + + +async def test_just_turned_off_group_context_reuse_end_to_end(hass, cleanup): + """Drive the issue #1378 scenario through the real event bus listeners. + + Unlike `test_just_turned_off_group_context_reuse`, which calls + `just_turned_off` directly, this test fires the service and state-changed + events on the bus. Light groups are normally expanded out of + `manager.lights`, but they can remain tracked in real setups (e.g., when a + group is nested inside another configured group or is unavailable during + setup), which is the configuration under which issue #1378 was reported. + """ + await setup_lights(hass, with_group=True) + _, switch = await setup_switch(hass, {CONF_LIGHTS: ["light.light_group"]}) + await hass.async_block_till_done() + manager = switch.manager + + group = "light.light_group" + member = "light.light_4" + assert member in manager.lights + # Simulate a setup in which the group entity itself remains tracked. + manager.lights.add(group) + + turn_off_context = Context() + # The group was turned off... + hass.bus.async_fire( + EVENT_STATE_CHANGED, + { + "entity_id": group, + "old_state": State(group, STATE_ON), + "new_state": State(group, STATE_OFF), + }, + context=turn_off_context, + ) + await hass.async_block_till_done() + assert group in manager.on_to_off_event + + # ...then an automation turned on a member light with a fresh context... + hass.bus.async_fire( + EVENT_CALL_SERVICE, + { + "domain": LIGHT_DOMAIN, + "service": SERVICE_TURN_ON, + "service_data": {ATTR_ENTITY_ID: [member]}, + }, + context=Context(), + ) + await hass.async_block_till_done() + assert member in manager.turn_on_event + + # ...which turned the group back on, but HA reused the old turn_off context. + with patch.object( + AdaptiveSwitch, + "_respond_to_off_to_on_event", + AsyncMock(), + ) as respond: + hass.bus.async_fire( + EVENT_STATE_CHANGED, + { + "entity_id": group, + "old_state": State(group, STATE_OFF), + "new_state": State(group, STATE_ON), + }, + context=turn_off_context, + ) + await hass.async_block_till_done() + + # Adaptation must not have been cancelled as a polling artifact. + respond.assert_called_once() + assert respond.call_args[0][0] == group + + @pytest.mark.parametrize("brightness_mode", ["linear", "tanh"]) @pytest.mark.parametrize(("dark", "light"), ([900, 1800], [1800, 900], [1800, 1800])) async def test_brightness_mode(hass, brightness_mode, dark, light): From 7afdf5bc5ca36bb17a999dba53ef84fc49c9b13e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 7 Jul 2026 09:38:26 -0700 Subject: [PATCH 0977/1077] Update manifest.json to v1.31.0 (#1486) --- custom_components/adaptive_lighting/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index fc854221..427b730a 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.30.1" + "version": "1.31.0" } From 4a87b5ef54a631b4ac9c3b3b248519a5be7ef0d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:02:37 -0700 Subject: [PATCH 0978/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20astral-?= =?UTF-8?q?sh/setup-uv=20action=20to=20v9=20(#1496)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 2 +- .github/workflows/install_dependencies/action.yml | 2 +- .github/workflows/markdown-code-runner.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d359b54a..99a5744e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,7 +28,7 @@ jobs: python-version: '3.14.2' - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v9.0.0 - name: Install dependencies run: uv sync --group docs diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index 910e5b35..a9bcceae 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -32,7 +32,7 @@ runs: with: python-version: ${{ inputs.python-version }} - name: Set up UV - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v9.0.0 - name: Install dependencies shell: bash run: | diff --git a/.github/workflows/markdown-code-runner.yml b/.github/workflows/markdown-code-runner.yml index 4ea1bb81..6f50157f 100644 --- a/.github/workflows/markdown-code-runner.yml +++ b/.github/workflows/markdown-code-runner.yml @@ -23,7 +23,7 @@ jobs: python-version: "3.14.2" - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v9.0.0 - name: Update generated content run: ./scripts/update-generated-content From f3af35f95eae5f23244fef441ea96027c6e9705c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 08:28:48 +0200 Subject: [PATCH 0979/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20astral-?= =?UTF-8?q?sh/setup-uv=20action=20to=20v10=20(#1501)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docs.yml | 2 +- .github/workflows/install_dependencies/action.yml | 2 +- .github/workflows/markdown-code-runner.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 99a5744e..77f66ca6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,7 +28,7 @@ jobs: python-version: '3.14.2' - name: Install uv - uses: astral-sh/setup-uv@v9.0.0 + uses: astral-sh/setup-uv@v10.0.1 - name: Install dependencies run: uv sync --group docs diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index a9bcceae..a4a77e38 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -32,7 +32,7 @@ runs: with: python-version: ${{ inputs.python-version }} - name: Set up UV - uses: astral-sh/setup-uv@v9.0.0 + uses: astral-sh/setup-uv@v10.0.1 - name: Install dependencies shell: bash run: | diff --git a/.github/workflows/markdown-code-runner.yml b/.github/workflows/markdown-code-runner.yml index 6f50157f..6c58609e 100644 --- a/.github/workflows/markdown-code-runner.yml +++ b/.github/workflows/markdown-code-runner.yml @@ -23,7 +23,7 @@ jobs: python-version: "3.14.2" - name: Install uv - uses: astral-sh/setup-uv@v9.0.0 + uses: astral-sh/setup-uv@v10.0.1 - name: Update generated content run: ./scripts/update-generated-content From 928d57f8be3699f78fadc59055b5a2c2af38d561 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 08:28:58 +0200 Subject: [PATCH 0980/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/setup-python=20action=20to=20v7=20(#1493)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docs.yml | 2 +- .github/workflows/install_dependencies/action.yml | 2 +- .github/workflows/markdown-code-runner.yml | 2 +- .github/workflows/pre-commit.yaml | 2 +- .github/workflows/update-test-matrix.yaml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 77f66ca6..52aab744 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.14.2' diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index a4a77e38..1e1e6691 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -28,7 +28,7 @@ runs: ref: ${{ inputs.core-version }} - name: Set up Python ${{ inputs.python-version }} id: python - uses: actions/setup-python@v6.1.0 + uses: actions/setup-python@v7.0.0 with: python-version: ${{ inputs.python-version }} - name: Set up UV diff --git a/.github/workflows/markdown-code-runner.yml b/.github/workflows/markdown-code-runner.yml index 6c58609e..6ca94545 100644 --- a/.github/workflows/markdown-code-runner.yml +++ b/.github/workflows/markdown-code-runner.yml @@ -18,7 +18,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14.2" diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 57263d00..901f2e0a 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -10,5 +10,5 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v7 - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/update-test-matrix.yaml b/.github/workflows/update-test-matrix.yaml index 5f948a67..cb3237b2 100644 --- a/.github/workflows/update-test-matrix.yaml +++ b/.github/workflows/update-test-matrix.yaml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.14.2" From afb447c4e81fcb74c7dc920a3a3243757d4b928a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 08:29:53 +0200 Subject: [PATCH 0981/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20pytz=20?= =?UTF-8?q?to=20v2026=20(#1436)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- webapp/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/requirements.txt b/webapp/requirements.txt index 882162f1..fd8793e1 100644 --- a/webapp/requirements.txt +++ b/webapp/requirements.txt @@ -2,5 +2,5 @@ # uv pip compile requirements.txt.in --output-file requirements.txt astral==2.2 # via -r requirements.txt.in -pytz==2023.3.post1 +pytz==2026.3.post1 # via astral From 19dd9723a32dfcc84441606c77dfe4801c249f92 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 08:30:13 +0200 Subject: [PATCH 0982/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20python?= =?UTF-8?q?=20to=20v3.14.7=20(#1419)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docs.yml | 2 +- .github/workflows/markdown-code-runner.yml | 2 +- .github/workflows/update-test-matrix.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 52aab744..8142e33d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v7 with: - python-version: '3.14.2' + python-version: '3.14.7' - name: Install uv uses: astral-sh/setup-uv@v10.0.1 diff --git a/.github/workflows/markdown-code-runner.yml b/.github/workflows/markdown-code-runner.yml index 6ca94545..a7d6a403 100644 --- a/.github/workflows/markdown-code-runner.yml +++ b/.github/workflows/markdown-code-runner.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v7 with: - python-version: "3.14.2" + python-version: "3.14.7" - name: Install uv uses: astral-sh/setup-uv@v10.0.1 diff --git a/.github/workflows/update-test-matrix.yaml b/.github/workflows/update-test-matrix.yaml index cb3237b2..e714cbf0 100644 --- a/.github/workflows/update-test-matrix.yaml +++ b/.github/workflows/update-test-matrix.yaml @@ -21,7 +21,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v7 with: - python-version: "3.14.2" + python-version: "3.14.7" - name: Update test matrix run: python scripts/update-test-matrix.py From a9fd62a1107e4af045fe28f03d961d7c7807dffc Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 08:35:50 +0200 Subject: [PATCH 0983/1077] docs: add cperuffo3 as a contributor for code (#1516) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index e2a901fd..c56b5bca 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1224,6 +1224,15 @@ "contributions": [ "translation" ] + }, + { + "login": "cperuffo3", + "name": "Corey Peruffo", + "avatar_url": "https://avatars.githubusercontent.com/u/87686305?v=4", + "profile": "https://github.com/cperuffo3", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index a819f788..fb2352e9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-134-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-135-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -668,6 +668,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark + From dce34134b6ea66000fbb986cab13ada7a2253996 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 08:37:09 +0200 Subject: [PATCH 0984/1077] docs: add imwithsam as a contributor for code (#1517) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .all-contributorsrc | 29 +++++++++++++++++++---------- README.md | 3 ++- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index c56b5bca..9dcd2773 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -87,7 +87,7 @@ }, { "login": "Repsionu", - "name": "Jüri Rebane", + "name": "J\u00fcri Rebane", "avatar_url": "https://avatars.githubusercontent.com/u/46962963?v=4", "profile": "https://github.com/Repsionu", "contributions": [ @@ -195,7 +195,7 @@ }, { "login": "Hypfer", - "name": "Sören Beye", + "name": "S\u00f6ren Beye", "avatar_url": "https://avatars.githubusercontent.com/u/974410?v=4", "profile": "http://hypfer.de/", "contributions": [ @@ -387,7 +387,7 @@ }, { "login": "brebtatv", - "name": "Tomáš Valigura", + "name": "Tom\u00e1\u0161 Valigura", "avatar_url": "https://avatars.githubusercontent.com/u/10747062?v=4", "profile": "https://github.com/brebtatv", "contributions": [ @@ -524,7 +524,7 @@ }, { "login": "letroll", - "name": "Julien Quiévreux", + "name": "Julien Qui\u00e9vreux", "avatar_url": "https://avatars.githubusercontent.com/u/255774?v=4", "profile": "http://www.latavernedutroll.fr", "contributions": [ @@ -642,7 +642,7 @@ }, { "login": "mstefany", - "name": "Martin Štefany", + "name": "Martin \u0160tefany", "avatar_url": "https://avatars.githubusercontent.com/u/57348587?v=4", "profile": "https://stefany.eu", "contributions": [ @@ -696,7 +696,7 @@ }, { "login": "jansigu", - "name": "Jan-Sigurd Sørensen", + "name": "Jan-Sigurd S\u00f8rensen", "avatar_url": "https://avatars.githubusercontent.com/u/8410766?v=4", "profile": "http://www.jan-sigurd.com", "contributions": [ @@ -849,7 +849,7 @@ }, { "login": "MrEbbinghaus", - "name": "Björn Ebbinghaus", + "name": "Bj\u00f6rn Ebbinghaus", "avatar_url": "https://avatars.githubusercontent.com/u/2965273?v=4", "profile": "https://blog.ebbinghaus.me/", "contributions": [ @@ -894,7 +894,7 @@ }, { "login": "TamilNeram", - "name": "தமிழ் நேரம்", + "name": "\u0ba4\u0bae\u0bbf\u0bb4\u0bcd \u0ba8\u0bc7\u0bb0\u0bae\u0bcd", "avatar_url": "https://avatars.githubusercontent.com/u/67970539?v=4", "profile": "https://github.com/TamilNeram", "contributions": [ @@ -939,7 +939,7 @@ }, { "login": "marazmarci", - "name": "Márton Maráz", + "name": "M\u00e1rton Mar\u00e1z", "avatar_url": "https://avatars.githubusercontent.com/u/1349654?v=4", "profile": "https://github.com/marazmarci", "contributions": [ @@ -1128,7 +1128,7 @@ }, { "login": "maksim2005UKR", - "name": "Горпиніч Максим Олександрович", + "name": "\u0413\u043e\u0440\u043f\u0438\u043d\u0456\u0447 \u041c\u0430\u043a\u0441\u0438\u043c \u041e\u043b\u0435\u043a\u0441\u0430\u043d\u0434\u0440\u043e\u0432\u0438\u0447", "avatar_url": "https://avatars.githubusercontent.com/u/233082001?v=4", "profile": "https://github.com/maksim2005UKR", "contributions": [ @@ -1233,6 +1233,15 @@ "contributions": [ "code" ] + }, + { + "login": "imwithsam", + "name": "Samson Brock", + "avatar_url": "https://avatars.githubusercontent.com/u/1934074?v=4", + "profile": "http://badmotivator.io/", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index fb2352e9..7873673b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-135-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-136-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -678,6 +678,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Add your contributions +
Bas Nijholt
Bas Nijholt

💻 🚧 🐛
Sven Serlier
Sven Serlier

📖
Will Puckett
Will Puckett

📖
vapescherov
vapescherov

💻
Travis Pew
Travis Pew

📖
Sindre Broch
Sindre Broch

📖
Denis Shulyaka
Denis Shulyaka

💻
Bas Nijholt
Bas Nijholt

💻 🚧 🐛
Sven Serlier
Sven Serlier

📖
Will Puckett
Will Puckett

📖
vapescherov
vapescherov

💻
Travis Pew
Travis Pew

📖
Sindre Broch
Sindre Broch

📖
Denis Shulyaka
Denis Shulyaka

💻
@RubenKelevra
@RubenKelevra

📖 💻
Jüri Rebane
Jüri Rebane

🌍
quantumlemur
quantumlemur

💻
Michael Kirsch
Michael Kirsch

💻
Nicholai Nissen
Nicholai Nissen

🌍
Martin Myhrman
Martin Myhrman

🌍
Michel Peterson
Michel Peterson

💻
@RubenKelevra
@RubenKelevra

📖 💻
Jüri Rebane
Jüri Rebane

🌍
quantumlemur
quantumlemur

💻
Michael Kirsch
Michael Kirsch

💻
Nicholai Nissen
Nicholai Nissen

🌍
Martin Myhrman
Martin Myhrman

🌍
Michel Peterson
Michel Peterson

💻
MangoScango
MangoScango

💻
Lynilia
Lynilia

🌍
LukaszP2
LukaszP2

🌍
Joscha Wagner
Joscha Wagner

🌍
skdzzz
skdzzz

🌍
Simon Gurcke
Simon Gurcke

💻
Sören Beye
Sören Beye

💻
MangoScango
MangoScango

💻
Lynilia
Lynilia

🌍
LukaszP2
LukaszP2

🌍
Joscha Wagner
Joscha Wagner

🌍
skdzzz
skdzzz

🌍
Simon Gurcke
Simon Gurcke

💻
Sören Beye
Sören Beye

💻
Hudson Brendon
Hudson Brendon

🌍
Gabriel Visser
Gabriel Visser

📖
Gleb
Gleb

🌍
Deleted user
Deleted user

🌍
Avi Miller
Avi Miller

📖 💻
Denys Dovhan
Denys Dovhan

🌍
David Stenbeck
David Stenbeck

📖
Hudson Brendon
Hudson Brendon

🌍
Gabriel Visser
Gabriel Visser

📖
Gleb
Gleb

🌍
Deleted user
Deleted user

🌍
Avi Miller
Avi Miller

📖 💻
Denys Dovhan
Denys Dovhan

🌍
David Stenbeck
David Stenbeck

📖
Kevin Addeman
Kevin Addeman

💻
covid10
covid10

🌍 💻
Michael Chisholm
Michael Chisholm

💻
Justin Paupore
Justin Paupore

💻
bedaes
bedaes

💻
awashingmachine
awashingmachine

🌍
Clayton Nummer
Clayton Nummer

💻
Kevin Addeman
Kevin Addeman

💻
covid10
covid10

🌍 💻
Michael Chisholm
Michael Chisholm

💻
Justin Paupore
Justin Paupore

💻
bedaes
bedaes

💻
awashingmachine
awashingmachine

🌍
Clayton Nummer
Clayton Nummer

💻
Robert Crandall
Robert Crandall

💻
Matt Forster
Matt Forster

💻
Mark Niemeyer
Mark Niemeyer

🌍 💻
Elliott Plack
Elliott Plack

📖
ngommers
ngommers

🌍
Robert Crandall
Robert Crandall

💻
Matt Forster
Matt Forster

💻
Mark Niemeyer
Mark Niemeyer

🌍 💻
Elliott Plack
Elliott Plack

📖
ngommers
ngommers

🌍
Andrew Berry
Andrew Berry

📖
Elliott Plack
Elliott Plack

📖
ngommers
ngommers

🌍
Andrew Berry
Andrew Berry

📖
Tomáš Valigura
Tomáš Valigura

🌍
Andrew Berry
Andrew Berry

📖
Tomáš Valigura
Tomáš Valigura

🌍
Benjamin Auquite
Benjamin Auquite

💻
Benjamin Auquite
Benjamin Auquite

💻
Skyler Carlson
Skyler Carlson

📖
Benjamin Auquite
Benjamin Auquite

💻
Skyler Carlson
Skyler Carlson

📖
Chris
Chris

💻
Benjamin Auquite
Benjamin Auquite

💻
Skyler Carlson
Skyler Carlson

📖
Chris
Chris

💻
Raman Gupta
Raman Gupta

💻
Skyler Carlson
Skyler Carlson

📖
Chris
Chris

💻
Raman Gupta
Raman Gupta

💻
igiannakas
igiannakas

💻
Tomáš Valigura
Tomáš Valigura

🌍
Benjamin Auquite
Benjamin Auquite

💻
Benjamin Auquite
Benjamin Auquite

💻 🐛
Skyler Carlson
Skyler Carlson

📖
Chris
Chris

💻
Raman Gupta
Raman Gupta

💻
Tomáš Valigura
Tomáš Valigura

🌍
Benjamin Auquite
Benjamin Auquite

💻 🐛
Benjamin Auquite
Benjamin Auquite

💻 🐛 🚧
Skyler Carlson
Skyler Carlson

📖
Chris
Chris

💻
Raman Gupta
Raman Gupta

💻
Hudson Brendon
Hudson Brendon

🌍
Gabriel Visser
Gabriel Visser

📖
Gleb
Gleb

🌍
Deleted user
Deleted user

🌍
Avi Miller
Avi Miller

📖 💻
Denys Dovhan
Denys Dovhan

🌍
David Stenbeck
David Stenbeck

📖
Hudson Brendon
Hudson Brendon

🌍
Gabriel Visser
Gabriel Visser

📖
Gleb
Gleb

🌍
Deleted user
Deleted user

🌍
Avi Miller
Avi Miller

📖 💻
Denys Dovhan
Denys Dovhan

🌍
David Stenbeck
David Stenbeck

📖
Chris
Chris

💻
Raman Gupta
Raman Gupta

💻
igiannakas
igiannakas

💻
Mario Guggenberger
Mario Guggenberger

💻
Raman Gupta
Raman Gupta

💻
igiannakas
igiannakas

💻
Mario Guggenberger
Mario Guggenberger

💻
Kendell R
Kendell R

🎨
Mario Guggenberger
Mario Guggenberger

💻
Kendell R
Kendell R

🎨
lukerix
lukerix

🌍
lukerix
lukerix

🌍
Michel Balzer
Michel Balzer

🌍
lukerix
lukerix

🌍
Maxime Bailleul
Maxime Bailleul

🌍
Michel Balzer
Michel Balzer

🌍
lukerix
lukerix

🌍
Maxime Bailleul
Maxime Bailleul

🌍
Michel Balzer
Michel Balzer

🌍
Enrico Gambini
Enrico Gambini

🌍
Maxime Bailleul
Maxime Bailleul

🌍
Michel Balzer
Michel Balzer

🌍
Enrico Gambini
Enrico Gambini

🌍
MirCore
MirCore

🌍
Michel Balzer
Michel Balzer

🌍
Enrico Gambini
Enrico Gambini

🌍
MirCore
MirCore

🌍
Fernando Belaza
Fernando Belaza

🌍
Enrico Gambini
Enrico Gambini

🌍
MirCore
MirCore

🌍
Fernando Belaza
Fernando Belaza

🌍
Vladimir Cravero
Vladimir Cravero

🌍
Fernando Belaza
Fernando Belaza

🌍
Vladimir Cravero
Vladimir Cravero

🌍
Julien Quiévreux
Julien Quiévreux

🌍
Julien Quiévreux
Julien Quiévreux

🌍
lightrabbit
lightrabbit

🌍
Julien Quiévreux
Julien Quiévreux

🌍
lightrabbit
lightrabbit

🌍
Arie6414
Arie6414

🌍
Julien Quiévreux
Julien Quiévreux

🌍
lightrabbit
lightrabbit

🌍
Arie6414
Arie6414

🌍
luixcaetano
luixcaetano

🌍
lightrabbit
lightrabbit

🌍
Arie6414
Arie6414

🌍
luixcaetano
luixcaetano

🌍
fmarcu
fmarcu

🌍
Arie6414
Arie6414

🌍
luixcaetano
luixcaetano

🌍
fmarcu
fmarcu

🌍
michaelkmoch
michaelkmoch

🌍
luixcaetano
luixcaetano

🌍
fmarcu
fmarcu

🌍
michaelkmoch
michaelkmoch

🌍
Fred
Fred

🌍
michaelkmoch
michaelkmoch

🌍
Fred
Fred

🌍
Z-weapon
Z-weapon

🌍
Z-weapon
Z-weapon

🌍
Kyle Bjordahl
Kyle Bjordahl

💻
Z-weapon
Z-weapon

🌍
Kyle Bjordahl
Kyle Bjordahl

💻
Olek Bruks
Olek Bruks

🌍
Z-weapon
Z-weapon

🌍
Kyle Bjordahl
Kyle Bjordahl

💻
Olek Bruks
Olek Bruks

🌍
Gabriele Baldassarre
Gabriele Baldassarre

🌍
Kyle Bjordahl
Kyle Bjordahl

💻
Olek Bruks
Olek Bruks

🌍
Gabriele Baldassarre
Gabriele Baldassarre

🌍
Pepijn Baart
Pepijn Baart

🌍
Z-weapon
Z-weapon

🌍
Kyle Bjordahl
Kyle Bjordahl

💻
Kyle Bjordahl
Kyle Bjordahl

💻 🐛
Olek Bruks
Olek Bruks

🌍
Gabriele Baldassarre
Gabriele Baldassarre

🌍
Pepijn Baart
Pepijn Baart

🌍
Olek Bruks
Olek Bruks

🌍
Gabriele Baldassarre
Gabriele Baldassarre

🌍
Pepijn Baart
Pepijn Baart

🌍
Artem Pastukhov
Artem Pastukhov

🌍
Gabriele Baldassarre
Gabriele Baldassarre

🌍
Pepijn Baart
Pepijn Baart

🌍
Artem Pastukhov
Artem Pastukhov

🌍
Martin Štefany
Martin Štefany

🌍
Artem Pastukhov
Artem Pastukhov

🌍
Martin Štefany
Martin Štefany

🌍
quenthal
quenthal

🌍
quenthal
quenthal

🌍
Luki72
Luki72

🌍
quenthal
quenthal

🌍
Luki72
Luki72

🌍
pantan-cymk
pantan-cymk

🌍
quenthal
quenthal

🌍
Luki72
Luki72

🌍
pantan-cymk
pantan-cymk

🌍
yousaf465
yousaf465

🌍
Luki72
Luki72

🌍
pantan-cymk
pantan-cymk

🌍
yousaf465
yousaf465

🌍
Pierre Belanger
Pierre Belanger

📖
pantan-cymk
pantan-cymk

🌍
yousaf465
yousaf465

🌍
Pierre Belanger
Pierre Belanger

📖
Jan-Sigurd Sørensen
Jan-Sigurd Sørensen

🌍
yousaf465
yousaf465

🌍
Pierre Belanger
Pierre Belanger

📖
Jan-Sigurd Sørensen
Jan-Sigurd Sørensen

🌍
EF01
EF01

🌍
Jan-Sigurd Sørensen
Jan-Sigurd Sørensen

🌍
EF01
EF01

🌍
Mr Snake
Mr Snake

🌍
Mr Snake
Mr Snake

🌍
hungrymachine1
hungrymachine1

🌍
Mr Snake
Mr Snake

🌍
hungrymachine1
hungrymachine1

🌍
4D4M-Github
4D4M-Github

🌍
Mr Snake
Mr Snake

🌍
hungrymachine1
hungrymachine1

🌍
4D4M-Github
4D4M-Github

🌍
Ivan
Ivan

🌍
hungrymachine1
hungrymachine1

🌍
4D4M-Github
4D4M-Github

🌍
Ivan
Ivan

🌍
Florent Cardoen
Florent Cardoen

🌍
4D4M-Github
4D4M-Github

🌍
Ivan
Ivan

🌍
Florent Cardoen
Florent Cardoen

🌍
moemeli
moemeli

🌍
Ivan
Ivan

🌍
Florent Cardoen
Florent Cardoen

🌍
moemeli
moemeli

🌍
saya6k
saya6k

🌍
moemeli
moemeli

🌍
saya6k
saya6k

🌍
droans
droans

💻
droans
droans

💻
Jonathan Kang
Jonathan Kang

💻
droans
droans

💻
Jonathan Kang
Jonathan Kang

💻
scuricvladimir
scuricvladimir

🌍
droans
droans

💻
Jonathan Kang
Jonathan Kang

💻
scuricvladimir
scuricvladimir

🌍
Pieter
Pieter

🌍
Jonathan Kang
Jonathan Kang

💻
scuricvladimir
scuricvladimir

🌍
Pieter
Pieter

🌍
san80068259
san80068259

🌍
scuricvladimir
scuricvladimir

🌍
Pieter
Pieter

🌍
san80068259
san80068259

🌍
Frosh
Frosh

💻
Pieter
Pieter

🌍
san80068259
san80068259

🌍
Frosh
Frosh

💻
Rafael Miranda
Rafael Miranda

🌍
Frosh
Frosh

💻
Rafael Miranda
Rafael Miranda

🌍
rVlad93
rVlad93

🌍
rVlad93
rVlad93

🌍
Björn Ebbinghaus
Björn Ebbinghaus

💻
rVlad93
rVlad93

🌍
Björn Ebbinghaus
Björn Ebbinghaus

💻
Marck
Marck

💻
rVlad93
rVlad93

🌍
Björn Ebbinghaus
Björn Ebbinghaus

💻
Marck
Marck

💻
Lucho Gizdov
Lucho Gizdov

🌍
Björn Ebbinghaus
Björn Ebbinghaus

💻
Marck
Marck

💻
Lucho Gizdov
Lucho Gizdov

🌍
MizterB
MizterB

💻
Marck
Marck

💻
Lucho Gizdov
Lucho Gizdov

🌍
MizterB
MizterB

💻
brietman
brietman

🌍
Lucho Gizdov
Lucho Gizdov

🌍
MizterB
MizterB

💻
brietman
brietman

🌍
தமிழ் நேரம்
தமிழ் நேரம்

🌍
brietman
brietman

🌍
தமிழ் நேரம்
தமிழ் நேரம்

🌍
Thunderstrike116
Thunderstrike116

🌍
Thunderstrike116
Thunderstrike116

🌍
immeteor2
immeteor2

🌍
Thunderstrike116
Thunderstrike116

🌍
immeteor2
immeteor2

🌍
Patrick Bassut
Patrick Bassut

🌍
Thunderstrike116
Thunderstrike116

🌍
immeteor2
immeteor2

🌍
Patrick Bassut
Patrick Bassut

🌍
Ricky Tigg
Ricky Tigg

🌍
immeteor2
immeteor2

🌍
Patrick Bassut
Patrick Bassut

🌍
Ricky Tigg
Ricky Tigg

🌍
Márton Maráz
Márton Maráz

💻
Patrick Bassut
Patrick Bassut

🌍
Ricky Tigg
Ricky Tigg

🌍
Márton Maráz
Márton Maráz

💻
Sara492
Sara492

🌍
Ricky Tigg
Ricky Tigg

🌍
Márton Maráz
Márton Maráz

💻
Sara492
Sara492

🌍
enpaga
enpaga

🌍
Sara492
Sara492

🌍
enpaga
enpaga

🌍
xuars
xuars

🌍
xuars
xuars

🌍
tinutac
tinutac

🌍
xuars
xuars

🌍
tinutac
tinutac

🌍
amelenty
amelenty

🌍
xuars
xuars

🌍
tinutac
tinutac

🌍
amelenty
amelenty

🌍
Rostyslav Dudka
Rostyslav Dudka

🌍
tinutac
tinutac

🌍
amelenty
amelenty

🌍
Rostyslav Dudka
Rostyslav Dudka

🌍
Helder Ferreira
Helder Ferreira

🌍
amelenty
amelenty

🌍
Rostyslav Dudka
Rostyslav Dudka

🌍
Helder Ferreira
Helder Ferreira

🌍
Piotr Laszczkowski
Piotr Laszczkowski

🌍
Rostyslav Dudka
Rostyslav Dudka

🌍
Helder Ferreira
Helder Ferreira

🌍
Piotr Laszczkowski
Piotr Laszczkowski

🌍
Reza
Reza

🌍
Piotr Laszczkowski
Piotr Laszczkowski

🌍
Reza
Reza

🌍
Luna Jernberg
Luna Jernberg

🌍
xuars
xuars

🌍
tinutac
tinutac

🌍
Default User
Default User

🌍
amelenty
amelenty

🌍
Rostyslav Dudka
Rostyslav Dudka

🌍
Helder Ferreira
Helder Ferreira

🌍
Piotr Laszczkowski
Piotr Laszczkowski

🌍
Reza
Reza

🌍
Reza
Reza

🌍
Luna Jernberg
Luna Jernberg

🌍
Reza
Reza

🌍
Luna Jernberg
Luna Jernberg

🌍
Jeff Wilson
Jeff Wilson

💻
Reza
Reza

🌍
Luna Jernberg
Luna Jernberg

🌍
Jeff Wilson
Jeff Wilson

💻
Rasmus Lundsgaard
Rasmus Lundsgaard

💻
Luna Jernberg
Luna Jernberg

🌍
Jeff Wilson
Jeff Wilson

💻
Rasmus Lundsgaard
Rasmus Lundsgaard

💻
Tom Matheussen
Tom Matheussen

💻
Jeff Wilson
Jeff Wilson

💻
Rasmus Lundsgaard
Rasmus Lundsgaard

💻
Tom Matheussen
Tom Matheussen

💻
ams2990
ams2990

💻
Rasmus Lundsgaard
Rasmus Lundsgaard

💻
Tom Matheussen
Tom Matheussen

💻
ams2990
ams2990

💻
DataGhost
DataGhost

💻
ams2990
ams2990

💻
DataGhost
DataGhost

💻
Furkan Kaya
Furkan Kaya

🌍
Furkan Kaya
Furkan Kaya

🌍
Rafael do Amaral Porciuncula
Rafael do Amaral Porciuncula

🌍
Furkan Kaya
Furkan Kaya

🌍
Rafael do Amaral Porciuncula
Rafael do Amaral Porciuncula

🌍
hhjuhl
hhjuhl

🌍
Furkan Kaya
Furkan Kaya

🌍
Rafael do Amaral Porciuncula
Rafael do Amaral Porciuncula

🌍
hhjuhl
hhjuhl

🌍
B.Athish
B.Athish

🌍
Rafael do Amaral Porciuncula
Rafael do Amaral Porciuncula

🌍
hhjuhl
hhjuhl

🌍
B.Athish
B.Athish

🌍
Горпиніч Максим Олександрович
Горпиніч Максим Олександрович

🌍
hhjuhl
hhjuhl

🌍
B.Athish
B.Athish

🌍
Горпиніч Максим Олександрович
Горпиніч Максим Олександрович

🌍
Masayuki Sugahara
Masayuki Sugahara

🌍
B.Athish
B.Athish

🌍
Горпиніч Максим Олександрович
Горпиніч Максим Олександрович

🌍
Masayuki Sugahara
Masayuki Sugahara

🌍
therealmate
therealmate

🌍
Masayuki Sugahara
Masayuki Sugahara

🌍
therealmate
therealmate

🌍
Dobby
Dobby

💻
Dobby
Dobby

💻
lenucksi
lenucksi

💻
Dobby
Dobby

💻
lenucksi
lenucksi

💻
edgimar
edgimar

💻
Dobby
Dobby

💻
lenucksi
lenucksi

💻
edgimar
edgimar

💻
Andrei LAZAROV
Andrei LAZAROV

📖
lenucksi
lenucksi

💻
edgimar
edgimar

💻
Andrei LAZAROV
Andrei LAZAROV

📖
Adam DeMuri
Adam DeMuri

💻
edgimar
edgimar

💻
Andrei LAZAROV
Andrei LAZAROV

📖
Adam DeMuri
Adam DeMuri

💻
Natanael
Natanael

🌍
Andrei LAZAROV
Andrei LAZAROV

📖
Adam DeMuri
Adam DeMuri

💻
Natanael
Natanael

🌍
Yllelder Bamir
Yllelder Bamir

🌍
Natanael
Natanael

🌍
Yllelder Bamir
Yllelder Bamir

🌍
Esspel
Esspel

🌍
Esspel
Esspel

🌍
Corey Peruffo
Corey Peruffo

💻
Samson Brock
Samson Brock

💻
From efee60cb9a61408b926bfb8c2a0a9479abcb9079 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 08:37:34 +0200 Subject: [PATCH 0985/1077] docs: add Dennis-Dekker as a contributor for code (#1518) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 9dcd2773..6cdb0789 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1242,6 +1242,15 @@ "contributions": [ "code" ] + }, + { + "login": "Dennis-Dekker", + "name": "Dennis Dekker", + "avatar_url": "https://avatars.githubusercontent.com/u/48018095?v=4", + "profile": "https://github.com/Dennis-Dekker", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 7873673b..a0cc031f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-136-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-137-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -679,6 +679,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Samson Brock
Samson Brock

💻 + Dennis Dekker
Dennis Dekker

💻 From cc93af84dea7b381f95a05983c1c12f79fc76c2b Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 08:37:57 +0200 Subject: [PATCH 0986/1077] docs: add proscar87 as a contributor for code (#1519) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 6cdb0789..3d8fb965 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1251,6 +1251,15 @@ "contributions": [ "code" ] + }, + { + "login": "proscar87", + "name": "proscar87", + "avatar_url": "https://avatars.githubusercontent.com/u/68169114?v=4", + "profile": "https://github.com/proscar87", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index a0cc031f..4695cf81 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-137-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-138-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -680,6 +680,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Samson Brock
Samson Brock

💻 Dennis Dekker
Dennis Dekker

💻 + proscar87
proscar87

💻 From 5df9b30d324a18cbf1482bafb08ab119653d01ff Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 08:45:48 +0200 Subject: [PATCH 0987/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20actions?= =?UTF-8?q?/checkout=20action=20to=20v7=20(#1479)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .github/workflows/docker-build.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/hassfest.yaml | 2 +- .github/workflows/install_dependencies/action.yml | 4 ++-- .github/workflows/main-to-master-sync.yml | 2 +- .github/workflows/markdown-code-runner.yml | 2 +- .github/workflows/pre-commit.yaml | 2 +- .github/workflows/pytest.yaml | 2 +- .github/workflows/update-test-matrix.yaml | 2 +- .github/workflows/validate.yml | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 00e8d89a..4783ef38 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -21,7 +21,7 @@ jobs: matrix: platform: [linux/amd64, linux/arm64] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: docker/setup-qemu-action@v4 - uses: docker/setup-buildx-action@v4 - uses: docker/login-action@v4 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8142e33d..2204a1ef 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Python uses: actions/setup-python@v7 diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml index b063de53..7313c4cc 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/hassfest.yaml @@ -11,5 +11,5 @@ jobs: validate_hassfest: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v6.0.2" + - uses: "actions/checkout@v7.0.1" - uses: home-assistant/actions/hassfest@master diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index 1e1e6691..9b8dbb08 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -14,14 +14,14 @@ runs: using: "composite" steps: - name: Check out code from GitHub - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: ${{ github.repository }} ref: ${{ github.ref }} persist-credentials: false fetch-depth: 0 - name: Check out code from GitHub - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: home-assistant/core path: core diff --git a/.github/workflows/main-to-master-sync.yml b/.github/workflows/main-to-master-sync.yml index 424f50b3..a2756894 100644 --- a/.github/workflows/main-to-master-sync.yml +++ b/.github/workflows/main-to-master-sync.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: main fetch-depth: 0 diff --git a/.github/workflows/markdown-code-runner.yml b/.github/workflows/markdown-code-runner.yml index a7d6a403..279a7f8a 100644 --- a/.github/workflows/markdown-code-runner.yml +++ b/.github/workflows/markdown-code-runner.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code from GitHub - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} ref: ${{ github.head_ref || github.ref }} diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 901f2e0a..e67ef449 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -9,6 +9,6 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-python@v7 - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index dd0e4559..8b2614f0 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -52,7 +52,7 @@ jobs: python-version: "3.14.2" steps: - name: Check out code from GitHub - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Install Home Assistant uses: ./.github/workflows/install_dependencies diff --git a/.github/workflows/update-test-matrix.yaml b/.github/workflows/update-test-matrix.yaml index e714cbf0..e67ae227 100644 --- a/.github/workflows/update-test-matrix.yaml +++ b/.github/workflows/update-test-matrix.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Python uses: actions/setup-python@v7 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3fa46b4e..83ce8ad9 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -11,7 +11,7 @@ jobs: validate_hacs: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v6" + - uses: "actions/checkout@v7" - name: HACS validation uses: "hacs/action@main" with: From 6d46b823135efa8c1746e301a4f044ffc426ed23 Mon Sep 17 00:00:00 2001 From: Corey Peruffo <87686305+cperuffo3@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:53:06 -0400 Subject: [PATCH 0988/1077] fix: use quantization-aware comparison in skip_redundant_commands filter (#1513) * fix: use quantization-aware comparison in skip_redundant_commands filter _remove_redundant_attributes() compared target values against light state with exact equality, but many targets can never round-trip exactly through a device with coarser resolution: - brightness: HA's 0-255 scale vs the 0-99 Z-Wave Multilevel Switch scale leaves 156 of 255 targets that never converge (e.g. 230 -> 89 -> 229), - color_temp_kelvin: the kelvin -> mired -> kelvin round trip leaves most kelvin targets off by up to ~21 K at 6500 K (e.g. 5500 -> 182 -> 5495). Such attributes survived the filter and were re-sent every interval forever, which on larger Z-Wave meshes is enough to jam the controller. Compare brightness with a tolerance of 2 (the exact worst case of the 0-99 scale) and color temperature in mired space, where devices actually quantize and where the comparison is exact at every kelvin value. Both are far below the manual-control detection thresholds (BRIGHTNESS_CHANGE = 25, COLOR_TEMP_CHANGE = 100), so they cannot mask a genuine user change. Fixes #1512 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017P2wLQYGQH6npY5op5CKVD * fix: tolerate one mired to cover both floor- and round-based conversions The previous exact mired equality assumed round-based kelvin<->mired conversion, but HA core's color_temperature_kelvin_to_mired() and color_temperature_mired_to_kelvin() both use math.floor, under which a target like 5500 K comes back as 5524 K in a different rounded mired bucket and would never be filtered. Flooring in the comparison instead would merely flip the failure onto integrations that round. Comparing with a tolerance of one mired converges for both conversion schemes (verified by brute force over 1000-10000 K: zero stuck targets under either pipeline) and can hide at most ~2 mireds, far below the ~5.5 mired just-noticeable difference for color temperature. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017P2wLQYGQH6npY5op5CKVD --------- Co-authored-by: Claude Fable 5 Co-authored-by: Bas Nijholt --- .../adaptive_lighting/adaptation_utils.py | 41 ++++++++- tests/test_adaptation_utils.py | 84 +++++++++++++++++-- 2 files changed, 117 insertions(+), 8 deletions(-) diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py index 26acd92b..20a227ca 100644 --- a/custom_components/adaptive_lighting/adaptation_utils.py +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -45,6 +45,15 @@ BRIGHTNESS_ATTRS = { ATTR_BRIGHTNESS_STEP_PCT, } +# Worst-case rounding error when Home Assistant's 0-255 brightness scale +# round-trips through a device with coarser resolution (e.g., the 0-99 Z-Wave +# Multilevel Switch scale). A light cannot report back a value more precise than +# its own scale, so exact equality would never hold for such targets and +# 'skip_redundant_commands' would keep sending them forever. The tolerance sits +# far below the manual-control-detection threshold (BRIGHTNESS_CHANGE = 25), so +# it cannot mask a genuine user change. +BRIGHTNESS_TOLERANCE = 2 + ServiceData = dict[str, Any] @@ -113,20 +122,46 @@ def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]: return service_datas +def _is_attribute_satisfied(key: str, value: Any, attributes: dict[str, Any]) -> bool: + """Whether the light's current state already satisfies this target value.""" + if key not in attributes: + return False + current = attributes[key] + if not isinstance(current, (int, float)) or not isinstance(value, (int, float)): + return value == current + if key == ATTR_BRIGHTNESS: + return abs(value - current) <= BRIGHTNESS_TOLERANCE + if key == ATTR_COLOR_TEMP_KELVIN and value > 0 and current > 0: + # Compare in mired space: most integrations quantize color temperature + # to whole mireds, and the kelvin error of that quantization grows + # quadratically with kelvin (~21 K at 6500 K, ~50 K at 10000 K), so no + # fixed kelvin tolerance fits the whole range. The tolerance of one + # mired absorbs the difference between conversion schemes: HA core's + # helpers floor (e.g. 5500 K -> 181 mired -> 5524 K) while some + # integrations round (5500 K -> 182 mired -> 5495 K), and no exact + # equality converges for both. One mired is far below the ~5.5 mired + # just-noticeable difference for color temperature. + return abs(round(1_000_000 / value) - round(1_000_000 / current)) <= 1 + return value == current + + def _remove_redundant_attributes( service_data: ServiceData, state: State, ) -> ServiceData: - """Filter service data by removing attributes that already equal the given state. + """Filter service data by removing attributes already satisfied by the state. Removes all attributes from service call data whose values are already present - in the target entity's state. + in the target entity's state. Quantized attributes (brightness, color temp) are + compared with a small tolerance: a light whose resolution is coarser than Home + Assistant's cannot report back the exact value it was given, so exact equality + would never hold and the attribute would never be filtered. """ attributes: dict[str, Any] = dict(state.attributes) return { k: v for k, v in service_data.items() - if k not in attributes or v != attributes[k] + if not _is_attribute_satisfied(k, v, attributes) } diff --git a/tests/test_adaptation_utils.py b/tests/test_adaptation_utils.py index 6caf335c..b5751454 100644 --- a/tests/test_adaptation_utils.py +++ b/tests/test_adaptation_utils.py @@ -95,14 +95,82 @@ async def test_split_service_call_data(input_data, expected_data_list): ), ( {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, - State("light.test", STATE_ON, {ATTR_BRIGHTNESS: 11}), + State("light.test", STATE_ON, {ATTR_BRIGHTNESS: 13}), {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}, ), + ( + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 230, ATTR_TRANSITION: 2}, + State("light.test", STATE_ON, {ATTR_BRIGHTNESS: 229}), + {ATTR_ENTITY_ID: "light.test", ATTR_TRANSITION: 2}, + ), + ( + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 230, ATTR_TRANSITION: 2}, + State("light.test", STATE_ON, {ATTR_BRIGHTNESS: 227}), + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 230, ATTR_TRANSITION: 2}, + ), + ( + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 5500, + ATTR_TRANSITION: 2, + }, + State("light.test", STATE_ON, {ATTR_COLOR_TEMP_KELVIN: 5495}), + {ATTR_ENTITY_ID: "light.test", ATTR_TRANSITION: 2}, + ), + ( + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 5500, + ATTR_TRANSITION: 2, + }, + State("light.test", STATE_ON, {ATTR_COLOR_TEMP_KELVIN: 5524}), + {ATTR_ENTITY_ID: "light.test", ATTR_TRANSITION: 2}, + ), + ( + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 6500, + ATTR_TRANSITION: 2, + }, + State("light.test", STATE_ON, {ATTR_COLOR_TEMP_KELVIN: 6494}), + {ATTR_ENTITY_ID: "light.test", ATTR_TRANSITION: 2}, + ), + ( + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 5500, + ATTR_TRANSITION: 2, + }, + State("light.test", STATE_ON, {ATTR_COLOR_TEMP_KELVIN: 5400}), + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 5500, + ATTR_TRANSITION: 2, + }, + ), + ( + {ATTR_ENTITY_ID: "light.test", ATTR_HS_COLOR: (30.0, 40.0)}, + State("light.test", STATE_ON, {ATTR_HS_COLOR: (30.0, 40.0)}), + {ATTR_ENTITY_ID: "light.test"}, + ), + ( + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10}, + State("light.test", STATE_ON, {ATTR_BRIGHTNESS: None}), + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10}, + ), ], ids=[ "pass all attributes on empty state", "remove attributes whose values equal the state", "keep attributes whose values differ from the state", + "remove brightness within quantization tolerance (0-99 device scale)", + "keep brightness outside quantization tolerance", + "remove color temp within one mired (round-converting integration)", + "remove color temp within one mired (floor-converting HA core helpers)", + "remove color temp within one mired (6500 K)", + "keep color temp more than one mired away", + "remove non-numeric attributes on exact equality", + "keep attribute when state value is None", ], ) async def test_remove_redundant_attributes( @@ -167,18 +235,18 @@ async def test_has_relevant_service_data_attributes( [], ), ( - [{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 11}], + [{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 15}], True, - [{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 11}], + [{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 15}], ), ( [ - {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 11}, + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 15}, {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 22}, ], True, [ - {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 11}, + {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 15}, {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 22}, ], ), @@ -192,6 +260,11 @@ async def test_has_relevant_service_data_attributes( {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 22}, ], ), + ( + [{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 11}], + True, + [], + ), ], ids=[ "single item passed through without filtering", @@ -201,6 +274,7 @@ async def test_has_relevant_service_data_attributes( "filter keeps item with relevant attribute that is different from state", "filter keeps two items with relevant attributes that are different from state", "filter removes item that equals state and keeps items that differs from state", + "filter removes item with relevant attribute within tolerance of the state", ], ) async def test_create_service_call_data_iterator( From 68c66a0ff30cf3bae7816d852ee36433b66f3dc2 Mon Sep 17 00:00:00 2001 From: Samson Brock Date: Sun, 6 Sep 2026 02:00:02 -0500 Subject: [PATCH 0989/1077] Fix options flow changes silently discarded for pre-refactor UI entries (#1504) * Fix options flow changes being silently discarded for UI-configured entries validate() merges config_entry.options then config_entry.data, on the assumption that data only ever holds YAML-imported settings (which should win) or, for UI-created entries, just the entry name (harmless to apply last). That assumption doesn't hold for entries created before data/options were split: their data still carries the full settings snapshot from initial setup. Applying it after options means any change made through the options flow for a key that already exists in data (e.g. adding a light) is silently ignored, even though the options flow reports success and the entry reloads without error. Reproduced on a real entry: added a light via the options flow, entry reloaded cleanly, but the light was never picked up by the switch's service-call interceptor ("No switch found for entity_id=...") because data still held the old light list and clobbered the updated options. Fix: only let data win over options for genuinely YAML-imported entries (config_entry.source == SOURCE_IMPORT), matching the existing use of that check elsewhere in this file. For UI-configured entries, apply options last so changes made through the options flow actually take effect. * Add focused tests for the data/options merge order in validate() Covers both source-specific contracts the merge logic relies on, per review feedback on this PR: - SOURCE_USER: options must win over data (this PR's actual fix - proven to fail against the pre-fix code, verified locally by reverting switch.py and re-running). - SOURCE_IMPORT: data must keep winning over options (the existing, intentional YAML-precedence behavior - unchanged by this PR, verified to already pass against the pre-fix code too). Verified against a real Home Assistant instance's test harness (pytest-homeassistant-custom-component + the actual installed homeassistant package), not just reasoned about statically. --------- Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/switch.py | 16 ++++++- tests/test_switch.py | 43 ++++++++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 3b042702..5db91823 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -565,8 +565,20 @@ def validate( if config_entry is not None: assert service_data is None assert defaults is None - data.update(config_entry.options) # come from options flow - data.update(config_entry.data) # all yaml settings come from data + if config_entry.source == SOURCE_IMPORT: + # YAML-configured entries: `data` is the authoritative YAML config + # and must win over any stray `options` from a prior UI setup. + data.update(config_entry.options) + data.update(config_entry.data) + else: + # UI-configured entries: settings are meant to live in `options` + # (see OptionsFlowHandler in config_flow.py). `data` here is + # either just the entry name, or - for entries created before + # data/options were split - a stale snapshot from initial setup. + # Applying it last would silently discard newer changes made + # through the options flow, so `options` must win instead. + data.update(config_entry.data) + data.update(config_entry.options) else: assert service_data is not None changed_settings = { diff --git a/tests/test_switch.py b/tests/test_switch.py index eabe226a..ce9d8ec8 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -75,6 +75,7 @@ from homeassistant.components.adaptive_lighting.switch import ( is_our_context, is_our_context_id, short_hash, + validate, ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -98,7 +99,7 @@ except ImportError: from homeassistant.components.template.light import LightTemplate from homeassistant.components.template import light as template_light -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, ConfigEntryState from homeassistant.const import ( ATTR_AREA_ID, ATTR_ENTITY_ID, @@ -3235,3 +3236,43 @@ async def test_detect_non_ha_changes_with_separate_turn_on_commands(hass): assert ( light.brightness == manual_brightness ), f"AL overrode manual brightness {manual_brightness} with {al_brightness}" + + +def test_validate_ui_options_win_over_stale_data(): + """A UI-configured entry's `options` (from the options flow) must win. + + `data` for a `SOURCE_USER` entry either only holds the entry name, or - + for entries created before `data`/`options` were split - a stale + snapshot from initial setup. Either way, a later change made through + the options flow (stored in `options`) must not be silently discarded + by that stale/legacy `data`. + """ + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={CONF_NAME: DEFAULT_NAME, CONF_LIGHTS: ["light.a"]}, + options={CONF_LIGHTS: ["light.a", "light.b"]}, + ) + + result = validate(entry) + + assert result[CONF_LIGHTS] == ["light.a", "light.b"] + + +def test_validate_yaml_data_wins_over_stray_options(): + """A YAML-imported entry's `data` must keep winning over `options`. + + YAML configuration is the source of truth for a `SOURCE_IMPORT` entry, + so any leftover `options` (e.g. from a UI setup that predates the YAML + import) must not override it. + """ + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IMPORT, + data={CONF_NAME: DEFAULT_NAME, CONF_LIGHTS: ["light.a"]}, + options={CONF_LIGHTS: ["light.b"]}, + ) + + result = validate(entry) + + assert result[CONF_LIGHTS] == ["light.a"] From c620d1480a73896b3f3f2d2aa12acf6c1ebec124 Mon Sep 17 00:00:00 2001 From: Dennis Dekker <48018095+Dennis-Dekker@users.noreply.github.com> Date: Sun, 6 Sep 2026 09:01:50 +0200 Subject: [PATCH 0990/1077] fix: re-adapt lights right after the auto-reset timer fires (#1506) When the auto-reset timer fired, its callback called manager.reset(), which pops the timer and calls timer.cancel() - cancelling the very task that was running the callback. The manual_control flag was cleared, but the re-adaptation that should follow was silently cancelled, so the light only changed on the next interval pass (with the normal transition). _AsyncSingleShotTimer.cancel() now never cancels the task that is currently running its own callback. The trailing assert in the callback is removed because it is reachable now and could trip when a new manual change comes in while the re-adaptation is still running. The existing auto-reset test also checks that a light.turn_on with the 'autoreset' context is sent. Fixes #1233 Co-authored-by: Dennis-Dekker <> Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/switch.py | 13 ++++++++++--- tests/test_switch.py | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 5db91823..696ae1a2 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2266,7 +2266,6 @@ class AdaptiveLightingManager: transition=switch.initial_transition, force=True, ) - assert self.manual_control[light] == LightControlAttributes.NONE self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset) @@ -3007,9 +3006,17 @@ class _AsyncSingleShotTimer: def cancel(self) -> None: """Cancel the timer.""" - if self.task: + # Never cancel the task that is currently running our own callback, e.g. + # when the auto-reset callback calls manager.reset(), which cancels the + # timer it is running in. That used to silently cancel the rest of the + # callback (the re-adaptation), see issue #1233. + try: + current_task = asyncio.current_task() + except RuntimeError: # no running event loop + current_task = None + if self.task and self.task is not current_task: self.task.cancel() - self.callback = None + self.callback = None def remaining_time(self) -> float: """Return the remaining time before the timer expires.""" diff --git a/tests/test_switch.py b/tests/test_switch.py index ce9d8ec8..a48800f2 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -914,11 +914,27 @@ async def test_auto_reset_manual_control(hass): switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] > 0 ) await update() + # The auto reset must also re-adapt the light right away, not only clear the + # flag. Collect the 'light.turn_on' calls made with the 'autoreset' context. + autoreset_calls: list[Event] = [] + + async def _on_call_service(event: Event) -> None: + if ( + event.data.get("domain") == LIGHT_DOMAIN + and event.data.get("service") == SERVICE_TURN_ON + and is_our_context(event.context, "autoreset") + ): + autoreset_calls.append(event) + + remove_listener = hass.bus.async_listen(EVENT_CALL_SERVICE, _on_call_service) await asyncio.sleep(0.3) # Should be enough time for auto reset + await hass.async_block_till_done() + remove_listener() assert not manual_control[light.entity_id], (light, manual_control) assert ( light.entity_id not in switch.extra_state_attributes["autoreset_time_remaining"] ) + assert autoreset_calls, "auto reset did not re-adapt the light" # Do a couple of quick changes and check that light is not reset for i in range(3): From 67d4a2f657a622ce0ea28fd67e7205393dbc2496 Mon Sep 17 00:00:00 2001 From: proscar87 <68169114+proscar87@users.noreply.github.com> Date: Sun, 6 Sep 2026 01:10:21 -0600 Subject: [PATCH 0991/1077] fix: clamp() collapsed to minimum when min_brightness > max_brightness (#1507) * fix: clamp() collapsed to minimum when min_brightness > max_brightness A user can intentionally set min_brightness > max_brightness (or the equivalent for color temperature) for an inverted timescale -- e.g. a porch light that should be brighter at night than during the day. clamp()'s max(minimum, min(value, maximum)) assumed minimum <= maximum; when inverted, min(value, maximum) is always <= maximum < minimum, so max(minimum, ...) always returns minimum. linear and tanh brightness modes -- both of which end in a clamp(brightness, min_brightness, max_brightness) call -- got stuck returning one fixed value regardless of the time of day. Fixes #1421 Co-Authored-By: Claude Sonnet 5 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * test: accept current Home Assistant brightness validation errors Keep asserting the maximum brightness limit without depending on the validation library's dictionary-path wording. --------- Co-authored-by: Oscar Pacheco Co-authored-by: Claude Sonnet 5 Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt Co-authored-by: Bas Nijholt --- .../adaptive_lighting/color_and_brightness.py | 12 ++- tests/test_color_and_brightness.py | 90 +++++++++++++++++++ tests/test_switch.py | 2 +- 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/color_and_brightness.py b/custom_components/adaptive_lighting/color_and_brightness.py index c91278fc..5e1d8a75 100644 --- a/custom_components/adaptive_lighting/color_and_brightness.py +++ b/custom_components/adaptive_lighting/color_and_brightness.py @@ -528,5 +528,13 @@ def lerp(x: float, x1: float, x2: float, y1: float, y2: float) -> float: def clamp(value: float, minimum: float, maximum: float) -> float: - """Clamp value between minimum and maximum.""" - return max(minimum, min(value, maximum)) + """Clamp value between minimum and maximum. + + `minimum` is not assumed to be <= `maximum`: a user may intentionally + configure `min_brightness > max_brightness` (or the equivalent for color + temperature) for an inverted timescale (#1421). Sort the bounds first so + that case clamps against the real lower/upper bound instead of + collapsing to `minimum` for every input. + """ + low, high = (minimum, maximum) if minimum <= maximum else (maximum, minimum) + return max(low, min(value, high)) diff --git a/tests/test_color_and_brightness.py b/tests/test_color_and_brightness.py index 32425055..86cec8a8 100644 --- a/tests/test_color_and_brightness.py +++ b/tests/test_color_and_brightness.py @@ -7,6 +7,8 @@ from astral.location import Location from homeassistant.components.adaptive_lighting.color_and_brightness import ( SunEvent, SunEvents, + SunLightSettings, + clamp, ) # Create a mock astral location object (its `.observer` is passed to `SunEvents`) @@ -207,3 +209,91 @@ def test_closest_event(tzinfo_and_location): event_name, ts = sun_events.closest_event(sunrise) assert event_name == SunEvent.SUNRISE assert ts == location.sunrise(sunrise.date()).timestamp() + + +def _make_brightness_settings( + tzinfo, + location, + *, + min_brightness, + max_brightness, + brightness_mode, +): + """Build a SunLightSettings with only the fields brightness_pct() needs.""" + return SunLightSettings( + name="test", + astral_observer=location.observer, + adapt_until_sleep=False, + max_brightness=max_brightness, + max_color_temp=6500, + min_brightness=min_brightness, + min_color_temp=2000, + sleep_brightness=1, + sleep_rgb_or_color_temp="color_temp", + sleep_color_temp=2000, + sleep_rgb_color=(255, 56, 0), + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + brightness_mode_time_dark=dt.timedelta(minutes=30), + brightness_mode_time_light=dt.timedelta(minutes=30), + brightness_mode=brightness_mode, + timezone=tzinfo, + ) + + +def test_clamp_handles_inverted_bounds(): + """A user can intentionally set min_brightness > max_brightness for an + inverted timescale (#1421, e.g. a porch light that should be brighter at + night than during the day). clamp() must still bound the value between + whichever of the two is actually smaller/larger, not silently collapse + to `minimum` for every input the way `max(minimum, min(value, maximum))` + does when minimum > maximum. + """ + assert clamp(50, 100, 15) == 50 + assert clamp(0, 100, 15) == 15 + assert clamp(200, 100, 15) == 100 + + +def test_clamp_normal_bounds_unaffected(): + """The ordinary min <= max case must keep behaving exactly as before.""" + assert clamp(50, 0, 100) == 50 + assert clamp(-10, 0, 100) == 0 + assert clamp(150, 0, 100) == 100 + + +@pytest.mark.parametrize("brightness_mode", ["linear", "tanh"]) +def test_brightness_pct_varies_with_inverted_brightness_bounds( + tzinfo_and_location, + brightness_mode, +): + """#1421: with min_brightness > max_brightness, linear/tanh modes got + stuck returning min_brightness for every sample, because the final + `clamp(brightness, self.min_brightness, self.max_brightness)` call + collapsed to `minimum` regardless of the computed value. Sampling a few + points around sunrise must show the brightness actually move instead of + being pinned to one value. + """ + tzinfo, location = tzinfo_and_location + settings = _make_brightness_settings( + tzinfo, + location, + min_brightness=100, + max_brightness=15, + brightness_mode=brightness_mode, + ) + + sunrise = location.sunrise(dt.datetime(2022, 6, 1).date()) + samples = [ + settings.brightness_pct( + sunrise + dt.timedelta(minutes=offset), + is_sleep=False, + ) + for offset in (-20, -10, 0, 10, 20) + ] + + assert len({round(value) for value in samples}) > 1, samples + assert all(15 <= value <= 100 for value in samples), samples diff --git a/tests/test_switch.py b/tests/test_switch.py index a48800f2..37e9df38 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1727,7 +1727,7 @@ async def test_change_switch_settings_service(hass): # Test changing to illegal max brightness with pytest.raises( voluptuous.error.MultipleInvalid, - match="value must be at most 100 for dictionary", + match="value must be at most 100", ): await change_switch_settings(**{CONF_MAX_BRIGHTNESS: 5000}) From 55f871fd0cdf07968c822908fe0a8d9e6db1c956 Mon Sep 17 00:00:00 2001 From: proscar87 <68169114+proscar87@users.noreply.github.com> Date: Sun, 6 Sep 2026 01:18:49 -0600 Subject: [PATCH 0992/1077] Adopt has_entity_name to fix duplicated entity ids on HA 2026.4+ (#1499) * Adopt has_entity_name to fix duplicated entity ids on HA 2026.4+ Since HA core 2026.4 (PR 166246) composes entity names as device name + entity name, and only strips the device prefix when the entity name starts with it. Adaptive Lighting's names ('Adaptive Lighting Sleep Mode: stairs' on device 'Adaptive Lighting: stairs') never match, so new installs get ids like switch.adaptive_lighting_stairs_adaptive_lighting_sleep_mode_stairs. Adopt has_entity_name: the main switch takes the device name ('Adaptive Lighting: '), the simple switches use their role ('Sleep Mode', 'Adapt Brightness', 'Adapt Color'). Unique ids are unchanged, so existing installs keep their entity ids via the registry. Fixes #1459 Co-Authored-By: Claude Fable 5 * Test the new entity ids and that existing ones survive The renamed constants were defined but never asserted, so neither the fresh-install ids nor the registry-preservation claim were covered. Co-Authored-By: Claude Fable 5 * test: keep the apply-service test light on Avoid generating brightness zero in the attribute-change helper, which turns the light off and makes the test depend on the current adaptive brightness. --------- Co-authored-by: proscar87 Co-authored-by: Claude Fable 5 Co-authored-by: Bas Nijholt Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/switch.py | 15 ++++--- tests/test_switch.py | 40 +++++++++++++++++-- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 696ae1a2..e5e63b3f 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -852,6 +852,8 @@ def _attributes_have_changed( class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" + _attr_has_entity_name = True + def __init__( self, hass: HomeAssistant, @@ -1003,9 +1005,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) @property - def name(self) -> str: + def name(self) -> str | None: """Return the name of the device if any.""" - return f"Adaptive Lighting: {self._name}" + # The main switch takes the device name "Adaptive Lighting: " + return None @property def unique_id(self) -> str: @@ -1024,7 +1027,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): identifiers={ (DOMAIN, self._name), }, - name=self._name, + name=f"Adaptive Lighting: {self._name}", entry_type=DeviceEntryType.SERVICE, ) @@ -1636,6 +1639,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): class SimpleSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" + _attr_has_entity_name = True + def __init__( self, which: str, @@ -1657,8 +1662,8 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): @property def name(self) -> str: - """Return the name of the device if any.""" - return self._name + """Return the name of the entity within its device.""" + return self._which @property def unique_id(self) -> str: diff --git a/tests/test_switch.py b/tests/test_switch.py index 37e9df38..24ead48d 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -155,9 +155,9 @@ ENTITY_LIGHT_2 = "light.light_2" ENTITY_LIGHT_3 = "light.light_3" _SWITCH_FMT = f"{SWITCH_DOMAIN}.{DOMAIN}" ENTITY_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}" -ENTITY_SLEEP_MODE_SWITCH = f"{_SWITCH_FMT}_sleep_mode_{DEFAULT_NAME}" -ENTITY_ADAPT_BRIGHTNESS_SWITCH = f"{_SWITCH_FMT}_adapt_brightness_{DEFAULT_NAME}" -ENTITY_ADAPT_COLOR_SWITCH = f"{_SWITCH_FMT}_adapt_color_{DEFAULT_NAME}" +ENTITY_SLEEP_MODE_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}_sleep_mode" +ENTITY_ADAPT_BRIGHTNESS_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}_adapt_brightness" +ENTITY_ADAPT_COLOR_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}_adapt_color" ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE @@ -1092,7 +1092,7 @@ async def test_apply_service(hass): assert entity_id not in switch.lights def increased_brightness(): - return (light._attr_brightness + 100) % 255 + return max(1, (light._attr_brightness + 100) % 255) def increased_color_temp(): return max( @@ -3254,6 +3254,38 @@ async def test_detect_non_ha_changes_with_separate_turn_on_commands(hass): ), f"AL overrode manual brightness {manual_brightness} with {al_brightness}" +async def test_fresh_install_entity_ids(hass): + """Test the entity ids a new install gets with device-relative naming.""" + _, switch = await setup_switch(hass, {}) + + assert switch.entity_id == ENTITY_SWITCH + assert switch.sleep_mode_switch.entity_id == ENTITY_SLEEP_MODE_SWITCH + assert switch.adapt_brightness_switch.entity_id == ENTITY_ADAPT_BRIGHTNESS_SWITCH + assert switch.adapt_color_switch.entity_id == ENTITY_ADAPT_COLOR_SWITCH + + +async def test_existing_entity_ids_are_preserved(hass): + """Test an install predating this change keeps its entity ids. + + The unique ids are unchanged, so the entity registry must keep the + classic `..._sleep_mode_` id instead of renaming the entity. + """ + classic_entity_id = f"{_SWITCH_FMT}_sleep_mode_{DEFAULT_NAME}" + assert classic_entity_id != ENTITY_SLEEP_MODE_SWITCH + + registry = entity_registry.async_get(hass) + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + f"{DEFAULT_NAME}_sleep_mode", + suggested_object_id=classic_entity_id.split(".", 1)[1], + ) + + _, switch = await setup_switch(hass, {}) + + assert switch.sleep_mode_switch.entity_id == classic_entity_id + + def test_validate_ui_options_win_over_stale_data(): """A UI-configured entry's `options` (from the options flow) must win. From cc99067c732a25e2dc1f927aa227485ecb6f0831 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 09:23:21 +0200 Subject: [PATCH 0993/1077] fix: keep adapting during polar night and midnight sun (#1489) Handle missing polar sunrise and sunset with a shared fallback. Bound offsets between actual solar anchors while preserving the daily lighting cycle and configured time behavior. Co-authored-by: Oscar Pacheco --- .../adaptive_lighting/color_and_brightness.py | 82 ++++++- tests/test_color_and_brightness.py | 215 ++++++++++++++++++ 2 files changed, 291 insertions(+), 6 deletions(-) diff --git a/custom_components/adaptive_lighting/color_and_brightness.py b/custom_components/adaptive_lighting/color_and_brightness.py index 5e1d8a75..e62beb0c 100644 --- a/custom_components/adaptive_lighting/color_and_brightness.py +++ b/custom_components/adaptive_lighting/color_and_brightness.py @@ -35,6 +35,12 @@ class SunEvent(str, Enum): _ORDER = (SunEvent.SUNRISE, SunEvent.NOON, SunEvent.SUNSET, SunEvent.MIDNIGHT) _ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))} +# On polar days without a sunrise/sunset, synthetic sun events are placed this +# far from solar noon (polar night) or solar midnight (midnight sun), giving a +# 1-hour synthetic "day" or "night" so the adaptation cycle keeps working. +_POLAR_SUN_EVENT_OFFSET = timedelta(minutes=30) +_POLAR_SUN_EVENT_EPSILON = timedelta(seconds=1) + utcnow: partial[datetime.datetime] = partial(datetime.datetime.now, UTC) utcnow.__doc__ = "Get now in UTC time." @@ -57,13 +63,73 @@ class SunEvents: sunset_offset: datetime.timedelta = datetime.timedelta() timezone: datetime.tzinfo = UTC + def _astral_sunrise_or_sunset( + self, + dt: datetime.date, + event: Literal[SunEvent.SUNRISE, SunEvent.SUNSET], + offset: datetime.timedelta, + ) -> datetime.datetime: + """Return the astral sunrise/sunset, with a fallback for polar regions. + + Above the polar circle the sun never crosses the horizon during polar + night and midnight sun, and `astral` raises a `ValueError` (see #1485). + On such days, synthesize a 1-hour "day" around solar noon (polar night) + or a 1-hour "night" around solar midnight (midnight sun), so the + adaptation cycle keeps working. The `(min/max)_(sunrise/sunset)_time` + options are applied on top of these synthetic times and can be used to + shape the resulting schedule. Configured offsets are limited to the + surrounding solar midnight/noon interval so they cannot invert the + required event order. + """ + astral_event = ( + astral.sun.sunrise if event == SunEvent.SUNRISE else astral.sun.sunset + ) + try: + return astral_event(self.astral_observer, dt) + offset + except ValueError: + noon = astral.sun.noon(self.astral_observer, dt) + midnight = astral.sun.midnight(self.astral_observer, dt) + next_midnight = astral.sun.midnight( + self.astral_observer, + dt + timedelta(days=1), + ) + noon_elevation = astral.sun.elevation(self.astral_observer, noon) + midnight_elevation = astral.sun.elevation(self.astral_observer, midnight) + # The sum of the sun's highest and lowest elevation of the day is + # ≈2x the solar declination, so its sign robustly distinguishes + # midnight sun from polar night, even on the boundary days where + # one elevation hovers around the horizon. + if noon_elevation + midnight_elevation > 0: + # Midnight sun: the sun stays above the horizon all day. + synthetic = ( + midnight + _POLAR_SUN_EVENT_OFFSET + if event == SunEvent.SUNRISE + else next_midnight - _POLAR_SUN_EVENT_OFFSET + ) + else: + # Polar night: the sun stays below the horizon all day. + sign = -1 if event == SunEvent.SUNRISE else 1 + synthetic = noon + sign * _POLAR_SUN_EVENT_OFFSET + + lower, upper = ( + (midnight, noon) if event == SunEvent.SUNRISE else (noon, next_midnight) + ) + return min( + max(synthetic + offset, lower + _POLAR_SUN_EVENT_EPSILON), + upper - _POLAR_SUN_EVENT_EPSILON, + ) + def sunrise(self, dt: datetime.date) -> datetime.datetime: """Return the (adjusted) sunrise time for the given datetime.""" sunrise = ( - astral.sun.sunrise(self.astral_observer, dt) + self._astral_sunrise_or_sunset( + dt, + SunEvent.SUNRISE, + self.sunrise_offset, + ) if self.sunrise_time is None - else self._replace_time(dt, self.sunrise_time) - ) + self.sunrise_offset + else self._replace_time(dt, self.sunrise_time) + self.sunrise_offset + ) if self.min_sunrise_time is not None: min_sunrise = self._replace_time(dt, self.min_sunrise_time) sunrise = max(min_sunrise, sunrise) @@ -75,10 +141,14 @@ class SunEvents: def sunset(self, dt: datetime.date) -> datetime.datetime: """Return the (adjusted) sunset time for the given datetime.""" sunset = ( - astral.sun.sunset(self.astral_observer, dt) + self._astral_sunrise_or_sunset( + dt, + SunEvent.SUNSET, + self.sunset_offset, + ) if self.sunset_time is None - else self._replace_time(dt, self.sunset_time) - ) + self.sunset_offset + else self._replace_time(dt, self.sunset_time) + self.sunset_offset + ) if self.min_sunset_time is not None: min_sunset = self._replace_time(dt, self.min_sunset_time) sunset = max(min_sunset, sunset) diff --git a/tests/test_color_and_brightness.py b/tests/test_color_and_brightness.py index 86cec8a8..e7f0a21a 100644 --- a/tests/test_color_and_brightness.py +++ b/tests/test_color_and_brightness.py @@ -1,10 +1,12 @@ import datetime as dt import zoneinfo +import astral.sun import pytest from astral import LocationInfo from astral.location import Location from homeassistant.components.adaptive_lighting.color_and_brightness import ( + _POLAR_SUN_EVENT_OFFSET, SunEvent, SunEvents, SunLightSettings, @@ -297,3 +299,216 @@ def test_brightness_pct_varies_with_inverted_brightness_bounds( assert len({round(value) for value in samples}) > 1, samples assert all(15 <= value <= 100 for value in samples), samples + + +# Tromsø, Norway (69.6°N) has polar night (Nov-Jan) and midnight sun (May-Jul). +TROMSO = Location( + LocationInfo( + name="Tromsø", + region="Norway", + timezone="Europe/Oslo", + latitude=69.6489, + longitude=18.9551, + ), +) +POLAR_NIGHT_DATE = dt.date(2026, 1, 7) +MIDNIGHT_SUN_DATE = dt.date(2026, 7, 7) +MCMURDO = Location( + LocationInfo( + name="McMurdo Station", + region="Antarctica", + timezone="Antarctica/McMurdo", + latitude=-77.8419, + longitude=166.6863, + ), +) + + +def _polar_sun_events(location=TROMSO, **kwargs): + defaults = { + "name": "test", + "astral_observer": location.observer, + "sunrise_time": None, + "min_sunrise_time": None, + "max_sunrise_time": None, + "sunset_time": None, + "min_sunset_time": None, + "max_sunset_time": None, + "timezone": zoneinfo.ZoneInfo(location.timezone), + } + return SunEvents(**{**defaults, **kwargs}) + + +def test_polar_night_synthesizes_short_day(): + # `astral` cannot compute sunrise/sunset (the sun never rises), see #1485 + with pytest.raises(ValueError): # noqa: PT011 + astral.sun.sunrise(TROMSO.observer, POLAR_NIGHT_DATE) + sun_events = _polar_sun_events() + noon = astral.sun.noon(TROMSO.observer, POLAR_NIGHT_DATE) + assert sun_events.sunrise(POLAR_NIGHT_DATE) == noon - _POLAR_SUN_EVENT_OFFSET + assert sun_events.sunset(POLAR_NIGHT_DATE) == noon + _POLAR_SUN_EVENT_OFFSET + + +def test_midnight_sun_synthesizes_short_night(): + # `astral` cannot compute sunrise/sunset (the sun never sets), see #1485 + with pytest.raises(ValueError): # noqa: PT011 + astral.sun.sunset(TROMSO.observer, MIDNIGHT_SUN_DATE) + sun_events = _polar_sun_events() + midnight = astral.sun.midnight(TROMSO.observer, MIDNIGHT_SUN_DATE) + next_midnight = astral.sun.midnight( + TROMSO.observer, + MIDNIGHT_SUN_DATE + dt.timedelta(days=1), + ) + assert sun_events.sunrise(MIDNIGHT_SUN_DATE) == midnight + _POLAR_SUN_EVENT_OFFSET + assert ( + sun_events.sunset(MIDNIGHT_SUN_DATE) == next_midnight - _POLAR_SUN_EVENT_OFFSET + ) + + +@pytest.mark.parametrize( + ("date", "midnight_sun"), + [(dt.date(2026, 1, 7), True), (dt.date(2026, 7, 7), False)], +) +def test_polar_fallback_handles_southern_hemisphere(date, midnight_sun): + sun_events = _polar_sun_events(MCMURDO) + noon = astral.sun.noon(MCMURDO.observer, date) + midnight = astral.sun.midnight(MCMURDO.observer, date) + next_midnight = astral.sun.midnight(MCMURDO.observer, date + dt.timedelta(days=1)) + + if midnight_sun: + assert sun_events.sunrise(date) == midnight + _POLAR_SUN_EVENT_OFFSET + assert sun_events.sunset(date) == next_midnight - _POLAR_SUN_EVENT_OFFSET + else: + assert sun_events.sunrise(date) == noon - _POLAR_SUN_EVENT_OFFSET + assert sun_events.sunset(date) == noon + _POLAR_SUN_EVENT_OFFSET + + +def test_boundary_day_with_real_sunrise_and_synthetic_sunset(): + # At the start of the midnight sun period, `astral` computes a real + # sunrise for this date but raises for sunset (this exact date depends on + # astral's numerics). The synthetic sunset must stay consistent with the + # nearly 24-hour day instead of collapsing into a polar-night day. + date = dt.date(2026, 5, 18) + astral.sun.sunrise(TROMSO.observer, date) # does not raise + with pytest.raises(ValueError): # noqa: PT011 + astral.sun.sunset(TROMSO.observer, date) + sun_events = _polar_sun_events() + day_length = sun_events.sunset(date) - sun_events.sunrise(date) + assert day_length > dt.timedelta(hours=22) + + +@pytest.mark.parametrize("date", [POLAR_NIGHT_DATE, MIDNIGHT_SUN_DATE]) +def test_sun_position_on_polar_days(date): + sun_events = _polar_sun_events() + datetime = dt.datetime(date.year, date.month, date.day, tzinfo=dt.timezone.utc) + noon, midnight = sun_events.noon_and_midnight(datetime) + assert sun_events.sun_position(noon) == 1 + assert sun_events.sun_position(midnight) == -1 + assert sun_events.sun_position(sun_events.sunrise(date)) == 0 + assert sun_events.sun_position(sun_events.sunset(date)) == 0 + + +def test_polar_night_min_max_times_shape_the_synthetic_day(): + # The (min/max)_(sunrise/sunset)_time options apply on top of the + # synthetic sun events, so users can still shape their schedule. + sun_events = _polar_sun_events( + max_sunrise_time=dt.time(9, 0), + min_sunset_time=dt.time(17, 0), + timezone=dt.timezone.utc, + ) + expected_sunrise = dt.datetime(2026, 1, 7, 9, 0, tzinfo=dt.timezone.utc) + expected_sunset = dt.datetime(2026, 1, 7, 17, 0, tzinfo=dt.timezone.utc) + assert sun_events.sunrise(POLAR_NIGHT_DATE) == expected_sunrise + assert sun_events.sunset(POLAR_NIGHT_DATE) == expected_sunset + + +@pytest.mark.parametrize("date", [POLAR_NIGHT_DATE, MIDNIGHT_SUN_DATE]) +@pytest.mark.parametrize( + ("sunrise_offset", "sunset_offset"), + [ + (dt.timedelta(hours=-20), dt.timedelta(hours=-20)), + (dt.timedelta(hours=-20), dt.timedelta(hours=20)), + (dt.timedelta(hours=20), dt.timedelta(hours=-20)), + (dt.timedelta(hours=20), dt.timedelta(hours=20)), + ], +) +def test_polar_offsets_cannot_invert_event_order( + date, + sunrise_offset, + sunset_offset, +): + sun_events = _polar_sun_events( + sunrise_offset=sunrise_offset, + sunset_offset=sunset_offset, + ) + + events = dict( + sun_events.sun_events(dt.datetime.combine(date, dt.time(), tzinfo=dt.UTC)), + ) + midnight = dt.datetime.fromtimestamp(events[SunEvent.MIDNIGHT], tz=dt.UTC) + next_midnight = astral.sun.midnight(TROMSO.observer, date + dt.timedelta(days=1)) + noon = dt.datetime.fromtimestamp(events[SunEvent.NOON], tz=dt.UTC) + sunrise = dt.datetime.fromtimestamp(events[SunEvent.SUNRISE], tz=dt.UTC) + sunset = dt.datetime.fromtimestamp(events[SunEvent.SUNSET], tz=dt.UTC) + + assert midnight < sunrise < noon < sunset < next_midnight + + +def test_polar_fallback_applies_offsets_within_solar_anchors(): + offset = dt.timedelta(minutes=15) + plain = _polar_sun_events() + shifted = _polar_sun_events( + sunrise_offset=offset, + sunset_offset=offset, + ) + + assert ( + shifted.sunrise(MIDNIGHT_SUN_DATE) - plain.sunrise(MIDNIGHT_SUN_DATE) == offset + ) + assert shifted.sunset(MIDNIGHT_SUN_DATE) - plain.sunset(MIDNIGHT_SUN_DATE) == offset + + +def test_sun_position_all_year_in_polar_region(): + # Covers the transitions into and out of polar night and midnight sun; + # `sun_position` internally validates the order of the sun events. + sun_events = _polar_sun_events() + datetime = dt.datetime(2026, 1, 1, tzinfo=dt.timezone.utc) + end = dt.datetime(2027, 1, 1, tzinfo=dt.timezone.utc) + while datetime < end: + position = sun_events.sun_position(datetime) + assert -1 <= position <= 1 + datetime += dt.timedelta(hours=8) + + +@pytest.mark.parametrize("date", [POLAR_NIGHT_DATE, MIDNIGHT_SUN_DATE]) +def test_brightness_and_color_on_polar_days(date): + settings = SunLightSettings( + name="test", + astral_observer=TROMSO.observer, + adapt_until_sleep=False, + max_brightness=100, + max_color_temp=5500, + min_brightness=30, + min_color_temp=2000, + sleep_brightness=1, + sleep_rgb_or_color_temp="color_temp", + sleep_color_temp=1000, + sleep_rgb_color=(255, 56, 0), + sunrise_time=None, + min_sunrise_time=None, + max_sunrise_time=None, + sunset_time=None, + min_sunset_time=None, + max_sunset_time=None, + brightness_mode_time_dark=dt.timedelta(hours=1), + brightness_mode_time_light=dt.timedelta(hours=1), + timezone=zoneinfo.ZoneInfo("Europe/Oslo"), + ) + datetime = dt.datetime(date.year, date.month, date.day, tzinfo=dt.timezone.utc) + noon, midnight = settings.sun.noon_and_midnight(datetime) + at_noon = settings.brightness_and_color(noon, is_sleep=False) + assert at_noon["brightness_pct"] == 100 + assert at_noon["color_temp_kelvin"] == 5500 + at_midnight = settings.brightness_and_color(midnight, is_sleep=False) + assert at_midnight["brightness_pct"] == 30 + assert at_midnight["color_temp_kelvin"] == 2000 From 9b0f03045b989179b4bf87af80a381bfda21fff3 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:54:58 -0700 Subject: [PATCH 0994/1077] docs: credit jaredjxyz for code contributions (#1520) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 29 +++++++++++++++++++---------- README.md | 9 +++++---- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 3d8fb965..c1c67a8b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -87,7 +87,7 @@ }, { "login": "Repsionu", - "name": "J\u00fcri Rebane", + "name": "Jüri Rebane", "avatar_url": "https://avatars.githubusercontent.com/u/46962963?v=4", "profile": "https://github.com/Repsionu", "contributions": [ @@ -195,7 +195,7 @@ }, { "login": "Hypfer", - "name": "S\u00f6ren Beye", + "name": "Sören Beye", "avatar_url": "https://avatars.githubusercontent.com/u/974410?v=4", "profile": "http://hypfer.de/", "contributions": [ @@ -387,7 +387,7 @@ }, { "login": "brebtatv", - "name": "Tom\u00e1\u0161 Valigura", + "name": "Tomáš Valigura", "avatar_url": "https://avatars.githubusercontent.com/u/10747062?v=4", "profile": "https://github.com/brebtatv", "contributions": [ @@ -524,7 +524,7 @@ }, { "login": "letroll", - "name": "Julien Qui\u00e9vreux", + "name": "Julien Quiévreux", "avatar_url": "https://avatars.githubusercontent.com/u/255774?v=4", "profile": "http://www.latavernedutroll.fr", "contributions": [ @@ -642,7 +642,7 @@ }, { "login": "mstefany", - "name": "Martin \u0160tefany", + "name": "Martin Štefany", "avatar_url": "https://avatars.githubusercontent.com/u/57348587?v=4", "profile": "https://stefany.eu", "contributions": [ @@ -696,7 +696,7 @@ }, { "login": "jansigu", - "name": "Jan-Sigurd S\u00f8rensen", + "name": "Jan-Sigurd Sørensen", "avatar_url": "https://avatars.githubusercontent.com/u/8410766?v=4", "profile": "http://www.jan-sigurd.com", "contributions": [ @@ -849,7 +849,7 @@ }, { "login": "MrEbbinghaus", - "name": "Bj\u00f6rn Ebbinghaus", + "name": "Björn Ebbinghaus", "avatar_url": "https://avatars.githubusercontent.com/u/2965273?v=4", "profile": "https://blog.ebbinghaus.me/", "contributions": [ @@ -894,7 +894,7 @@ }, { "login": "TamilNeram", - "name": "\u0ba4\u0bae\u0bbf\u0bb4\u0bcd \u0ba8\u0bc7\u0bb0\u0bae\u0bcd", + "name": "தமிழ் நேரம்", "avatar_url": "https://avatars.githubusercontent.com/u/67970539?v=4", "profile": "https://github.com/TamilNeram", "contributions": [ @@ -939,7 +939,7 @@ }, { "login": "marazmarci", - "name": "M\u00e1rton Mar\u00e1z", + "name": "Márton Maráz", "avatar_url": "https://avatars.githubusercontent.com/u/1349654?v=4", "profile": "https://github.com/marazmarci", "contributions": [ @@ -1128,7 +1128,7 @@ }, { "login": "maksim2005UKR", - "name": "\u0413\u043e\u0440\u043f\u0438\u043d\u0456\u0447 \u041c\u0430\u043a\u0441\u0438\u043c \u041e\u043b\u0435\u043a\u0441\u0430\u043d\u0434\u0440\u043e\u0432\u0438\u0447", + "name": "Горпиніч Максим Олександрович", "avatar_url": "https://avatars.githubusercontent.com/u/233082001?v=4", "profile": "https://github.com/maksim2005UKR", "contributions": [ @@ -1260,6 +1260,15 @@ "contributions": [ "code" ] + }, + { + "login": "jaredjxyz", + "name": "Jared Jensen", + "avatar_url": "https://avatars.githubusercontent.com/u/10385335?v=4", + "profile": "http://jaredj.xyz/", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 4695cf81..7979ec41 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-138-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-139-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -669,6 +669,10 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Esspel
Esspel

🌍 Corey Peruffo
Corey Peruffo

💻 + Samson Brock
Samson Brock

💻 + Dennis Dekker
Dennis Dekker

💻 + proscar87
proscar87

💻 + Jared Jensen
Jared Jensen

💻 @@ -678,9 +682,6 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Add your contributions - Samson Brock
Samson Brock

💻 - Dennis Dekker
Dennis Dekker

💻 - proscar87
proscar87

💻 From 61896eb86a40c61a79ad309c72750edd482cc111 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:55:27 -0700 Subject: [PATCH 0995/1077] docs: credit mueslo for code contributions (#1521) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index c1c67a8b..7649cd78 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1269,6 +1269,15 @@ "contributions": [ "code" ] + }, + { + "login": "mueslo", + "name": "mueslo", + "avatar_url": "https://avatars.githubusercontent.com/u/847751?v=4", + "profile": "https://github.com/mueslo", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 7979ec41..7b13e678 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-139-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-140-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -673,6 +673,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Dennis Dekker
Dennis Dekker

💻 proscar87
proscar87

💻 Jared Jensen
Jared Jensen

💻 + mueslo
mueslo

💻 From e453541a788790e084d680f07ff28fc98fb9a336 Mon Sep 17 00:00:00 2001 From: Jared Jensen Date: Sun, 6 Sep 2026 00:59:38 -0700 Subject: [PATCH 0996/1077] feat: expose per-attribute manual-control state (#1469) Adds two new read-only state attributes on the AdaptiveSwitch entity: - manual_control_brightness: list of light entity_ids whose brightness axis is currently in manual override (i.e. AL is paused for brightness on those lights). - manual_control_color: same, for the color axis. These mirror the existing 'manual_control' attribute (which is a union of both axes) but expose the LightControlAttributes bitfield that AL already tracks internally per-light. Why: the existing 'manual_control' state attribute and the adaptive_lighting.manual_control event are useful, but neither lets a template or dashboard see which axis is paused without subscribing to events. This is especially important with take_over_control_mode: pause_changed, where one axis can be manual while the other still adapts. Now a Lovelace card or template sensor can display brightness/color manual state directly via state_attr(). Test: extends test_manual_control to assert the new attributes reflect the bitfield correctly. No new platform, no breaking changes. Co-authored-by: Bas Nijholt --- README.md | 14 ++++++++++++++ custom_components/adaptive_lighting/switch.py | 12 ++++++++++++ docs/advanced/manual-control.md | 14 ++++++++++++++ tests/test_switch.py | 15 +++++++++++++++ 4 files changed, 55 insertions(+) diff --git a/README.md b/README.md index 7b13e678..af0c9611 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,20 @@ This feature is available when `take_over_control` is enabled. Additionally, enabling `detect_non_ha_changes` allows Adaptive Lighting to detect all state changes, including those made outside of Home Assistant, by comparing the light's state to its previously used settings. The `adaptive_lighting.manual_control` event is fired when a light is marked as "manually controlled," allowing for integration with automations 🤖. +The Adaptive Lighting switch exposes these read-only attributes for its lights: + +- `manual_control`: lights with any attribute marked as manually controlled. +- `manual_control_brightness`: lights with brightness marked as manually controlled. +- `manual_control_color`: lights with color marked as manually controlled. + +These lists report manual-control flags. Actual adaptation also depends on `take_over_control_mode` and the brightness/color adaptation switches. For example, under the default `pause_all` mode, manually changing only brightness leaves `manual_control_color` empty while pausing both brightness and color adaptation. Under `pause_changed`, color can continue adapting. + +The attributes are absent when the Adaptive Lighting switch is off. Use a fallback when checking them in templates: + +```jinja +{{ 'light.bedroom' in (state_attr('switch.adaptive_lighting_bedroom', 'manual_control_brightness') or []) }} +``` + > ⚠️ **_Caution: Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable `detect_non_ha_changes` if you encounter such issues._** diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index e5e63b3f..0cd23a7b 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1151,6 +1151,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): extra_state_attributes["manual_control"] = [ light for light in self.lights if self.manager.manual_control.get(light) ] + extra_state_attributes["manual_control_brightness"] = [ + light + for light in self.lights + if self.manager.manual_control.get(light, LightControlAttributes.NONE) + & LightControlAttributes.BRIGHTNESS + ] + extra_state_attributes["manual_control_color"] = [ + light + for light in self.lights + if self.manager.manual_control.get(light, LightControlAttributes.NONE) + & LightControlAttributes.COLOR + ] extra_state_attributes.update(self._settings) timers = self.manager.auto_reset_manual_control_timers extra_state_attributes["autoreset_time_remaining"] = { diff --git a/docs/advanced/manual-control.md b/docs/advanced/manual-control.md index 36720f3c..4e84ca3f 100644 --- a/docs/advanced/manual-control.md +++ b/docs/advanced/manual-control.md @@ -20,6 +20,20 @@ This feature is available when `take_over_control` is enabled. Additionally, enabling `detect_non_ha_changes` allows Adaptive Lighting to detect all state changes, including those made outside of Home Assistant, by comparing the light's state to its previously used settings. The `adaptive_lighting.manual_control` event is fired when a light is marked as "manually controlled," allowing for integration with automations 🤖. +The Adaptive Lighting switch exposes these read-only attributes for its lights: + +- `manual_control`: lights with any attribute marked as manually controlled. +- `manual_control_brightness`: lights with brightness marked as manually controlled. +- `manual_control_color`: lights with color marked as manually controlled. + +These lists report manual-control flags. Actual adaptation also depends on `take_over_control_mode` and the brightness/color adaptation switches. For example, under the default `pause_all` mode, manually changing only brightness leaves `manual_control_color` empty while pausing both brightness and color adaptation. Under `pause_changed`, color can continue adapting. + +The attributes are absent when the Adaptive Lighting switch is off. Use a fallback when checking them in templates: + +```jinja +{{ 'light.bedroom' in (state_attr('switch.adaptive_lighting_bedroom', 'manual_control_brightness') or []) }} +``` + > ⚠️ **_Caution: Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable `detect_non_ha_changes` if you encounter such issues._** diff --git a/tests/test_switch.py b/tests/test_switch.py index 24ead48d..3ba39aca 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -738,10 +738,19 @@ async def test_manual_control( await turn_light(True, brightness=increased_brightness()) # Check that ENTITY_LIGHT_1 is manually controlled assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS + # Per-attribute state attributes should reflect this + state_attrs = hass.states.get(switch.entity_id).attributes + assert ENTITY_LIGHT_1 in state_attrs["manual_control"] + assert ENTITY_LIGHT_1 in state_attrs["manual_control_brightness"] + assert ENTITY_LIGHT_1 not in state_attrs["manual_control_color"] # Test adaptive_lighting.set_manual_control await change_manual_control(False) # Check that ENTITY_LIGHT_1 is not manually controlled assert not manual_control[ENTITY_LIGHT_1] + state_attrs = hass.states.get(switch.entity_id).attributes + assert ENTITY_LIGHT_1 not in state_attrs["manual_control"] + assert ENTITY_LIGHT_1 not in state_attrs["manual_control_brightness"] + assert ENTITY_LIGHT_1 not in state_attrs["manual_control_color"] # Check that toggling light off to on resets manual control await change_manual_control(True) @@ -865,6 +874,9 @@ async def test_manual_control( assert not manual_control[ENTITY_LIGHT_1] await change_manual_control(True) assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL + state_attrs = hass.states.get(switch.entity_id).attributes + assert state_attrs["manual_control_brightness"] == [ENTITY_LIGHT_1] + assert state_attrs["manual_control_color"] == [ENTITY_LIGHT_1] # Check that manual control `False` unsets all attributes await change_manual_control(False) @@ -875,6 +887,9 @@ async def test_manual_control( assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS await change_manual_control("color") assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.COLOR + state_attrs = hass.states.get(switch.entity_id).attributes + assert state_attrs["manual_control_brightness"] == [] + assert state_attrs["manual_control_color"] == [ENTITY_LIGHT_1] @flaky(max_runs=3, min_passes=1) From 68c0e4db69a4026d0ead942d11507ecf2dbabe7e Mon Sep 17 00:00:00 2001 From: mueslo Date: Sun, 6 Sep 2026 10:04:50 +0200 Subject: [PATCH 0997/1077] fix: preserve Home Assistant area target exclusions (#1511) * Exclude 'service' lights (entity_category) from area intercept turn-on Fixes #1510 When Adaptive Lighting intercepts an area/label-targeted light.turn_on, the intercept rewrites the call to target only the managed lights (see modify_service_data), so Home Assistant's own handler turns on only those. AL then re-issues light.turn_on for the remaining 'skipped' (unmanaged) entities so they still come on. The problem: HA excludes entities with an entity_category (config/diagnostic, e.g. the Home Assistant Voice LED ring) from area/label expansion, but AL's re-issue did not, so AL was the sole thing turning these service lights on. Changes: - Keep re-issuing skipped lights so unmanaged normal lights still come on, but filter out 'service' lights (entity_category set) from that re-issue. - Add _is_service_light helper (registry-based) and divert a *managed* service light to 'skipped' in _separate_entity_ids, so it is excluded from the intercept turn-on while still being adapted when on. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add toggle regression test for service light exclusion from area intercept Mirrors test_service_light_excluded_from_area_intercept_turn_on but uses light.toggle: a service light (entity_category set) in an area must remain off when AL intercepts an area toggle. The managed lights still toggle on. Refs #1510, PR #1511. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Only exclude service lights from indirect (area/device/label) expansion Previously _is_service_light filtered service lights unconditionally, which also excluded a service light explicitly named in `entity_id`. Home Assistant only excludes such lights from indirect area/device/label expansion and turns them on when directly targeted, so AL must mirror that: service lights are now excluded from the intercept/re-issue only when not directly targeted (`direct_entity_ids`). Adds a regression test for the direct-target case. Refs #1510, PR #1511. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Use if/elif/else for indirect service-light exclusion; inline check Address review: collapse the two separate `if is_service`/"if not is_service" into an `if/elif/else` chain (ruff PLR5501) and inline the service check as `self._is_service_light(...) and entity_id not in direct_entity_ids`, which conceptually is `is_indirect_service`. Matches HA's indirect-only exclusion and keeps the flow simple. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Filter service lights directly in _get_entity_list expansion Move service-light filtering to the single expansion site AdaptiveLightingManager._get_entity_list. Area/device expansion is the only place where HA excludes entity_category lights, so filtering there mirrors HA and makes the later direct_entity_ids / skipped_normal guards unnecessary. Explicit entity_id targets bypass expansion and therefore still turn on service lights, matching HA. Fully reverts the direct_entity_ids / skipped_normal addition per review. --------- Co-authored-by: mueslo Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/switch.py | 8 ++ tests/test_switch.py | 78 ++++++++++++++++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 0cd23a7b..fb210744 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1859,6 +1859,13 @@ class AdaptiveLightingManager: self._context_cnt += 1 return context + def _is_excluded_from_area(self, entity_id: str) -> bool: + """Match Home Assistant's exclusions for indirect area targets.""" + entry = entity_registry.async_get(self.hass).async_get(entity_id) + return entry is not None and ( + entry.entity_category is not None or entry.hidden_by is not None + ) + def _separate_entity_ids( self, entity_ids: list[str], @@ -2398,6 +2405,7 @@ class AdaptiveLightingManager: entity_id for entity_id in area_entity_ids if entity_id.startswith(LIGHT_DOMAIN) + and not self._is_excluded_from_area(entity_id) ] entity_ids.extend(eids) _LOGGER.debug( diff --git a/tests/test_switch.py b/tests/test_switch.py index 3ba39aca..148c8bd3 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -38,6 +38,7 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_INITIAL_TRANSITION, CONF_MANUAL_CONTROL, CONF_MAX_BRIGHTNESS, + CONF_MIN_BRIGHTNESS, CONF_MIN_COLOR_TEMP, CONF_MULTI_LIGHT_INTERCEPT, CONF_PREFER_RGB_COLOR, @@ -112,6 +113,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_OFF, STATE_ON, + EntityCategory, ) from homeassistant.const import __version__ as ha_version from homeassistant.core import Context, Event, HomeAssistant, State @@ -1299,7 +1301,11 @@ async def test_state_change_handlers(hass): 4. Assert all possible problems that would result. Also tests significant changes. """ - switch, (light, *_) = await setup_lights_and_switch(hass) + # Keep adaptive brightness distinct from the manual values 20, 40, and 50. + switch, (light, *_) = await setup_lights_and_switch( + hass, + {CONF_MIN_BRIGHTNESS: 50, CONF_MAX_BRIGHTNESS: 50}, + ) context = switch.create_context("test") # needs to be passed to update method # [Config options]: @@ -3339,3 +3345,73 @@ def test_validate_yaml_data_wins_over_stray_options(): result = validate(entry) assert result[CONF_LIGHTS] == ["light.a"] + + +@pytest.mark.parametrize("service", [SERVICE_TURN_ON, SERVICE_TOGGLE]) +@pytest.mark.parametrize("explicit", [False, True], ids=["area", "direct"]) +@pytest.mark.parametrize("managed", [False, True], ids=["unmanaged", "managed"]) +@pytest.mark.parametrize( + "registry_settings", + [ + {}, + {"entity_category": EntityCategory.CONFIG}, + {"entity_category": EntityCategory.DIAGNOSTIC}, + {"hidden_by": entity_registry.RegistryEntryHider.USER}, + ], + ids=["normal", "config", "diagnostic", "hidden"], +) +async def test_intercept_preserves_area_target_exclusions( + hass: HomeAssistant, + service: str, + explicit: bool, + managed: bool, + registry_settings: dict[str, Any], +): + """Area calls exclude hidden/categorized lights; direct calls honor them.""" + await setup_lights(hass) + mock_area_registry(hass) + registry = entity_registry.async_get(hass) + lights = [ENTITY_LIGHT_1, ENTITY_LIGHT_2, ENTITY_LIGHT_3] + for light in lights: + registry.async_update_entity(light, area_id="test-area") + registry.async_update_entity(ENTITY_LIGHT_3, **registry_settings) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: lights}, + blocking=True, + ) + await hass.async_block_till_done() + await setup_switch( + hass, + { + CONF_LIGHTS: ( + [ENTITY_LIGHT_1, ENTITY_LIGHT_3] if managed else [ENTITY_LIGHT_1] + ), + CONF_INTERCEPT: True, + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + assert all(hass.states.get(light).state == STATE_OFF for light in lights) + + target = {ATTR_ENTITY_ID: lights} if explicit else {ATTR_AREA_ID: "test-area"} + await hass.services.async_call(LIGHT_DOMAIN, service, target, blocking=True) + await hass.async_block_till_done() + + # Both normal lights turn on; only the managed one gets adaptive brightness. + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 128 + assert hass.states.get(ENTITY_LIGHT_2).state == STATE_ON + assert hass.states.get(ENTITY_LIGHT_2).attributes.get(ATTR_BRIGHTNESS) != 128 + target_state = hass.states.get(ENTITY_LIGHT_3) + if registry_settings and not explicit: + assert target_state.state == STATE_OFF + else: + assert target_state.state == STATE_ON + if managed: + assert target_state.attributes[ATTR_BRIGHTNESS] == 128 + else: + assert target_state.attributes.get(ATTR_BRIGHTNESS) != 128 From a2186ecf223fbee0742f56a75a6f09e248d9d0a5 Mon Sep 17 00:00:00 2001 From: proscar87 <68169114+proscar87@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:12:28 -0600 Subject: [PATCH 0998/1077] perf: stagger periodic adaptation updates (#1500) Spread recurring updates with a deterministic per-switch offset while keeping turn-on adaptation immediate. Register the delayed listener on Home Assistant's event loop and cover cancellation, reconfiguration, and cadence with real timer tests. Closes #939. Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/switch.py | 37 ++++++-- tests/test_switch.py | 85 +++++++++++++++++++ 2 files changed, 117 insertions(+), 5 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index fb210744..44ad9c60 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import datetime +import hashlib import logging import zoneinfo from copy import deepcopy @@ -62,6 +63,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.event import ( EventStateChangedData, + async_call_later, async_track_state_change_event, async_track_time_interval, ) @@ -1081,6 +1083,17 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self.remove_listeners.append(remove_sleep) self._expand_light_groups() + def _stagger_offset(self, adaptation_interval: timedelta) -> timedelta: + """Return a stable relative delay to spread periodic updates. + + Hashing the switch ID gives a best-effort spread without configuration. + It does not delay the immediate turn-on adaptation or guarantee a minimum + gap between switches. + """ + digest = hashlib.sha256(self.unique_id.encode()).digest() + fraction = int.from_bytes(digest[:8], byteorder="big") / 2**64 + return adaptation_interval * fraction + def _update_time_interval_listener(self) -> None: """Create or recreate the adaptation interval listener. @@ -1101,11 +1114,25 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): + timedelta(seconds=processing_overhead_time) ) - self.remove_interval = async_track_time_interval( - self.hass, - action=self._async_update_at_interval_action, - interval=adaptation_interval, - ) + @callback + def _start_periodic_listener(_now: datetime.datetime | None = None) -> None: + self.remove_interval = async_track_time_interval( + self.hass, + action=self._async_update_at_interval_action, + interval=adaptation_interval, + ) + + # Register after the offset. The first periodic tick is at offset + + # interval, then subsequent ticks keep the configured interval. + offset = self._stagger_offset(adaptation_interval) + if offset > timedelta(0): + self.remove_interval = async_call_later( + self.hass, + offset.total_seconds(), + _start_periodic_listener, + ) + else: + _start_periodic_listener() def _call_on_remove_callbacks(self) -> None: """Call callbacks registered by async_on_remove.""" diff --git a/tests/test_switch.py b/tests/test_switch.py index 148c8bd3..b96e56fe 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1589,6 +1589,91 @@ async def test_async_update_at_interval_action(hass): await switch._async_update_at_interval_action() +async def test_stagger_offset_deterministic_and_bounded(hass): + """Test switches get stable relative delays within the interval.""" + interval = datetime.timedelta(seconds=90) + + _, switch_a = await setup_switch(hass, {CONF_NAME: "switch_a"}) + _, switch_b = await setup_switch(hass, {CONF_NAME: "switch_b"}) + + offset_a_1 = switch_a._stagger_offset(interval) + offset_a_2 = switch_a._stagger_offset(interval) + assert offset_a_1 == offset_a_2 + + offset_b = switch_b._stagger_offset(interval) + assert offset_a_1 != offset_b + + for offset in (offset_a_1, offset_b): + assert datetime.timedelta(0) <= offset < interval + + +async def test_disable_cancels_pending_stagger(hass): + """Test disabling the switch cancels delayed interval registration.""" + switch_module = "homeassistant.components.adaptive_lighting.switch" + with ( + patch( + f"{switch_module}.AdaptiveSwitch._stagger_offset", + return_value=datetime.timedelta(seconds=10), + ) as mock_offset, + patch( + f"{switch_module}.async_track_time_interval", + return_value=lambda: None, + ) as mock_track_interval, + ): + _, switch = await setup_switch(hass, {}) + mock_offset.return_value = datetime.timedelta(seconds=0.05) + switch._update_time_interval_listener() + await switch.async_turn_off() + await asyncio.sleep(0.1) + + mock_track_interval.assert_not_called() + + +async def test_reconfigure_replaces_stagger_and_preserves_interval(hass): + """Test the replacement starts at offset + interval and keeps its cadence.""" + calls: list[float] = [] + two_calls = asyncio.Event() + loop = asyncio.get_running_loop() + stagger = datetime.timedelta(seconds=0.2) + + async def record_interval(_now=None): + calls.append(loop.time()) + if len(calls) == 2: + two_calls.set() + + with patch( + "homeassistant.components.adaptive_lighting.switch.AdaptiveSwitch._stagger_offset", + return_value=datetime.timedelta(seconds=10), + ) as mock_offset: + _, switch = await setup_switch(hass, {}) + switch._interval = datetime.timedelta(0) + mock_offset.return_value = stagger + effective_interval = ( + switch._interval + + datetime.timedelta(milliseconds=switch._send_split_delay) + + datetime.timedelta(seconds=0.5) + ) + + with patch.object( + switch, + "_async_update_at_interval_action", + side_effect=record_interval, + ): + switch._update_time_interval_listener() + await asyncio.sleep(0.02) + + replacement_started = loop.time() + switch._update_time_interval_listener() + await asyncio.wait_for(two_calls.wait(), timeout=2) + await switch.async_turn_off() + + first_delay = calls[0] - replacement_started + interval_seconds = effective_interval.total_seconds() + expected_first_delay = interval_seconds + stagger.total_seconds() + assert expected_first_delay - 0.1 <= first_delay < expected_first_delay + 0.5 + assert interval_seconds - 0.1 <= calls[1] - calls[0] < interval_seconds + 0.5 + + @pytest.mark.parametrize("separate_turn_on_commands", (True, False)) async def test_separate_turn_on_commands(hass, separate_turn_on_commands): """Test 'separate_turn_on_commands' argument.""" From c7339678a678ac8f794ddb328cca82ecfe9a64f6 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 10:32:01 +0200 Subject: [PATCH 0999/1077] ci: check out the workflow commit during test setup (#1522) --- .github/workflows/install_dependencies/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index 9b8dbb08..76959703 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -17,7 +17,7 @@ runs: uses: actions/checkout@v7 with: repository: ${{ github.repository }} - ref: ${{ github.ref }} + ref: ${{ github.sha }} persist-credentials: false fetch-depth: 0 - name: Check out code from GitHub From c03a19c5a3606aa4bb4297beac6c28b9683b52b4 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 10:34:44 +0200 Subject: [PATCH 1000/1077] docs: add marijneken as a contributor for doc (#1526) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 7649cd78..b6c91cdc 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1278,6 +1278,15 @@ "contributions": [ "code" ] + }, + { + "login": "marijneken", + "name": "Marijn Eken", + "avatar_url": "https://avatars.githubusercontent.com/u/928998?v=4", + "profile": "https://github.com/marijneken", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index af0c9611..63d36af0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-140-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-141-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -689,6 +689,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Jared Jensen
Jared Jensen

💻 mueslo
mueslo

💻 + + Marijn Eken
Marijn Eken

📖 + From 17d9b3970078e5cf31a38711d998c29e72fb3fe6 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 10:35:30 +0200 Subject: [PATCH 1001/1077] fix: restore Czech options placeholders (#1523) --- custom_components/adaptive_lighting/translations/cs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/cs.json b/custom_components/adaptive_lighting/translations/cs.json index 74291134..58e171ef 100644 --- a/custom_components/adaptive_lighting/translations/cs.json +++ b/custom_components/adaptive_lighting/translations/cs.json @@ -18,7 +18,7 @@ "step": { "init": { "title": "Nastavení Adaptivního osvětlení", - "description": "Všechna nastavení komponenty Adaptivního osvětlení. Názvy možností odpovídají nastavení YAML. Pokud máte v konfiguraci YAML definovánu položku 'adaptive_lighting', nezobrazí se žádné možnosti.", + "description": "Nakonfigurujte komponentu Adaptive Lighting. Názvy voleb odpovídají nastavení YAML. Pokud je tato položka definována v YAML, žádné volby se zde nezobrazí. Interaktivní grafy znázorňující vliv parametrů najdete v [této webové aplikaci]({webapp_url}). Další podrobnosti najdete v [oficiální dokumentaci]({docs_url}).", "data": { "lights": "lights: Seznam světel (entity_id), které mají být ovládané (může být prázdný). 🌟", "initial_transition": "initial_transition: Prodlení pro změnu z 'vypnuto' do 'zapnuto' (sekundy)", From efa64d01be8f156b5e7c30877659f57317078f33 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 10:35:35 +0200 Subject: [PATCH 1002/1077] fix: allow negative service offsets (#1524) --- custom_components/adaptive_lighting/services.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 09979bac..77ba0b7a 100644 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -185,7 +185,7 @@ change_switch_settings: example: 0 selector: number: - min: 0 + min: -86400 max: 86300 sunrise_time: description: Set a fixed time (HH:MM:SS) for sunrise. 🌅 @@ -199,7 +199,7 @@ change_switch_settings: example: '' selector: number: - min: 0 + min: -86400 max: 86300 sunset_time: description: Set a fixed time (HH:MM:SS) for sunset. 🌇 From 66e6d5ff5f4e03e491eeaa7ebd513837e48e19cb Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 10:36:25 +0200 Subject: [PATCH 1003/1077] docs: add kasiom as a contributor for translation (#1527) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index b6c91cdc..7267d02e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1287,6 +1287,15 @@ "contributions": [ "doc" ] + }, + { + "login": "kasiom", + "name": "Milan K.", + "avatar_url": "https://avatars.githubusercontent.com/u/2422245?v=4", + "profile": "https://github.com/kasiom", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 63d36af0..93dff7f6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-141-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-142-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -691,6 +691,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Marijn Eken
Marijn Eken

📖 + Milan K.
Milan K.

🌍 From ee12752927457f1f75b573de7fc71f5b83572d38 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 10:37:50 +0200 Subject: [PATCH 1004/1077] docs: add callistoprime as a contributor for bug (#1528) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 7267d02e..ade8e7f1 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1296,6 +1296,15 @@ "contributions": [ "translation" ] + }, + { + "login": "callistoprime", + "name": "Callisto", + "avatar_url": "https://avatars.githubusercontent.com/u/178052328?v=4", + "profile": "https://github.com/callistoprime", + "contributions": [ + "bug" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 93dff7f6..e80489a2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-142-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-143-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -692,6 +692,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Marijn Eken
Marijn Eken

📖 Milan K.
Milan K.

🌍 + Callisto
Callisto

🐛 From edc8f70996d7c6429a40f3e7b29ab14e5cffa274 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 10:38:49 +0200 Subject: [PATCH 1005/1077] docs: add davidgeiger as a contributor for bug (#1529) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index ade8e7f1..1ebedf37 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1305,6 +1305,15 @@ "contributions": [ "bug" ] + }, + { + "login": "davidgeiger", + "name": "David Geiger", + "avatar_url": "https://avatars.githubusercontent.com/u/5699049?v=4", + "profile": "https://github.com/davidgeiger", + "contributions": [ + "bug" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index e80489a2..d3e3ed2c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-143-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-144-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -693,6 +693,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Marijn Eken
Marijn Eken

📖 Milan K.
Milan K.

🌍 Callisto
Callisto

🐛 + David Geiger
David Geiger

🐛 From 8f78233e11d83b70eadde2cf95a48c12dfcdc2e3 Mon Sep 17 00:00:00 2001 From: Marijn Eken Date: Sun, 6 Sep 2026 10:43:37 +0200 Subject: [PATCH 1006/1077] docs: clarify that UI setup needs no YAML entry (#1031) * Fixed outdated info in README.md The README says to always add an adaptive_lighting: entry in the YAML, where this seems to be no longer needed (or even preferred). * docs: clarify YAML is optional for UI setup --------- Co-authored-by: Bas Nijholt Co-authored-by: Bas Nijholt --- README.md | 2 +- docs/configuration.md | 10 ++-------- docs/getting-started.md | 41 +++++++++++++---------------------------- docs/index.md | 15 ++------------- 4 files changed, 18 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index d3e3ed2c..72a77484 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ adaptive_lighting: lights: - light.living_room_lights ``` -Note: If you plan to strictly use the UI, the `adaptive_lighting:` entry must still be added to the YAML. +If you configure Adaptive Lighting through the UI, no `adaptive_lighting:` entry is needed in `configuration.yaml`. Instances configured through YAML must be edited in YAML. Transform your home's atmosphere with Adaptive Lighting 🏠, and experience the benefits of intelligent, sun-synchronized lighting today! diff --git a/docs/configuration.md b/docs/configuration.md index 21c7028e..237a53ae 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -8,17 +8,11 @@ Adaptive Lighting supports configuration through both YAML and the Home Assistan ## Basic Configuration -The minimal configuration requires only adding the integration to your `configuration.yaml`: - -```yaml -adaptive_lighting: -``` - -You can then configure everything through the UI at **Settings** → **Devices & Services** → **Adaptive Lighting** → **Configure**. +The simplest setup uses the Home Assistant UI. Go to **Settings** → **Devices & Services** → **Add Integration** → **Adaptive Lighting**. No `adaptive_lighting:` entry is needed in `configuration.yaml`. ## YAML Configuration -For YAML configuration, you can specify lights and options directly: +Alternatively, you can specify lights and options in `configuration.yaml`: ```yaml adaptive_lighting: diff --git a/docs/getting-started.md b/docs/getting-started.md index 68c253eb..48826e84 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -34,40 +34,23 @@ Or use this button to open HACS directly: ## Configuration -### Step 1: Add to configuration.yaml - -Add the following to your `configuration.yaml`: - -```yaml -adaptive_lighting: -``` - -> [!NOTE] -> This entry is required even if you plan to configure everything through the UI. - -### Step 2: Restart Home Assistant - -Restart Home Assistant for the changes to take effect. - -### Step 3: Add the Integration - -1. Go to **Settings** → **Devices & Services** -2. Click **+ Add Integration** -3. Search for "Adaptive Lighting" -4. Follow the setup wizard to select your lights - -### Step 4: Configure Your Lights - -You can configure Adaptive Lighting in two ways: +Choose one of two configuration methods: === "Via UI" 1. Go to **Settings** → **Devices & Services** - 2. Find Adaptive Lighting and click **Configure** - 3. Adjust settings as needed + 2. Click **+ Add Integration** + 3. Search for "Adaptive Lighting" + 4. Follow the setup wizard to name your Adaptive Lighting instance + 5. Find Adaptive Lighting and click **Configure** + 6. Select your lights and adjust the settings + + No `adaptive_lighting:` entry is needed in `configuration.yaml`. === "Via YAML" + Instances configured through YAML must be edited in YAML. + ```yaml adaptive_lighting: - name: "Living Room" @@ -80,7 +63,9 @@ You can configure Adaptive Lighting in two ways: max_color_temp: 5500 ``` -## Basic Configuration Example + Restart Home Assistant after changing the YAML configuration. + +## Basic YAML Configuration Example Here's a simple configuration to get you started: diff --git a/docs/index.md b/docs/index.md index bc3acebe..f01cbfa2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -46,19 +46,8 @@ Adaptive Lighting provides four switches (using "living_room" as an example comp ## Quick Start 1. **Install via HACS**: Search for "Adaptive Lighting" in the [Home Assistant Community Store](https://hacs.xyz/) -2. **Add to configuration**: Add `adaptive_lighting:` to your `configuration.yaml` -3. **Configure**: Go to **Settings** → **Devices & Services** → **Add Integration** → **Adaptive Lighting** -4. **Select your lights**: Choose which lights to control and enjoy automatic adaptation! - -```yaml -# Minimal configuration.yaml entry -adaptive_lighting: - lights: - - light.living_room -``` - -> [!TIP] -> **Using the UI exclusively?** Even if you plan to configure everything through the UI, the `adaptive_lighting:` entry must still be present in your `configuration.yaml`. +2. **Add the integration**: Go to **Settings** → **Devices & Services** → **Add Integration** → **Adaptive Lighting**, then name your instance +3. **Configure**: Open Adaptive Lighting, click **Configure**, select your lights, and adjust the settings. No YAML entry is needed. [Get Started →](getting-started.md){ .md-button .md-button--primary } [View All Options →](configuration.md){ .md-button } From fdce4c9102126e5486201a921f4370d4b820cc64 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 10:45:08 +0200 Subject: [PATCH 1007/1077] fix: keep adaptation updates from restarting manual-control timers (#1525) * Fix autoreset timeout during partial adaptation * Test manual timeout renewal for mixed light requests --- custom_components/adaptive_lighting/switch.py | 4 +- tests/test_switch.py | 140 +++++++++++++++++- 2 files changed, 141 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 44ad9c60..eb62d7b6 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2497,9 +2497,11 @@ class AdaptiveLightingManager: if ( timer is not None and timer.is_running() + and not is_our_context(event.context) + and not self.is_proactively_adapting(event.context.id) and event.time_fired > timer.start_time # type: ignore[operator] ): - # Restart the auto reset timer + # Only external turn-ons extend manual control, not our adaptations. timer.start() if service == SERVICE_TURN_OFF: diff --git a/tests/test_switch.py b/tests/test_switch.py index b96e56fe..52b1dd27 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -895,10 +895,11 @@ async def test_manual_control( @flaky(max_runs=3, min_passes=1) -async def test_auto_reset_manual_control(hass): +@pytest.mark.parametrize("mode", list(TakeOverControlMode)) +async def test_auto_reset_manual_control(hass, mode): switch, (light, *_) = await setup_lights_and_switch( hass, - {CONF_AUTORESET_CONTROL: 0.1}, + {CONF_AUTORESET_CONTROL: 0.1, CONF_TAKE_OVER_CONTROL_MODE: mode}, ) context = switch.create_context("test") # needs to be passed to update method manual_control = switch.manager.manual_control @@ -965,6 +966,141 @@ async def test_auto_reset_manual_control(hass): assert not manual_control[light.entity_id] +@pytest.mark.parametrize("intercept", [False, True]) +@pytest.mark.parametrize("mode", list(TakeOverControlMode)) +@pytest.mark.parametrize( + ("attribute", "value", "next_value", "manual_attributes"), + [ + (ATTR_BRIGHTNESS, 10, 20, LightControlAttributes.BRIGHTNESS), + (ATTR_COLOR_TEMP_KELVIN, 2000, 2200, LightControlAttributes.COLOR), + ], +) +async def test_interval_adaptation_preserves_manual_control_timeout( + hass, + freezer, + cleanup, + intercept, + mode, + attribute, + value, + next_value, + manual_attributes, +): + """Adaptation must not postpone auto reset; another manual change must.""" + switch, (light, *_) = await setup_lights_and_switch( + hass, + { + CONF_AUTORESET_CONTROL: 7200, + CONF_TAKE_OVER_CONTROL_MODE: mode, + CONF_DETECT_NON_HA_CHANGES: False, + CONF_INTERCEPT: intercept, + }, + ) + + async def change_manually(value): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id, attribute: value}, + blocking=True, + ) + await hass.async_block_till_done() + + for manual_value in (value, next_value): + # A new external change to the already-manual axis restarts the timer. + await change_manually(manual_value) + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + for elapsed in (90, 180): + freezer.tick(90) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.manager.get_manual_control_attributes(light.entity_id) + == manual_attributes + ) + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][ + light.entity_id + ] + == 7200 - elapsed + ) + + # An external bare turn-on also keeps its existing timer restart behavior. + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + + +@pytest.mark.parametrize("mode", list(TakeOverControlMode)) +@pytest.mark.parametrize("service_data", [{}, {ATTR_BRIGHTNESS: 20}]) +async def test_mixed_turn_on_restarts_manual_control_timeout( + hass, + freezer, + cleanup, + mode, + service_data, +): + """A mixed-target request must renew manual control on its skipped light.""" + switch, (manual_light, _, off_light) = await setup_lights_and_switch( + hass, + { + CONF_AUTORESET_CONTROL: 7200, + CONF_TAKE_OVER_CONTROL_MODE: mode, + CONF_DETECT_NON_HA_CHANGES: False, + CONF_INTERCEPT: True, + }, + all_lights=True, + ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: manual_light.entity_id, ATTR_BRIGHTNESS: 10}, + blocking=True, + ) + await hass.async_block_till_done() + freezer.tick(90) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: [manual_light.entity_id, off_light.entity_id], + **service_data, + }, + blocking=True, + ) + await hass.async_block_till_done() + + # HA dispatches the original external event before interception. It renews + # manual control even though the skipped target is replayed in our context. + assert hass.states.is_state(off_light.entity_id, STATE_ON) + assert is_our_context( + switch.manager.turn_on_event[manual_light.entity_id].context, + "skipped", + ) + assert ( + switch.manager.get_manual_control_attributes(manual_light.entity_id) + == LightControlAttributes.BRIGHTNESS + ) + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][ + manual_light.entity_id + ] + == 7200 + ) + + async def test_adaptation_attribute_selection(hass): """Test the 'manual control' tracking.""" switch, (light, *_) = await setup_lights_and_switch(hass) From 28f0fe8c5865a2d91e65e0adaccbfe8a0460ff61 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 10:58:20 +0200 Subject: [PATCH 1008/1077] test: keep split-command brightness checks deterministic (#1532) --- tests/test_switch.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_switch.py b/tests/test_switch.py index 52b1dd27..aa70332c 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1815,7 +1815,12 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands): """Test 'separate_turn_on_commands' argument.""" switch, (light, *_) = await setup_lights_and_switch( hass, - {CONF_SEPARATE_TURN_ON_COMMANDS: separate_turn_on_commands}, + { + CONF_SEPARATE_TURN_ON_COMMANDS: separate_turn_on_commands, + # Keep normal brightness distinct from sleep mode at any time of day. + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, ) # We just turn sleep mode on and off which should change the # brightness and color. We don't test whether the number are exactly From 548e5048eca502cf73926161525cae604cc9e651 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:34:59 +0200 Subject: [PATCH 1009/1077] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Pin=20dependenci?= =?UTF-8?q?es=20(#1475)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docker-build.yml | 12 ++++++------ .github/workflows/docs.yml | 8 ++++---- .github/workflows/install_dependencies/action.yml | 4 ++-- .github/workflows/main-to-master-sync.yml | 2 +- .github/workflows/markdown-code-runner.yml | 4 ++-- .github/workflows/pre-commit.yaml | 4 ++-- .github/workflows/pytest.yaml | 2 +- .github/workflows/release-drafter.yml | 2 +- .github/workflows/toc.yaml | 2 +- .github/workflows/update-test-matrix.yaml | 6 +++--- .github/workflows/validate.yml | 2 +- 11 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 4783ef38..8444ac4e 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -21,16 +21,16 @@ jobs: matrix: platform: [linux/amd64, linux/arm64] steps: - - uses: actions/checkout@v7 - - uses: docker/setup-qemu-action@v4 - - uses: docker/setup-buildx-action@v4 - - uses: docker/login-action@v4 + - uses: actions/checkout@v7.0.1 + - uses: docker/setup-qemu-action@v4.3.0 + - uses: docker/setup-buildx-action@v4.3.0 + - uses: docker/login-action@v4.6.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta - uses: docker/metadata-action@v6 + uses: docker/metadata-action@v6.2.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | @@ -39,7 +39,7 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest,enable={{is_default_branch}} - - uses: docker/build-push-action@v7 + - uses: docker/build-push-action@v7.3.0 with: context: . platforms: ${{ matrix.platform }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2204a1ef..8eae2558 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,10 +20,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@v7.0.1 - name: Set up Python - uses: actions/setup-python@v7 + uses: actions/setup-python@v7.0.0 with: python-version: '3.14.7' @@ -47,7 +47,7 @@ jobs: echo "Webapp integrated at site/simulator/" - name: Upload artifact - uses: actions/upload-pages-artifact@v5 + uses: actions/upload-pages-artifact@v5.0.0 with: path: ./site @@ -61,4 +61,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v5 + uses: actions/deploy-pages@v5.0.1 diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml index 76959703..0afbe9ba 100644 --- a/.github/workflows/install_dependencies/action.yml +++ b/.github/workflows/install_dependencies/action.yml @@ -14,14 +14,14 @@ runs: using: "composite" steps: - name: Check out code from GitHub - uses: actions/checkout@v7 + uses: actions/checkout@v7.0.1 with: repository: ${{ github.repository }} ref: ${{ github.sha }} persist-credentials: false fetch-depth: 0 - name: Check out code from GitHub - uses: actions/checkout@v7 + uses: actions/checkout@v7.0.1 with: repository: home-assistant/core path: core diff --git a/.github/workflows/main-to-master-sync.yml b/.github/workflows/main-to-master-sync.yml index a2756894..6b16b8e5 100644 --- a/.github/workflows/main-to-master-sync.yml +++ b/.github/workflows/main-to-master-sync.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@v7.0.1 with: ref: main fetch-depth: 0 diff --git a/.github/workflows/markdown-code-runner.yml b/.github/workflows/markdown-code-runner.yml index 279a7f8a..9614ee7d 100644 --- a/.github/workflows/markdown-code-runner.yml +++ b/.github/workflows/markdown-code-runner.yml @@ -11,14 +11,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code from GitHub - uses: actions/checkout@v7 + uses: actions/checkout@v7.0.1 with: repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} ref: ${{ github.head_ref || github.ref }} fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v7 + uses: actions/setup-python@v7.0.0 with: python-version: "3.14.7" diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index e67ef449..16a9cc58 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -9,6 +9,6 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 - - uses: actions/setup-python@v7 + - uses: actions/checkout@v7.0.1 + - uses: actions/setup-python@v7.0.0 - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 8b2614f0..b34a7262 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -52,7 +52,7 @@ jobs: python-version: "3.14.2" steps: - name: Check out code from GitHub - uses: actions/checkout@v7 + uses: actions/checkout@v7.0.1 - name: Install Home Assistant uses: ./.github/workflows/install_dependencies diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 973577d1..97912b67 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -17,7 +17,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: release-drafter/release-drafter@v7 + - uses: release-drafter/release-drafter@v7.7.0 with: dry-run: ${{ github.event_name == 'pull_request' }} env: diff --git a/.github/workflows/toc.yaml b/.github/workflows/toc.yaml index a2665757..cde1b6d4 100644 --- a/.github/workflows/toc.yaml +++ b/.github/workflows/toc.yaml @@ -7,6 +7,6 @@ jobs: name: TOC Generator runs-on: ubuntu-latest steps: - - uses: technote-space/toc-generator@v4 + - uses: technote-space/toc-generator@v4.3.1 with: TOC_TITLE: "" diff --git a/.github/workflows/update-test-matrix.yaml b/.github/workflows/update-test-matrix.yaml index e67ae227..1556e3a0 100644 --- a/.github/workflows/update-test-matrix.yaml +++ b/.github/workflows/update-test-matrix.yaml @@ -16,10 +16,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v7 + uses: actions/checkout@v7.0.1 - name: Set up Python - uses: actions/setup-python@v7 + uses: actions/setup-python@v7.0.0 with: python-version: "3.14.7" @@ -39,7 +39,7 @@ jobs: - name: Create Pull Request if: steps.changes.outputs.changed == 'true' - uses: peter-evans/create-pull-request@v8 + uses: peter-evans/create-pull-request@v8.1.1 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: "ci: update HA Core test matrix versions" diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 83ce8ad9..b7c1a44b 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -11,7 +11,7 @@ jobs: validate_hacs: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v7" + - uses: "actions/checkout@v7.0.1" - name: HACS validation uses: "hacs/action@main" with: From 4b0638a4d81442acb62d9ae6ae7eeaaa7ce067ca Mon Sep 17 00:00:00 2001 From: Adam DeMuri Date: Sun, 6 Sep 2026 03:35:02 -0600 Subject: [PATCH 1010/1077] Only run the `validate_hacs` CI action on the upstream repo. (#1423) This avoids running this action on forks, since they likely won't have issues or topics enabled, and shouldn't need them. Co-authored-by: Bas Nijholt Co-authored-by: Bas Nijholt --- .github/workflows/validate.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index b7c1a44b..885b40c2 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -10,6 +10,7 @@ on: jobs: validate_hacs: runs-on: "ubuntu-latest" + if: github.repository == 'basnijholt/adaptive-lighting' # Don't run on forked repos steps: - uses: "actions/checkout@v7.0.1" - name: HACS validation From c9d9c75b146dca87cf88071a3ff38e7f4d8b2d7e Mon Sep 17 00:00:00 2001 From: Jan <134940586+MSL-DA@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:35:07 +0200 Subject: [PATCH 1011/1077] Enhance Danish translation description for adaptive lighting (#1488) * Enhance Danish translation description for adaptive lighting Updated the description in the Danish translation for adaptive lighting settings to include documentation and webapp URLs. * Update custom_components/adaptive_lighting/translations/da.json Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/translations/da.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/da.json b/custom_components/adaptive_lighting/translations/da.json index f89bd614..7f2e13c5 100644 --- a/custom_components/adaptive_lighting/translations/da.json +++ b/custom_components/adaptive_lighting/translations/da.json @@ -18,7 +18,7 @@ "step": { "init": { "title": "Adaptiv Belysnings indstillinger", - "description": "Alle indstillinger tilhørende en Adaptiv Belysnings komponent. Indstillingernes navne svarer til YAML indstillingernes. Ingen indstillinger vises hvis du allerede har konfigureret den i YAML.", + "description": "Alle indstillinger tilhørende en Adaptiv Belysnings komponent. Indstillingernes navne svarer til YAML indstillingernes. Ingen indstillinger vises hvis du allerede har konfigureret den i YAML. For interaktive grafer, der viser parametereffekter, besøg [denne webapp]({webapp_url}). Yderligere detaljer finder du i den [officielle dokumentation]({docs_url}).", "data": { "lights": "lights: lyskilder", "initial_transition": "initial_transition: Hvor lang overgang når lyset går fra 'off' til 'on' eller når 'sleep_state' skiftes. (i sekunder)", From cd1e653da576b42161685142da746165f692c310 Mon Sep 17 00:00:00 2001 From: frankysan Date: Sun, 6 Sep 2026 11:35:11 +0200 Subject: [PATCH 1012/1077] Minor update to Swedish translation (#1412) Removed a superfluous "Swedish: " from the description of the manual control option. Co-authored-by: Bas Nijholt Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/translations/sv.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/sv.json b/custom_components/adaptive_lighting/translations/sv.json index 29ce0971..1e5a0bee 100644 --- a/custom_components/adaptive_lighting/translations/sv.json +++ b/custom_components/adaptive_lighting/translations/sv.json @@ -182,7 +182,7 @@ "description": "Strömbrytarens ”entity_id\" i vilken lampan ska (av)markeras som \"manuellt styrd\". 📝" } }, - "description": "Swedish: Markera om en lampa är \"styrd manuellt\"." + "description": "Markera om en lampa är \"styrd manuellt\"." }, "apply": { "description": "Tillämpar nuvarande Adaptiv Ljussätting inställningar till lampor.", From 179cb2ef308a44ef77dabedbdb3f0da9cffd4faa Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:35:48 +0200 Subject: [PATCH 1013/1077] docs: add MSL-DA as a contributor for translation (#1536) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 1ebedf37..1b6121fb 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1314,6 +1314,15 @@ "contributions": [ "bug" ] + }, + { + "login": "MSL-DA", + "name": "Jan", + "avatar_url": "https://avatars.githubusercontent.com/u/134940586?v=4", + "profile": "https://github.com/MSL-DA", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 72a77484..10dfec7e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-144-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-145-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -694,6 +694,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Milan K.
Milan K.

🌍 Callisto
Callisto

🐛 David Geiger
David Geiger

🐛 + Jan
Jan

🌍 From 92c071f4f3b0890cea725d340680a6aaf0e74571 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:36:23 +0200 Subject: [PATCH 1014/1077] docs: add frankysan as a contributor for translation (#1537) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 1b6121fb..0f2c265e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1323,6 +1323,15 @@ "contributions": [ "translation" ] + }, + { + "login": "frankysan", + "name": "frankysan", + "avatar_url": "https://avatars.githubusercontent.com/u/6353605?v=4", + "profile": "https://github.com/frankysan", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 10dfec7e..93fda776 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-145-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-146-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -695,6 +695,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Callisto
Callisto

🐛 David Geiger
David Geiger

🐛 Jan
Jan

🌍 + frankysan
frankysan

🌍 From 6b7dbc3a662c50df1106fb4e83c0e806ee150f4e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 11:36:51 +0200 Subject: [PATCH 1015/1077] Add Lonsonho ZB-RGBCW to troubleshooting (#756) * Add Lonsonho ZB-RGBCW to troubleshooting * add link --- README.md | 2 ++ docs/troubleshooting.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 93fda776..d7897d5d 100644 --- a/README.md +++ b/README.md @@ -447,6 +447,8 @@ These lights are known to exhibit disadvantageous behaviour due to firmware bugs - Ikea Tradfri bulbs/drivers (and related Ikea smart light products) - Unsupported simultaneous transition of brightness and color: When receiving such a command, they switch the brightness instantly and only transition the color. To get smooth transitions of both brightness and color, enable `separate_turn_on_commands`. - Unresponsiveness during color transitions: No other commands are processed during an ongoing color transition, e.g., turn-off commands are ignored and lights stay on despite being reported as off to Home Assistant. The default config with long transitions thus results in long periods of unresponsiveness. To work around this, disable transitions by setting `transition` to `0`, and increase the adaptation frequency by setting `interval` to a short time, e.g., `15` seconds, to retain the impression of smooth continuous adaptations. Keeping the `initial_transition` is recommended for a smooth fade-in (lights are usually not turned off momentarily after being turned on, in which case a short period of unresponsiveness is tolerable). +- [Lonsonho ZB-RGBCW](https://www.zigbee2mqtt.io/devices/ZB-RGBCW.html#lonsonho-zb-rgbcw) + - Some Zigbee2MQTT/eWeLight firmware combinations do not turn the bulb on when the initial `light.turn_on` call includes brightness or color, although later adjustments work. Disable `intercept` for affected bulbs. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index cdff4487..170f5ddf 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -95,6 +95,8 @@ These lights are known to exhibit disadvantageous behaviour due to firmware bugs - Ikea Tradfri bulbs/drivers (and related Ikea smart light products) - Unsupported simultaneous transition of brightness and color: When receiving such a command, they switch the brightness instantly and only transition the color. To get smooth transitions of both brightness and color, enable `separate_turn_on_commands`. - Unresponsiveness during color transitions: No other commands are processed during an ongoing color transition, e.g., turn-off commands are ignored and lights stay on despite being reported as off to Home Assistant. The default config with long transitions thus results in long periods of unresponsiveness. To work around this, disable transitions by setting `transition` to `0`, and increase the adaptation frequency by setting `interval` to a short time, e.g., `15` seconds, to retain the impression of smooth continuous adaptations. Keeping the `initial_transition` is recommended for a smooth fade-in (lights are usually not turned off momentarily after being turned on, in which case a short period of unresponsiveness is tolerable). +- [Lonsonho ZB-RGBCW](https://www.zigbee2mqtt.io/devices/ZB-RGBCW.html#lonsonho-zb-rgbcw) + - Some Zigbee2MQTT/eWeLight firmware combinations do not turn the bulb on when the initial `light.turn_on` call includes brightness or color, although later adjustments work. Disable `intercept` for affected bulbs. From 4c1140c51705ed2bd4385546d69b10e79e5808cb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:38:40 +0200 Subject: [PATCH 1016/1077] [pre-commit.ci] pre-commit autoupdate (#1229) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0) - [github.com/astral-sh/ruff-pre-commit: v0.11.13 → v0.16.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.13...v0.16.5) - https://github.com/psf/black → https://github.com/psf/black-pre-commit-mirror - [github.com/psf/black-pre-commit-mirror: 25.1.0 → 26.5.1](https://github.com/psf/black-pre-commit-mirror/compare/25.1.0...26.5.1) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * ci: preserve Ruff lint baseline --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .pre-commit-config.yaml | 8 ++++---- .ruff.toml | 10 +++++++--- custom_components/adaptive_lighting/switch.py | 2 +- scripts/update-test-matrix.py | 2 +- tests/test_switch.py | 2 +- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1321a487..b5cd6d52 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-added-large-files - id: trailing-whitespace @@ -8,11 +8,11 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.13 + rev: v0.16.5 hooks: - id: ruff args: ["--fix"] - - repo: https://github.com/psf/black - rev: 25.1.0 + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 26.5.1 hooks: - id: black diff --git a/.ruff.toml b/.ruff.toml index fe94764b..6a2187b0 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,6 +1,6 @@ # The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml -target-version = "py310" +target-version = "py312" [lint] select = ["ALL"] @@ -8,19 +8,23 @@ select = ["ALL"] # by the codebase. The plan is to fix them all (when sensible) and then enable them. ignore = [ "ANN", - "ANN101", # Missing type annotation for {name} in method "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {name} + "CPY001", # Missing copyright notice at top of file "D401", # First line of docstring should be in imperative mood "E501", # line too long "FBT001", # Boolean positional arg in function definition "FBT002", # Boolean default value in function definition "FIX004", # Line contains HACK, consider resolving the issue - "PD901", # df is a bad variable name. Be kinder to your future self. "PERF203", # `try`-`except` within a loop incurs performance overhead + "PLC0415", # `import` should be at the top-level of a file "PLR0913", # Too many arguments to function call (N > 5) + "PLR0917", # Too many positional arguments "PLR2004", # Magic value used in comparison, consider replacing X with a constant variable + "RUF059", # Unpacked variable is never used "S101", # Use of assert detected "SLF001", # Private member accessed + "UP017", # Use datetime.UTC alias + "UP042", # Replace str, Enum inheritance with StrEnum ] [lint.per-file-ignores] diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index eb62d7b6..d851702b 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -3028,7 +3028,7 @@ class AdaptiveLightingManager: class _AsyncSingleShotTimer: - def __init__(self, delay: float, callback: Callable[[], None | Any]) -> None: + def __init__(self, delay: float, callback: Callable[[], Any | None]) -> None: """Initialize the timer.""" self.delay = delay self.callback = callback diff --git a/scripts/update-test-matrix.py b/scripts/update-test-matrix.py index c37b7f25..59b18baa 100755 --- a/scripts/update-test-matrix.py +++ b/scripts/update-test-matrix.py @@ -28,7 +28,7 @@ def get_ha_core_versions() -> list[str]: # Paginate through all tags to ensure we get older versions too while True: url = f"https://api.github.com/repos/home-assistant/core/tags?per_page=100&page={page}" - with urllib.request.urlopen(url) as response: # noqa: S310 + with urllib.request.urlopen(url) as response: tags = json.loads(response.read().decode()) if not tags: diff --git a/tests/test_switch.py b/tests/test_switch.py index aa70332c..0d16f64f 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1697,7 +1697,7 @@ async def test_offset_too_large(hass): which makes the adaptive lighting algorithm fail with a ValueError. """ _, switch = await setup_switch(hass, {CONF_SUNRISE_OFFSET: 3600 * 12}) - with pytest.raises(ValueError, match="sun events.*not in the expected order"): + with pytest.raises(ValueError, match=r"sun events.*not in the expected order"): await switch._update_attrs_and_maybe_adapt_lights( context=switch.create_context("test"), ) From 187185698e94519808950959d7daf4a80a0f318d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 11:42:55 +0200 Subject: [PATCH 1017/1077] Add test_expand_light_groups (#319) * Add test_expand_light_groups * Add imports * import --------- Co-authored-by: Benjamin Auquite --- tests/test_switch.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_switch.py b/tests/test_switch.py index 0d16f64f..37395f70 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -71,6 +71,7 @@ from homeassistant.components.adaptive_lighting.switch import ( AdaptiveSwitch, SimpleSwitch, _attributes_have_changed, + _expand_light_groups, color_difference_redmean, create_context, is_our_context, @@ -2551,6 +2552,19 @@ def test_lerp_color_hsv(): lerp_color_hsv((255, 0, 0), (0, 255, 0), 1.1) +async def test_expand_light_groups_waits_for_group_state(hass): + """Test expansion waits until a light group's state is available.""" + await setup_switch(hass, {}) + group = "light.pending_group" + members = ["light.light_1", "light.light_2"] + + assert _expand_light_groups(hass, [group]) == [group] + + hass.states.async_set(group, STATE_ON, {ATTR_ENTITY_ID: members}) + + assert _expand_light_groups(hass, [group]) == members + + @pytest.mark.parametrize("proactive_service_call_adaptation", [True, False]) @pytest.mark.parametrize("take_over_control", [True, False]) @pytest.mark.parametrize("multi_light_intercept", [True, False]) From 4973f79c3a8afb381aded9f2fafef26f49ae3695 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 11:46:44 +0200 Subject: [PATCH 1018/1077] Restore missing Weblate translations (#1533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: restore missing Weblate translations Import only translated keys absent from current main whose English source is unchanged. Existing locale values and stale hard-coded URL descriptions remain untouched. Co-authored-by: Hosted Weblate Co-authored-by: NataN Co-authored-by: Allan Himidi-Rattenborg Co-authored-by: Hans Henrik Juhl Co-authored-by: Belkin Fahri Co-authored-by: Максим Горпиніч Co-authored-by: Enric Pagès i Gassull Co-authored-by: Lukas Tynovsky Co-authored-by: Rutger Co-authored-by: Yllelder Co-authored-by: Hosted Weblate user 144010 Co-authored-by: Loïc R Co-authored-by: Maxime Bailleul Co-authored-by: Pose marto Co-authored-by: Esspel Co-authored-by: Max * fix: omit inaccurate restored translations Drop the imported strings flagged by review for semantic omissions or visible translation errors. English fallback remains available for these keys. * fix: drop unclear Brazilian Portuguese imports --------- Co-authored-by: Hosted Weblate Co-authored-by: NataN Co-authored-by: Allan Himidi-Rattenborg Co-authored-by: Hans Henrik Juhl Co-authored-by: Belkin Fahri Co-authored-by: Максим Горпиніч Co-authored-by: Enric Pagès i Gassull Co-authored-by: Lukas Tynovsky Co-authored-by: Rutger Co-authored-by: Yllelder Co-authored-by: Hosted Weblate user 144010 Co-authored-by: Loïc R Co-authored-by: Maxime Bailleul Co-authored-by: Pose marto Co-authored-by: Esspel Co-authored-by: Max --- .../adaptive_lighting/translations/bg.json | 7 + .../adaptive_lighting/translations/cs.json | 4 + .../adaptive_lighting/translations/da.json | 5 + .../adaptive_lighting/translations/es.json | 10 +- .../adaptive_lighting/translations/fr.json | 4 + .../adaptive_lighting/translations/nl.json | 13 +- .../adaptive_lighting/translations/pt-BR.json | 131 +++++++++++++++++- .../adaptive_lighting/translations/sv.json | 13 +- .../adaptive_lighting/translations/uk.json | 13 +- 9 files changed, 195 insertions(+), 5 deletions(-) diff --git a/custom_components/adaptive_lighting/translations/bg.json b/custom_components/adaptive_lighting/translations/bg.json index f8f6f7e9..9ef6690d 100644 --- a/custom_components/adaptive_lighting/translations/bg.json +++ b/custom_components/adaptive_lighting/translations/bg.json @@ -8,6 +8,13 @@ "data": { "name": "Име" } + }, + "menu": { + "title": "Създай или дублирай", + "description": "Искате ли да създадете нов екземпляр или да дублирате съществуващ?", + "data": { + "action": "Действие" + } } }, "abort": { diff --git a/custom_components/adaptive_lighting/translations/cs.json b/custom_components/adaptive_lighting/translations/cs.json index 58e171ef..71a93590 100644 --- a/custom_components/adaptive_lighting/translations/cs.json +++ b/custom_components/adaptive_lighting/translations/cs.json @@ -8,6 +8,10 @@ "data": { "name": "Název" } + }, + "menu": { + "title": "Vytvořit nebo duplikovat", + "description": "Chcete vytvořit novou instanci nebo duplikovat stávající?" } }, "abort": { diff --git a/custom_components/adaptive_lighting/translations/da.json b/custom_components/adaptive_lighting/translations/da.json index 7f2e13c5..3d6d5951 100644 --- a/custom_components/adaptive_lighting/translations/da.json +++ b/custom_components/adaptive_lighting/translations/da.json @@ -8,6 +8,11 @@ "data": { "name": "Navn" } + }, + "menu": { + "data": { + "action": "Handling" + } } }, "abort": { diff --git a/custom_components/adaptive_lighting/translations/es.json b/custom_components/adaptive_lighting/translations/es.json index eff8172d..600187d3 100644 --- a/custom_components/adaptive_lighting/translations/es.json +++ b/custom_components/adaptive_lighting/translations/es.json @@ -26,7 +26,8 @@ "brightness_mode_time_dark": "(Ignorado si `brightness_mode='default'`) La duración, en segundos, de la transición del brillo después/antes del amanecer/atardecer. 📈📉.", "sunset_time": "Fijar una hora (HH:MM:SS) para el atardecer. 🌇", "min_sunset_time": "Define el atardecer virtual más temprano (HH:MM:SS), permitiendo atardeceres más tardíos. 🌇", - "adapt_delay": "Tiempo de espera (segundos) entre el encendido de la luz y Adaptive Lighting aplicando cambios. Puede ayudar a evitar parpadeos. ⏲️" + "adapt_delay": "Tiempo de espera (segundos) entre el encendido de la luz y Adaptive Lighting aplicando cambios. Puede ayudar a evitar parpadeos. ⏲️", + "take_over_control_mode": "El modo de pausa de adaptación cuando otras fuentes cambian el brillo y/o el color de las luces. `pause_all` siempre pausa tanto la adaptación de brillo como la de color. `pause_changed` pausa la adaptación solo de los atributos cambiados y continúa adaptando los atributos sin cambios, por ejemplo, continúa la adaptación de color cuando solo se cambió el brillo." }, "data": { "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Al encender las luces. Si el valor es `true`, AL adapta sólo si se llama `light.turn_on` sin especificar el color o brillo. ❌🌈 Esto, por ejemplo, previene la adaptación al activar una escena. Si el valor es `false`, AL adapta independientemente de la presencia de color o brillo en `service_data` inicial. Necesita `take_over_control` habilitado. 🕵️", @@ -193,6 +194,13 @@ "user": { "title": "Elige un nombre para la instancia de Adaptive Lighting", "description": "Cada instancia puede contener múltiples luces!" + }, + "menu": { + "title": "Crear o duplicar", + "description": "¿Quieres crear una nueva instancia o duplicar una existente?", + "data": { + "action": "Acción" + } } }, "abort": { diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json index b816a008..495c5dae 100644 --- a/custom_components/adaptive_lighting/translations/fr.json +++ b/custom_components/adaptive_lighting/translations/fr.json @@ -8,6 +8,10 @@ "data": { "name": "Nom" } + }, + "menu": { + "title": "Créer ou dupliquer", + "description": "Voulez-vous créer une nouvelle instance, ou dupliquer une existante ?" } }, "abort": { diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json index 98a1b51b..9f1abfe5 100644 --- a/custom_components/adaptive_lighting/translations/nl.json +++ b/custom_components/adaptive_lighting/translations/nl.json @@ -8,6 +8,13 @@ "data": { "name": "Naam" } + }, + "menu": { + "data": { + "action": "Actie" + }, + "title": "Maak of dupliceer", + "description": "Wil je een nieuwe instantie aanmaken of een bestaande dupliceren?" } }, "abort": { @@ -75,7 +82,8 @@ "max_sunset_time": "Stel de tijd (HH:MM:SS) in voor de laatste virtuele zonsondergang, maakt eerdere zonsondergangen mogelijk. 🌇", "sunrise_time": "Stel een vaste tijd (HH:MM:SS) in voor zonsopkomst. 🌅", "brightness_mode_time_dark": "(Negeer wanneer `brightness_mode='default'`) De duur in seconden van oplopende/aflopende helderheid na/voor zonsopkomst/zonsondergang. 📈📉.", - "max_sunrise_time": "Stel de tijd (HH:MM:SS) in voor de laatste virtuele zonsopkomst, maakt eerdere zonsopkomsten mogelijk. 🌅" + "max_sunrise_time": "Stel de tijd (HH:MM:SS) in voor de laatste virtuele zonsopkomst, maakt eerdere zonsopkomsten mogelijk. 🌅", + "take_over_control_mode": "De adaptie pauzeermodus wanneer andere bronnen de helderheid en/of kleur van lampen veranderen. `pause_all` pauzeert altijd verandering van zowel helderheid als kleur. `pause_changed` pauzeert alleen de verandering van de extern veranderde eigenschappen en blijft onveranderde eigenschappen aanpassen, bijv. doorgaan met kleur veranderen als alleen helderheid extern is veranderd." } } }, @@ -173,6 +181,9 @@ }, "min_sunset_time": { "description": "Stel de tijd (HH:MM:SS) in voor de meest vroege virtuele zonsondergang, maakt latere zonsondergangen mogelijk. 🌇" + }, + "take_over_control_mode": { + "description": "De adaptie pauzeermodus wanneer andere bronnen de helderheid en/of kleur van lampen veranderen. `pause_all` pauzeert altijd verandering van zowel helderheid als kleur. `pause_changed` pauzeert alleen de verandering van de extern veranderde eigenschappen en blijft onveranderde eigenschappen aanpassen, bijv. doorgaan met kleur veranderen als alleen helderheid extern is veranderd." } }, "description": "Wijzig alle gewenste instellingen in de schakelaar. Alle opties hier zijn hetzelfde als in de configuratie." diff --git a/custom_components/adaptive_lighting/translations/pt-BR.json b/custom_components/adaptive_lighting/translations/pt-BR.json index c4fc796f..549090e6 100644 --- a/custom_components/adaptive_lighting/translations/pt-BR.json +++ b/custom_components/adaptive_lighting/translations/pt-BR.json @@ -8,6 +8,13 @@ "data": { "name": "Nome" } + }, + "menu": { + "data": { + "action": "Ação" + }, + "description": "Você deseja criar uma nova instância ou duplicar uma já existente?", + "title": "Criar ou Duplicar" } }, "abort": { @@ -43,7 +50,28 @@ "skip_redundant_commands": "skip_redundant_commands: Deixar de enviar comandos de adaptação cujo estado alvo já seja igual ao estado atual da luz. Minimiza o tráfego de rede e melhora a responsividade da adaptação em algumas situações. 📉Desative se os estados físicos das luzes podem ficar diferentes do estado registrado no HA.", "transition_until_sleep": "transition_until_sleep: Quando ativada, a Iluminação Adaptativa considerará as configurações de sono como o valor mínimo, transicionando para esses valores após o pôr do sol. 🌙", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Ao ligar as luzes inicialmente. Se definido como `true`, a Iluminação Adaptativa se adapta somente se o comando `light.turn_on` for chamado sem especificar cor ou brilho. ❌🌈 Isso, por exemplo, impede a adaptação ao ativar uma cena. Se false, a Iluminação Adaptativa se adapta independentemente da presença de cor ou brilho nos dados iniciais do `service_data`. Precisa de `take_over_control` ativado. 🕵️", - "intercept": "intercept: Interceptar e adaptar os chamados `light.turn_on` para ativar a adaptação instantânea de cor e brilho. 🏎️ Desative para luzes que não suportam `light.turn_on` com cor e brilho." + "intercept": "intercept: Interceptar e adaptar os chamados `light.turn_on` para ativar a adaptação instantânea de cor e brilho. 🏎️ Desative para luzes que não suportam `light.turn_on` com cor e brilho.", + "include_config_in_attributes": "include_config_in_attributes: Mostra todas as opções como atributos no interruptor do Home Assistant quando está definido para `true`. 📝", + "multi_light_intercept": "multi_light_intercept: Interceptar e adaptar chamadas de 'light.turn_on' que visem múltiplas luzes. ➗⚠️ Isso pode resultar na divisão de uma única chamada de 'light.turn_on' em múltiplas chamadas, por exemplo, quando as luzes estão em interruptores diferentes. Exige que 'intercept' esteja ativado." + }, + "data_description": { + "interval": "Frequência, em segundos, para adaptar as luzes. 🔄", + "sunrise_time": "Define um horário fixo (HH:MM:SS) para o nascer do sol. 🌅", + "autoreset_control_seconds": "Redefine automaticamente o controle manual após um período de tempo definido em segundos. Defina 0 para desabilitar. ⏲️", + "transition": "Duração da transição, em segundos, quando as luzes mudam. 🕑", + "sleep_brightness": "Porcentagem do brilho das luzes no modo dormir. 😴", + "initial_transition": "Duração da primeira transição, em segundos, quando as luzes alternarem de 'desligado' para 'ligado'. ⏲️", + "sleep_transition": "Duração da transição em segundos quando o modo dormir é alterado. 😴", + "sunset_offset": "Ajusta o horário do pôr do sol com um deslocamento positivo ou negativo em segundos. ⏰", + "sunrise_offset": "Ajusta o horário do nascer do sol com um deslocamento positivo ou negativo em segundos. ⏰", + "sunset_time": "Definir um horário fixo (HH:MM:SS) para o pôr do sol. 🌇", + "sleep_color_temp": "Temperatura de Cor no modo dormir (usado quando `sleep_rgb_or_color_temp` é `color_temp`) em Kelvin. 😴", + "sleep_rgb_or_color_temp": "Use `\"rgb_color\"` ou `\"color_temp\"` no modo dormir. 🌙", + "adapt_delay": "Tempo de espera (segundos) entre a luz ligar e a aplicação das mudanças da iluminação adaptativa. Pode ajudar a evitar que a luz pisque. ⏲️", + "min_sunrise_time": "Defina o horário virtual mais cedo do nascer do sol (HH:MM:SS), permitindo nasceres do sol mais tarde. 🌅", + "max_sunrise_time": "Defina o horário virtual mais recente do nascer do sol (HH:MM:SS), permitindo nasceres do sol mais cedo. 🌅", + "max_sunset_time": "Defina o horário virtual mais recente do pôr do sol (HH:MM:SS), permitindo pores do sol mais cedo. 🌇", + "min_sunset_time": "Defina o horário virtual mais cedo do pôr do sol (HH:MM:SS), permitindo pores do sol mais tarde. 🌇" } } }, @@ -51,5 +79,106 @@ "option_error": "Opção inválida", "entity_missing": "Uma luz selecionada não foi encontrada" } + }, + "services": { + "change_switch_settings": { + "fields": { + "sleep_transition": { + "description": "Duração da transição em segundos quando o \"modo dormir\" é alternado. 😴" + }, + "entity_id": { + "description": "ID da entidade do switch. 📝" + }, + "max_brightness": { + "description": "Porcentagem máxima do brilho. 💡" + }, + "autoreset_control_seconds": { + "description": "Redefine automaticamente o controle manual após um período de tempo definido em segundos. Defina 0 para desabilitar. ⏲️" + }, + "transition": { + "description": "Duração da transição, em segundos, quando as luzes mudam. 🕑" + }, + "sleep_brightness": { + "description": "Porcentagem do brilho das luzes no modo dormir. 😴" + }, + "turn_on_lights": { + "description": "Se deve ligar as luzes que estão atualmente desligadas. 🔆" + }, + "initial_transition": { + "description": "Duração da primeira transição, em segundos, quando as luzes alternarem de 'desligado' para 'ligado'. ⏲️" + }, + "sunset_offset": { + "description": "Ajusta o horário do pôr do sol com um deslocamento positivo ou negativo em segundos. ⏰" + }, + "sunrise_offset": { + "description": "Ajusta o horário do nascer do sol com um deslocamento positivo ou negativo em segundos. ⏰" + }, + "sunset_time": { + "description": "Definir um horário fixo (HH:MM:SS) para o pôr do sol. 🌇" + }, + "max_color_temp": { + "description": "Temperatura de cor mais fria em Kelvin. ❄️" + }, + "sleep_color_temp": { + "description": "Temperatura de Cor no modo dormir (usado quando `sleep_rgb_or_color_temp` é `color_temp`) em Kelvin. 😴" + }, + "sunrise_time": { + "description": "Define um horário fixo (HH:MM:SS) para o nascer do sol. 🌅" + }, + "include_config_in_attributes": { + "description": "Exibe todas as opções como atributos no interruptor no Home Assistant quando definido para `true`. 📝" + }, + "sleep_rgb_or_color_temp": { + "description": "Use `\"rgb_color\"` ou `\"color_temp\"` no modo dormir. 🌙" + }, + "adapt_delay": { + "description": "Tempo de espera (segundos) entre a luz ligar e a aplicação das mudanças da iluminação adaptativa. Pode ajudar a evitar que a luz pisque. ⏲️" + }, + "separate_turn_on_commands": { + "description": "Usa chamada separada de `light.turn_on` para cor e brilho, necessário para alguns tipos de luz. 🔀" + }, + "use_defaults": { + "description": "Define os valores padrão não especificados nessa chamada de serviço. Opções: \"current\" (padrão, mantém os valores atuais), \"factory\" (reinicia para os padrões documentados) ou \"configuration\" (retorna aos padrões de configuração do interruptor). ⚙️" + }, + "max_sunrise_time": { + "description": "Defina o horário virtual mais recente do nascer do sol (HH:MM:SS), permitindo nasceres do sol mais cedo. 🌅" + }, + "min_sunset_time": { + "description": "Defina o horário virtual mais cedo do pôr do sol (HH:MM:SS), permitindo pores do sol mais tarde. 🌇" + } + }, + "description": "Altere quaisquer configurações que você quiser . Todas as opções aqui são as mesmas que no fluxo de configuração." + }, + "apply": { + "fields": { + "turn_on_lights": { + "description": "Se deve ligar as luzes que estão atualmente desligadas. 🔆" + }, + "lights": { + "description": "Uma luz (ou lista de luzes) para aplicar as configurações. 💡" + }, + "transition": { + "description": "Duração da transição, em segundos, quando as luzes mudam. 🕑" + }, + "entity_id": { + "description": "O 'entity_id' do interruptor com as configurações para aplicar. 📝" + }, + "adapt_brightness": { + "description": "Se deve adaptar o brilho da luz. 🌞" + }, + "adapt_color": { + "description": "Se deve adaptar a cor das luzes que suportam este recurso. 🌈" + } + }, + "description": "Aplica as configurações atuais de iluminação adaptativa nas luzes." + }, + "set_manual_control": { + "description": "Marque se uma luz é 'controlada manualmente'.", + "fields": { + "lights": { + "description": "entity_id(s) das luzes, se não especificadas, todas as luzes do interruptor são selecionadas. 💡" + } + } + } } } diff --git a/custom_components/adaptive_lighting/translations/sv.json b/custom_components/adaptive_lighting/translations/sv.json index 1e5a0bee..6ce670b2 100644 --- a/custom_components/adaptive_lighting/translations/sv.json +++ b/custom_components/adaptive_lighting/translations/sv.json @@ -8,6 +8,13 @@ "data": { "name": "Namn" } + }, + "menu": { + "data": { + "action": "Åtgärd" + }, + "title": "Skapa eller duplicera", + "description": "Vill du skapa en ny instans eller duplicera en befintlig?" } }, "abort": { @@ -68,7 +75,8 @@ "max_sunrise_time": "Ställ in den senaste virtuella soluppgångstiden (TT: MM: SS), vilket möjliggör tidigare soluppgångar. 🌅", "brightness_mode": "Ljusstyrkeinställing att använda. Möjliga värden är \"default\", \"linear\" och \"tanh\" (använder \"brightness_mode_time_dark\" och \"brightness_mode_time_light\"). 📈", "brightness_mode_time_light": "(Ignoreras om `brightness_mode='default'`) Varaktigheten i sekunder för att öka/minska ljusstyrkan efter/före soluppgång/solnedgång. 📈📉.", - "brightness_mode_time_dark": "(Ignoreras om `brightness_mode='default'`) Varaktigheten i sekunder för att öka/minska ljusstyrkan efter/före soluppgång/solnedgång. 📈📉." + "brightness_mode_time_dark": "(Ignoreras om `brightness_mode='default'`) Varaktigheten i sekunder för att öka/minska ljusstyrkan efter/före soluppgång/solnedgång. 📈📉.", + "take_over_control_mode": "Anpassningspausläget när andra källor ändrar ljusstyrka och/eller färg på belysningen. `pause_all` pausar alltid både ljusstyrka och färganpassning. `pause_changed` pausar endast anpassningen av de ändrade attributen och fortsätter att anpassa oförändrade attribut, t.ex. fortsätter färganpassningen när endast ljusstyrkan har ändrats." } } }, @@ -166,6 +174,9 @@ }, "use_defaults": { "description": "Ställer in standardvärden som inte anges i detta serviceanrop. Alternativ: \"current\" (standard, behåller nuvarande värden), \"factory\" (återställer till dokumenterade standardinställningar) eller \"configuration\" (återgår till strömbrytarens standardinställningar). ⚙️" + }, + "take_over_control_mode": { + "description": "Anpassningspausläget när andra källor ändrar ljusstyrka och/eller färg på belysningen. `pause_all` pausar alltid både ljusstyrka och färganpassning. `pause_changed` pausar endast anpassningen av de ändrade attributen och fortsätter att anpassa oförändrade attribut, t.ex. fortsätter färganpassningen när endast ljusstyrkan har ändrats." } }, "description": "Ändra vilka inställningar du vill ha i strömbrytaren. All dessa inställningar är likadana som i config flow." diff --git a/custom_components/adaptive_lighting/translations/uk.json b/custom_components/adaptive_lighting/translations/uk.json index 749271d5..7c271fcc 100644 --- a/custom_components/adaptive_lighting/translations/uk.json +++ b/custom_components/adaptive_lighting/translations/uk.json @@ -8,6 +8,13 @@ "data": { "name": "Ім’я" } + }, + "menu": { + "data": { + "action": "Дія" + }, + "title": "Створити або дублювати", + "description": "Ви хочете створити новий екземпляр чи скопіювати існуючий?" } }, "abort": { @@ -68,7 +75,8 @@ "max_sunrise_time": "Встановіть найпізніший час віртуального сходу сонця (ГГ:ХХ:СС), враховуючи більш ранні сходи сонця. 🌅", "max_sunset_time": "Встановіть найновіший час віртуального заходу сонця (ГГ:ХХ:СС), враховуючи більш ранні заходи сонця. 🌇", "sleep_rgb_or_color_temp": "Використовуйте `\"rgb_color\"` або `\"color_temp\"` у режимі сну. 🌙", - "adapt_delay": "Час очікування (секунди) між увімкненням світла та застосуванням змін системою адаптивного освітлення. Може допомогти уникнути мерехтіння. ⏲️" + "adapt_delay": "Час очікування (секунди) між увімкненням світла та застосуванням змін системою адаптивного освітлення. Може допомогти уникнути мерехтіння. ⏲️", + "take_over_control_mode": "Режим призупинення адаптації, коли інші джерела змінюють яскравість та/або колір світла. `pause_all` завжди призупиняє адаптацію як яскравості, так і кольору. `pause_changed` призупиняє адаптацію лише змінених атрибутів та продовжує адаптацію незмінних атрибутів, наприклад, продовжує адаптацію кольору, коли змінювалася лише яскравість." } } }, @@ -192,6 +200,9 @@ }, "turn_on_lights": { "description": "Чи вмикати світло, яке наразі вимкнене. 🔆" + }, + "take_over_control_mode": { + "description": "Режим призупинення адаптації, коли інші джерела змінюють яскравість та/або колір світла. `pause_all` завжди призупиняє адаптацію як яскравості, так і кольору. `pause_changed` призупиняє адаптацію лише змінених атрибутів та продовжує адаптацію незмінних атрибутів, наприклад, продовжує адаптацію кольору, коли змінювалася лише яскравість." } }, "description": "Змініть будь-які налаштування, які ви бажаєте, у цьому перемикачі. Усі опції тут такі ж, як і в поточному конфігураційному файлі." From 72128f07aec08d56a967b6122bee5d07cf4c31c1 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 11:46:49 +0200 Subject: [PATCH 1019/1077] Restore missing Russian option labels (#1534) * feat: restore missing Russian option labels Restore the translated option labels from PR #1277 that are still absent on current main. Existing Russian values and stale descriptions remain untouched. Co-authored-by: belozorov_sv * fix: drop ambiguous Russian brightness timing labels --------- Co-authored-by: belozorov_sv --- .../adaptive_lighting/translations/ru.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/translations/ru.json b/custom_components/adaptive_lighting/translations/ru.json index 79f3cd02..77800d7e 100644 --- a/custom_components/adaptive_lighting/translations/ru.json +++ b/custom_components/adaptive_lighting/translations/ru.json @@ -46,7 +46,16 @@ "skip_redundant_commands": "Skip_redundant_commands: Пропустить отправку команд адаптации, целевое состояние которых уже равно известному состоянию источника света. Минимизирует сетевой трафик и улучшает скорость адаптации в некоторых ситуациях. 📉Отключите, если физические состояния освещения не синхронизируются с записанным состоянием HA.", "intercept": "intercept: перехватывать и адаптировать вызовы `light.turn_on` для обеспечения мгновенной адаптации цвета и яркости. 🏎️ Отключите источники света, которые не поддерживают `light.turn_on` с цветом и яркостью.", "include_config_in_attributes": "include_config_in_attributes: отображать все параметры в качестве атрибутов на переключателе в Home Assistant, если установлено значение `true`. 📝", - "transition_until_sleep": "transition_until_sleep: когда включено, адаптивное освещение будет рассматривать настройки сна как минимальные, переходя к этим значениям после захода солнца. 🌙" + "transition_until_sleep": "transition_until_sleep: когда включено, адаптивное освещение будет рассматривать настройки сна как минимальные, переходя к этим значениям после захода солнца. 🌙", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Использовать либо 'rgb_color', либо 'color_temp' в режиме сна. 🌙", + "sleep_rgb_color": "sleep_rgb_color: Цвет RGB в режиме сна (используется при 'sleep_rgb_or_color_temp' как 'rgb_color'). 🌈", + "min_sunrise_time": "min_sunrise_time: Самое раннее время виртуального восхода (ЧЧ:ММ:СС). 🌅", + "max_sunrise_time": "max_sunrise_time: Самое позднее время виртуального восхода (ЧЧ:ММ:СС). 🌅", + "min_sunset_time": "min_sunset_time: Самое раннее время виртуального заката (ЧЧ:ММ:СС). 🌇", + "max_sunset_time": "max_sunset_time: Самое позднее время виртуального заката (ЧЧ:ММ:СС). 🌇", + "brightness_mode": "brightness_mode: Режим яркости для использования (default, linear, tanh). 📈", + "autoreset_control_seconds": "autoreset_control_seconds: Автосброс ручного управления через X секунд. ⏲️", + "send_split_delay": "send_split_delay: Задержка между отдельными командами включения для источников света. ⏲️" }, "data_description": { "sleep_rgb_or_color_temp": "Используйте либо `\"rgb_color\"`, либо `\"color_temp\"` в спящем режиме. 🌙", From 397b50d4929d10d2c38a0abcfd76a3d0dfe70bb0 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:48:19 +0200 Subject: [PATCH 1020/1077] docs: add belkin as a contributor for translation (#1539) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 0f2c265e..a0faa2d7 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1332,6 +1332,15 @@ "contributions": [ "translation" ] + }, + { + "login": "belkin", + "name": "Belkin", + "avatar_url": "https://avatars.githubusercontent.com/u/3419659?v=4", + "profile": "http://belkinfahri.com", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index d7897d5d..9ec80c54 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-146-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-147-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -698,6 +698,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark David Geiger
David Geiger

🐛 Jan
Jan

🌍 frankysan
frankysan

🌍 + Belkin
Belkin

🌍 From 3df57870199aa0cbab844a55140f1145ec833b93 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:48:49 +0200 Subject: [PATCH 1021/1077] docs: add LukTyn as a contributor for translation (#1540) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index a0faa2d7..5774e3e0 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1341,6 +1341,15 @@ "contributions": [ "translation" ] + }, + { + "login": "LukTyn", + "name": "LukTyn", + "avatar_url": "https://avatars.githubusercontent.com/u/1812796?v=4", + "profile": "https://github.com/LukTyn", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 9ec80c54..c048fd6b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-147-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-148-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -700,6 +700,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark frankysan
frankysan

🌍 Belkin
Belkin

🌍 + + LukTyn
LukTyn

🌍 + From 8056fcc5f802086a64cbfcb0257d86d74a8bd300 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:49:26 +0200 Subject: [PATCH 1022/1077] docs: add rutgerkra as a contributor for translation (#1541) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 5774e3e0..bdd90810 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1350,6 +1350,15 @@ "contributions": [ "translation" ] + }, + { + "login": "rutgerkra", + "name": "rutgerkra", + "avatar_url": "https://avatars.githubusercontent.com/u/7963187?v=4", + "profile": "https://github.com/rutgerkra", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index c048fd6b..d5b037cc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-148-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-149-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -702,6 +702,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark LukTyn
LukTyn

🌍 + rutgerkra
rutgerkra

🌍 From 95ccc02789054c1d4505f8d2f1fc5681d9d1abcc Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:49:57 +0200 Subject: [PATCH 1023/1077] docs: add sergeybelozorov as a contributor for translation (#1542) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index bdd90810..0df9a78c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1359,6 +1359,15 @@ "contributions": [ "translation" ] + }, + { + "login": "sergeybelozorov", + "name": "sergeybelozorov", + "avatar_url": "https://avatars.githubusercontent.com/u/94930734?v=4", + "profile": "https://github.com/sergeybelozorov", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index d5b037cc..6a533883 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-149-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-150-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -703,6 +703,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark LukTyn
LukTyn

🌍 rutgerkra
rutgerkra

🌍 + sergeybelozorov
sergeybelozorov

🌍 From d6ddd3f27e7b222719164b08e0c4cdbe81b60ab4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 11:52:39 +0200 Subject: [PATCH 1024/1077] docs: credit Weblate-only translators (#1543) --- .all-contributorsrc | 27 +++++++++++++++++++++++++++ README.md | 5 ++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 0df9a78c..072934bd 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1368,6 +1368,33 @@ "contributions": [ "translation" ] + }, + { + "login": "weblate-Lamster", + "name": "Allan Himidi-Rattenborg", + "avatar_url": "https://hosted.weblate.org/avatar/128/Lamster.png", + "profile": "https://hosted.weblate.org/user/Lamster/", + "contributions": [ + "translation" + ] + }, + { + "login": "weblate-posemartonis", + "name": "Pose marto", + "avatar_url": "https://hosted.weblate.org/avatar/128/posemartonis.png", + "profile": "https://hosted.weblate.org/user/posemartonis/", + "contributions": [ + "translation" + ] + }, + { + "login": "weblate-jf.cosse", + "name": "Jean-Francois Cosse", + "avatar_url": "https://hosted.weblate.org/avatar/128/jf.cosse.png", + "profile": "https://hosted.weblate.org/user/jf.cosse/", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 6a533883..c48a3c81 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-150-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-153-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -704,6 +704,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark LukTyn
LukTyn

🌍 rutgerkra
rutgerkra

🌍 sergeybelozorov
sergeybelozorov

🌍 + Allan Himidi-Rattenborg
Allan Himidi-Rattenborg

🌍 + Pose marto
Pose marto

🌍 + Jean-Francois Cosse
Jean-Francois Cosse

🌍 From 508959fff3e9360563ed48ce986edf2596c2f1bd Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:58:24 +0200 Subject: [PATCH 1025/1077] docs: add bisquit2003 as a contributor for bug (#1544) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 072934bd..14224074 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1395,6 +1395,15 @@ "contributions": [ "translation" ] + }, + { + "login": "bisquit2003", + "name": "bisquit2003", + "avatar_url": "https://avatars.githubusercontent.com/u/98059406?v=4", + "profile": "https://github.com/bisquit2003", + "contributions": [ + "bug" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index c48a3c81..1e4bd028 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-153-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-154-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -707,6 +707,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Allan Himidi-Rattenborg
Allan Himidi-Rattenborg

🌍 Pose marto
Pose marto

🌍 Jean-Francois Cosse
Jean-Francois Cosse

🌍 + bisquit2003
bisquit2003

🐛 From 00036558289b70fb3df6d7eb9e6cba40aa101887 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:59:17 +0200 Subject: [PATCH 1026/1077] docs: add chewth91 as a contributor for bug (#1545) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 14224074..95d4ec6c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1404,6 +1404,15 @@ "contributions": [ "bug" ] + }, + { + "login": "chewth91", + "name": "chewth91", + "avatar_url": "https://avatars.githubusercontent.com/u/29686018?v=4", + "profile": "https://github.com/chewth91", + "contributions": [ + "bug" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 1e4bd028..afac01dc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-154-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-155-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -709,6 +709,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Jean-Francois Cosse
Jean-Francois Cosse

🌍 bisquit2003
bisquit2003

🐛 + + chewth91
chewth91

🐛 + From a04a533f45895b08c69c146853188b5450b82654 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 12:17:22 +0200 Subject: [PATCH 1027/1077] ci: support one year of Home Assistant releases (#1546) --- .github/workflows/pytest.yaml | 30 ++++++--------- docs/getting-started.md | 2 +- hacs.json | 2 +- scripts/update-test-matrix.py | 11 ++---- tests/test_switch.py | 71 +++++++---------------------------- 5 files changed, 30 insertions(+), 86 deletions(-) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index b34a7262..650ee02c 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -14,24 +14,6 @@ jobs: fail-fast: false matrix: include: - - core-version: "2024.12.5" - python-version: "3.12" - - core-version: "2025.1.4" - python-version: "3.12" - - core-version: "2025.2.5" - python-version: "3.13" - - core-version: "2025.3.4" - python-version: "3.13" - - core-version: "2025.4.4" - python-version: "3.13" - - core-version: "2025.5.3" - python-version: "3.13" - - core-version: "2025.6.3" - python-version: "3.13" - - core-version: "2025.7.4" - python-version: "3.13" - - core-version: "2025.8.3" - python-version: "3.13" - core-version: "2025.9.4" python-version: "3.13" - core-version: "2025.10.4" @@ -46,7 +28,17 @@ jobs: python-version: "3.13" - core-version: "2026.3.4" python-version: "3.14.2" - - core-version: "2026.4.3" + - core-version: "2026.4.4" + python-version: "3.14.2" + - core-version: "2026.5.4" + python-version: "3.14.2" + - core-version: "2026.6.4" + python-version: "3.14.2" + - core-version: "2026.7.4" + python-version: "3.14.2" + - core-version: "2026.8.3" + python-version: "3.14.2" + - core-version: "2026.9.1" python-version: "3.14.2" - core-version: "dev" python-version: "3.14.2" diff --git a/docs/getting-started.md b/docs/getting-started.md index 48826e84..706a9640 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -8,7 +8,7 @@ This guide will help you install and configure Adaptive Lighting for the first t ## Prerequisites -- [Home Assistant](https://www.home-assistant.io/) 2024.12.0 or newer +- [Home Assistant](https://www.home-assistant.io/) 2025.9.0 or newer - [HACS](https://hacs.xyz/) (Home Assistant Community Store) installed ## Installation diff --git a/hacs.json b/hacs.json index 545d0ec5..0887a9a7 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { "name": "Adaptive Lighting", "render_readme": true, - "homeassistant": "2024.12.0" + "homeassistant": "2025.9.0" } diff --git a/scripts/update-test-matrix.py b/scripts/update-test-matrix.py index 59b18baa..fea69423 100755 --- a/scripts/update-test-matrix.py +++ b/scripts/update-test-matrix.py @@ -15,9 +15,9 @@ import re import urllib.request from pathlib import Path -# Minimum HA Core version to include in the test matrix -# This should be the oldest version we want to support -MIN_VERSION = (2024, 12) +# Keep the latest stable release month and the preceding 12 monthly release lines. +# Update this explicit floor when dropping another supported release month. +MIN_VERSION = (2025, 9) def get_ha_core_versions() -> list[str]: @@ -82,11 +82,8 @@ def get_python_version(ha_version: str) -> str: """Determine Python version based on HA Core version.""" parts = ha_version.split(".") year, month = int(parts[0]), int(parts[1]) - # 2024.x and 2025.1 use Python 3.12. - # 2025.2 through 2026.2 use Python 3.13. + # 2025.9 through 2026.2 use Python 3.13. # 2026.3+ uses Python 3.14. - if year == 2024 or (year == 2025 and month == 1): - return "3.12" if year > 2026 or (year == 2026 and month >= 3): return "3.14.2" return "3.13" diff --git a/tests/test_switch.py b/tests/test_switch.py index 37395f70..9b73563e 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -5,7 +5,6 @@ import asyncio import contextlib import datetime import logging -from collections import OrderedDict from copy import deepcopy from random import randint from typing import Any @@ -90,17 +89,8 @@ from homeassistant.components.light import ( ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN - -try: - # HA >= 2025.8 - from homeassistant.components.template.light import ( - StateLightEntity as LightTemplate, - ) -except ImportError: - # HA < 2025.8 - from homeassistant.components.template.light import LightTemplate - from homeassistant.components.template import light as template_light +from homeassistant.components.template.light import StateLightEntity as LightTemplate from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, ConfigEntryState from homeassistant.const import ( ATTR_AREA_ID, @@ -116,7 +106,6 @@ from homeassistant.const import ( STATE_ON, EntityCategory, ) -from homeassistant.const import __version__ as ha_version from homeassistant.core import Context, Event, HomeAssistant, State from homeassistant.helpers import area_registry as ar from homeassistant.helpers import entity_registry @@ -125,6 +114,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util.color import color_temperature_mired_to_kelvin from tests.common import MockConfigEntry +from tests.common import mock_area_registry as mock_ha_area_registry # HA 2026.6 removed the legacy `light: platform: template` YAML format # (home-assistant/core#169615); use the modern `template:` format there. @@ -1854,56 +1844,21 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands): assert sleep_color_temp != color_temp -# Vendored in this function as it was broken -# https://github.com/home-assistant/core/pull/112150 (my PR and reported issue) -# Then removed: https://github.com/home-assistant/core/pull/112172 -# Then re-added: https://github.com/home-assistant/core/pull/113453 -# This version is no longer the same as the one in HA because of the many changes -# that have been made in 2024. def mock_area_registry( hass: HomeAssistant, ) -> ar.AreaRegistry: """Mock the Area Registry.""" - registry = ar.AreaRegistry(hass) - registry._area_data = {} - area_kwargs = { - "name": "Test Area", - "normalized_name": "test-area", - "id": "test-area", - "picture": None, - } - year, month = (int(x) for x in ha_version.split(".")[:2]) - dt = datetime.date(year, month, 1) - if dt >= datetime.date(2023, 1, 1): - area_kwargs["aliases"] = {} - if dt >= datetime.date(2024, 2, 1): - area_kwargs["icon"] = None - if dt >= datetime.date(2024, 3, 1): - area_kwargs["floor_id"] = "test-floor" - if dt >= datetime.date(2024, 11, 1): - area_kwargs.pop("normalized_name") - if dt >= datetime.date(2025, 2, 1): - area_kwargs["humidity_entity_id"] = None - area_kwargs["temperature_entity_id"] = None - - # This mess... 🤯 - if dt >= datetime.date(2024, 2, 1) and dt != datetime.date(2024, 4, 1): - # 2024.4 removed AreaRegistryItems and then added it back in 2024.5: - # https://github.com/home-assistant/core/pull/114777 - registry.areas = ar.AreaRegistryItems() - elif dt == datetime.date(2024, 4, 1): - from homeassistant.helpers.normalized_name_base_registry import ( - NormalizedNameBaseRegistryItems, - ) - - registry.areas = NormalizedNameBaseRegistryItems() - else: - registry.areas = OrderedDict() - - area = ar.AreaEntry(**area_kwargs) - registry.areas[area.id] = area - hass.data[ar.DATA_REGISTRY] = registry - return registry + area = ar.AreaEntry( + aliases=set(), + floor_id="test-floor", + humidity_entity_id=None, + icon=None, + id="test-area", + name="Test Area", + picture=None, + temperature_entity_id=None, + ) + return mock_ha_area_registry(hass, {area.id: area}) async def test_light_switch_in_specific_area(hass): From ecf2403422785901e43b2052ffe5f1055eea18ce Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 12:32:40 +0200 Subject: [PATCH 1028/1077] fix: publish manual-control state when it changes (#1538) --- custom_components/adaptive_lighting/switch.py | 14 +++ tests/test_switch.py | 111 ++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index d851702b..c37cbefb 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2319,6 +2319,18 @@ class AdaptiveLightingManager: ) self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset) + self._schedule_manual_control_state_update(light) + + def _schedule_manual_control_state_update(self, *lights: str) -> None: + """Publish shared manual-control state on every affected switch.""" + # State publication must not expand groups or change tracked lights. + for entry in self.hass.config_entries.async_entries(DOMAIN): + entry_data = self.hass.data[DOMAIN].get(entry.entry_id) + if entry_data is None: + continue + switch = entry_data.get(SWITCH_DOMAIN) + if switch is not None and set(lights).intersection(switch.lights): + switch.async_schedule_update_ha_state() def add_manual_control_attributes( self, @@ -2419,6 +2431,8 @@ class AdaptiveLightingManager: self.our_last_state_on_change.pop(light, None) self.last_service_data.pop(light, None) self.cancel_ongoing_adaptation_calls(light) + if reset_manual_control: + self._schedule_manual_control_state_update(*lights) def _get_entity_list(self, service_data: ServiceData) -> list[str]: if ATTR_ENTITY_ID in service_data: diff --git a/tests/test_switch.py b/tests/test_switch.py index 9b73563e..fe8df901 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1092,6 +1092,117 @@ async def test_mixed_turn_on_restarts_manual_control_timeout( ) +@pytest.mark.parametrize("intercept", [True, False]) +@pytest.mark.parametrize( + ("service_data", "brightness", "color"), + [ + ({ATTR_BRIGHTNESS: 200}, True, False), + ({"brightness_step": 5}, True, False), + ({ATTR_COLOR_TEMP_KELVIN: 4000}, False, True), + ({ATTR_BRIGHTNESS: 200, ATTR_COLOR_TEMP_KELVIN: 4000}, True, True), + ], +) +async def test_manual_control_state_updates_without_adaptation( + hass, + intercept, + service_data, + brightness, + color, +): + """Publish manual state on service changes and resets, without an interval tick.""" + switch, (light, *_) = await setup_lights_and_switch( + hass, + { + CONF_INTERCEPT: intercept, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + events = [] + hass.bus.async_listen(f"{DOMAIN}.manual_control", events.append) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id, **service_data}, + blocking=True, + context=Context(), + ) + await hass.async_block_till_done() + attrs = hass.states.get(switch.entity_id).attributes + assert attrs["manual_control"] == [light.entity_id] + assert attrs["manual_control_brightness"] == ( + [light.entity_id] if brightness else [] + ) + assert attrs["manual_control_color"] == ([light.entity_id] if color else []) + assert len(events) == 1 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: light.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + attrs = hass.states.get(switch.entity_id).attributes + assert attrs["manual_control"] == [] + assert attrs["manual_control_brightness"] == [] + assert attrs["manual_control_color"] == [] + + +async def test_manual_control_state_updates_shared_switches(hass): + """Publish shared state on both profiles when one receives a service call.""" + switch, (light, *_) = await setup_lights_and_switch(hass) + _, other = await setup_switch( + hass, + {CONF_NAME: "other", CONF_LIGHTS: [light.entity_id]}, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [light.entity_id], + CONF_MANUAL_CONTROL: "brightness", + }, + blocking=True, + ) + await hass.async_block_till_done() + for profile in (switch, other): + attrs = hass.states.get(profile.entity_id).attributes + assert attrs["manual_control"] == [light.entity_id] + assert attrs["manual_control_brightness"] == [light.entity_id] + assert attrs["manual_control_color"] == [] + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + {ATTR_ENTITY_ID: other.entity_id, CONF_MANUAL_CONTROL: False}, + blocking=True, + ) + await hass.async_block_till_done() + for profile in (switch, other): + attrs = hass.states.get(profile.entity_id).attributes + assert attrs["manual_control"] == [] + assert attrs["manual_control_brightness"] == [] + assert attrs["manual_control_color"] == [] + + +async def test_manual_control_state_ignores_incomplete_entries(hass): + """An entry awaiting platform setup must not break another profile's updates.""" + switch, (light, *_) = await setup_lights_and_switch(hass) + pending = MockConfigEntry(domain=DOMAIN, data={CONF_NAME: "pending"}) + pending.add_to_hass(hass) + hass.data[DOMAIN][pending.entry_id] = {} + + switch.manager.set_manual_control_attributes(light.entity_id) + await hass.async_block_till_done() + assert hass.states.get(switch.entity_id).attributes["manual_control"] == [ + light.entity_id, + ] + + async def test_adaptation_attribute_selection(hass): """Test the 'manual control' tracking.""" switch, (light, *_) = await setup_lights_and_switch(hass) From 722779d59bbe7b1fd6fb222fd932b1dd4317ab71 Mon Sep 17 00:00:00 2001 From: rhtenhove Date: Sun, 6 Sep 2026 12:35:45 +0200 Subject: [PATCH 1029/1077] Allow opting out of manual-control reset on sleep changes (#1063) * Disable manual control reset on sleep mode change * adapt test to new behavior * fix comment * add switch + test * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update auto-generated content * fix: preserve existing sleep-toggle reset defaults * fix: keep cancelling stale adaptations on sleep changes --------- Co-authored-by: Bas Nijholt Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: Bas Nijholt --- README.md | 83 +++++++++--------- custom_components/adaptive_lighting/const.py | 14 +++ .../adaptive_lighting/strings.json | 1 + custom_components/adaptive_lighting/switch.py | 10 ++- .../adaptive_lighting/translations/en.json | 1 + docs/configuration.md | 83 +++++++++--------- tests/test_switch.py | 86 ++++++++++++++++++- 7 files changed, 193 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index afac01dc..ecf064b2 100644 --- a/README.md +++ b/README.md @@ -123,47 +123,48 @@ The YAML and frontend configuration methods support all of the options listed be -| Variable name | Description | Default | Type | -|:-------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:----------------------------------------| -| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | -| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | -| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | -| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | -| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | -| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | -| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | -| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | -| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | -| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | -| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | -| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | -| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | -| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | -| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | -| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | -| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | -| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | -| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | -| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | -| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | -| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | -| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | -| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | -| `take_over_control` | Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒 | `True` | `bool` | -| `take_over_control_mode` | The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed. | `pause_all` | one of `['pause_all', 'pause_changed']` | -| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues. | `False` | `bool` | -| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | -| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | -| `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | -| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | -| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | -| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | -| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | -| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | -| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | -| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| Variable name | Description | Default | Type | +|:--------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:----------------------------------------| +| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | +| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | +| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | +| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | +| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | +| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | +| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | +| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | +| `take_over_control` | Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒 | `True` | `bool` | +| `take_over_control_mode` | The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed. | `pause_all` | one of `['pause_all', 'pause_changed']` | +| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues. | `False` | `bool` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | +| `reset_manual_control_on_sleep_mode_change` | Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴 | `True` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | +| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | +| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | +| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | +| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 502318f0..59d5959b 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -243,6 +243,15 @@ DOCS[CONF_AUTORESET_CONTROL] = ( "Set to 0 to disable. ⏲️" ) +( + CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, + DEFAULT_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, +) = ("reset_manual_control_on_sleep_mode_change", True) +DOCS[CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE] = ( + "Reset manual control when the sleep mode switch is toggled. " + "Set to `false` to preserve manual control across sleep mode changes. 😴" +) + CONF_SKIP_REDUNDANT_COMMANDS, DEFAULT_SKIP_REDUNDANT_COMMANDS = ( "skip_redundant_commands", False, @@ -394,6 +403,11 @@ VALIDATION_TUPLES: list[tuple[str, Any, Any]] = [ ), (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), (CONF_ADAPT_ONLY_ON_BARE_TURN_ON, DEFAULT_ADAPT_ONLY_ON_BARE_TURN_ON, bool), + ( + CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, + DEFAULT_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, + bool, + ), (CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool), (CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY, int_between(0, 10000)), (CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, cv.positive_float), diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index cf816d4f..070db7ec 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -58,6 +58,7 @@ "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", + "reset_manual_control_on_sleep_mode_change": "reset_manual_control_on_sleep_mode_change: Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay", "adapt_delay": "adapt_delay", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index c37cbefb..782f01f1 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -115,6 +115,7 @@ from .const import ( CONF_MULTI_LIGHT_INTERCEPT, CONF_ONLY_ONCE, CONF_PREFER_RGB_COLOR, + CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, CONF_SEND_SPLIT_DELAY, CONF_SEPARATE_TURN_ON_COMMANDS, CONF_SKIP_REDUNDANT_COMMANDS, @@ -960,6 +961,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] self._adapt_only_on_bare_turn_on = data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL] + self._reset_manual_control_on_sleep_mode_change = data[ + CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE + ] self._skip_redundant_commands = data[CONF_SKIP_REDUNDANT_COMMANDS] self._intercept = data[CONF_INTERCEPT] self._multi_light_intercept = data[CONF_MULTI_LIGHT_INTERCEPT] @@ -1644,8 +1648,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._name, event, ) - # Reset the manually controlled status when the "sleep mode" changes - self.manager.reset(*self.lights) + self.manager.reset( + *self.lights, + reset_manual_control=self._reset_manual_control_on_sleep_mode_change, + ) await self._update_attrs_and_maybe_adapt_lights( context=self.create_context("sleep", parent=event.context), transition=self._sleep_transition, diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 43bb2105..4bf3fcb9 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -59,6 +59,7 @@ "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", + "reset_manual_control_on_sleep_mode_change": "reset_manual_control_on_sleep_mode_change: Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay", "adapt_delay": "adapt_delay", diff --git a/docs/configuration.md b/docs/configuration.md index 237a53ae..7edaac57 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -32,47 +32,48 @@ All configuration options are listed below with their default values. These opti -| Variable name | Description | Default | Type | -|:-------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:----------------------------------------| -| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | -| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | -| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | -| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | -| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | -| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | -| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | -| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | -| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | -| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | -| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | -| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | -| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | -| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | -| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | -| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | -| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | -| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | -| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | -| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | -| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | -| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | -| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | -| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | -| `take_over_control` | Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒 | `True` | `bool` | -| `take_over_control_mode` | The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed. | `pause_all` | one of `['pause_all', 'pause_changed']` | -| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues. | `False` | `bool` | -| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | -| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | -| `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | -| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | -| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | -| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | -| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | -| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | -| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | -| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| Variable name | Description | Default | Type | +|:--------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:----------------------------------------| +| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | +| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | +| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` | +| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | +| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` | +| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` | +| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` | +| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` | +| `take_over_control` | Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒 | `True` | `bool` | +| `take_over_control_mode` | The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed. | `pause_all` | one of `['pause_all', 'pause_changed']` | +| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues. | `False` | `bool` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | +| `reset_manual_control_on_sleep_mode_change` | Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴 | `True` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | +| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | +| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | +| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | +| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | diff --git a/tests/test_switch.py b/tests/test_switch.py index fe8df901..6d2e169a 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -41,6 +41,7 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_MIN_COLOR_TEMP, CONF_MULTI_LIGHT_INTERCEPT, CONF_PREFER_RGB_COLOR, + CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, CONF_SEPARATE_TURN_ON_COMMANDS, CONF_SLEEP_RGB_OR_COLOR_TEMP, CONF_SUNRISE_OFFSET, @@ -771,7 +772,7 @@ async def test_manual_control( manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS ), manual_control - # Check that toggling (sleep mode) switch resets manual control + # Toggling the main or sleep switch resets manual control by default. for entity_id in [switch.entity_id, switch.sleep_mode_switch.entity_id]: await change_manual_control(True) assert manual_control[ENTITY_LIGHT_1] @@ -885,6 +886,89 @@ async def test_manual_control( assert state_attrs["manual_control_color"] == [ENTITY_LIGHT_1] +@pytest.mark.parametrize("reset_on_sleep", [None, True, False]) +async def test_sleep_mode_manual_control_reset(hass, reset_on_sleep): + """Keep the old default and preserve manual brightness only when opted out.""" + options = {CONF_MIN_BRIGHTNESS: 50, CONF_MAX_BRIGHTNESS: 50} + if reset_on_sleep is not None: + options[CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE] = reset_on_sleep + switch, (light, *_) = await setup_lights_and_switch(hass, options) + + for service, adapted_brightness in [(SERVICE_TURN_ON, 3), (SERVICE_TURN_OFF, 128)]: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id, ATTR_BRIGHTNESS: 200}, + blocking=True, + ) + await hass.async_block_till_done() + assert switch.manager.get_manual_control_attributes(light.entity_id) + await hass.services.async_call( + SWITCH_DOMAIN, + service, + {ATTR_ENTITY_ID: switch.sleep_mode_switch.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + if reset_on_sleep is False: + assert switch.manager.get_manual_control_attributes(light.entity_id) + assert hass.states.get(light.entity_id).attributes[ATTR_BRIGHTNESS] == 200 + else: + assert not switch.manager.get_manual_control_attributes(light.entity_id) + assert ( + hass.states.get(light.entity_id).attributes[ATTR_BRIGHTNESS] + == adapted_brightness + ) + + +async def test_sleep_mode_preserves_manual_control_and_cancels_old_adaptation(hass): + """A queued pre-sleep command must not overwrite a preserved manual setting.""" + switch, (light, *_) = await setup_lights_and_switch( + hass, + {CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE: False}, + ) + waiting = asyncio.Event() + release = asyncio.Event() + + async def pending_service_data(): + waiting.set() + await release.wait() + yield {ATTR_ENTITY_ID: light.entity_id, ATTR_BRIGHTNESS: 1} + + data = AdaptationData( + light.entity_id, + switch.create_context("test"), + 0, + pending_service_data(), + force=False, + max_length=1, + attributes=LightControlAttributes.BRIGHTNESS, + ) + task = asyncio.create_task(switch.execute_cancellable_adaptation_calls(data)) + await waiting.wait() + try: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id, ATTR_BRIGHTNESS: 200}, + blocking=True, + ) + await hass.async_block_till_done() + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: switch.sleep_mode_switch.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + finally: + release.set() + await task + await hass.async_block_till_done() + assert switch.manager.get_manual_control_attributes(light.entity_id) + assert hass.states.get(light.entity_id).attributes[ATTR_BRIGHTNESS] == 200 + + @flaky(max_runs=3, min_passes=1) @pytest.mark.parametrize("mode", list(TakeOverControlMode)) async def test_auto_reset_manual_control(hass, mode): From 288d9d344a3d8033ec7f636d58db3c45548d36ae Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:37:02 +0200 Subject: [PATCH 1030/1077] docs: add rhtenhove as a contributor for code (#1547) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 95d4ec6c..41547d6b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1413,6 +1413,15 @@ "contributions": [ "bug" ] + }, + { + "login": "rhtenhove", + "name": "rhtenhove", + "avatar_url": "https://avatars.githubusercontent.com/u/10206967?v=4", + "profile": "https://github.com/rhtenhove", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index ecf064b2..bd7a7acb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-155-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-156-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -712,6 +712,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark chewth91
chewth91

🐛 + rhtenhove
rhtenhove

💻 From 1969a8cdff5fb95c6d24f3458c983d52bf0dfbf0 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:50:53 +0200 Subject: [PATCH 1031/1077] docs: add mrbillpapas as a contributor for ideas (#1548) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 41547d6b..16e80547 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1422,6 +1422,15 @@ "contributions": [ "code" ] + }, + { + "login": "mrbillpapas", + "name": "Bill Papas", + "avatar_url": "https://avatars.githubusercontent.com/u/45721000?v=4", + "profile": "https://github.com/mrbillpapas", + "contributions": [ + "ideas" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index bd7a7acb..8d570c26 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-156-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-157-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -713,6 +713,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark chewth91
chewth91

🐛 rhtenhove
rhtenhove

💻 + Bill Papas
Bill Papas

🤔 From 6fdbffc7fc46f0ea2264c7ea1588e3d09197a6f4 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:51:34 +0200 Subject: [PATCH 1032/1077] docs: add haasn as a contributor for ideas (#1549) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 16e80547..17880095 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1431,6 +1431,15 @@ "contributions": [ "ideas" ] + }, + { + "login": "haasn", + "name": "Niklas Haas", + "avatar_url": "https://avatars.githubusercontent.com/u/1149047?v=4", + "profile": "https://niklashaas.de", + "contributions": [ + "ideas" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 8d570c26..7cd639e0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-157-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-158-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -714,6 +714,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark chewth91
chewth91

🐛 rhtenhove
rhtenhove

💻 Bill Papas
Bill Papas

🤔 + Niklas Haas
Niklas Haas

🤔 From dda664ac7e61d929ed859c84f0c21a9feea67cce Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:52:07 +0200 Subject: [PATCH 1033/1077] docs: add djurny as a contributor for ideas (#1550) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 17880095..c7c59254 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1440,6 +1440,15 @@ "contributions": [ "ideas" ] + }, + { + "login": "djurny", + "name": "Tom Urlings", + "avatar_url": "https://avatars.githubusercontent.com/u/950171?v=4", + "profile": "https://github.com/djurny", + "contributions": [ + "ideas" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 7cd639e0..4f10f483 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-158-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-159-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -715,6 +715,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark rhtenhove
rhtenhove

💻 Bill Papas
Bill Papas

🤔 Niklas Haas
Niklas Haas

🤔 + Tom Urlings
Tom Urlings

🤔 From 8fb22adc45a8f1f8c36db28c55a19ea115155fcb Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:52:39 +0200 Subject: [PATCH 1034/1077] docs: add BenoitAnastay as a contributor for ideas (#1551) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index c7c59254..8b164338 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1449,6 +1449,15 @@ "contributions": [ "ideas" ] + }, + { + "login": "BenoitAnastay", + "name": "Benoit Anastay", + "avatar_url": "https://avatars.githubusercontent.com/u/45088785?v=4", + "profile": "http://anastay.dev", + "contributions": [ + "ideas" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 4f10f483..ce425f35 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-159-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-160-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -716,6 +716,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Bill Papas
Bill Papas

🤔 Niklas Haas
Niklas Haas

🤔 Tom Urlings
Tom Urlings

🤔 + Benoit Anastay
Benoit Anastay

🤔 From 17b0aa1103151180f3a0a0811af34e0948fc3e54 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:53:12 +0200 Subject: [PATCH 1035/1077] docs: add GollyJer as a contributor for ideas (#1552) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 8b164338..d2e76cb9 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1458,6 +1458,15 @@ "contributions": [ "ideas" ] + }, + { + "login": "GollyJer", + "name": "Jeremy Gollehon", + "avatar_url": "https://avatars.githubusercontent.com/u/689204?v=4", + "profile": "https://isjustawesome.com", + "contributions": [ + "ideas" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index ce425f35..ec74721e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-160-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-161-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -717,6 +717,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Niklas Haas
Niklas Haas

🤔 Tom Urlings
Tom Urlings

🤔 Benoit Anastay
Benoit Anastay

🤔 + Jeremy Gollehon
Jeremy Gollehon

🤔 From f97e502cbc79e30a07eb8f991976aba29c1fab5a Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:53:46 +0200 Subject: [PATCH 1036/1077] docs: add jrbergen as a contributor for ideas (#1553) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index d2e76cb9..be2a096b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1467,6 +1467,15 @@ "contributions": [ "ideas" ] + }, + { + "login": "jrbergen", + "name": "jrbergen", + "avatar_url": "https://avatars.githubusercontent.com/u/6237646?v=4", + "profile": "https://github.com/jrbergen", + "contributions": [ + "ideas" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index ec74721e..4719367f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-161-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-162-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -719,6 +719,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Benoit Anastay
Benoit Anastay

🤔 Jeremy Gollehon
Jeremy Gollehon

🤔 + + jrbergen
jrbergen

🤔 + From 6254b56dab9af194ebd65b6e5d172178ac1f07d2 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:54:18 +0200 Subject: [PATCH 1037/1077] docs: add b-rad15 as a contributor for ideas (#1554) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index be2a096b..7da20513 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1476,6 +1476,15 @@ "contributions": [ "ideas" ] + }, + { + "login": "b-rad15", + "name": "Bradley O'Connell", + "avatar_url": "https://avatars.githubusercontent.com/u/25830163?v=4", + "profile": "https://github.com/b-rad15", + "contributions": [ + "ideas" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 4719367f..278ee2e3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-162-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-163-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -721,6 +721,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark jrbergen
jrbergen

🤔 + Bradley O'Connell
Bradley O'Connell

🤔 From 00185e75e03c586ec7a3448dff6fb63355dd1264 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:54:51 +0200 Subject: [PATCH 1038/1077] docs: add 00schteven as a contributor for ideas (#1555) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 7da20513..86ead797 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1485,6 +1485,15 @@ "contributions": [ "ideas" ] + }, + { + "login": "00schteven", + "name": "00schteven", + "avatar_url": "https://avatars.githubusercontent.com/u/76514745?v=4", + "profile": "https://github.com/00schteven", + "contributions": [ + "ideas" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 278ee2e3..38ac40a4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-163-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-164-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -722,6 +722,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark jrbergen
jrbergen

🤔 Bradley O'Connell
Bradley O'Connell

🤔 + 00schteven
00schteven

🤔 From ac0336fe345658278b789ecc933dcd89cbc6b167 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:55:26 +0200 Subject: [PATCH 1039/1077] docs: add protyposis as a contributor for ideas (#1556) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 86ead797..1f673c0d 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -447,7 +447,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/189372?v=4", "profile": "http://protyposis.net", "contributions": [ - "code" + "code", + "ideas" ] }, { diff --git a/README.md b/README.md index 38ac40a4..be49d7ad 100644 --- a/README.md +++ b/README.md @@ -572,7 +572,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Chris
Chris

💻 Raman Gupta
Raman Gupta

💻 igiannakas
igiannakas

💻 - Mario Guggenberger
Mario Guggenberger

💻 + Mario Guggenberger
Mario Guggenberger

💻 🤔 Kendell R
Kendell R

🎨 From 623dd65aef264be2d0cfb57e785419ec82897cc5 Mon Sep 17 00:00:00 2001 From: Adam DeMuri Date: Sun, 6 Sep 2026 05:04:18 -0600 Subject: [PATCH 1040/1077] Register service actions in async_setup for Bronze tier compliance (#1403) * Register service actions in async_setup for Bronze tier compliance - Move 'apply' and 'set_manual_control' service registration from async_setup_entry to async_setup. - Move service handlers to module-level functions in switch.py. - Update apply_service_schema to support dynamic defaults for transition duration. - Clean up related unused imports and fix Python 3.10 syntax compatibility. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Clean up and add tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix lint errors * Automated update of generated docs * Re-add types * Make transition not required again * fix: validate global service targets * fix: document optional apply transition * fix: derive service docs from schema markers * docs: clarify service target options * fix: preserve entity service target handling --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- README.md | 6 +- .../adaptive_lighting/__init__.py | 43 ++- .../adaptive_lighting/_docs_helpers.py | 17 +- custom_components/adaptive_lighting/const.py | 20 +- .../adaptive_lighting/services.yaml | 10 +- .../adaptive_lighting/strings.json | 4 - custom_components/adaptive_lighting/switch.py | 270 +++++++++--------- .../adaptive_lighting/translations/en.json | 4 - docs/services.md | 6 +- tests/test_init.py | 134 ++++++++- tests/test_switch.py | 133 ++++++++- 11 files changed, 483 insertions(+), 164 deletions(-) diff --git a/README.md b/README.md index be49d7ad..2ed03505 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,7 @@ adaptive_lighting: #### `adaptive_lighting.apply` `adaptive_lighting.apply` applies Adaptive Lighting settings to lights on demand. +Provide a switch in `entity_id`, a list of `lights`, or both. @@ -212,7 +213,7 @@ adaptive_lighting: | Service data attribute | Description | Required | Type | |:-------------------------|:--------------------------------------------------------------------------------------|:-----------|:---------------------| -| `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ✅ | list of `entity_id`s | +| `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ❌ | list of `entity_id`s | | `lights` | A light (or list of lights) to apply the settings to. 💡 | ❌ | list of `entity_id`s | | `transition` | Duration of transition when lights change, in seconds. 🕑 | ❌ | `float` 0-6553 | | `adapt_brightness` | Whether to adapt the brightness of the light. 🌞 | ❌ | bool | @@ -224,6 +225,7 @@ adaptive_lighting: #### `adaptive_lighting.set_manual_control` `adaptive_lighting.set_manual_control` can mark (or unmark) whether a light is "manually controlled", meaning that when a light has `manual_control`, the light is not adapted. +Provide a switch in `entity_id`, a list of `lights`, or both. @@ -234,7 +236,7 @@ adaptive_lighting: | Service data attribute | Description | Required | Type | |:-------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:-----------------------------------------| -| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s | +| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ❌ | list of `entity_id`s | | `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s | | `manual_control` | Whether to add ("true") or remove ("false") all adapted attributes of the light from the "manual_control" list, or the name of an attribute for selective addition. 🔒 | ❌ | bool or one of `['brightness', 'color']` | diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index 8e61e93b..0ac9ef71 100644 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -1,20 +1,33 @@ """Adaptive Lighting integration in Home-Assistant.""" import logging +from functools import partial from typing import Any import homeassistant.helpers.config_validation as cv import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_SOURCE +from homeassistant.const import CONF_SOURCE, Platform from homeassistant.core import Event, HomeAssistant +from homeassistant.helpers import service from .const import ( _DOMAIN_SCHEMA, # pyright: ignore[reportPrivateUsage] ATTR_ADAPTIVE_LIGHTING_MANAGER, CONF_NAME, DOMAIN, + SERVICE_APPLY, + SERVICE_CHANGE_SWITCH_SETTINGS, + SERVICE_SET_MANUAL_CONTROL, + SET_MANUAL_CONTROL_SCHEMA, UNDO_UPDATE_LISTENER, + apply_service_schema, + change_switch_settings_schema, +) +from .switch import ( + handle_apply_service, + handle_change_switch_settings, + handle_set_manual_control_service, ) _LOGGER = logging.getLogger(__name__) @@ -47,6 +60,34 @@ async def reload_configuration_yaml(event: Event) -> None: async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: """Import integration from config.""" + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_APPLY, + service_func=partial(handle_apply_service, hass), + schema=apply_service_schema(), + ) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_SET_MANUAL_CONTROL, + service_func=partial(handle_set_manual_control_service, hass), + schema=SET_MANUAL_CONTROL_SCHEMA, + ) + + if register_platform_service := getattr( + service, + "async_register_platform_entity_service", + None, + ): + register_platform_service( + hass, + DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + entity_domain=Platform.SWITCH, + func=handle_change_switch_settings, + schema=change_switch_settings_schema(), + ) + if DOMAIN in config: for entry in config[DOMAIN]: hass.async_create_task( diff --git a/custom_components/adaptive_lighting/_docs_helpers.py b/custom_components/adaptive_lighting/_docs_helpers.py index 0c4ed45d..c90d0c87 100644 --- a/custom_components/adaptive_lighting/_docs_helpers.py +++ b/custom_components/adaptive_lighting/_docs_helpers.py @@ -74,22 +74,21 @@ def generate_config_markdown_table() -> str: return df.to_markdown(index=False) -def _schema_to_dict(schema: vol.Schema) -> dict[str, tuple[Any, Any]]: - result: dict[str, tuple[Any, Any]] = {} +def _schema_to_dict(schema: vol.Schema) -> dict[str, tuple[bool, Any]]: + result: dict[str, tuple[bool, Any]] = {} for key, value in schema.schema.items(): - if isinstance(key, vol.Optional): - default_value = key.default - result[key.schema] = (default_value, value) + if isinstance(key, vol.Required | vol.Optional): + required = isinstance(key, vol.Required) and key.default == vol.UNDEFINED + result[key.schema] = (required, value) return result def _generate_service_markdown_table( - schema: dict[str, tuple[Any, Any]] | vol.Schema, + schema: vol.Schema, alternative_docs: dict[str, str] | None = None, ) -> str: - schema_dict = _schema_to_dict(schema) if isinstance(schema, vol.Schema) else schema rows: list[dict[str, str]] = [] - for k, (default, type_) in schema_dict.items(): + for k, (required, type_) in _schema_to_dict(schema).items(): if alternative_docs is not None and k in alternative_docs: description = alternative_docs[k] else: @@ -97,7 +96,7 @@ def _generate_service_markdown_table( row = { "Service data attribute": f"`{k}`", "Description": description, - "Required": "✅" if default == vol.UNDEFINED else "❌", + "Required": "✅" if required else "❌", "Type": _type_to_str(type_), } rows.append(row) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 59d5959b..4e258587 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -473,16 +473,13 @@ _DOMAIN_SCHEMA = vol.Schema( ) -def apply_service_schema(initial_transition: int = 1) -> vol.Schema: +def apply_service_schema() -> vol.Schema: """Return the schema for the apply service.""" return vol.Schema( { vol.Optional(CONF_ENTITY_ID): cv.entity_ids, # type: ignore[arg-type] vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, # type: ignore[arg-type] - vol.Optional( - CONF_TRANSITION, - default=initial_transition, - ): VALID_TRANSITION, + vol.Optional(CONF_TRANSITION): VALID_TRANSITION, vol.Optional(ATTR_ADAPT_BRIGHTNESS, default=True): cv.boolean, vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean, vol.Optional(CONF_PREFER_RGB_COLOR, default=False): cv.boolean, @@ -491,6 +488,19 @@ def apply_service_schema(initial_transition: int = 1) -> vol.Schema: ) +def change_switch_settings_schema() -> dict[vol.Marker, Any]: + """Return the schema for the change_switch_settings service.""" + args: dict[vol.Marker, Any] = { + vol.Optional(CONF_USE_DEFAULTS, default="current"): cv.string, + } + # Modifying these after init isn't possible + skip = (CONF_INTERVAL, CONF_NAME, CONF_LIGHTS) + for k, _, valid in VALIDATION_TUPLES: + if k not in skip: + args[vol.Optional(k)] = valid + return args + + SET_MANUAL_CONTROL_SCHEMA = vol.Schema( { vol.Optional(CONF_ENTITY_ID): cv.entity_ids, # type: ignore[arg-type] diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 77ba0b7a..2471e83a 100644 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -64,13 +64,11 @@ set_manual_control: boolean: null change_switch_settings: description: Change any settings you'd like in the switch. All options here are the same as in the config flow. + target: + entity: + integration: adaptive_lighting + domain: switch fields: - entity_id: - description: Entity ID of the switch. 📝 - required: true - selector: - entity: - domain: switch use_defaults: description: 'Sets the default values not specified in this service call. Options: "current" (default, retains current values), "factory" (resets to documented defaults), or "configuration" (reverts to switch config defaults). ⚙️' example: current diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 070db7ec..cf47241e 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -156,10 +156,6 @@ "name": "change_switch_settings", "description": "Change any settings you'd like in the switch. All options here are the same as in the config flow.", "fields": { - "entity_id": { - "description": "Entity ID of the switch. 📝", - "name": "entity_id" - }, "use_defaults": { "description": "Sets the default values not specified in this service call. Options: \"current\" (default, retains current values), \"factory\" (resets to documented defaults), or \"configuration\" (reverts to switch config defaults). ⚙️", "name": "use_defaults" diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 782f01f1..0b49e8d9 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -14,7 +14,6 @@ from typing import TYPE_CHECKING, Any import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util import ulid_transform -import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP_KELVIN, @@ -58,7 +57,8 @@ from homeassistant.core import ( State, callback, ) -from homeassistant.helpers import entity_platform, entity_registry +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_platform, entity_registry, service from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.event import ( @@ -139,15 +139,12 @@ from .const import ( ICON_COLOR_TEMP, ICON_MAIN, ICON_SLEEP, - SERVICE_APPLY, SERVICE_CHANGE_SWITCH_SETTINGS, - SERVICE_SET_MANUAL_CONTROL, - SET_MANUAL_CONTROL_SCHEMA, SLEEP_MODE_SWITCH, TURNING_OFF_DELAY, VALIDATION_TUPLES, TakeOverControlMode, - apply_service_schema, + change_switch_settings_schema, replace_none_str, ) from .hass_utils import area_entities, setup_service_call_interceptor @@ -164,7 +161,7 @@ if TYPE_CHECKING: from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity_platform import AddEntitiesCallback - from homeassistant.helpers.typing import NoEventData, VolDictType + from homeassistant.helpers.typing import NoEventData try: from homeassistant.helpers.sun import get_astral_observer @@ -243,16 +240,22 @@ def _switches_with_lights( ) -> AdaptiveSwitches: """Get all switches that control at least one of the lights passed.""" config_entries = hass.config_entries.async_entries(DOMAIN) - data = hass.data[DOMAIN] - switches: AdaptiveSwitches = [] + data = hass.data.get(DOMAIN, {}) + loaded_switches: AdaptiveSwitches = [] + for config in config_entries: + entry = data.get(config.entry_id) + if not isinstance(entry, dict) or SWITCH_DOMAIN not in entry: + continue + loaded_switches.append(entry[SWITCH_DOMAIN]) + + if not loaded_switches: + return [] + all_check_lights = ( _expand_light_groups(hass, lights) if expand_light_groups else set(lights) ) - for config in config_entries: - entry = data.get(config.entry_id) - if entry is None: # entry might be disabled and therefore missing - continue - switch = data[config.entry_id][SWITCH_DOMAIN] + switches: AdaptiveSwitches = [] + for switch in loaded_switches: switch._expand_light_groups(hass=hass) # Check if any of the lights are in the switch's lights if set(switch.lights) & set(all_check_lights): @@ -299,7 +302,7 @@ def _switches_from_service_call( service_call: ServiceCall, ) -> AdaptiveSwitches: data = service_call.data - lights = data[CONF_LIGHTS] + lights = data.get(CONF_LIGHTS) switch_entity_ids: list[str] | None = data.get("entity_id") if not lights and not switch_entity_ids: @@ -310,7 +313,12 @@ def _switches_from_service_call( " use case. Currently, you must pass either an adaptive-lighting switch or" " the lights to an `adaptive_lighting` service call." ) - raise ValueError(msg) + raise ServiceValidationError(msg) + + domain_data = hass.data.get(DOMAIN) + if not domain_data: + msg = "adaptive-lighting: No Adaptive Lighting config entries are loaded." + raise ServiceValidationError(msg) if switch_entity_ids is not None: if len(switch_entity_ids) > 1 and lights: @@ -318,32 +326,59 @@ def _switches_from_service_call( "adaptive-lighting: Cannot pass multiple switches with lights argument." f" Invalid service data received: {service_call.data}" ) - raise ValueError(msg) + raise ServiceValidationError(msg) switches: AdaptiveSwitches = [] + config_ids: set[str] = set() ent_reg = entity_registry.async_get(hass) for entity_id in switch_entity_ids: ent_entry = ent_reg.async_get(entity_id) - assert ent_entry is not None + if ent_entry is None: + msg = f"adaptive-lighting: Entity '{entity_id}' not found in registry." + raise ServiceValidationError(msg) + if ent_entry.platform != DOMAIN: + msg = ( + f"adaptive-lighting: Entity '{entity_id}' is not registered by" + " Adaptive Lighting." + ) + raise ServiceValidationError(msg) config_id = ent_entry.config_entry_id - switches.append(hass.data[DOMAIN][config_id][SWITCH_DOMAIN]) + config_data = domain_data.get(config_id) if config_id else None + if ( + config_id is None + or not isinstance(config_data, dict) + or SWITCH_DOMAIN not in config_data + ): + msg = ( + f"adaptive-lighting: Adaptive Lighting entry for entity '{entity_id}'" + " is not loaded." + ) + raise ServiceValidationError(msg) + if config_id not in config_ids: + switches.append(config_data[SWITCH_DOMAIN]) + config_ids.add(config_id) return switches if lights: - switch = _switch_with_lights(hass, lights) + try: + switch = _switch_with_lights(hass, lights) + except NoSwitchFoundError as err: + raise ServiceValidationError(str(err)) from err return [switch] msg = ( "adaptive-lighting: Incorrect data provided in service call." f" Entities not found in the integration. Service data: {service_call.data}" ) - raise ValueError(msg) + raise ServiceValidationError(msg) async def handle_change_switch_settings( - switch: AdaptiveSwitch, + switch: AdaptiveSwitch | SimpleSwitch, service_call: ServiceCall, ) -> None: """Allows HASS to change config values via a service call.""" + if not isinstance(switch, AdaptiveSwitch): + return data = service_call.data which = data.get(CONF_USE_DEFAULTS, "current") if which == "current": # use whatever we're already using. @@ -376,7 +411,79 @@ async def handle_change_switch_settings( ) -async def async_setup_entry( # noqa: PLR0915 +async def handle_apply_service(hass: HomeAssistant, service_call: ServiceCall) -> None: + """Handle the entity service apply.""" + data = service_call.data + _LOGGER.debug( + "Called 'adaptive_lighting.apply' service with '%s'", + data, + ) + switches = _switches_from_service_call(hass, service_call) + lights = data[CONF_LIGHTS] + for switch in switches: + all_lights = switch.lights if not lights else _expand_light_groups(hass, lights) + switch.manager.lights.update(all_lights) + for light in all_lights: + if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light): + context = switch.create_context( + "service", + parent=service_call.context, + ) + transition = data.get(CONF_TRANSITION) + if transition is None: + transition = switch.initial_transition + await switch._adapt_light( # pylint: disable=protected-access + light, + context=context, + transition=transition, + adapt_brightness=data[ATTR_ADAPT_BRIGHTNESS], + adapt_color=data[ATTR_ADAPT_COLOR], + prefer_rgb_color=data[CONF_PREFER_RGB_COLOR], + force=True, + ) + + +async def handle_set_manual_control_service( + hass: HomeAssistant, + service_call: ServiceCall, +) -> None: + """Set or unset lights as manually controlled.""" + data = service_call.data + _LOGGER.debug( + "Called 'adaptive_lighting.set_manual_control' service with '%s'", + data, + ) + switches = _switches_from_service_call(hass, service_call) + lights = data[CONF_LIGHTS] + for switch in switches: + all_lights = switch.lights if not lights else _expand_light_groups(hass, lights) + manual_attributes = manual_control_event_attribute_to_flags( + data[CONF_MANUAL_CONTROL], + ) + + if manual_attributes: + for light in all_lights: + switch.manager.set_manual_control_attributes( + light, + manual_attributes, + ) + switch.fire_manual_control_event(light, service_call.context) + else: + switch.manager.reset(*all_lights) + if switch.is_on: + context = switch.create_context( + "service", + parent=service_call.context, + ) + await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access + context=context, + lights=all_lights, + transition=switch.initial_transition, + force=True, + ) + + +async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, @@ -446,112 +553,14 @@ async def async_setup_entry( # noqa: PLR0915 update_before_add=True, ) - @callback - async def handle_apply(service_call: ServiceCall) -> None: - """Handle the entity service apply.""" - data = service_call.data - _LOGGER.debug( - "Called 'adaptive_lighting.apply' service with '%s'", - data, + if not hasattr(service, "async_register_platform_entity_service"): + platform = entity_platform.current_platform.get() + assert platform is not None + platform.async_register_entity_service( + SERVICE_CHANGE_SWITCH_SETTINGS, + change_switch_settings_schema(), + handle_change_switch_settings, ) - switches = _switches_from_service_call(hass, service_call) - lights = data[CONF_LIGHTS] - for switch in switches: - if not lights: - all_lights = switch.lights - else: - all_lights = _expand_light_groups(hass, lights) - switch.manager.lights.update(all_lights) - for light in all_lights: - if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light): - context = switch.create_context( - "service", - parent=service_call.context, - ) - await switch._adapt_light( # pylint: disable=protected-access - light, - context=context, - transition=data[CONF_TRANSITION], - adapt_brightness=data[ATTR_ADAPT_BRIGHTNESS], - adapt_color=data[ATTR_ADAPT_COLOR], - prefer_rgb_color=data[CONF_PREFER_RGB_COLOR], - force=True, - ) - - @callback - async def handle_set_manual_control(service_call: ServiceCall) -> None: - """Set or unset lights as 'manually controlled'.""" - data = service_call.data - _LOGGER.debug( - "Called 'adaptive_lighting.set_manual_control' service with '%s'", - data, - ) - switches = _switches_from_service_call(hass, service_call) - lights = data[CONF_LIGHTS] - for switch in switches: - if not lights: - all_lights = switch.lights - else: - all_lights = _expand_light_groups(hass, lights) - - manual_attributes = manual_control_event_attribute_to_flags( - service_call.data[CONF_MANUAL_CONTROL], - ) - - if manual_attributes: - for light in all_lights: - switch.manager.set_manual_control_attributes( - light, - manual_attributes, - ) - switch.fire_manual_control_event( - light, - service_call.context, - ) - else: - switch.manager.reset(*all_lights) - if switch.is_on: - context = switch.create_context( - "service", - parent=service_call.context, - ) - # pylint: disable=protected-access - await switch._update_attrs_and_maybe_adapt_lights( - context=context, - lights=all_lights, - transition=switch.initial_transition, - force=True, - ) - - # Register `apply` service - hass.services.async_register( - domain=DOMAIN, - service=SERVICE_APPLY, - service_func=handle_apply, - schema=apply_service_schema(switch.initial_transition), - ) - - # Register `set_manual_control` service - hass.services.async_register( - domain=DOMAIN, - service=SERVICE_SET_MANUAL_CONTROL, - service_func=handle_set_manual_control, - schema=SET_MANUAL_CONTROL_SCHEMA, - ) - - args: VolDictType = {vol.Optional(CONF_USE_DEFAULTS, default="current"): cv.string} - # Modifying these after init isn't possible - skip = (CONF_INTERVAL, CONF_NAME, CONF_LIGHTS) - for k, _, valid in VALIDATION_TUPLES: - if k not in skip: - args[vol.Optional(k)] = valid - platform = entity_platform.current_platform.get() - assert platform is not None - platform.async_register_entity_service( - SERVICE_CHANGE_SWITCH_SETTINGS, - args, - handle_change_switch_settings, - ) def validate( @@ -1846,9 +1855,12 @@ class AdaptiveLightingManager: ) def disable(self) -> None: - """Disable the listener by removing all subscribed handlers.""" + """Disable listeners and pending automatic manual-control resets.""" for remove in self.listener_removers: remove() + for timer in self.auto_reset_manual_control_timers.values(): + timer.cancel() + self.auto_reset_manual_control_timers.clear() def set_proactively_adapting(self, context_id: str, entity_id: str) -> None: """Declare the adaptation with context_id as proactively adapting, diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 4bf3fcb9..9d218f62 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -157,10 +157,6 @@ "name": "change_switch_settings", "description": "Change any settings you'd like in the switch. All options here are the same as in the config flow.", "fields": { - "entity_id": { - "description": "Entity ID of the switch. 📝", - "name": "entity_id" - }, "use_defaults": { "description": "Sets the default values not specified in this service call. Options: \"current\" (default, retains current values), \"factory\" (resets to documented defaults), or \"configuration\" (reverts to switch config defaults). ⚙️", "name": "use_defaults" diff --git a/docs/services.md b/docs/services.md index 306aa7c3..6258017a 100644 --- a/docs/services.md +++ b/docs/services.md @@ -9,6 +9,7 @@ Adaptive Lighting provides three services for programmatic control, allowing you ## adaptive_lighting.apply Applies the current Adaptive Lighting settings to lights on demand. Useful for forcing an immediate update or applying settings to lights that aren't in the regular adaptation cycle. +Provide a switch in `entity_id`, a list of `lights`, or both. ### Parameters @@ -20,7 +21,7 @@ Applies the current Adaptive Lighting settings to lights on demand. Useful for f | Service data attribute | Description | Required | Type | |:-------------------------|:--------------------------------------------------------------------------------------|:-----------|:---------------------| -| `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ✅ | list of `entity_id`s | +| `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ❌ | list of `entity_id`s | | `lights` | A light (or list of lights) to apply the settings to. 💡 | ❌ | list of `entity_id`s | | `transition` | Duration of transition when lights change, in seconds. 🕑 | ❌ | `float` 0-6553 | | `adapt_brightness` | Whether to adapt the brightness of the light. 🌞 | ❌ | bool | @@ -58,6 +59,7 @@ data: ## adaptive_lighting.set_manual_control Marks or unmarks a light as "manually controlled". When a light is marked as manually controlled, Adaptive Lighting will not adjust it until the manual control flag is cleared. +Provide a switch in `entity_id`, a list of `lights`, or both. ### Parameters @@ -69,7 +71,7 @@ Marks or unmarks a light as "manually controlled". When a light is marked as man | Service data attribute | Description | Required | Type | |:-------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:-----------------------------------------| -| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s | +| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ❌ | list of `entity_id`s | | `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s | | `manual_control` | Whether to add ("true") or remove ("false") all adapted attributes of the light from the "manual_control" list, or the name of an attribute for selective addition. 🔒 | ❌ | bool or one of `['brightness', 'color']` | diff --git a/tests/test_init.py b/tests/test_init.py index 6bbfd599..aaf23851 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,12 +1,21 @@ """Tests for Adaptive Lighting integration.""" +import pytest +import voluptuous.error from homeassistant.components import adaptive_lighting from homeassistant.components.adaptive_lighting.const import ( + CONF_LIGHTS, DEFAULT_NAME, + SERVICE_APPLY, + SERVICE_CHANGE_SWITCH_SETTINGS, + SERVICE_SET_MANUAL_CONTROL, UNDO_UPDATE_LISTENER, ) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_NAME +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import service from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -53,3 +62,126 @@ async def test_unload_entry(hass): assert entry.state == ConfigEntryState.NOT_LOADED assert adaptive_lighting.DOMAIN not in hass.data + + +async def test_services_survive_entry_unload_and_reload(hass): + """Test integration services remain registered across entry lifecycle.""" + assert await async_setup_component(hass, adaptive_lighting.DOMAIN, {}) + service_names = ( + SERVICE_APPLY, + SERVICE_CHANGE_SWITCH_SETTINGS, + SERVICE_SET_MANUAL_CONTROL, + ) + services = hass.services.async_services()[adaptive_lighting.DOMAIN] + assert SERVICE_APPLY in services + assert SERVICE_SET_MANUAL_CONTROL in services + if hasattr(service, "async_register_platform_entity_service"): + assert SERVICE_CHANGE_SWITCH_SETTINGS in services + else: + assert SERVICE_CHANGE_SWITCH_SETTINGS not in services + + entry = MockConfigEntry( + domain=adaptive_lighting.DOMAIN, + data={CONF_NAME: DEFAULT_NAME}, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + registered = { + name: hass.services.async_services()[adaptive_lighting.DOMAIN][name] + for name in service_names + } + switch = hass.data[adaptive_lighting.DOMAIN][entry.entry_id][SWITCH_DOMAIN] + assert await hass.config_entries.async_unload(entry.entry_id) + + for name in service_names: + assert ( + hass.services.async_services()[adaptive_lighting.DOMAIN][name] + is registered[name] + ) + + with pytest.raises(ServiceValidationError, match="No Adaptive Lighting"): + await hass.services.async_call( + adaptive_lighting.DOMAIN, + SERVICE_APPLY, + {ATTR_ENTITY_ID: switch.entity_id}, + blocking=True, + ) + + assert await hass.config_entries.async_setup(entry.entry_id) + for name in service_names: + assert ( + hass.services.async_services()[adaptive_lighting.DOMAIN][name] + is registered[name] + ) + + await hass.services.async_call( + adaptive_lighting.DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + {ATTR_ENTITY_ID: switch.entity_id}, + blocking=True, + ) + + +async def test_service_call_without_loaded_entry(hass): + """Test global services reject calls when no profile is loaded.""" + assert await async_setup_component(hass, adaptive_lighting.DOMAIN, {}) + + with pytest.raises(ServiceValidationError, match="No Adaptive Lighting"): + await hass.services.async_call( + adaptive_lighting.DOMAIN, + SERVICE_APPLY, + {CONF_LIGHTS: ["light.test"]}, + blocking=True, + ) + + pending_entry = MockConfigEntry( + domain=adaptive_lighting.DOMAIN, + data={CONF_NAME: "pending"}, + ) + pending_entry.add_to_hass(hass) + hass.data[adaptive_lighting.DOMAIN] = {pending_entry.entry_id: {}} + with pytest.raises(ServiceValidationError, match="not found in any switch"): + await hass.services.async_call( + adaptive_lighting.DOMAIN, + SERVICE_APPLY, + {CONF_LIGHTS: ["light.test"]}, + blocking=True, + ) + + +async def test_apply_rejects_unknown_light(hass): + """Test the apply service rejects an unknown light target.""" + entry = MockConfigEntry( + domain=adaptive_lighting.DOMAIN, + data={CONF_NAME: DEFAULT_NAME}, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + with pytest.raises(ServiceValidationError, match="not found in any switch"): + await hass.services.async_call( + adaptive_lighting.DOMAIN, + SERVICE_APPLY, + {CONF_LIGHTS: ["light.does_not_exist"]}, + blocking=True, + ) + + +async def test_change_switch_settings_requires_entity_target(hass): + """Test change_switch_settings rejects a missing entity target.""" + entry = MockConfigEntry( + domain=adaptive_lighting.DOMAIN, + data={CONF_NAME: DEFAULT_NAME}, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + + with pytest.raises( + voluptuous.error.MultipleInvalid, + match=r"must contain at least one of entity_id.*area_id", + ): + await hass.services.async_call( + adaptive_lighting.DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + {}, + blocking=True, + ) diff --git a/tests/test_switch.py b/tests/test_switch.py index 6d2e169a..421a430e 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -95,6 +95,7 @@ from homeassistant.components.template.light import StateLightEntity as LightTem from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, ConfigEntryState from homeassistant.const import ( ATTR_AREA_ID, + ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_LIGHTS, @@ -108,6 +109,7 @@ from homeassistant.const import ( EntityCategory, ) from homeassistant.core import Context, Event, HomeAssistant, State +from homeassistant.exceptions import Unauthorized from homeassistant.helpers import area_registry as ar from homeassistant.helpers import entity_registry from homeassistant.helpers.entity_platform import async_get_platforms @@ -1488,6 +1490,54 @@ async def test_apply_service(hass): assert old_state[ATTR_COLOR_TEMP_KELVIN] == new_state[ATTR_COLOR_TEMP_KELVIN] +async def test_apply_service_uses_each_switch_transition(hass): + """Test global apply resolves omitted transition for each profile.""" + await setup_lights(hass) + _, switch_1 = await setup_switch( + hass, + { + CONF_NAME: "switch 1", + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_INITIAL_TRANSITION: 3, + }, + ) + _, switch_2 = await setup_switch( + hass, + { + CONF_NAME: "switch 2", + CONF_LIGHTS: [ENTITY_LIGHT_2], + CONF_INITIAL_TRANSITION: 7, + }, + ) + + with ( + patch.object(switch_1, "_adapt_light", new=AsyncMock()) as adapt_1, + patch.object(switch_2, "_adapt_light", new=AsyncMock()) as adapt_2, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + {ATTR_ENTITY_ID: [switch_1.entity_id, switch_2.entity_id]}, + blocking=True, + ) + assert adapt_1.await_args.kwargs["transition"] == 3 + assert adapt_2.await_args.kwargs["transition"] == 7 + + adapt_1.reset_mock() + adapt_2.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: [switch_1.entity_id, switch_2.entity_id], + CONF_TRANSITION: 0, + }, + blocking=True, + ) + assert adapt_1.await_args.kwargs["transition"] == 0 + assert adapt_2.await_args.kwargs["transition"] == 0 + + async def test_switch_off_on_off(hass): """Test switch rapid off_on_off.""" @@ -1837,10 +1887,16 @@ def test_is_our_context(): async def test_unload_switch(hass): """Test removing Adaptive Lighting.""" - entry, _ = await setup_switch(hass, {}) + entry, switch = await setup_switch(hass, {}) + switch.manager.set_auto_reset_manual_control_times([ENTITY_LIGHT_1], 60) + switch.manager.set_manual_control_attributes(ENTITY_LIGHT_1) + timer = switch.manager.auto_reset_manual_control_timers[ENTITY_LIGHT_1] + assert timer.is_running() + assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert DOMAIN not in hass.data + assert not timer.is_running() @pytest.mark.parametrize("state", [STATE_ON, STATE_OFF, None]) @@ -2152,6 +2208,81 @@ async def test_change_switch_settings_service(hass): assert switch._sun_light_settings.min_color_temp == 2500 +@pytest.mark.parametrize("target", ["entity", "area", "device", "all"]) +async def test_change_switch_settings_entity_targets(hass, device_registry, target): + """Test settings changes through Home Assistant entity targets.""" + _, switch = await setup_switch(hass, {}) + mock_area_registry(hass) + registry_entry = entity_registry.async_get(hass).async_get(switch.entity_id) + assert registry_entry is not None + assert registry_entry.device_id is not None + device_registry.async_update_device( + registry_entry.device_id, + area_id="test-area", + ) + service_data = { + "entity": {ATTR_ENTITY_ID: switch.entity_id}, + "area": {ATTR_AREA_ID: "test-area"}, + "device": {ATTR_DEVICE_ID: registry_entry.device_id}, + "all": {ATTR_ENTITY_ID: "all"}, + }[target] + + with patch.object( + switch, + "_set_changeable_settings", + wraps=switch._set_changeable_settings, + ) as set_settings: + await hass.services.async_call( + DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + {**service_data, CONF_MAX_BRIGHTNESS: 50}, + blocking=True, + ) + + set_settings.assert_called_once() + assert switch._sun_light_settings.max_brightness == 50 + + +async def test_change_switch_settings_ignores_unknown_entity(hass): + """Test an unknown entity target does not change a loaded profile.""" + _, switch = await setup_switch(hass, {}) + + with patch.object(switch, "_set_changeable_settings") as set_settings: + await hass.services.async_call( + DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + { + ATTR_ENTITY_ID: "switch.does_not_exist", + CONF_MAX_BRIGHTNESS: 50, + }, + blocking=True, + ) + + set_settings.assert_not_called() + + +async def test_change_switch_settings_checks_entity_permissions( + hass, + hass_read_only_user, +): + """Test settings changes require permission to control the target entity.""" + _, switch = await setup_switch(hass, {}) + + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_MAX_BRIGHTNESS: 50, + }, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) + + assert switch._sun_light_settings.max_brightness == DEFAULT_MAX_BRIGHTNESS + + async def test_cancellable_service_calls_task(hass): """Test the creation and execution of the task that wraps adaptation service calls.""" light, *_ = await setup_lights(hass) From 08e3b817a39c2304e100af9bbc5cf5443ec696d4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 13:08:17 +0200 Subject: [PATCH 1041/1077] docs: add automation alternatives for custom lighting profiles (#1535) * docs: add automation alternatives for custom lighting profiles * docs: make automation restart behavior explicit * docs: validate automation examples in Home Assistant * docs: clarify automation prerequisites * test: exercise automation startup lifecycle --- README.md | 344 +++++++++++---- docs/automation-examples.md | 344 +++++++++++---- tests/test_automation_examples.py | 691 ++++++++++++++++++++++++++++++ 3 files changed, 1233 insertions(+), 146 deletions(-) create mode 100644 tests/test_automation_examples.py diff --git a/README.md b/README.md index 2ed03505..2fc73516 100644 --- a/README.md +++ b/README.md @@ -268,29 +268,27 @@ The following keys are disallowed: ## :robot: Automation examples +Replace every entity ID below with the IDs from your Home Assistant instance. Fresh Adaptive Lighting profiles use child IDs such as `switch.adaptive_lighting_living_room_sleep_mode`; profiles created before the device-based entity change may retain older IDs. + +Blocks that begin with `- alias` are entries for `automations.yaml`. Blocks with a top-level `script:` or `adaptive_lighting:` key are complete `configuration.yaml` examples. If your configuration uses `script: !include scripts.yaml`, omit that outer key and place its contents in `scripts.yaml`. + +`change_switch_settings` updates a profile while its main switch is off, but lights are adapted only while that switch is on. It preserves manual-control flags, so manually controlled lights remain paused. +
-Reset the manual_control status of a light after an hour. +Automatically reset manual control after one hour. + +Use the built-in timeout so every new manual change renews a single timer for that light: ```yaml -- alias: "Adaptive lighting: reset manual_control after 1 hour" - mode: parallel - trigger: - platform: event - event_type: adaptive_lighting.manual_control - variables: - light: "{{ trigger.event.data.entity_id }}" - switch: "{{ trigger.event.data.switch }}" - action: - - delay: "01:00:00" - - condition: template - value_template: "{{ light in state_attr(switch, 'manual_control') }}" - - service: adaptive_lighting.set_manual_control - data: - entity_id: "{{ switch }}" - lights: "{{ light }}" - manual_control: false +adaptive_lighting: + - name: "Living Room" + lights: + - light.living_room + autoreset_control_seconds: 3600 ``` +This is a top-level `configuration.yaml` example. The timer clears manual control and immediately readapts a light when both it and the Adaptive Lighting switch are on. +
@@ -302,67 +300,267 @@ The following keys are disallowed: - platform: state entity_id: input_boolean.sleep_mode - platform: homeassistant - event: start # in case the states aren't properly restored + event: start # apply the helper's restored state variables: sleep_mode: "{{ states('input_boolean.sleep_mode') }}" - action: - service: "switch.turn_{{ sleep_mode }}" - entity_id: - - switch.adaptive_lighting_sleep_mode_living_room - - switch.adaptive_lighting_sleep_mode_bedroom -``` - -Set your sunrise and sunset time based on your alarm. The below script sets sunset_time exactly 12 hours after the custom sunrise time. - -```yaml -iphone_carly_wakeup: - alias: iPhone Carly Wakeup - sequence: - - condition: state - entity_id: input_boolean.carly_iphone_wakeup - state: "off" - - service: input_datetime.set_datetime - target: - entity_id: input_datetime.carly_iphone_wakeup - data: - time: '{{ now().strftime("%H:%M:%S") }}' - - service: input_boolean.turn_on - target: - entity_id: input_boolean.carly_iphone_wakeup - - repeat: - count: > - {{ (states.switch - | map(attribute="entity_id") - | select(">","switch.adaptive_lighting_al_") - | select("<", "switch.adaptive_lighting_al_z") - | join(",") - ).split(",") | length }} - sequence: - - service: adaptive_lighting.change_switch_settings - data: - entity_id: switch.adaptive_lighting_al_den_ceilingfan_lights - sunrise_time: '{{ now().strftime("%H:%M:%S") }}' - sunset_time: > - {{ (as_timestamp(now()) + 12*60*60) | timestamp_custom("%H:%M:%S") }} - - service: script.turn_on - target: - entity_id: script.run_wakeup_routine - - service: input_boolean.turn_off + conditions: + - condition: template + value_template: "{{ sleep_mode in ['on', 'off'] }}" + actions: + - action: "switch.turn_{{ sleep_mode }}" target: entity_id: - - input_boolean.carly_iphone_winddown - - input_boolean.carly_iphone_bedtime - - service: input_datetime.set_datetime - target: - entity_id: input_datetime.wakeup_time - data: - time: '{{ now().strftime("%H:%M:%S") }}' - - service: script.adaptive_lighting_disable_sleep_mode - mode: queued - icon: mdi:weather-sunset - max: 10 + - switch.adaptive_lighting_living_room_sleep_mode + - switch.adaptive_lighting_bedroom_sleep_mode ``` +
+ +
+Set sunrise and sunset from an alarm. + +Call this script from your alarm automation. It sets one Adaptive Lighting profile's sunrise to the current time and its sunset to 12 hours later on the local clock. + +```yaml +script: + set_adaptive_lighting_alarm_times: + alias: "Adaptive lighting: set times from alarm" + variables: + alarm_time: '{{ now().strftime("%H:%M:%S") }}' + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_alarm_lights + sunrise_time: "{{ alarm_time }}" + sunset_time: > + {{ (strptime(alarm_time, "%H:%M:%S") + timedelta(hours=12)) + .strftime("%H:%M:%S") }} +``` + +
+ +
+Use a Schedule helper as a step-based custom lighting profile. + +Create a [Schedule helper](https://www.home-assistant.io/integrations/schedule/) named `Adaptive Lighting Profile`. Add time blocks with Additional data like this: + +```yaml +brightness_pct: 20 +color_temp_kelvin: 2500 +``` + +Use different values for each block. The automation below applies the active block whenever the schedule state or its attributes change. Setting both brightness limits and both color temperature limits to the same value keeps each block at its setpoint. Outside a block, the configured Adaptive Lighting settings are restored. + +```yaml +- alias: "Adaptive lighting: apply scheduled profile" + triggers: + - trigger: state + entity_id: schedule.adaptive_lighting_profile + - trigger: homeassistant + event: start + actions: + - choose: + - conditions: + - condition: state + entity_id: schedule.adaptive_lighting_profile + state: "on" + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_living_room + min_brightness: > + {{ state_attr('schedule.adaptive_lighting_profile', 'brightness_pct') | int(1) }} + max_brightness: > + {{ state_attr('schedule.adaptive_lighting_profile', 'brightness_pct') | int(1) }} + min_color_temp: > + {{ state_attr('schedule.adaptive_lighting_profile', 'color_temp_kelvin') | int(2000) }} + max_color_temp: > + {{ state_attr('schedule.adaptive_lighting_profile', 'color_temp_kelvin') | int(2000) }} + default: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_living_room + use_defaults: configuration + mode: restart +``` + +This creates step changes at block boundaries. It does not interpolate between schedule points. Runtime settings also reset when Home Assistant restarts, so the startup trigger reapplies the active block. The default branch restores every configured setting; restore only the four fields explicitly if other automations also change runtime settings. + +
+ +
+Reduce daytime brightness when an illuminance sensor detects strong daylight. + +Keep a low configured `min_brightness` for late night and let an automation lower `max_brightness` while the room has ample daylight. Use a sensor that is not significantly affected by the controlled lights to avoid a feedback loop. + +```yaml +- alias: "Adaptive lighting: limit brightness in daylight" + triggers: + - trigger: numeric_state + entity_id: sensor.living_room_illuminance + above: 300 + - trigger: numeric_state + entity_id: sensor.living_room_illuminance + below: 200 + - trigger: homeassistant + event: start + id: startup + actions: + - if: + - condition: trigger + id: startup + then: + - wait_template: > + {{ is_number(states('sensor.living_room_illuminance')) }} + timeout: "00:05:00" + continue_on_timeout: false + - choose: + - conditions: + - condition: numeric_state + entity_id: sensor.living_room_illuminance + above: 300 + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_living_room + max_brightness: 30 + - conditions: + - condition: numeric_state + entity_id: sensor.living_room_illuminance + below: 200 + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_living_room + max_brightness: 100 + mode: restart +``` + +The separate 200 and 300 lux thresholds add hysteresis. After a restart, the automation waits for a numeric sensor state before evaluating it. If the initial value is between the thresholds, Adaptive Lighting keeps its configured maximum. Replace `30` and `100` with your desired daytime limit and normal maximum. + +`min_brightness` and `max_brightness` are the solar-midnight and daytime endpoints of the brightness curve. Setting `min_brightness` higher than `max_brightness` is supported and creates an inverted curve that is brighter at night and dimmer during the day. If you only want a daytime limit, keep the reduced maximum at or above the configured minimum. + +
+ +
+Turn on Hue-controlled lights with the current Adaptive Lighting values. + +For a Hue button exposed to Home Assistant, call this script from the button automation. It turns on the listed lights directly with the current Adaptive Lighting brightness and color. + +```yaml +script: + living_room_adaptive_lighting: + alias: "Living room: adaptive lighting" + sequence: + - action: adaptive_lighting.apply + data: + entity_id: switch.adaptive_lighting_living_room + lights: + - light.living_room_ceiling + - light.living_room_table + turn_on_lights: true + transition: 0 +``` + +This requires Home Assistant to receive the button event. The one-shot `apply` call works while the main Adaptive Lighting switch is off, turns on the listed lights, and applies values even if a light is marked as manually controlled. It leaves the profile switch and manual-control state unchanged. + +Adaptive Lighting does not update scenes stored on the Hue Bridge, so scenes activated only inside Hue cannot use this script and retain Hue's operation when Home Assistant is unavailable. + +
+ +
+Use a fixed RGB stage before sleep mode. + +This script starts sleep mode with a fixed dim red color, waits 30 minutes, and then restores the configured Adaptive Lighting settings. The main profile switch and the light must already be on. + +```yaml +script: + adaptive_lighting_bedtime: + alias: "Adaptive lighting: bedtime" + mode: restart + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_bedroom + sleep_rgb_or_color_temp: rgb_color + sleep_rgb_color: [255, 56, 0] + sleep_brightness: 20 + - action: switch.turn_on + target: + entity_id: switch.adaptive_lighting_bedroom_sleep_mode + - delay: "00:30:00" + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_bedroom + use_defaults: configuration +``` + +The light must support RGB color. The first stage uses a fixed brightness rather than following the normal brightness curve. When sleep mode changes from off to on, the default `reset_manual_control_on_sleep_mode_change: true` returns manually controlled lights to Adaptive Lighting control so they receive the stage. If you disable that option, manually controlled lights remain paused. Restoring configuration defaults resets every runtime setting on this Adaptive Lighting switch, so restore only the sleep fields explicitly if other automations also change runtime settings. + +Stopping this script or reloading scripts during the delay prevents the final action, leaving the runtime overrides active. To recover, call `adaptive_lighting.change_switch_settings` for the profile with `use_defaults: configuration`. A Home Assistant restart reloads the configured settings. + +
+ +
+Run a fixed virtual day across midnight. + +Fixed virtual sunrise and sunset times can cross midnight. This configuration ramps an indoor garden from its minimum at 16:00 to its maximum at 22:00, then back to its minimum at 04:00. + +```yaml +adaptive_lighting: + - name: "Indoor Garden" + lights: + - light.indoor_garden + sunrise_time: "16:00:00" + sunset_time: "04:00:00" + min_brightness: 10 + max_brightness: 100 + brightness_mode: linear + brightness_mode_time_dark: 0 + brightness_mode_time_light: 21600 # 6 hours +``` + +Adaptive Lighting changes brightness and color while a light is on; it does not manage the light's power schedule. This separate automation turns the example light on and off: + +```yaml +- alias: "Indoor garden: power schedule" + triggers: + - trigger: time + at: "16:00:00" + id: turn_on + - trigger: time + at: "04:00:00" + id: turn_off + - trigger: homeassistant + event: start + id: startup + actions: + - choose: + - conditions: + - condition: trigger + id: turn_on + sequence: + - action: light.turn_on + target: + entity_id: light.indoor_garden + - conditions: + - condition: trigger + id: startup + - condition: time + after: "16:00:00" + before: "04:00:00" + sequence: + - action: light.turn_on + target: + entity_id: light.indoor_garden + default: + - action: light.turn_off + target: + entity_id: light.indoor_garden +``` + +Use `min_sunrise_time`, `max_sunrise_time`, `min_sunset_time`, or `max_sunset_time` instead when you want to constrain astronomical sunrise or sunset to an earliest or latest time rather than replace it. +
diff --git a/docs/automation-examples.md b/docs/automation-examples.md index e2d136f6..9a8c21f3 100644 --- a/docs/automation-examples.md +++ b/docs/automation-examples.md @@ -12,29 +12,27 @@ Real-world automation examples showing how to integrate Adaptive Lighting with y +Replace every entity ID below with the IDs from your Home Assistant instance. Fresh Adaptive Lighting profiles use child IDs such as `switch.adaptive_lighting_living_room_sleep_mode`; profiles created before the device-based entity change may retain older IDs. + +Blocks that begin with `- alias` are entries for `automations.yaml`. Blocks with a top-level `script:` or `adaptive_lighting:` key are complete `configuration.yaml` examples. If your configuration uses `script: !include scripts.yaml`, omit that outer key and place its contents in `scripts.yaml`. + +`change_switch_settings` updates a profile while its main switch is off, but lights are adapted only while that switch is on. It preserves manual-control flags, so manually controlled lights remain paused. +
-Reset the manual_control status of a light after an hour. +Automatically reset manual control after one hour. + +Use the built-in timeout so every new manual change renews a single timer for that light: ```yaml -- alias: "Adaptive lighting: reset manual_control after 1 hour" - mode: parallel - trigger: - platform: event - event_type: adaptive_lighting.manual_control - variables: - light: "{{ trigger.event.data.entity_id }}" - switch: "{{ trigger.event.data.switch }}" - action: - - delay: "01:00:00" - - condition: template - value_template: "{{ light in state_attr(switch, 'manual_control') }}" - - service: adaptive_lighting.set_manual_control - data: - entity_id: "{{ switch }}" - lights: "{{ light }}" - manual_control: false +adaptive_lighting: + - name: "Living Room" + lights: + - light.living_room + autoreset_control_seconds: 3600 ``` +This is a top-level `configuration.yaml` example. The timer clears manual control and immediately readapts a light when both it and the Adaptive Lighting switch are on. +
@@ -46,69 +44,269 @@ Real-world automation examples showing how to integrate Adaptive Lighting with y - platform: state entity_id: input_boolean.sleep_mode - platform: homeassistant - event: start # in case the states aren't properly restored + event: start # apply the helper's restored state variables: sleep_mode: "{{ states('input_boolean.sleep_mode') }}" - action: - service: "switch.turn_{{ sleep_mode }}" - entity_id: - - switch.adaptive_lighting_sleep_mode_living_room - - switch.adaptive_lighting_sleep_mode_bedroom -``` - -Set your sunrise and sunset time based on your alarm. The below script sets sunset_time exactly 12 hours after the custom sunrise time. - -```yaml -iphone_carly_wakeup: - alias: iPhone Carly Wakeup - sequence: - - condition: state - entity_id: input_boolean.carly_iphone_wakeup - state: "off" - - service: input_datetime.set_datetime - target: - entity_id: input_datetime.carly_iphone_wakeup - data: - time: '{{ now().strftime("%H:%M:%S") }}' - - service: input_boolean.turn_on - target: - entity_id: input_boolean.carly_iphone_wakeup - - repeat: - count: > - {{ (states.switch - | map(attribute="entity_id") - | select(">","switch.adaptive_lighting_al_") - | select("<", "switch.adaptive_lighting_al_z") - | join(",") - ).split(",") | length }} - sequence: - - service: adaptive_lighting.change_switch_settings - data: - entity_id: switch.adaptive_lighting_al_den_ceilingfan_lights - sunrise_time: '{{ now().strftime("%H:%M:%S") }}' - sunset_time: > - {{ (as_timestamp(now()) + 12*60*60) | timestamp_custom("%H:%M:%S") }} - - service: script.turn_on - target: - entity_id: script.run_wakeup_routine - - service: input_boolean.turn_off + conditions: + - condition: template + value_template: "{{ sleep_mode in ['on', 'off'] }}" + actions: + - action: "switch.turn_{{ sleep_mode }}" target: entity_id: - - input_boolean.carly_iphone_winddown - - input_boolean.carly_iphone_bedtime - - service: input_datetime.set_datetime - target: - entity_id: input_datetime.wakeup_time - data: - time: '{{ now().strftime("%H:%M:%S") }}' - - service: script.adaptive_lighting_disable_sleep_mode - mode: queued - icon: mdi:weather-sunset - max: 10 + - switch.adaptive_lighting_living_room_sleep_mode + - switch.adaptive_lighting_bedroom_sleep_mode ```
+
+Set sunrise and sunset from an alarm. + +Call this script from your alarm automation. It sets one Adaptive Lighting profile's sunrise to the current time and its sunset to 12 hours later on the local clock. + +```yaml +script: + set_adaptive_lighting_alarm_times: + alias: "Adaptive lighting: set times from alarm" + variables: + alarm_time: '{{ now().strftime("%H:%M:%S") }}' + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_alarm_lights + sunrise_time: "{{ alarm_time }}" + sunset_time: > + {{ (strptime(alarm_time, "%H:%M:%S") + timedelta(hours=12)) + .strftime("%H:%M:%S") }} +``` + +
+ +
+Use a Schedule helper as a step-based custom lighting profile. + +Create a [Schedule helper](https://www.home-assistant.io/integrations/schedule/) named `Adaptive Lighting Profile`. Add time blocks with Additional data like this: + +```yaml +brightness_pct: 20 +color_temp_kelvin: 2500 +``` + +Use different values for each block. The automation below applies the active block whenever the schedule state or its attributes change. Setting both brightness limits and both color temperature limits to the same value keeps each block at its setpoint. Outside a block, the configured Adaptive Lighting settings are restored. + +```yaml +- alias: "Adaptive lighting: apply scheduled profile" + triggers: + - trigger: state + entity_id: schedule.adaptive_lighting_profile + - trigger: homeassistant + event: start + actions: + - choose: + - conditions: + - condition: state + entity_id: schedule.adaptive_lighting_profile + state: "on" + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_living_room + min_brightness: > + {{ state_attr('schedule.adaptive_lighting_profile', 'brightness_pct') | int(1) }} + max_brightness: > + {{ state_attr('schedule.adaptive_lighting_profile', 'brightness_pct') | int(1) }} + min_color_temp: > + {{ state_attr('schedule.adaptive_lighting_profile', 'color_temp_kelvin') | int(2000) }} + max_color_temp: > + {{ state_attr('schedule.adaptive_lighting_profile', 'color_temp_kelvin') | int(2000) }} + default: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_living_room + use_defaults: configuration + mode: restart +``` + +This creates step changes at block boundaries. It does not interpolate between schedule points. Runtime settings also reset when Home Assistant restarts, so the startup trigger reapplies the active block. The default branch restores every configured setting; restore only the four fields explicitly if other automations also change runtime settings. + +
+ +
+Reduce daytime brightness when an illuminance sensor detects strong daylight. + +Keep a low configured `min_brightness` for late night and let an automation lower `max_brightness` while the room has ample daylight. Use a sensor that is not significantly affected by the controlled lights to avoid a feedback loop. + +```yaml +- alias: "Adaptive lighting: limit brightness in daylight" + triggers: + - trigger: numeric_state + entity_id: sensor.living_room_illuminance + above: 300 + - trigger: numeric_state + entity_id: sensor.living_room_illuminance + below: 200 + - trigger: homeassistant + event: start + id: startup + actions: + - if: + - condition: trigger + id: startup + then: + - wait_template: > + {{ is_number(states('sensor.living_room_illuminance')) }} + timeout: "00:05:00" + continue_on_timeout: false + - choose: + - conditions: + - condition: numeric_state + entity_id: sensor.living_room_illuminance + above: 300 + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_living_room + max_brightness: 30 + - conditions: + - condition: numeric_state + entity_id: sensor.living_room_illuminance + below: 200 + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_living_room + max_brightness: 100 + mode: restart +``` + +The separate 200 and 300 lux thresholds add hysteresis. After a restart, the automation waits for a numeric sensor state before evaluating it. If the initial value is between the thresholds, Adaptive Lighting keeps its configured maximum. Replace `30` and `100` with your desired daytime limit and normal maximum. + +`min_brightness` and `max_brightness` are the solar-midnight and daytime endpoints of the brightness curve. Setting `min_brightness` higher than `max_brightness` is supported and creates an inverted curve that is brighter at night and dimmer during the day. If you only want a daytime limit, keep the reduced maximum at or above the configured minimum. + +
+ +
+Turn on Hue-controlled lights with the current Adaptive Lighting values. + +For a Hue button exposed to Home Assistant, call this script from the button automation. It turns on the listed lights directly with the current Adaptive Lighting brightness and color. + +```yaml +script: + living_room_adaptive_lighting: + alias: "Living room: adaptive lighting" + sequence: + - action: adaptive_lighting.apply + data: + entity_id: switch.adaptive_lighting_living_room + lights: + - light.living_room_ceiling + - light.living_room_table + turn_on_lights: true + transition: 0 +``` + +This requires Home Assistant to receive the button event. The one-shot `apply` call works while the main Adaptive Lighting switch is off, turns on the listed lights, and applies values even if a light is marked as manually controlled. It leaves the profile switch and manual-control state unchanged. + +Adaptive Lighting does not update scenes stored on the Hue Bridge, so scenes activated only inside Hue cannot use this script and retain Hue's operation when Home Assistant is unavailable. + +
+ +
+Use a fixed RGB stage before sleep mode. + +This script starts sleep mode with a fixed dim red color, waits 30 minutes, and then restores the configured Adaptive Lighting settings. The main profile switch and the light must already be on. + +```yaml +script: + adaptive_lighting_bedtime: + alias: "Adaptive lighting: bedtime" + mode: restart + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_bedroom + sleep_rgb_or_color_temp: rgb_color + sleep_rgb_color: [255, 56, 0] + sleep_brightness: 20 + - action: switch.turn_on + target: + entity_id: switch.adaptive_lighting_bedroom_sleep_mode + - delay: "00:30:00" + - action: adaptive_lighting.change_switch_settings + data: + entity_id: switch.adaptive_lighting_bedroom + use_defaults: configuration +``` + +The light must support RGB color. The first stage uses a fixed brightness rather than following the normal brightness curve. When sleep mode changes from off to on, the default `reset_manual_control_on_sleep_mode_change: true` returns manually controlled lights to Adaptive Lighting control so they receive the stage. If you disable that option, manually controlled lights remain paused. Restoring configuration defaults resets every runtime setting on this Adaptive Lighting switch, so restore only the sleep fields explicitly if other automations also change runtime settings. + +Stopping this script or reloading scripts during the delay prevents the final action, leaving the runtime overrides active. To recover, call `adaptive_lighting.change_switch_settings` for the profile with `use_defaults: configuration`. A Home Assistant restart reloads the configured settings. + +
+ +
+Run a fixed virtual day across midnight. + +Fixed virtual sunrise and sunset times can cross midnight. This configuration ramps an indoor garden from its minimum at 16:00 to its maximum at 22:00, then back to its minimum at 04:00. + +```yaml +adaptive_lighting: + - name: "Indoor Garden" + lights: + - light.indoor_garden + sunrise_time: "16:00:00" + sunset_time: "04:00:00" + min_brightness: 10 + max_brightness: 100 + brightness_mode: linear + brightness_mode_time_dark: 0 + brightness_mode_time_light: 21600 # 6 hours +``` + +Adaptive Lighting changes brightness and color while a light is on; it does not manage the light's power schedule. This separate automation turns the example light on and off: + +```yaml +- alias: "Indoor garden: power schedule" + triggers: + - trigger: time + at: "16:00:00" + id: turn_on + - trigger: time + at: "04:00:00" + id: turn_off + - trigger: homeassistant + event: start + id: startup + actions: + - choose: + - conditions: + - condition: trigger + id: turn_on + sequence: + - action: light.turn_on + target: + entity_id: light.indoor_garden + - conditions: + - condition: trigger + id: startup + - condition: time + after: "16:00:00" + before: "04:00:00" + sequence: + - action: light.turn_on + target: + entity_id: light.indoor_garden + default: + - action: light.turn_off + target: + entity_id: light.indoor_garden +``` + +Use `min_sunrise_time`, `max_sunrise_time`, `min_sunset_time`, or `max_sunset_time` instead when you want to constrain astronomical sunrise or sunset to an earliest or latest time rather than replace it. + +
+ > [!TIP] diff --git a/tests/test_automation_examples.py b/tests/test_automation_examples.py new file mode 100644 index 00000000..e5f3c2d5 --- /dev/null +++ b/tests/test_automation_examples.py @@ -0,0 +1,691 @@ +"""Execute the automation examples published in README.md.""" + +from __future__ import annotations + +import asyncio +import re +from datetime import UTC, datetime, timedelta +from pathlib import Path +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest +import yaml +from homeassistant.components import automation, script +from homeassistant.components.adaptive_lighting.adaptation_utils import ( + LightControlAttributes, +) +from homeassistant.components.adaptive_lighting.const import ( + CONF_INITIAL_TRANSITION, + CONF_LIGHTS, + CONF_MAX_BRIGHTNESS, + CONF_MAX_COLOR_TEMP, + CONF_MIN_BRIGHTNESS, + CONF_MIN_COLOR_TEMP, + CONF_NAME, + CONF_SLEEP_BRIGHTNESS, + CONF_SLEEP_COLOR_TEMP, + CONF_SLEEP_RGB_OR_COLOR_TEMP, + CONF_SUNRISE_TIME, + CONF_SUNSET_TIME, + CONF_TRANSITION, + DOMAIN, +) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP_KELVIN, + ATTR_RGB_COLOR, +) +from homeassistant.components.light import ( + DOMAIN as LIGHT_DOMAIN, +) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + EVENT_CALL_SERVICE, + EVENT_STATE_CHANGED, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import CoreState, Event, HomeAssistant, State, callback +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed + +from .test_switch import setup_switch + +if TYPE_CHECKING: + from collections.abc import Callable + +README = Path(__file__).resolve().parents[1] / "README.md" + + +def _yaml_documents(summary: str) -> list[object]: + """Load every YAML fence from one README details block.""" + readme = README.read_text() + details = re.search( + rf"{re.escape(summary)}(.*?)", + readme, + flags=re.DOTALL, + ) + assert details is not None, f"README example not found: {summary}" + blocks = re.findall(r"```yaml\n(.*?)```", details.group(1), flags=re.DOTALL) + assert blocks, f"README example has no YAML: {summary}" + return [yaml.safe_load(block) for block in blocks] + + +async def _setup_automation(hass: HomeAssistant, config: object) -> None: + """Load an extracted automation through Home Assistant.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + {automation.DOMAIN: config}, + ) + await hass.async_block_till_done() + + +async def _setup_script(hass: HomeAssistant, config: dict) -> None: + """Load an extracted script through Home Assistant.""" + assert await async_setup_component(hass, script.DOMAIN, config) + await hass.async_block_till_done() + + +async def _setup_template_lights( + hass: HomeAssistant, + names: list[str], +) -> None: + """Create color-temperature and RGB lights with documented entity IDs.""" + lights = [ + { + "name": name, + "unique_id": name.lower().replace(" ", "_"), + "turn_on": None, + "turn_off": None, + "set_level": None, + "set_temperature": None, + "set_rgb": None, + } + for name in names + ] + assert await async_setup_component( + hass, + "template", + {"template": {"light": lights}}, + ) + await hass.async_block_till_done() + + +def _state_waiter( + hass: HomeAssistant, + entity_id: str, + predicate: Callable[[State], bool], +) -> tuple[asyncio.Future[State], Callable[[], None]]: + """Return a future that resolves when an entity state matches a predicate.""" + future = hass.loop.create_future() + + @callback + def state_changed(event: Event) -> None: + new_state = event.data.get("new_state") + if ( + not future.done() + and new_state is not None + and new_state.entity_id == entity_id + and predicate(new_state) + ): + future.set_result(new_state) + + remove_listener = hass.bus.async_listen(EVENT_STATE_CHANGED, state_changed) + return future, remove_listener + + +def _prepare_hass_startup(hass: HomeAssistant) -> None: + """Reset the standard running test fixture to exercise a real HA start.""" + hass.set_state(CoreState.not_running) + + +async def test_schedule_profile_executes_blocks_and_restore( + hass: HomeAssistant, +) -> None: + """Catch ignored attribute changes, incomplete restore, or switch coupling.""" + summary = "Use a Schedule helper as a step-based custom lighting profile." + automation_config = _yaml_documents(summary)[-1] + _, adaptive_switch = await setup_switch( + hass, + { + CONF_NAME: "Living Room", + CONF_LIGHTS: ["light.manually_controlled"], + CONF_MIN_BRIGHTNESS: 8, + CONF_MAX_BRIGHTNESS: 88, + CONF_MIN_COLOR_TEMP: 2100, + CONF_MAX_COLOR_TEMP: 5100, + }, + ) + adaptive_switch.manager.set_manual_control_attributes( + "light.manually_controlled", + LightControlAttributes.BRIGHTNESS, + ) + hass.states.async_set( + "schedule.adaptive_lighting_profile", + STATE_OFF, + ) + await _setup_automation(hass, automation_config) + + hass.states.async_set( + "schedule.adaptive_lighting_profile", + STATE_ON, + {"brightness_pct": 20, "color_temp_kelvin": 2500}, + ) + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.min_brightness == 20 + assert adaptive_switch._sun_light_settings.max_brightness == 20 + assert adaptive_switch._sun_light_settings.min_color_temp == 2500 + assert adaptive_switch._sun_light_settings.max_color_temp == 2500 + + hass.states.async_set( + "schedule.adaptive_lighting_profile", + STATE_ON, + {"brightness_pct": 60, "color_temp_kelvin": 4000}, + ) + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.min_brightness == 60 + assert adaptive_switch._sun_light_settings.max_brightness == 60 + assert adaptive_switch._sun_light_settings.min_color_temp == 4000 + assert adaptive_switch._sun_light_settings.max_color_temp == 4000 + + hass.states.async_set("schedule.adaptive_lighting_profile", STATE_OFF) + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.min_brightness == 8 + assert adaptive_switch._sun_light_settings.max_brightness == 88 + assert adaptive_switch._sun_light_settings.min_color_temp == 2100 + assert adaptive_switch._sun_light_settings.max_color_temp == 5100 + assert adaptive_switch.manager.manual_control["light.manually_controlled"] + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: adaptive_switch.entity_id}, + blocking=True, + ) + hass.states.async_set( + "schedule.adaptive_lighting_profile", + STATE_ON, + {"brightness_pct": 35, "color_temp_kelvin": 2750}, + ) + await hass.async_block_till_done() + assert adaptive_switch.is_on is False + assert adaptive_switch._sun_light_settings.min_brightness == 35 + assert adaptive_switch._sun_light_settings.max_color_temp == 2750 + + +async def test_schedule_profile_reapplies_at_startup(hass: HomeAssistant) -> None: + """Verify startup applies the already-active schedule block.""" + _prepare_hass_startup(hass) + summary = "Use a Schedule helper as a step-based custom lighting profile." + automation_config = _yaml_documents(summary)[-1] + _, adaptive_switch = await setup_switch(hass, {CONF_NAME: "Living Room"}) + hass.states.async_set( + "schedule.adaptive_lighting_profile", + STATE_ON, + {"brightness_pct": 20, "color_temp_kelvin": 2500}, + ) + await _setup_automation(hass, automation_config) + + await hass.async_start() + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.min_brightness == 20 + assert adaptive_switch._sun_light_settings.max_brightness == 20 + assert adaptive_switch._sun_light_settings.min_color_temp == 2500 + assert adaptive_switch._sun_light_settings.max_color_temp == 2500 + + +async def test_lux_profile_executes_hysteresis(hass: HomeAssistant) -> None: + """Catch missing threshold actions or changes inside the dead band.""" + summary = ( + "Reduce daytime brightness when an illuminance sensor detects strong daylight." + ) + automation_config = _yaml_documents(summary)[0] + _, adaptive_switch = await setup_switch( + hass, + {CONF_NAME: "Living Room", CONF_MAX_BRIGHTNESS: 80}, + ) + hass.states.async_set("sensor.living_room_illuminance", "250") + await _setup_automation(hass, automation_config) + + hass.states.async_set("sensor.living_room_illuminance", "350") + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.max_brightness == 30 + + hass.states.async_set("sensor.living_room_illuminance", "250") + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.max_brightness == 30 + + hass.states.async_set("sensor.living_room_illuminance", "150") + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.max_brightness == 100 + + hass.states.async_set("sensor.living_room_illuminance", "250") + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.max_brightness == 100 + + +async def test_lux_profile_executes_unknown_recovery_at_startup( + hass: HomeAssistant, +) -> None: + """Catch a startup hang or failure to recover from an unknown sensor.""" + _prepare_hass_startup(hass) + summary = ( + "Reduce daytime brightness when an illuminance sensor detects strong daylight." + ) + automation_config = _yaml_documents(summary)[0] + _, adaptive_switch = await setup_switch( + hass, + {CONF_NAME: "Living Room", CONF_MAX_BRIGHTNESS: 80}, + ) + hass.states.async_set("sensor.living_room_illuminance", "unknown") + await _setup_automation(hass, automation_config) + waiting, remove_listener = _state_waiter( + hass, + "automation.adaptive_lighting_limit_brightness_in_daylight", + lambda state: state.attributes.get("current") == 1, + ) + + await hass.async_start() + await asyncio.wait_for(waiting, timeout=1) + remove_listener() + assert adaptive_switch._sun_light_settings.max_brightness == 80 + + hass.states.async_set("sensor.living_room_illuminance", "350") + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.max_brightness == 30 + + +async def test_hue_script_applies_current_values_to_fresh_profile_targets( + hass: HomeAssistant, +) -> None: + """Catch an invalid script wrapper or a one-shot apply that skips off lights.""" + summary = "Turn on Hue-controlled lights with the current Adaptive Lighting values." + script_config = _yaml_documents(summary)[0] + await _setup_template_lights( + hass, + ["Living Room Ceiling", "Living Room Table"], + ) + _, adaptive_switch = await setup_switch( + hass, + { + CONF_NAME: "Living Room", + CONF_LIGHTS: [ + "light.living_room_ceiling", + "light.living_room_table", + ], + CONF_SUNRISE_TIME: "06:00:00", + CONF_SUNSET_TIME: "18:00:00", + CONF_MIN_BRIGHTNESS: 10, + CONF_MAX_BRIGHTNESS: 80, + CONF_MIN_COLOR_TEMP: 2000, + CONF_MAX_COLOR_TEMP: 5000, + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + }, + ) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: adaptive_switch.entity_id}, + blocking=True, + ) + adaptive_switch.manager.set_manual_control_attributes( + "light.living_room_ceiling", + LightControlAttributes.BRIGHTNESS | LightControlAttributes.COLOR, + ) + await _setup_script(hass, script_config) + + noon = datetime(2026, 9, 6, 12, tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(UTC) + with patch( + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", + return_value=noon, + ): + await hass.services.async_call( + script.DOMAIN, + "living_room_adaptive_lighting", + blocking=True, + ) + await hass.async_block_till_done() + + for entity_id in ("light.living_room_ceiling", "light.living_room_table"): + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 204 + assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 5000 + assert adaptive_switch.is_on is False + assert adaptive_switch.manager.manual_control["light.living_room_ceiling"] + + +async def test_rgb_bedtime_script_applies_stage_then_restores_configuration( + hass: HomeAssistant, +) -> None: + """Catch a wrong fresh child ID, delayed first stage, or missing restoration.""" + summary = "Use a fixed RGB stage before sleep mode." + script_config = _yaml_documents(summary)[0] + await _setup_template_lights(hass, ["Bedroom"]) + _, adaptive_switch = await setup_switch( + hass, + { + CONF_NAME: "Bedroom", + CONF_LIGHTS: ["light.bedroom"], + CONF_SLEEP_BRIGHTNESS: 7, + CONF_SLEEP_COLOR_TEMP: 2300, + CONF_SLEEP_RGB_OR_COLOR_TEMP: "color_temp", + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + }, + ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.bedroom"}, + blocking=True, + ) + adaptive_switch.manager.set_manual_control_attributes( + "light.bedroom", + LightControlAttributes.ALL, + ) + await _setup_script(hass, script_config) + light_staged, remove_light_listener = _state_waiter( + hass, + "light.bedroom", + lambda state: ( + state.attributes.get(ATTR_BRIGHTNESS) == 51 + and state.attributes.get(ATTR_RGB_COLOR) == (255, 56, 0) + ), + ) + sleep_enabled, remove_sleep_listener = _state_waiter( + hass, + "switch.adaptive_lighting_bedroom_sleep_mode", + lambda state: state.state == STATE_ON, + ) + + await hass.services.async_call( + script.DOMAIN, + "adaptive_lighting_bedtime", + blocking=False, + ) + await asyncio.wait_for(asyncio.gather(light_staged, sleep_enabled), timeout=1) + remove_light_listener() + remove_sleep_listener() + # Let the script continue from the completed switch action into its delay. + await asyncio.sleep(0) + sleep_state = hass.states.get("switch.adaptive_lighting_bedroom_sleep_mode") + assert sleep_state is not None + assert sleep_state.state == STATE_ON + assert adaptive_switch._sun_light_settings.sleep_brightness == 20 + assert adaptive_switch._sun_light_settings.sleep_rgb_or_color_temp == "rgb_color" + assert adaptive_switch._sun_light_settings.sleep_rgb_color == [255, 56, 0] + assert not adaptive_switch.manager.manual_control["light.bedroom"] + bedroom_state = hass.states.get("light.bedroom") + assert bedroom_state is not None + assert bedroom_state.attributes[ATTR_BRIGHTNESS] == 51 + assert bedroom_state.attributes[ATTR_RGB_COLOR] == (255, 56, 0) + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=30)) + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.sleep_brightness == 7 + assert adaptive_switch._sun_light_settings.sleep_rgb_or_color_temp == "color_temp" + assert adaptive_switch._sun_light_settings.sleep_color_temp == 2300 + bedroom_state = hass.states.get("light.bedroom") + assert bedroom_state is not None + assert bedroom_state.attributes[ATTR_COLOR_TEMP_KELVIN] == pytest.approx( + 2300, + abs=5, + ) + + +async def test_fixed_virtual_day_curve_and_power_automation( + hass: HomeAssistant, + freezer, +) -> None: + """Catch a broken cross-midnight curve or power-trigger branch.""" + summary = "Run a fixed virtual day across midnight." + integration_config, automation_config = _yaml_documents(summary) + await _setup_template_lights(hass, ["Indoor Garden"]) + assert isinstance(integration_config, dict) + assert await async_setup_component(hass, DOMAIN, integration_config) + await hass.async_block_till_done() + entry = hass.config_entries.async_entries(DOMAIN)[0] + adaptive_switch = hass.data[DOMAIN][entry.entry_id][SWITCH_DOMAIN] + + expected_brightness = { + datetime(2026, 9, 6, 16, tzinfo=dt_util.DEFAULT_TIME_ZONE): 10, + datetime(2026, 9, 6, 22, tzinfo=dt_util.DEFAULT_TIME_ZONE): 100, + datetime(2026, 9, 7, 4, tzinfo=dt_util.DEFAULT_TIME_ZONE): 10, + } + for now, expected in expected_brightness.items(): + assert adaptive_switch._sun_light_settings.brightness_pct( + now, + False, + ) == pytest.approx( + expected, + ) + + freezer.move_to( + datetime(2026, 9, 6, 15, 59, tzinfo=dt_util.DEFAULT_TIME_ZONE), + ) + await _setup_automation(hass, automation_config) + await hass.async_start() + async_fire_time_changed( + hass, + datetime(2026, 9, 6, 16, tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(UTC), + ) + await hass.async_block_till_done() + garden_state = hass.states.get("light.indoor_garden") + assert garden_state is not None + assert garden_state.state == STATE_ON + + async_fire_time_changed( + hass, + datetime(2026, 9, 7, 4, tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(UTC), + ) + await hass.async_block_till_done() + garden_state = hass.states.get("light.indoor_garden") + assert garden_state is not None + assert garden_state.state == STATE_OFF + + +@pytest.mark.parametrize( + ("start_hour", "expected_state"), + [(10, STATE_OFF), (22, STATE_ON)], +) +async def test_fixed_virtual_day_reconciles_power_at_startup( + hass: HomeAssistant, + freezer, + start_hour: int, + expected_state: str, +) -> None: + """Catch a power schedule that misses a trigger while HA is offline.""" + _prepare_hass_startup(hass) + summary = "Run a fixed virtual day across midnight." + _, automation_config = _yaml_documents(summary) + await _setup_template_lights(hass, ["Indoor Garden"]) + await _setup_automation(hass, automation_config) + freezer.move_to( + datetime(2026, 9, 6, start_hour, tzinfo=dt_util.DEFAULT_TIME_ZONE), + ) + + await hass.async_start() + await hass.async_block_till_done() + + garden_state = hass.states.get("light.indoor_garden") + assert garden_state is not None + assert garden_state.state == expected_state + + +async def test_autoreset_manual_control_uses_one_renewable_timer( + hass: HomeAssistant, +) -> None: + """Validate the documented built-in timeout and its renewal behavior.""" + summary = "Automatically reset manual control after one hour." + integration_config = _yaml_documents(summary)[0] + await _setup_template_lights(hass, ["Living Room"]) + assert isinstance(integration_config, dict) + assert await async_setup_component(hass, DOMAIN, integration_config) + await hass.async_block_till_done() + entry = hass.config_entries.async_entries(DOMAIN)[0] + adaptive_switch = hass.data[DOMAIN][entry.entry_id][SWITCH_DOMAIN] + assert adaptive_switch._auto_reset_manual_control_time == 3600 + + service_data = { + ATTR_ENTITY_ID: adaptive_switch.entity_id, + CONF_LIGHTS: ["light.living_room"], + "manual_control": True, + } + await hass.services.async_call( + DOMAIN, + "set_manual_control", + service_data, + blocking=True, + ) + first_timer = adaptive_switch.manager.auto_reset_manual_control_timers[ + "light.living_room" + ] + first_task = first_timer.task + + await hass.services.async_call( + DOMAIN, + "set_manual_control", + service_data, + blocking=True, + ) + renewed_timer = adaptive_switch.manager.auto_reset_manual_control_timers[ + "light.living_room" + ] + await asyncio.sleep(0) + assert renewed_timer is first_timer + assert renewed_timer.task is not first_task + assert first_task is not None + assert first_task.cancelled() + assert adaptive_switch.manager.manual_control["light.living_room"] + + await renewed_timer.callback() + assert not adaptive_switch.manager.manual_control["light.living_room"] + + +async def test_sleep_toggle_uses_fresh_profile_entity_ids( + hass: HomeAssistant, +) -> None: + """Execute state triggers against fresh child entity IDs.""" + summary = ( + 'Toggle multiple Adaptive Lighting switches to "sleep mode" using an ' + "input_boolean.sleep_mode." + ) + automation_config = _yaml_documents(summary)[0] + assert await async_setup_component( + hass, + "input_boolean", + {"input_boolean": {"sleep_mode": {}}}, + ) + await setup_switch(hass, {CONF_NAME: "Living Room"}) + await setup_switch(hass, {CONF_NAME: "Bedroom"}) + await _setup_automation(hass, automation_config) + + await hass.services.async_call( + "input_boolean", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "input_boolean.sleep_mode"}, + blocking=True, + ) + await hass.async_block_till_done() + for entity_id in ( + "switch.adaptive_lighting_living_room_sleep_mode", + "switch.adaptive_lighting_bedroom_sleep_mode", + ): + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON + + await hass.services.async_call( + "input_boolean", + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "input_boolean.sleep_mode"}, + blocking=True, + ) + await hass.async_block_till_done() + for entity_id in ( + "switch.adaptive_lighting_living_room_sleep_mode", + "switch.adaptive_lighting_bedroom_sleep_mode", + ): + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + + +async def test_sleep_toggle_applies_restored_state_at_startup( + hass: HomeAssistant, +) -> None: + """Verify startup applies the input boolean's restored state.""" + _prepare_hass_startup(hass) + summary = ( + 'Toggle multiple Adaptive Lighting switches to "sleep mode" using an ' + "input_boolean.sleep_mode." + ) + automation_config = _yaml_documents(summary)[0] + assert await async_setup_component( + hass, + "input_boolean", + {"input_boolean": {"sleep_mode": {}}}, + ) + await setup_switch(hass, {CONF_NAME: "Living Room"}) + await hass.services.async_call( + "input_boolean", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "input_boolean.sleep_mode"}, + blocking=True, + ) + await _setup_automation(hass, automation_config) + + await hass.async_start() + await hass.async_block_till_done() + state = hass.states.get("switch.adaptive_lighting_living_room_sleep_mode") + assert state is not None + assert state.state == STATE_ON + + +async def test_alarm_script_updates_its_profile_once( + hass: HomeAssistant, + freezer, +) -> None: + """Execute the alarm script and verify one profile update with a 12-hour day.""" + summary = "Set sunrise and sunset from an alarm." + script_config = _yaml_documents(summary)[0] + freezer.move_to( + datetime(2026, 11, 1, 0, 30, tzinfo=dt_util.DEFAULT_TIME_ZONE), + ) + _, adaptive_switch = await setup_switch(hass, {CONF_NAME: "Alarm Lights"}) + await _setup_script(hass, script_config) + calls = [] + + def record_service_call(event) -> None: + if ( + event.data["domain"] == DOMAIN + and event.data["service"] == "change_switch_settings" + ): + calls.append(event) + + hass.bus.async_listen(EVENT_CALL_SERVICE, record_service_call) + await hass.services.async_call( + script.DOMAIN, + "set_adaptive_lighting_alarm_times", + blocking=True, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + sunrise = adaptive_switch._sun_light_settings.sunrise_time + sunset = adaptive_switch._sun_light_settings.sunset_time + assert sunrise is not None + assert sunset is not None + sunrise_seconds = sunrise.hour * 3600 + sunrise.minute * 60 + sunrise.second + sunset_seconds = sunset.hour * 3600 + sunset.minute * 60 + sunset.second + assert (sunset_seconds - sunrise_seconds) % (24 * 3600) == 12 * 3600 From 6e7febee8fd7f8fbc9e67208dde6a27f4c353d4a Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 13:40:01 +0200 Subject: [PATCH 1042/1077] docs: add Mariuss811 as a contributor for bug (#1557) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 1f673c0d..a2267d8a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1495,6 +1495,15 @@ "contributions": [ "ideas" ] + }, + { + "login": "Mariuss811", + "name": "Wosten", + "avatar_url": "https://avatars.githubusercontent.com/u/54115696?v=4", + "profile": "https://github.com/Mariuss811", + "contributions": [ + "bug" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 2fc73516..54140896 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-164-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-165-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -923,6 +923,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark jrbergen
jrbergen

🤔 Bradley O'Connell
Bradley O'Connell

🤔 00schteven
00schteven

🤔 + Wosten
Wosten

🐛 From 9525d2f24328fc56b9625ceaba1f83436e61f21e Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 13:40:04 +0200 Subject: [PATCH 1043/1077] docs: add GollyJer as a contributor for bug (#1558) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index a2267d8a..dc13dc6c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1466,7 +1466,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/689204?v=4", "profile": "https://isjustawesome.com", "contributions": [ - "ideas" + "ideas", + "bug" ] }, { diff --git a/README.md b/README.md index 54140896..33234bde 100644 --- a/README.md +++ b/README.md @@ -917,7 +917,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Niklas Haas
Niklas Haas

🤔 Tom Urlings
Tom Urlings

🤔 Benoit Anastay
Benoit Anastay

🤔 - Jeremy Gollehon
Jeremy Gollehon

🤔 + Jeremy Gollehon
Jeremy Gollehon

🤔 🐛 jrbergen
jrbergen

🤔 From 1f137d2b36a58555db266dd22dbae26f74ebe4b3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 13:50:55 +0200 Subject: [PATCH 1044/1077] Fix causal context links for intercepted calls (#1559) --- custom_components/adaptive_lighting/switch.py | 5 +- tests/test_switch.py | 111 +++++++++++++++++- 2 files changed, 110 insertions(+), 6 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 0b49e8d9..62052944 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2119,7 +2119,7 @@ class AdaptiveLightingManager: for eid in _entity_ids: # Must add a new context otherwise _adapt_light will bail out - context = switch.create_context("intercept") + context = switch.create_context("intercept", parent=call.context) self.clear_proactively_adapting(eid) self.set_proactively_adapting(context.id, eid) _LOGGER.debug( @@ -2140,7 +2140,7 @@ class AdaptiveLightingManager: assert set(skipped) == set(entity_ids) return # The call will be intercepted with the original data # Call light turn_on service for skipped entities - context = self.create_context("skipped") + context = self.create_context("skipped", parent=call.context) _LOGGER.debug( "(5) _service_interceptor_turn_on_handler: calling `light.turn_on` with skipped='%s', service_data: '%s', context='%s'", skipped, @@ -2180,6 +2180,7 @@ class AdaptiveLightingManager: adaptation_data = await switch.prepare_adaptation_data( entity_ids[0], transition, + context=switch.create_context("adapt_lights", parent=call.context), ) if adaptation_data is None: return diff --git a/tests/test_switch.py b/tests/test_switch.py index 421a430e..5013fe3c 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -15,6 +15,7 @@ import pytest import ulid_transform import voluptuous.error from flaky import flaky +from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.adaptive_lighting.adaptation_utils import ( AdaptationData, LightControlAttributes, @@ -2332,11 +2333,11 @@ async def test_service_calls_task_cancellation(hass): async def _turn_on_and_track_event_contexts( hass: HomeAssistant, - context_id: str, + context_id: str | Context, entity_id, return_full_events: bool = False, ): - context = Context(id=context_id) + context = context_id if isinstance(context_id, Context) else Context(id=context_id) event_context_ids = [] events = [] @@ -2359,6 +2360,105 @@ async def _turn_on_and_track_event_contexts( return event_context_ids +async def _admin_context(hass: HomeAssistant, context_id: str) -> Context: + """Create a user-originated context accepted by entity service checks.""" + user = await hass.auth.async_create_user(context_id, group_ids=[GROUP_ID_ADMIN]) + return Context( + id=context_id, + parent_id="automation_origin", + user_id=user.id, + ) + + +async def test_apply_service_context_links_to_origin(hass): + """The apply service links its light call to the originating service call.""" + switch, (_, _, light) = await setup_lights_and_switch(hass) + origin = await _admin_context(hass, "apply_origin") + events: list[Event] = [] + + async def listener(event: Event) -> None: + if ( + event.data.get("domain") == LIGHT_DOMAIN + and event.data.get("service") == SERVICE_TURN_ON + ): + events.append(event) + + remove_listener = hass.bus.async_listen(EVENT_CALL_SERVICE, listener) + try: + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [light.entity_id], + CONF_TURN_ON_LIGHTS: True, + }, + blocking=True, + context=origin, + ) + await hass.async_block_till_done() + finally: + remove_listener() + + assert len(events) == 1 + assert events[0].context.parent_id == origin.id + assert events[0].context.user_id is None + + +async def test_single_light_intercept_keeps_origin_context(hass): + """A directly intercepted call keeps the original context unchanged.""" + await setup_lights_and_switch(hass, {CONF_INTERCEPT: True}, True) + origin = await _admin_context(hass, "single_intercept_origin") + + events = await _turn_on_and_track_event_contexts( + hass, + origin, + ENTITY_LIGHT_3, + return_full_events=True, + ) + + assert len(events) == 1 + assert events[0].context.id == origin.id + assert events[0].context.parent_id == origin.parent_id + assert events[0].context.user_id == origin.user_id + + +async def test_multi_profile_intercept_context_links_to_origin(hass): + """A secondary profile adaptation links its new call to the origin.""" + lights, _, _ = await setup_proactive_multiple_lights_two_switches(hass) + origin = await _admin_context(hass, "multi_profile_origin") + + events = await _turn_on_and_track_event_contexts( + hass, + origin, + lights[:2], + return_full_events=True, + ) + secondary_events = [event for event in events if ":ntrc:" in event.context.id] + + assert len(secondary_events) == 1 + assert secondary_events[0].context.parent_id == origin.id + assert secondary_events[0].context.user_id is None + + +async def test_skipped_light_context_links_to_origin(hass): + """A split call for an unmanaged light links its new call to the origin.""" + lights, _, _ = await setup_proactive_multiple_lights_two_switches(hass) + origin = await _admin_context(hass, "skipped_light_origin") + + events = await _turn_on_and_track_event_contexts( + hass, + origin, + [lights[0], lights[2]], + return_full_events=True, + ) + skipped_events = [event for event in events if ":skpp:" in event.context.id] + + assert len(skipped_events) == 1 + assert skipped_events[0].context.parent_id == origin.id + assert skipped_events[0].context.user_id is None + + def _mock_sun_light_settings(switch: AdaptiveSwitch, settings: dict[str, Any]): sun_light_settings_mock = Mock() sun_light_settings_mock.get_settings = Mock(return_value=settings) @@ -2415,9 +2515,10 @@ async def test_proactive_adaptation_with_separate_commands(hass): }, ) + origin = await _admin_context(hass, "separate_commands_origin") events = await _turn_on_and_track_event_contexts( hass, - "test_context", + origin, ENTITY_LIGHT_3, return_full_events=True, ) @@ -2428,8 +2529,10 @@ async def test_proactive_adaptation_with_separate_commands(hass): # Expect two service calls assert len(event_context_ids) == 2, event_context_ids - assert event_context_ids[0] == "test_context" + assert event_context_ids[0] == origin.id assert is_our_context_id(event_context_ids[1]) + assert events[1].context.parent_id == origin.id + assert events[1].context.user_id is None # Expect adapted light state state = hass.states.get(ENTITY_LIGHT_3) From c8de38f779635ab23f0234b93259fddf6a475082 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 14:03:08 +0200 Subject: [PATCH 1045/1077] Fix missing follow-up commands during multi-light interception (#1560) * fix: adapt every member during split multi-light interception * test: detect template light color-mode storage directly --- .../adaptive_lighting/adaptation_utils.py | 24 +- custom_components/adaptive_lighting/switch.py | 33 +- tests/test_adaptation_utils.py | 47 +++ tests/test_switch.py | 350 ++++++++++++++++++ 4 files changed, 443 insertions(+), 11 deletions(-) diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py index 20a227ca..91defc95 100644 --- a/custom_components/adaptive_lighting/adaptation_utils.py +++ b/custom_components/adaptive_lighting/adaptation_utils.py @@ -275,6 +275,7 @@ def prepare_adaptation_data( split: bool, filter_by_state: bool, force: bool, + already_applied: LightControlAttributes = LightControlAttributes.NONE, ) -> AdaptationData: """Prepares a data object carrying all data required to execute an adaptation.""" _LOGGER.debug( @@ -292,6 +293,25 @@ def prepare_adaptation_data( else: sleep_time = split_delay + # Keep the original split timing, but omit attributes carried by an + # intercepted turn-on shared with other lights. Do this before state + # filtering: members can have different brightness/color already satisfied. + applied_attrs = ( + BRIGHTNESS_ATTRS + if LightControlAttributes.BRIGHTNESS in already_applied + else set() + ) | (COLOR_ATTRS if LightControlAttributes.COLOR in already_applied else set()) + if applied_attrs: + service_datas = [ + {key: value for key, value in data.items() if key not in applied_attrs} + for data in service_datas + ] + service_datas = [ + data + for data in service_datas + if _has_relevant_service_data_attributes(data) + ] + service_data_iterator = _create_service_call_data_iterator( hass, service_datas, @@ -306,8 +326,8 @@ def prepare_adaptation_data( sleep_time=sleep_time, service_call_datas=service_data_iterator, force=force, - max_length=service_datas_length, - attributes=attributes, + max_length=len(service_datas), + attributes=attributes & ~already_applied, ) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 62052944..d2195be0 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1272,6 +1272,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): prefer_rgb_color: bool | None = None, force: bool = False, context: Context | None = None, + already_applied: LightControlAttributes = LightControlAttributes.NONE, ) -> AdaptationData | None: """Prepare `AdaptationData` for adapting a light.""" adaptation_attributes = self.manager.get_adaption_control_attributes( @@ -1365,6 +1366,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): split=self._separate_turn_on_commands, filter_by_state=self._skip_redundant_commands, force=force, + already_applied=already_applied, ) async def _adapt_light( @@ -2206,18 +2208,31 @@ class AdaptiveLightingManager: # We cannot know here whether there is another call to follow (since the # state can change until the next call), so we just schedule it and let # it sort out by itself. - for entity_id in entity_ids: + already_applied = get_light_control_attributes(first_service_data) + for index, entity_id in enumerate(entity_ids): self.set_proactively_adapting(call.context.id, entity_id) + if index: + # Each member needs its own remaining commands and cancellation. + # Consuming its first iterator item could discard a color command + # when only the shared brightness command has been applied. + adaptation_data = await switch.prepare_adaptation_data( + entity_id, + transition, + context=switch.create_context("adapt_lights", parent=call.context), + already_applied=already_applied, + ) + if adaptation_data is None or not adaptation_data.max_length: + continue self.set_proactively_adapting(adaptation_data.context.id, entity_id) - adaptation_data.initial_sleep = True + adaptation_data.initial_sleep = True - # Don't await to avoid blocking the service call. - # Assign to a variable only to await in tests. - self.adaptation_tasks.add( - asyncio.create_task( - switch.execute_cancellable_adaptation_calls(adaptation_data), - ), - ) + # Don't await to avoid blocking the service call. + # Assign to a variable only to await in tests. + self.adaptation_tasks.add( + asyncio.create_task( + switch.execute_cancellable_adaptation_calls(adaptation_data), + ), + ) # Remove tasks that are done if done_tasks := [t for t in self.adaptation_tasks if t.done()]: self.adaptation_tasks.difference_update(done_tasks) diff --git a/tests/test_adaptation_utils.py b/tests/test_adaptation_utils.py index b5751454..e0ec07de 100644 --- a/tests/test_adaptation_utils.py +++ b/tests/test_adaptation_utils.py @@ -554,3 +554,50 @@ def test_get_light_control_attributes( ): """Test determination of light control attributes.""" assert get_light_control_attributes(service_data) == expected_flags + + +@pytest.mark.parametrize( + ("already_applied", "expected"), + [ + ( + LightControlAttributes.BRIGHTNESS, + [ + { + ATTR_ENTITY_ID: "light.test", + ATTR_COLOR_TEMP_KELVIN: 3448, + ATTR_TRANSITION: 1, + }, + ], + ), + ( + LightControlAttributes.COLOR, + [{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 171, ATTR_TRANSITION: 1}], + ), + (LightControlAttributes.ALL, []), + ], +) +async def test_remaining_split_commands_preserve_transition( + hass, + already_applied, + expected, +): + """Removing the shared command must not redistribute its transition time.""" + data = prepare_adaptation_data( + hass, + "light.test", + Context(), + transition=2, + split_delay=0.1, + service_data={ + ATTR_ENTITY_ID: "light.test", + ATTR_BRIGHTNESS: 171, + ATTR_COLOR_TEMP_KELVIN: 3448, + ATTR_TRANSITION: 2, + }, + split=True, + filter_by_state=False, + force=False, + already_applied=already_applied, + ) + assert [command async for command in data.service_call_datas] == expected + assert data.sleep_time == 1.1 diff --git a/tests/test_switch.py b/tests/test_switch.py index 5013fe3c..6ebc35ca 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -44,6 +44,7 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_PREFER_RGB_COLOR, CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, CONF_SEPARATE_TURN_ON_COMMANDS, + CONF_SKIP_REDUNDANT_COMMANDS, CONF_SLEEP_RGB_OR_COLOR_TEMP, CONF_SUNRISE_OFFSET, CONF_SUNRISE_TIME, @@ -88,6 +89,7 @@ from homeassistant.components.light import ( ATTR_TRANSITION, ATTR_XY_COLOR, SERVICE_TURN_OFF, + ColorMode, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -4039,3 +4041,351 @@ async def test_intercept_preserves_area_target_exclusions( assert target_state.attributes[ATTR_BRIGHTNESS] == 128 else: assert target_state.attributes.get(ATTR_BRIGHTNESS) != 128 + + +@pytest.mark.parametrize("split", [False, True]) +@pytest.mark.parametrize("target_group", [False, True]) +@pytest.mark.parametrize("skip_redundant", [False, True]) +async def test_multi_light_intercept_adapts_every_member( + hass, + split, + target_group, + skip_redundant, + cleanup, +): + """Each member receives brightness and color, including split follow-up calls.""" + lights = await setup_lights(hass, with_group=True) + # The second member already has target brightness; its color still needs work. + set_light_brightness(lights[4], 171) + lights[4].async_write_ha_state() + members = ["light.light_4", "light.light_5"] + _, switch = await setup_switch( + hass, + { + CONF_LIGHTS: members, + CONF_INTERCEPT: True, + CONF_MULTI_LIGHT_INTERCEPT: True, + CONF_SEPARATE_TURN_ON_COMMANDS: split, + CONF_SKIP_REDUNDANT_COMMANDS: skip_redundant, + CONF_INITIAL_TRANSITION: 0, + }, + ) + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 67, + ATTR_COLOR_TEMP_KELVIN: 3448, + "force_rgb_color": False, + }, + ) + events = await _turn_on_and_track_event_contexts( + hass, + "multi_light_split", + "light.light_group" if target_group else members, + return_full_events=True, + ) + await asyncio.gather(*switch.manager.adaptation_tasks) + await hass.async_block_till_done() + + if split: + color_targets = { + event.data["service_data"][ATTR_ENTITY_ID] + for event in events + if ATTR_COLOR_TEMP_KELVIN in event.data["service_data"] + } + assert color_targets == set(members) + for entity_id in members: + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 171 + assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 3448 + + +@pytest.mark.parametrize("physical_off", [False, True]) +async def test_split_command_stays_off_after_turn_off(hass, physical_off): + """An actual OFF between brightness and color cancels the pending command.""" + switch, _ = await setup_lights_and_switch( + hass, + {CONF_INTERCEPT: True, CONF_SEPARATE_TURN_ON_COMMANDS: True}, + all_lights=True, + ) + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 67, + ATTR_COLOR_TEMP_KELVIN: 3448, + "force_rgb_color": False, + }, + ) + events = await _turn_on_and_track_event_contexts( + hass, + "split_then_off", + ENTITY_LIGHT_3, + return_full_events=True, + ) + assert len(events) == 1 + assert hass.states.get(ENTITY_LIGHT_3).state == STATE_ON + if physical_off: + # Device reports may retain the context of the preceding turn-on. + state = hass.states.get(ENTITY_LIGHT_3) + hass.states.async_set( + ENTITY_LIGHT_3, + STATE_OFF, + state.attributes, + context=Context(id="split_then_off"), + ) + else: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_3}, + blocking=True, + ) + await hass.async_block_till_done() + await asyncio.gather(*switch.manager.adaptation_tasks) + await hass.async_block_till_done() + + turn_on_events = [ + event for event in events if event.data["service"] == SERVICE_TURN_ON + ] + assert len(turn_on_events) == 1 + assert hass.states.get(ENTITY_LIGHT_3).state == STATE_OFF + + +@pytest.mark.parametrize("brightness_only_member", [0, 1]) +async def test_multi_light_split_with_brightness_only_member( + hass, + brightness_only_member, + cleanup, +): + """A brightness-only member must not consume another member's color command.""" + lights = await setup_lights(hass, with_group=True) + members = ["light.light_4", "light.light_5"] + light = lights[3 + brightness_only_member] + # Legacy YAML support outlived the old entity storage fields. + if hasattr(light, "_supported_color_modes"): + light._supported_color_modes = {ColorMode.BRIGHTNESS} + light._color_mode = ColorMode.BRIGHTNESS + else: + light._attr_supported_color_modes = {ColorMode.BRIGHTNESS} + light._attr_color_mode = ColorMode.BRIGHTNESS + light.async_write_ha_state() + _, switch = await setup_switch( + hass, + { + CONF_LIGHTS: members, + CONF_INTERCEPT: True, + CONF_MULTI_LIGHT_INTERCEPT: True, + CONF_SEPARATE_TURN_ON_COMMANDS: True, + CONF_INITIAL_TRANSITION: 0, + }, + ) + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 67, + ATTR_COLOR_TEMP_KELVIN: 3448, + "force_rgb_color": False, + }, + ) + events = await _turn_on_and_track_event_contexts( + hass, + "mixed_split", + members, + return_full_events=True, + ) + await asyncio.gather(*switch.manager.adaptation_tasks) + await hass.async_block_till_done() + color_targets = [ + event.data["service_data"][ATTR_ENTITY_ID] + for event in events + if ATTR_COLOR_TEMP_KELVIN in event.data["service_data"] + ] + assert color_targets == [members[1 - brightness_only_member]] + for entity_id in members: + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 171 + assert ( + hass.states.get(members[1 - brightness_only_member]).attributes[ + ATTR_COLOR_TEMP_KELVIN + ] + == 3448 + ) + + +@pytest.mark.parametrize("off_member", [0, 1]) +@pytest.mark.parametrize("physical_off", [False, True]) +async def test_multi_light_split_cancels_only_member_turned_off( + hass, + off_member, + physical_off, + cleanup, +): + """An OFF member stays off while the other finishes its color adaptation.""" + await setup_lights(hass, with_group=True) + members = ["light.light_4", "light.light_5"] + _, switch = await setup_switch( + hass, + { + CONF_LIGHTS: members, + CONF_INTERCEPT: True, + CONF_MULTI_LIGHT_INTERCEPT: True, + CONF_SEPARATE_TURN_ON_COMMANDS: True, + CONF_INITIAL_TRANSITION: 0, + }, + ) + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 67, + ATTR_COLOR_TEMP_KELVIN: 3448, + "force_rgb_color": False, + }, + ) + resume = asyncio.Event() + original_execute = switch._execute_adaptation_calls + + async def wait_before_followup(data): + await resume.wait() + await original_execute(data) + + with patch.object(switch, "_execute_adaptation_calls", new=wait_before_followup): + events = await _turn_on_and_track_event_contexts( + hass, + "member_off", + members, + return_full_events=True, + ) + off_entity = members[off_member] + state = hass.states.get(off_entity) + assert state.state == STATE_ON + if physical_off: + hass.states.async_set( + off_entity, + STATE_OFF, + state.attributes, + context=Context(id="member_off"), + ) + else: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: off_entity}, + blocking=True, + ) + await hass.async_block_till_done() + resume.set() + await asyncio.gather(*switch.manager.adaptation_tasks) + await hass.async_block_till_done() + + color_targets = [ + event.data["service_data"][ATTR_ENTITY_ID] + for event in events + if ATTR_COLOR_TEMP_KELVIN in event.data["service_data"] + ] + assert color_targets == [members[1 - off_member]] + assert hass.states.get(off_entity).state == STATE_OFF + other_state = hass.states.get(members[1 - off_member]) + assert other_state.state == STATE_ON + assert other_state.attributes[ATTR_BRIGHTNESS] == 171 + assert other_state.attributes[ATTR_COLOR_TEMP_KELVIN] == 3448 + + +@pytest.mark.parametrize( + "off_action", + [SERVICE_TURN_OFF, SERVICE_TOGGLE, "physical", "transition"], +) +async def test_forced_split_apply_stays_off(hass, off_action, cleanup): + """Even forced apply must cancel its remaining color command after OFF.""" + switch, _ = await setup_lights_and_switch( + hass, + {CONF_INTERCEPT: True, CONF_SEPARATE_TURN_ON_COMMANDS: True}, + all_lights=True, + ) + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 67, + ATTR_COLOR_TEMP_KELVIN: 3448, + "force_rgb_color": False, + }, + ) + events = [] + light_on = asyncio.Event() + waiting_for_color = asyncio.Event() + resume = asyncio.Event() + calls_read = 0 + original_next = AdaptationData.next_service_call_data + + async def next_with_color_barrier(data): + nonlocal calls_read + calls_read += 1 + if calls_read == 2: + waiting_for_color.set() + await resume.wait() + return await original_next(data) + + async def track_service(event): + if event.data["domain"] == LIGHT_DOMAIN: + events.append(event) + + async def track_state(event): + if ( + event.data[ATTR_ENTITY_ID] == ENTITY_LIGHT_3 + and event.data["new_state"] is not None + and event.data["new_state"].state == STATE_ON + ): + light_on.set() + + hass.bus.async_listen(EVENT_CALL_SERVICE, track_service) + hass.bus.async_listen(EVENT_STATE_CHANGED, track_state) + with patch.object( + AdaptationData, + "next_service_call_data", + new=next_with_color_barrier, + ): + applying = asyncio.create_task( + hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [ENTITY_LIGHT_3], + CONF_TURN_ON_LIGHTS: True, + }, + blocking=True, + ), + ) + await asyncio.wait_for(waiting_for_color.wait(), timeout=1) + await asyncio.wait_for(light_on.wait(), timeout=1) + if off_action == "physical": + state = hass.states.get(ENTITY_LIGHT_3) + hass.states.async_set( + ENTITY_LIGHT_3, + STATE_OFF, + state.attributes, + context=state.context, + ) + else: + service_data = {ATTR_ENTITY_ID: ENTITY_LIGHT_3} + if off_action == "transition": + service_data[ATTR_TRANSITION] = 1 + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF if off_action == "transition" else off_action, + service_data, + blocking=True, + ) + await hass.async_block_till_done() + resume.set() + await applying + await hass.async_block_till_done() + + turn_on_events = [ + event for event in events if event.data["service"] == SERVICE_TURN_ON + ] + assert len(turn_on_events) == 1 + assert ATTR_BRIGHTNESS in turn_on_events[0].data["service_data"] + assert ATTR_COLOR_TEMP_KELVIN not in turn_on_events[0].data["service_data"] + assert hass.states.get(ENTITY_LIGHT_3).state == STATE_OFF From bbe5f3837d5538db91f4d1a789e84026297ebee1 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 14:25:24 +0200 Subject: [PATCH 1046/1077] Fix autoreset timer renewal for unchanged physical light states (#1561) * fix: preserve manual-control timeout across polls * fix: update manual baseline after adaptive writes * fix: seed manual baseline from tracked changes --- custom_components/adaptive_lighting/switch.py | 123 +++++- tests/test_switch.py | 358 +++++++++++++++++- 2 files changed, 471 insertions(+), 10 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index d2195be0..f5f0e938 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1437,6 +1437,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): data.context.id, ) light = service_data[ATTR_ENTITY_ID] + self.manager.invalidate_manual_control_state( + light, + get_light_control_attributes(service_data), + ) self.manager.last_service_data[light] = { **self.manager.last_service_data.get(light, {}), **service_data, @@ -1624,10 +1628,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): and not is_our_context(event.context) ): service_data = self.manager.turn_on_event[entity_id].data[ATTR_SERVICE_DATA] + manual_attributes = get_light_control_attributes(service_data) if self.manager._mark_manual_control_if_non_bare_turn_on( entity_id, service_data, ): + new_state = event.data["new_state"] + assert new_state is not None + self.manager.update_manual_control_state( + entity_id, + new_state, + manual_attributes, + ) _LOGGER.debug( "Marked attributes from service_data as manually controlled for '%s' " "with context.id='%s'. Continuing to adapt remaining attributes. " @@ -1802,6 +1814,15 @@ class AdaptiveLightingManager: self.our_last_state_on_change: dict[str, list[State]] = {} # Track last 'service_data' to 'light.turn_on' resulting from this integration self.last_service_data: dict[str, dict[str, Any]] = {} + # Track reported states that established manual control of each axis + self.last_manual_control_state: dict[ + str, + dict[LightControlAttributes, dict[str, Any]], + ] = {} + self.pending_manual_control_state: dict[ + str, + dict[LightControlAttributes, str], + ] = {} # Track ongoing split adaptations to be able to cancel them self.adaptation_tasks_brightness: dict[str, asyncio.Task[None]] = {} self.adaptation_tasks_color: dict[str, asyncio.Task[None]] = {} @@ -2382,6 +2403,68 @@ class AdaptiveLightingManager: new = current | attributes self.set_manual_control_attributes(light, new) + def invalidate_manual_control_state( + self, + light: str, + attributes: LightControlAttributes, + ) -> None: + """Stop comparing adapted attributes with an older physical state.""" + states = self.last_manual_control_state.get(light) + pending = self.pending_manual_control_state.get(light) + for attribute in LightControlAttributes: + if attribute in attributes and states is not None: + states.pop(attribute, None) + if attribute in attributes and pending is not None: + pending.pop(attribute, None) + if states == {}: + self.last_manual_control_state.pop(light) + if pending == {}: + self.pending_manual_control_state.pop(light) + + def update_manual_control_state( + self, + light: str, + state: State, + attributes: LightControlAttributes, + ) -> None: + """Record the reported state that established manual control of each axis.""" + states = self.last_manual_control_state.setdefault(light, {}) + for attribute in LightControlAttributes: + if attribute in attributes: + states[attribute] = dict(state.attributes) + + def mark_manual_control_state_pending( + self, + light: str, + attributes: LightControlAttributes, + context_id: str, + ) -> None: + """Wait for the reported state produced by a tracked service call.""" + pending = self.pending_manual_control_state.setdefault(light, {}) + for attribute in LightControlAttributes: + if attribute in attributes: + pending[attribute] = context_id + + def consume_pending_manual_control_state( + self, + light: str, + state: State, + context_id: str | None = None, + ) -> None: + """Record a tracked service's reported state once it is available.""" + pending = self.pending_manual_control_state.get(light) + if pending is None: + return + attributes = LightControlAttributes.NONE + for attribute, pending_context_id in tuple(pending.items()): + if context_id is None or context_id == pending_context_id: + attributes |= attribute + pending.pop(attribute) + if not pending: + self.pending_manual_control_state.pop(light) + if attributes: + self.update_manual_control_state(light, state, attributes) + def get_adaption_control_attributes( self, switch: AdaptiveSwitch, @@ -2460,6 +2543,8 @@ class AdaptiveLightingManager: light, ) self.manual_control[light] = LightControlAttributes.NONE + self.last_manual_control_state.pop(light, None) + self.pending_manual_control_state.pop(light, None) if timer := self.auto_reset_manual_control_timers.pop(light, None): timer.cancel() self.our_last_state_on_change.pop(light, None) @@ -2615,7 +2700,6 @@ class AdaptiveLightingManager: if old_state is not None and old_state.state == STATE_OFF else None ) - if new_on: _LOGGER.debug( "Detected a '%s' 'state_changed' event: '%s' with context.id='%s'", @@ -2660,6 +2744,11 @@ class AdaptiveLightingManager: self.start_transition_timer(entity_id) elif last_state is not None: self.our_last_state_on_change[entity_id].append(new_on) + self.consume_pending_manual_control_state( + entity_id, + new_on, + new_on.context.id, + ) if old_on and new_off: # Tracks 'on' → 'off' state changes @@ -2737,6 +2826,11 @@ class AdaptiveLightingManager: # Light was already on and 'light.turn_on' was not called by # the adaptive_lighting integration. + self.mark_manual_control_state_pending( + light, + turn_on_attributes, + turn_on_event.context.id, + ) self.add_manual_control_attributes(light, turn_on_attributes) switch.fire_manual_control_event(light, turn_on_event.context) _LOGGER.debug( @@ -2804,14 +2898,29 @@ class AdaptiveLightingManager: await async_update_entity(self.hass, light) refreshed_state = self.hass.states.get(light) assert refreshed_state is not None + self.consume_pending_manual_control_state(light, refreshed_state) - changed_attributes = _attributes_have_changed( - old_attributes=last_service_data, - new_attributes=refreshed_state.attributes, - light=light, - context=context, - ) + manual_control = self.get_manual_control_attributes(light) + manual_control_states = self.last_manual_control_state.get(light, {}) + changed_attributes = LightControlAttributes.NONE + for attribute in LightControlAttributes: + old_attributes = ( + manual_control_states.get(attribute, last_service_data) + if attribute in manual_control + else last_service_data + ) + changed_attributes |= attribute & _attributes_have_changed( + old_attributes=dict(old_attributes), + new_attributes=refreshed_state.attributes, + light=light, + context=context, + ) if changed_attributes: + self.update_manual_control_state( + light, + refreshed_state, + changed_attributes, + ) _LOGGER.debug( "%s: State attributes %s of '%s' changed (%s) wrt 'last_service_data' (%s) (context.id=%s)", switch._name, diff --git a/tests/test_switch.py b/tests/test_switch.py index 6ebc35ca..5c89df06 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -27,6 +27,8 @@ from homeassistant.components.adaptive_lighting.color_and_brightness import ( from homeassistant.components.adaptive_lighting.const import ( ADAPT_BRIGHTNESS_SWITCH, ADAPT_COLOR_SWITCH, + ATTR_ADAPT_BRIGHTNESS, + ATTR_ADAPT_COLOR, ATTR_ADAPTIVE_LIGHTING_MANAGER, CONF_ADAPT_ONLY_ON_BARE_TURN_ON, CONF_ADAPT_UNTIL_SLEEP, @@ -117,7 +119,10 @@ from homeassistant.helpers import area_registry as ar from homeassistant.helpers import entity_registry from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.setup import async_setup_component -from homeassistant.util.color import color_temperature_mired_to_kelvin +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired, + color_temperature_mired_to_kelvin, +) from tests.common import MockConfigEntry from tests.common import mock_area_registry as mock_ha_area_registry @@ -722,7 +727,7 @@ async def test_manual_control( _LOGGER.debug("End of change_manual_control") def increased_brightness(): - return (light._attr_brightness + 100) % 255 + return max(1, (light._attr_brightness + 100) % 255) def increased_color_temp(): return max( @@ -1122,6 +1127,353 @@ async def test_interval_adaptation_preserves_manual_control_timeout( ) +@pytest.mark.parametrize("intercept", [False, True]) +@pytest.mark.parametrize("mode", list(TakeOverControlMode)) +@pytest.mark.parametrize( + "manual_attribute", + [LightControlAttributes.BRIGHTNESS, LightControlAttributes.COLOR], +) +async def test_tracked_change_seeds_non_ha_baseline( + hass, + freezer, + cleanup, + intercept, + mode, + manual_attribute, +): + """A tracked service change must not be detected again by the next poll.""" + switch, (light, *_) = await setup_lights_and_switch( + hass, + { + CONF_AUTORESET_CONTROL: 7200, + CONF_TAKE_OVER_CONTROL_MODE: mode, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_INTERCEPT: intercept, + }, + ) + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + force=True, + transition=0, + ) + await hass.async_block_till_done() + + if manual_attribute == LightControlAttributes.BRIGHTNESS: + adaptive_value = light.brightness + attribute = ATTR_BRIGHTNESS + difference = 120 + else: + adaptive_value = light.color_temp_kelvin + attribute = ATTR_COLOR_TEMP_KELVIN + difference = 500 + assert adaptive_value is not None + manual_value = ( + adaptive_value - difference + if adaptive_value >= difference + else adaptive_value + difference + ) + + events = [] + hass.bus.async_listen(f"{DOMAIN}.manual_control", events.append) + service_context = Context() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id, attribute: manual_value}, + blocking=True, + context=service_context, + ) + await hass.async_block_till_done() + assert ( + switch.manager.get_manual_control_attributes(light.entity_id) + == manual_attribute + ) + assert len(events) == 1 + + freezer.tick(90) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7110 + ) + assert len(events) == 1 + + if manual_attribute == LightControlAttributes.BRIGHTNESS: + set_light_brightness(light, adaptive_value) + else: + light._attr_color_temp_kelvin = adaptive_value + if hasattr(light, "_temperature"): + light._temperature = color_temperature_kelvin_to_mired(adaptive_value) + light.async_set_context(service_context) + light.async_write_ha_state() + await hass.async_block_till_done() + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + assert len(events) == 2 + + +@pytest.mark.parametrize("intercept", [False, True]) +@pytest.mark.parametrize("mode", list(TakeOverControlMode)) +@pytest.mark.parametrize( + ("first_manual_attribute", "second_manual_attribute"), + [ + (LightControlAttributes.BRIGHTNESS, LightControlAttributes.COLOR), + (LightControlAttributes.COLOR, LightControlAttributes.BRIGHTNESS), + ], +) +async def test_unchanged_non_ha_change_preserves_manual_control_timeout( + hass, + freezer, + cleanup, + intercept, + mode, + first_manual_attribute, + second_manual_attribute, +): + """An unchanged physical state must not renew its manual-control timeout.""" + switch, lights = await setup_lights_and_switch( + hass, + { + CONF_AUTORESET_CONTROL: 7200, + CONF_TAKE_OVER_CONTROL_MODE: mode, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_INTERCEPT: intercept, + }, + ) + light = lights[0] + lights_by_entity = {item.entity_id: item for item in lights} + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + force=True, + transition=0, + ) + await hass.async_block_till_done() + + adaptive_brightness = light.brightness + assert adaptive_brightness is not None + adaptive_color_temp = light.color_temp_kelvin + assert adaptive_color_temp is not None + manual_values = { + LightControlAttributes.BRIGHTNESS: ( + adaptive_brightness - 120 + if adaptive_brightness >= 120 + else adaptive_brightness + 120 + ), + LightControlAttributes.COLOR: ( + adaptive_color_temp - 500 + if adaptive_color_temp >= 2500 + else adaptive_color_temp + 500 + ), + } + + def set_physical_state(attribute): + if attribute == LightControlAttributes.BRIGHTNESS: + set_light_brightness(light, manual_values[attribute]) + else: + color_temp_kelvin = manual_values[attribute] + light._attr_color_temp_kelvin = color_temp_kelvin + if hasattr(light, "_temperature"): + light._temperature = color_temperature_kelvin_to_mired( + color_temp_kelvin, + ) + + async def flush_physical_state(hass, entity_id): + lights_by_entity[entity_id].async_write_ha_state() + + with patch( + "homeassistant.components.adaptive_lighting.switch.async_update_entity", + new=AsyncMock(side_effect=flush_physical_state), + ): + set_physical_state(first_manual_attribute) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.manager.get_manual_control_attributes(light.entity_id) + == first_manual_attribute + ) + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + + freezer.tick(90) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.manager.get_manual_control_attributes(light.entity_id) + == first_manual_attribute + ) + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7110 + ) + + freezer.tick(90) + set_physical_state(second_manual_attribute) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.manager.get_manual_control_attributes(light.entity_id) + == LightControlAttributes.ALL + ) + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + + +@pytest.mark.parametrize("intercept", [False, True]) +@pytest.mark.parametrize("mode", list(TakeOverControlMode)) +@pytest.mark.parametrize( + "manual_attribute", + [LightControlAttributes.BRIGHTNESS, LightControlAttributes.COLOR], +) +async def test_apply_updates_non_ha_change_baseline( + hass, + freezer, + cleanup, + intercept, + mode, + manual_attribute, +): + """An adaptive apply must become the baseline for later physical changes.""" + switch, lights = await setup_lights_and_switch( + hass, + { + CONF_AUTORESET_CONTROL: 7200, + CONF_TAKE_OVER_CONTROL_MODE: mode, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_INTERCEPT: intercept, + }, + ) + light = lights[0] + lights_by_entity = {item.entity_id: item for item in lights} + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + force=True, + transition=0, + ) + await hass.async_block_till_done() + + adaptive_value = ( + light.brightness + if manual_attribute == LightControlAttributes.BRIGHTNESS + else light.color_temp_kelvin + ) + assert adaptive_value is not None + difference = 120 if manual_attribute == LightControlAttributes.BRIGHTNESS else 500 + manual_value = ( + adaptive_value - difference + if adaptive_value >= difference + else adaptive_value + difference + ) + + def set_physical_state(value=manual_value): + if manual_attribute == LightControlAttributes.BRIGHTNESS: + set_light_brightness(light, value) + else: + light._attr_color_temp_kelvin = value + if hasattr(light, "_temperature"): + light._temperature = color_temperature_kelvin_to_mired(value) + + async def flush_physical_state(hass, entity_id): + lights_by_entity[entity_id].async_write_ha_state() + + with patch( + "homeassistant.components.adaptive_lighting.switch.async_update_entity", + new=AsyncMock(side_effect=flush_physical_state), + ): + set_physical_state() + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.manager.get_manual_control_attributes(light.entity_id) + == manual_attribute + ) + + direction = 1 if manual_value < adaptive_value else -1 + small_change = ( + 15 if manual_attribute == LightControlAttributes.BRIGHTNESS else 60 + ) + freezer.tick(90) + set_physical_state(manual_value + direction * small_change) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7110 + ) + + adapt_brightness = manual_attribute == LightControlAttributes.COLOR + adapt_color = manual_attribute == LightControlAttributes.BRIGHTNESS + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [light.entity_id], + ATTR_ADAPT_BRIGHTNESS: adapt_brightness, + ATTR_ADAPT_COLOR: adapt_color, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert ( + switch.manager.get_manual_control_attributes(light.entity_id) + == manual_attribute + ) + + freezer.tick(90) + pre_apply_value = manual_value + direction * small_change * 2 + set_physical_state(pre_apply_value) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [light.entity_id], + ATTR_ADAPT_BRIGHTNESS: not adapt_brightness, + ATTR_ADAPT_COLOR: not adapt_color, + }, + blocking=True, + ) + await hass.async_block_till_done() + applied_value = ( + light.brightness + if manual_attribute == LightControlAttributes.BRIGHTNESS + else light.color_temp_kelvin + ) + assert applied_value != pre_apply_value + + freezer.tick(90) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7110 + ) + + set_physical_state(pre_apply_value) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + + @pytest.mark.parametrize("mode", list(TakeOverControlMode)) @pytest.mark.parametrize("service_data", [{}, {ATTR_BRIGHTNESS: 20}]) async def test_mixed_turn_on_restarts_manual_control_timeout( @@ -2808,7 +3160,7 @@ async def test_two_switches_for_single_light(hass): _LOGGER.debug("Turn light %s, to %s", state, kwargs) def increased_brightness(): - return (light1._attr_brightness + 100) % 255 + return max(1, (light1._attr_brightness + 100) % 255) def increased_color_temp(): return max( From e8c300cf7550c5f07a55925a814cff72f824210c Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 16:54:03 +0200 Subject: [PATCH 1047/1077] docs: add zpriddy as a contributor for ideas (#1562) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index dc13dc6c..8a913877 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1505,6 +1505,15 @@ "contributions": [ "bug" ] + }, + { + "login": "zpriddy", + "name": "Zachary Priddy", + "avatar_url": "https://avatars.githubusercontent.com/u/1858679?v=4", + "profile": "http://zpriddy.com", + "contributions": [ + "ideas" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 33234bde..2f62d2d0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-165-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-166-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -924,6 +924,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Bradley O'Connell
Bradley O'Connell

🤔 00schteven
00schteven

🤔 Wosten
Wosten

🐛 + Zachary Priddy
Zachary Priddy

🤔 From 563225608774f43f8f981b68d26a5029630ab15b Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 16:56:29 +0200 Subject: [PATCH 1048/1077] docs: add abkslm as a contributor for bug (#1563) * docs: update README.md * docs: update .all-contributorsrc * Keep contributor names in their original encoding --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 8a913877..161b5633 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1514,6 +1514,15 @@ "contributions": [ "ideas" ] + }, + { + "login": "abkslm", + "name": "Andrew Blakeslee Moore", + "avatar_url": "https://avatars.githubusercontent.com/u/60765958?v=4", + "profile": "http://blakeslee.me", + "contributions": [ + "bug" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 2f62d2d0..3afc9a0d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-166-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-167-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -925,6 +925,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark 00schteven
00schteven

🤔 Wosten
Wosten

🐛 Zachary Priddy
Zachary Priddy

🤔 + Andrew Blakeslee Moore
Andrew Blakeslee Moore

🐛 From 42c7cd696f9a711baba5b35a0705cdc900d16198 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 17:07:53 +0200 Subject: [PATCH 1049/1077] Add focused manual-control and lifecycle regression tests (#1565) --- tests/test_switch.py | 183 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 182 insertions(+), 1 deletion(-) diff --git a/tests/test_switch.py b/tests/test_switch.py index 5c89df06..8b354c71 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -48,6 +48,7 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_SEPARATE_TURN_ON_COMMANDS, CONF_SKIP_REDUNDANT_COMMANDS, CONF_SLEEP_RGB_OR_COLOR_TEMP, + CONF_SLEEP_TRANSITION, CONF_SUNRISE_OFFSET, CONF_SUNRISE_TIME, CONF_SUNSET_TIME, @@ -113,7 +114,7 @@ from homeassistant.const import ( STATE_ON, EntityCategory, ) -from homeassistant.core import Context, Event, HomeAssistant, State +from homeassistant.core import Context, CoreState, Event, HomeAssistant, State from homeassistant.exceptions import Unauthorized from homeassistant.helpers import area_registry as ar from homeassistant.helpers import entity_registry @@ -896,6 +897,135 @@ async def test_manual_control( assert state_attrs["manual_control_color"] == [ENTITY_LIGHT_1] +async def test_sleep_mode_does_not_emit_manual_control_event(hass): + """Sleep adaptation is internal; only an external change emits manual control.""" + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_INTERCEPT: True, + CONF_SLEEP_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + events = [] + remove_listener = hass.bus.async_listen(f"{DOMAIN}.manual_control", events.append) + for service, brightness in [(SERVICE_TURN_ON, 3), (SERVICE_TURN_OFF, 128)]: + await hass.services.async_call( + SWITCH_DOMAIN, + service, + {ATTR_ENTITY_ID: switch.sleep_mode_switch.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == brightness + assert hass.states.get(switch.entity_id).attributes["manual_control"] == [] + assert events == [] + + # Positive control: the same live listener must see a real manual change. + context = Context() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_BRIGHTNESS: 77}, + context=context, + blocking=True, + ) + await hass.async_block_till_done() + remove_listener() + assert len(events) == 1 + assert events[0].context == context + assert events[0].data == { + ATTR_ENTITY_ID: ENTITY_LIGHT_1, + SWITCH_DOMAIN: switch.entity_id, + CONF_MANUAL_CONTROL: LightControlAttributes.BRIGHTNESS, + } + + +async def test_reload_cancels_old_manual_reset_and_keeps_service_tracking(hass): + """An unloaded timer must not re-adapt the replacement profile's light.""" + await setup_lights(hass) + entry, switch = await setup_switch( + hass, + { + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_AUTORESET_CONTROL: 60, + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_BRIGHTNESS: 77}, + blocking=True, + ) + await hass.async_block_till_done() + old_timer = switch.manager.auto_reset_manual_control_timers[ENTITY_LIGHT_1] + assert old_timer.is_running() + assert await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + assert not old_timer.is_running() + new_switch = hass.data[DOMAIN][entry.entry_id][SWITCH_DOMAIN] + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 128 + + events = [] + remove_listener = hass.bus.async_listen(f"{DOMAIN}.manual_control", events.append) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_BRIGHTNESS: 99}, + blocking=True, + ) + await hass.async_block_till_done() + remove_listener() + assert len(events) == 1 + assert events[0].data[SWITCH_DOMAIN] == new_switch.entity_id + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 99 + assert hass.states.get(new_switch.entity_id).attributes[ + "manual_control_brightness" + ] == [ENTITY_LIGHT_1] + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_manual_control_expiry_does_not_adapt_disabled_profile(hass): + """Expiry clears ownership without sending light commands for a disabled profile.""" + switch, _ = await setup_lights_and_switch( + hass, + {CONF_LIGHTS: [ENTITY_LIGHT_1], CONF_AUTORESET_CONTROL: 1}, + ) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: switch.entity_id}, + blocking=True, + ) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + {ATTR_ENTITY_ID: switch.entity_id, CONF_MANUAL_CONTROL: True}, + blocking=True, + ) + await hass.async_block_till_done() + assert switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + light_before = hass.states.get(ENTITY_LIGHT_1) + calls = [] + remove_listener = hass.bus.async_listen(EVENT_CALL_SERVICE, calls.append) + timer = switch.manager.auto_reset_manual_control_timers[ENTITY_LIGHT_1] + await timer.task + await hass.async_block_till_done() + remove_listener() + assert not timer.is_running() + assert hass.states.get(switch.entity_id).state == STATE_OFF + assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + assert hass.states.get(ENTITY_LIGHT_1) == light_before + assert not [event for event in calls if event.data["domain"] == LIGHT_DOMAIN] + + @pytest.mark.parametrize("reset_on_sleep", [None, True, False]) async def test_sleep_mode_manual_control_reset(hass, reset_on_sleep): """Keep the old default and preserve manual brightness only when opted out.""" @@ -3303,6 +3433,57 @@ async def test_expand_light_groups_waits_for_group_state(hass): assert _expand_light_groups(hass, [group]) == members +async def test_group_created_during_startup_tracks_manual_member_changes(hass): + """A profile loaded before its group must track members once HA starts.""" + hass.set_state(CoreState.not_running) + await setup_lights(hass) + _, switch = await setup_switch( + hass, + { + CONF_LIGHTS: ["light.light_group"], + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + group_entry = MockConfigEntry( + domain="group", + title="Light Group", + data={}, + options={"group_type": "light", "entities": [ENTITY_LIGHT_2, ENTITY_LIGHT_3]}, + ) + group_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(group_entry.entry_id) + await hass.async_start() + await hass.async_block_till_done() + + # Target the member directly. A still-unexpanded group would miss this call. + member = ENTITY_LIGHT_3 + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: member}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(member).attributes[ATTR_BRIGHTNESS] == 128 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: member, ATTR_BRIGHTNESS: 77}, + blocking=True, + ) + await hass.async_block_till_done() + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert hass.states.get(member).attributes[ATTR_BRIGHTNESS] == 77 + assert hass.states.get(switch.entity_id).attributes[ + "manual_control_brightness" + ] == [member] + + @pytest.mark.parametrize("proactive_service_call_adaptation", [True, False]) @pytest.mark.parametrize("take_over_control", [True, False]) @pytest.mark.parametrize("multi_light_intercept", [True, False]) From c19715dadd52ceabe5b8dcd7d3c47f5fc2040389 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 17:09:35 +0200 Subject: [PATCH 1050/1077] Publish line and branch coverage reports in CI (#1564) * Report line and branch coverage in CI * docs: explain coverage reports and behavioral tests * Calculate coverage percentages from counts for older versions --- .github/workflows/pytest.yaml | 45 +++++++++++++++++++++++++++++++++++ tests/README.md | 10 +++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 650ee02c..8a0ebf82 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -53,6 +53,7 @@ jobs: core-version: ${{ matrix.core-version }} - name: Run pytest + id: pytest timeout-minutes: 60 run: | export PYTHONPATH=${PYTHONPATH}:${PWD} @@ -64,7 +65,51 @@ jobs: --timeout=9 \ --durations=10 \ --cov=homeassistant.components.adaptive_lighting \ + --cov-branch \ + --cov-report=term-missing \ --cov-report=xml \ + --cov-report=json \ + --cov-report=html \ -o console_output_style=count \ -p no:sugar \ tests/components/adaptive_lighting + + - name: Write coverage summary + if: ${{ !cancelled() }} + env: + CORE_VERSION: ${{ matrix.core-version }} + PYTHON_VERSION: ${{ matrix.python-version }} + PYTEST_OUTCOME: ${{ steps.pytest.outcome }} + run: | + { + echo "### Coverage: Home Assistant ${CORE_VERSION}, Python ${PYTHON_VERSION}" + echo + if [[ -f core/coverage.json ]]; then + echo "| Metric | Executed | Total | Coverage |" + echo "| --- | ---: | ---: | ---: |" + jq -r ' + def percent(covered; total): + if total == 0 then 100 else (covered / total * 10000 | round) / 100 end; + .totals + | "| Lines | \(.covered_lines) | \(.num_statements) | \(percent(.covered_lines; .num_statements))% |\n" + + "| Branches | \(.covered_branches) | \(.num_branches) | \(percent(.covered_branches; .num_branches))% |" + ' core/coverage.json + else + echo "Coverage JSON was not generated. See the pytest step for details." + fi + } >> "${GITHUB_STEP_SUMMARY}" + + if [[ ! -f core/coverage.json && "${PYTEST_OUTCOME}" == "success" ]]; then + exit 1 + fi + + - name: Upload coverage reports + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7.0.1 + with: + name: coverage-${{ matrix.core-version }}-py${{ matrix.python-version }} + path: | + core/coverage.xml + core/coverage.json + core/htmlcov/ + if-no-files-found: warn diff --git a/tests/README.md b/tests/README.md index 3ccec21c..761c67f7 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,8 +1,16 @@ # Developer notes for the tests directory -To run the tests, check out the [CI configuration](../.github/workflows/pytest.yml) to see how they are executed in the CI pipeline. +To run the tests, check out the [CI configuration](../.github/workflows/pytest.yaml) to see how they are executed in the CI pipeline. Alternatively, you can use the provided Docker image to run the tests locally or run them with VS Code directly in the dev container. +## Coverage reports + +Open a `pytest` workflow run in GitHub Actions to see line and branch coverage in each job's summary. Download its `coverage--py` artifact for the XML and JSON reports and the browsable HTML report. After extracting it, open `htmlcov/index.html` to inspect missing lines and branches. + +Coverage measures executed code, not whether assertions would catch a bug. Add tests for observable behavior: emitted light commands, final states, manual-control events, and timer expiry. The integration suite runs inside Home Assistant with simulated lights; it does not establish physical-device behavior. It also does not execute every documentation generator included in the package's coverage total. + +The tests in `test_automation_examples.py` load YAML directly from `README.md` and execute it through Home Assistant's automation and script engines. Edit the README source when changing those examples, then run `./scripts/update-generated-content` to update the documentation pages. + ## Prerequisites Before running tests with Docker, you need a local Home Assistant core checkout with symlinks: From 472796215bc1dc48949a97393ee722c3149ceb73 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 17:18:42 +0200 Subject: [PATCH 1051/1077] Implement collapsible sections for options flow (#1313) * Implement collapsible sections for options flow Replace single-form options flow with collapsible sections: - Basic options always visible (9 fields) - Advanced options in collapsed section (collapsed by default) - Single form, no multi-step navigation needed Changes: - const.py: Add BASIC_OPTIONS set defining which options are basic - config_flow.py: Use section() from data_entry_flow to wrap advanced options - strings.json: Restructure with sections.advanced for section translations - tests: Update to handle nested section input format * Update README.md, strings.json, and services.yaml * Fix: Use vol.Required for section to render properly * Fix section structure: separate basic and advanced fields in strings.json * Remove accidentally added files * Add local directories to gitignore * Update README.md, strings.json, and services.yaml * Preserve options behavior with collapsible sections * Test serialized advanced options section * fix: preserve config metadata when retrying options * fix: keep options form defaults serializable --------- Co-authored-by: github-actions[bot] --- .github/update-strings.py | 61 +++++++- .../adaptive_lighting/config_flow.py | 45 ++++-- custom_components/adaptive_lighting/const.py | 13 ++ .../adaptive_lighting/strings.json | 114 +++++++------- .../adaptive_lighting/translations/af.json | 18 ++- .../adaptive_lighting/translations/bg.json | 106 +++++++------ .../adaptive_lighting/translations/ca.json | 74 ++++----- .../adaptive_lighting/translations/cs.json | 106 +++++++------ .../adaptive_lighting/translations/da.json | 92 ++++++------ .../adaptive_lighting/translations/de.json | 106 +++++++------ .../adaptive_lighting/translations/el.json | 10 +- .../adaptive_lighting/translations/en.json | 114 +++++++------- .../adaptive_lighting/translations/es.json | 82 +++++----- .../adaptive_lighting/translations/et.json | 38 +++-- .../adaptive_lighting/translations/fi.json | 78 +++++----- .../adaptive_lighting/translations/fr.json | 94 ++++++------ .../adaptive_lighting/translations/gl.json | 20 ++- .../adaptive_lighting/translations/hr.json | 18 ++- .../adaptive_lighting/translations/hu.json | 76 +++++----- .../adaptive_lighting/translations/id.json | 76 +++++----- .../adaptive_lighting/translations/it.json | 96 ++++++------ .../adaptive_lighting/translations/ja.json | 22 ++- .../adaptive_lighting/translations/ko.json | 106 +++++++------ .../adaptive_lighting/translations/nb.json | 94 ++++++------ .../adaptive_lighting/translations/nl.json | 108 +++++++------- .../adaptive_lighting/translations/pl.json | 92 ++++++------ .../adaptive_lighting/translations/pt-BR.json | 82 +++++----- .../adaptive_lighting/translations/pt.json | 28 ++-- .../adaptive_lighting/translations/ro.json | 23 ++- .../adaptive_lighting/translations/ru.json | 114 +++++++------- .../adaptive_lighting/translations/sk.json | 76 +++++----- .../adaptive_lighting/translations/sl.json | 72 +++++---- .../adaptive_lighting/translations/sv.json | 94 ++++++------ .../adaptive_lighting/translations/ta.json | 70 +++++---- .../adaptive_lighting/translations/tr.json | 78 +++++----- .../adaptive_lighting/translations/uk.json | 94 ++++++------ .../adaptive_lighting/translations/ur.json | 76 +++++----- .../translations/zh-Hans.json | 106 +++++++------ tests/test_config_flow.py | 140 ++++++++++++++++-- 39 files changed, 1694 insertions(+), 1218 deletions(-) diff --git a/.github/update-strings.py b/.github/update-strings.py index bfb329a0..0e6a4500 100644 --- a/.github/update-strings.py +++ b/.github/update-strings.py @@ -2,6 +2,7 @@ import json import sys +from copy import deepcopy from pathlib import Path import homeassistant.helpers.config_validation as cv @@ -14,9 +15,35 @@ from custom_components.adaptive_lighting import const folder = Path("custom_components") / "adaptive_lighting" strings_fname = folder / "strings.json" en_fname = folder / "translations" / "en.json" +translation_fnames = (folder / "translations").glob("*.json") with strings_fname.open() as f: strings = json.load(f) + +def _partition_options(values): + """Partition option translations into basic and advanced dictionaries.""" + basic = { + key: values[key] + for key, _, _ in const.VALIDATION_TUPLES + if key in const.BASIC_OPTIONS and key in values + } + advanced = { + key: values[key] + for key, _, _ in const.VALIDATION_TUPLES + if key not in const.BASIC_OPTIONS and key in values + } + return basic, advanced + + +def _migrate_translation_options(step): + """Move translated advanced options under the advanced section.""" + sections = step.setdefault("sections", {}) + advanced = sections.setdefault("advanced", {}) + for key in ("data", "data_description"): + values = {**advanced.get(key, {}), **step.get(key, {})} + step[key], advanced[key] = _partition_options(values) + + # Set "options" data = {} data_description = {} @@ -27,8 +54,19 @@ for k, _, typ in const.VALIDATION_TUPLES: data_description[k] = desc else: data[k] = f"{k}: {desc}" -strings["options"]["step"]["init"]["data"] = data -strings["options"]["step"]["init"]["data_description"] = data_description +basic_data, advanced_data = _partition_options(data) +basic_descriptions, advanced_descriptions = _partition_options(data_description) +options_step = strings["options"]["step"]["init"] +options_step["data"] = basic_data +options_step["data_description"] = basic_descriptions +options_step["sections"] = { + "advanced": { + "name": "Advanced settings", + "description": "Additional settings for fine-tuning Adaptive Lighting.", + "data": advanced_data, + "data_description": advanced_descriptions, + }, +} # Set "services" services_filename = Path("custom_components") / "adaptive_lighting" / "services.yaml" @@ -58,13 +96,22 @@ with en_fname.open() as f: en = json.load(f) en["config"]["step"]["user"] = strings["config"]["step"]["user"] -en["options"]["step"]["init"]["data"] = data -en["options"]["step"]["init"]["data_description"] = data_description -en["options"]["step"]["init"]["description"] = strings["options"]["step"]["init"][ - "description" -] +en["options"]["step"]["init"] = deepcopy(options_step) en["services"] = services_json with en_fname.open("w") as f: json.dump(en, f, indent=2, ensure_ascii=False) f.write("\n") + +# Keep translated labels and descriptions when moving advanced options into a section. +for translation_fname in translation_fnames: + if translation_fname == en_fname: + continue + with translation_fname.open() as f: + translation = json.load(f) + if "options" not in translation: + continue + _migrate_translation_options(translation["options"]["step"]["init"]) + with translation_fname.open("w") as f: + json.dump(translation, f, indent=2, ensure_ascii=False) + f.write("\n") diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index eb7b4a0a..3ffe5400 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -4,12 +4,13 @@ import logging from typing import Any import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.const import CONF_NAME from homeassistant.core import callback from homeassistant.helpers.selector import EntitySelector, EntitySelectorConfig from .const import ( # pylint: disable=unused-import + BASIC_OPTIONS, CONF_LIGHTS, DOMAIN, EXTRA_VALIDATION, @@ -24,6 +25,7 @@ OPTIONS_FLOW_DESCRIPTION_PLACEHOLDERS = { "webapp_url": "https://basnijholt.github.io/adaptive-lighting", "docs_url": "https://github.com/basnijholt/adaptive-lighting#readme", } +ADVANCED_OPTIONS_SECTION = "advanced" class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -127,10 +129,21 @@ def validate_options(user_input: dict[str, Any], errors: dict[str, str]) -> None class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for Adaptive Lighting.""" + def _flatten_section_input(self, user_input: dict[str, Any]) -> dict[str, Any]: + """Flatten section input by merging nested 'advanced' dict into top level.""" + flat_input: dict[str, Any] = {} + for key, value in user_input.items(): + if key == ADVANCED_OPTIONS_SECTION and isinstance(value, dict): + flat_input.update(value) + else: + flat_input[key] = value + return flat_input + async def async_step_init(self, user_input: dict[str, Any] | None = None): - """Handle options flow.""" + """Handle options flow with collapsible sections.""" conf = self.config_entry data = validate(conf) + form_data = {**conf.data, **conf.options} if conf.source == config_entries.SOURCE_IMPORT: return self.async_show_form( step_id="init", @@ -139,15 +152,18 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ) errors: dict[str, str] = {} if user_input is not None: - validate_options(user_input, errors) + flat_input = self._flatten_section_input(user_input) + validate_options(flat_input, errors) if not errors: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(title="", data=flat_input) + data.update(flat_input) + form_data.update(flat_input) # Validate that all configured lights still exist all_lights = set(self.hass.states.async_entity_ids("light")) for configured_light in data[CONF_LIGHTS]: if configured_light not in all_lights: - errors = {CONF_LIGHTS: "entity_missing"} + errors[CONF_LIGHTS] = "entity_missing" _LOGGER.error( "%s: light entity %s is configured, but was not found", data[CONF_NAME], @@ -163,15 +179,24 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ), } - options_schema = {} + basic_schema: dict[vol.Marker, Any] = {} + advanced_schema: dict[vol.Marker, Any] = {} for name, default, validation in VALIDATION_TUPLES: - key = vol.Optional(name, default=conf.options.get(name, default)) - value = to_replace.get(name, validation) - options_schema[key] = value + key = vol.Optional(name, default=form_data.get(name, default)) + schema = basic_schema if name in BASIC_OPTIONS else advanced_schema + schema[key] = to_replace.get(name, validation) + + full_schema = { + **basic_schema, + vol.Required(ADVANCED_OPTIONS_SECTION): data_entry_flow.section( + vol.Schema(advanced_schema), + data_entry_flow.SectionConfig(collapsed=True), + ), + } return self.async_show_form( step_id="init", - data_schema=vol.Schema(options_schema), + data_schema=vol.Schema(full_schema), errors=errors, description_placeholders=OPTIONS_FLOW_DESCRIPTION_PLACEHOLDERS, ) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 4e258587..58b37734 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -323,6 +323,19 @@ DOCS_APPLY = { CONF_LIGHTS: "A light (or list of lights) to apply the settings to. 💡", } +# Basic options shown at top level in options flow (not in collapsed section) +BASIC_OPTIONS: set[str] = { + CONF_LIGHTS, + CONF_MIN_BRIGHTNESS, + CONF_MAX_BRIGHTNESS, + CONF_MIN_COLOR_TEMP, + CONF_MAX_COLOR_TEMP, + CONF_SLEEP_BRIGHTNESS, + CONF_SLEEP_COLOR_TEMP, + CONF_TRANSITION, + CONF_INTERVAL, +} + def int_between(min_int: int, max_int: int) -> vol.All: """Return an integer between 'min_int' and 'max_int'.""" diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index cf47241e..ff576a2b 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -29,68 +29,78 @@ "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟", "interval": "interval", "transition": "transition", - "initial_transition": "initial_transition", "min_brightness": "min_brightness: Minimum brightness percentage. 💡", "max_brightness": "max_brightness: Maximum brightness percentage. 💡", "min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥", "max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️", - "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", "sleep_brightness": "sleep_brightness", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp", - "sleep_color_temp": "sleep_color_temp", - "sleep_rgb_color": "sleep_rgb_color", - "sleep_transition": "sleep_transition", - "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", - "sunrise_time": "sunrise_time", - "min_sunrise_time": "min_sunrise_time", - "max_sunrise_time": "max_sunrise_time", - "sunrise_offset": "sunrise_offset", - "sunset_time": "sunset_time", - "min_sunset_time": "min_sunset_time", - "max_sunset_time": "max_sunset_time", - "sunset_offset": "sunset_offset", - "brightness_mode": "brightness_mode", - "brightness_mode_time_dark": "brightness_mode_time_dark", - "brightness_mode_time_light": "brightness_mode_time_light", - "take_over_control": "take_over_control: Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒", - "take_over_control_mode": "take_over_control_mode", - "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", - "autoreset_control_seconds": "autoreset_control_seconds", - "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", - "reset_manual_control_on_sleep_mode_change": "reset_manual_control_on_sleep_mode_change: Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴", - "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", - "send_split_delay": "send_split_delay", - "adapt_delay": "adapt_delay", - "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.", - "intercept": "intercept: Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness.", - "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled.", - "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝" + "sleep_color_temp": "sleep_color_temp" }, "data_description": { "interval": "Frequency to adapt the lights, in seconds. 🔄", "transition": "Duration of transition when lights change, in seconds. 🕑", - "initial_transition": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", "sleep_brightness": "Brightness percentage of lights in sleep mode. 😴", - "sleep_rgb_or_color_temp": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", - "sleep_color_temp": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", - "sleep_rgb_color": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", - "sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", - "sunrise_time": "Set a fixed time (HH:MM:SS) for sunrise. 🌅", - "min_sunrise_time": "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅", - "max_sunrise_time": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", - "sunrise_offset": "Adjust sunrise time with a positive or negative offset in seconds. ⏰", - "sunset_time": "Set a fixed time (HH:MM:SS) for sunset. 🌇", - "min_sunset_time": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", - "max_sunset_time": "Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇", - "sunset_offset": "Adjust sunset time with a positive or negative offset in seconds. ⏰", - "brightness_mode": "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", - "brightness_mode_time_dark": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", - "brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.", - "take_over_control_mode": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.", - "autoreset_control_seconds": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", - "send_split_delay": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", - "adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️" + "sleep_color_temp": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴" + }, + "sections": { + "advanced": { + "name": "Advanced settings", + "description": "Additional settings for fine-tuning Adaptive Lighting.", + "data": { + "initial_transition": "initial_transition", + "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp", + "sleep_rgb_color": "sleep_rgb_color", + "sleep_transition": "sleep_transition", + "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", + "sunrise_time": "sunrise_time", + "min_sunrise_time": "min_sunrise_time", + "max_sunrise_time": "max_sunrise_time", + "sunrise_offset": "sunrise_offset", + "sunset_time": "sunset_time", + "min_sunset_time": "min_sunset_time", + "max_sunset_time": "max_sunset_time", + "sunset_offset": "sunset_offset", + "brightness_mode": "brightness_mode", + "brightness_mode_time_dark": "brightness_mode_time_dark", + "brightness_mode_time_light": "brightness_mode_time_light", + "take_over_control": "take_over_control: Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒", + "take_over_control_mode": "take_over_control_mode", + "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", + "autoreset_control_seconds": "autoreset_control_seconds", + "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", + "reset_manual_control_on_sleep_mode_change": "reset_manual_control_on_sleep_mode_change: Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴", + "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", + "send_split_delay": "send_split_delay", + "adapt_delay": "adapt_delay", + "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.", + "intercept": "intercept: Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness.", + "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled.", + "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝" + }, + "data_description": { + "initial_transition": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "sleep_rgb_or_color_temp": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", + "sleep_rgb_color": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "sunrise_time": "Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "min_sunrise_time": "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅", + "max_sunrise_time": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", + "sunrise_offset": "Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "sunset_time": "Set a fixed time (HH:MM:SS) for sunset. 🌇", + "min_sunset_time": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", + "max_sunset_time": "Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇", + "sunset_offset": "Adjust sunset time with a positive or negative offset in seconds. ⏰", + "brightness_mode": "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", + "brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.", + "take_over_control_mode": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.", + "autoreset_control_seconds": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "send_split_delay": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", + "adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️" + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/af.json b/custom_components/adaptive_lighting/translations/af.json index 4a0cd87a..84f5c9a3 100644 --- a/custom_components/adaptive_lighting/translations/af.json +++ b/custom_components/adaptive_lighting/translations/af.json @@ -26,12 +26,18 @@ "step": { "init": { "title": "Aanpasbare beligting opsies", - "data": { - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Wanneer ligte aanvanklik aangeskakel word. As dit op \"true\" gestel is, pas AL slegs aan as \"light.turn_on\" opgeroep word sonder om kleur of helderheid te spesifiseer. ❌🌈 Dit verhoed bv. aanpassing wanneer 'n toneel geaktiveer word. As `onwaar`, pas AL aan ongeag die teenwoordigheid van kleur of helderheid in die aanvanklike `diens_data`. Moet `oorname_beheer` geaktiveer moet word. 🕵️ " - }, - "data_description": { - "sunrise_offset": "Pas sonsopkomstyd aan met 'n positiewe of negatiewe afwyking in sekondes. ⏰", - "sunset_offset": "Pas sonsondergangtyd aan met 'n positiewe of negatiewe afwyking in sekondes. ⏰" + "data": {}, + "data_description": {}, + "sections": { + "advanced": { + "data": { + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Wanneer ligte aanvanklik aangeskakel word. As dit op \"true\" gestel is, pas AL slegs aan as \"light.turn_on\" opgeroep word sonder om kleur of helderheid te spesifiseer. ❌🌈 Dit verhoed bv. aanpassing wanneer 'n toneel geaktiveer word. As `onwaar`, pas AL aan ongeag die teenwoordigheid van kleur of helderheid in die aanvanklike `diens_data`. Moet `oorname_beheer` geaktiveer moet word. 🕵️ " + }, + "data_description": { + "sunrise_offset": "Pas sonsopkomstyd aan met 'n positiewe of negatiewe afwyking in sekondes. ⏰", + "sunset_offset": "Pas sonsondergangtyd aan met 'n positiewe of negatiewe afwyking in sekondes. ⏰" + } + } } } } diff --git a/custom_components/adaptive_lighting/translations/bg.json b/custom_components/adaptive_lighting/translations/bg.json index 9ef6690d..a4e62001 100644 --- a/custom_components/adaptive_lighting/translations/bg.json +++ b/custom_components/adaptive_lighting/translations/bg.json @@ -30,65 +30,73 @@ "lights": "lights: Списък от entity_ids на лампи за контрол (може да е празен). 🌟", "interval": "интервал", "transition": "преход", - "initial_transition": "начален преход", "min_brightness": "min_brightness: Минимален процент на яркост. 💡", "max_brightness": "max_brightness: Максимален процент на яркост. 💡", "min_color_temp": "min_color_temp: Най-топла цветова температура в Келвини. 🔥", "max_color_temp": "max_color_temp: Най-студена цветова температура в Келвини. ❄️", - "prefer_rgb_color": "prefer_rgb_color: Дали да се предпочита RGB цветова корекция пред температура на светлината, когато е възможно. 🌈", "sleep_brightness": "яркост при сън", - "sleep_rgb_or_color_temp": "RGB или цветова температура при сън", - "sleep_color_temp": "цветова температура при сън", - "sleep_rgb_color": "RGB цвят при сън", - "sleep_transition": "преход при сън", - "transition_until_sleep": "transition_until_sleep: Когато е активирано, Adaptive Lighting ще третира настройките за сън като минимум, преминавайки към тези стойности след залез. 🌙", - "sunrise_time": "време на изгрев", - "min_sunrise_time": "минимално време на изгрев", - "max_sunrise_time": "максимално време на изгрев", - "sunrise_offset": "отместване на изгрева", - "sunset_time": "време на залез", - "min_sunset_time": "минимално време на залез", - "max_sunset_time": "максимално време на залез", - "sunset_offset": "отместване на залеза", - "brightness_mode": "режим на яркост", - "brightness_mode_time_dark": "време на режим на яркост при тъмно", - "brightness_mode_time_light": "време на режим на яркост при светло", - "take_over_control": "take_over_control: Деактивира Adaptive Lighting, ако друг източник извика \"light.turn_on\", докато лампите са включени и се адаптират. Имайте предвид, че това извиква \"homeassistant.update_entity\" на всеки \"interval\"! 🔒", - "detect_non_ha_changes": "detect_non_ha_changes: Открива и спира адаптации за промени в състоянието, които не са \"light.turn_on\". Изисква \"take_over_control\" активиран. 🕵️ Внимание: ⚠️ Някои лампиможе лъжливо да указват 'включено' състояние, което може да доведе до неочаквано включване на лампите. Деактивирайте тази функция, ако се сблъскате с такива проблеми.", - "autoreset_control_seconds": "секунди за автоматично нулиране на контрола", - "only_once": "only_once: Адаптира лампите само когато са включени (\"true\") или продължава да ги адаптира (\"false\"). 🔄", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: При първоначално включване на лампите. Ако е зададено на \"true\", Адаптивно Осветление се адаптира само ако е извикано \"light.turn_on\" без указване на цвят или яркост. ❌🌈 Това например предотвратява адаптация при активиране на сцена. Ако е \"false\", Адаптивно Осветление се адаптира независимо от наличието на цвят или яркост в първоначалните \"service_data\". Изисква \"take_over_control\" активиран. 🕵️ ", - "separate_turn_on_commands": "separate_turn_on_commands: Използва отделни \"light.turn_on\" команди за цвят и яркост, необходими за някои типове светлини. 🔀", - "send_split_delay": "забавяне при изпращане на разделени", - "adapt_delay": "забавяне при адаптация", - "skip_redundant_commands": "skip_redundant_commands: Пропуска изпращането на команди за адаптация, чиято целева състояние вече е равно на известното състояние на светлината. Минимизира мрежовия трафик и подобрява отговорността на адаптацията в някои ситуации. 📉Деактивирайте, ако физическите състояния на лампите се разминават с записаното състояние на HA.", - "intercept": "intercept: Прихваща и адаптира \"light.turn_on\" повиквания, позволявайки моментална адаптация на цвета и яркостта. 🏎️ Деактивирайте за светлини, които не поддържат \"light.turn_on\" с цвят и яркост.", - "multi_light_intercept": "multi_light_intercept: Прихваща и адаптира \"light.turn_on\" повиквания, които целят множество светлини. ➗⚠️ Това може да доведе до разделяне на едно \"light.turn_on\" повикване на множество повиквания, например когато лампите са в различни превключватели. Изисква \"intercept\" да бъде активиран.", - "include_config_in_attributes": "include_config_in_attributes: Показва всички опции като атрибути на ключа в Home Assistant, когато е зададено на \"true\". 📝" + "sleep_color_temp": "цветова температура при сън" }, "data_description": { "interval": "Честота за адаптиране на лампите, в секунди. 🔄", "transition": "Продължителност на прехода, когато лампите се променят, в секунди. 🕑", - "initial_transition": "Продължителност на първия преход, когато лампите преминават от \"off\" на \"on\" в секунди. ⏲️", "sleep_brightness": "Процент на яркостта на лампите в режим на сън. 😴", - "sleep_rgb_or_color_temp": "Използвайте или \"\"rgb_color\"\" или \"\"color_temp\"\" в режим на сън. 🌙", - "sleep_color_temp": "Цветова температура в режим на сън (използва се, когато \"sleep_rgb_or_color_temp\" е \"color_temp\") в Келвин. 😴", - "sleep_rgb_color": "RGB цвят в режим на сън (използва се, когато \"sleep_rgb_or_color_temp\" е \"rgb_color\"). 🌈", - "sleep_transition": "Продължителност на прехода, когато се превключва \"режим на сън\" в секунди. 😴", - "sunrise_time": "Задайте фиксирано време (HH:MM:SS) за изгрев. 🌅", - "min_sunrise_time": "Задайте най-ранното виртуално време за изгрев (HH:MM:SS), позволяващо по-късни изгреви. 🌅", - "max_sunrise_time": "Задайте най-късното виртуално време за изгрев (HH:MM:SS), позволяващо по-ранни изгреви. 🌅", - "sunrise_offset": "Регулирайте времето на изгрев с положителен или отрицателен отместване в секунди. ⏰", - "sunset_time": "Задайте фиксирано време (HH:MM:SS) за залез. 🌇", - "min_sunset_time": "Задайте най-ранното виртуално време за залез (HH:MM:SS), позволяващо по-късни залези. 🌇", - "max_sunset_time": "Задайте най-късното виртуално време за залез (HH:MM:SS), позволяващо по-ранни залези. 🌇", - "sunset_offset": "Регулирайте времето на залез с положителен или отрицателен отместване в секунди. ⏰", - "brightness_mode": "Режим на яркост за използване. Възможни стойности са \"default\", \"linear\" и \"tanh\" (използва \"brightness_mode_time_dark\" и \"brightness_mode_time_light\"). 📈", - "brightness_mode_time_dark": "(Игнорира се, ако \"brightness_mode='default'\") Продължителност в секунди за увеличаване/намаляване на яркостта преди/след изгрев/залез. 📈📉", - "brightness_mode_time_light": "(Игнорира се, ако \"brightness_mode='default'\") Продължителност в секунди за увеличаване/намаляване на яркостта след/преди изгрев/залез. 📈📉.", - "autoreset_control_seconds": "Автоматично нулиране на ръчния контрол след определен брой секунди. Задайте на 0 за деактивиране. ⏲️", - "send_split_delay": "Забавяне (ms) между \"separate_turn_on_commands\" за светлини, които не поддържат едновременна настройка на яркост и цвят. ⏲️", - "adapt_delay": "Време за изчакване (секунди) между включване на светлината и прилагане на промени от Адаптивно Осветление. Може да помогне за избягване на трептене. ⏲️" + "sleep_color_temp": "Цветова температура в режим на сън (използва се, когато \"sleep_rgb_or_color_temp\" е \"color_temp\") в Келвин. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "начален преход", + "prefer_rgb_color": "prefer_rgb_color: Дали да се предпочита RGB цветова корекция пред температура на светлината, когато е възможно. 🌈", + "sleep_rgb_or_color_temp": "RGB или цветова температура при сън", + "sleep_rgb_color": "RGB цвят при сън", + "sleep_transition": "преход при сън", + "transition_until_sleep": "transition_until_sleep: Когато е активирано, Adaptive Lighting ще третира настройките за сън като минимум, преминавайки към тези стойности след залез. 🌙", + "sunrise_time": "време на изгрев", + "min_sunrise_time": "минимално време на изгрев", + "max_sunrise_time": "максимално време на изгрев", + "sunrise_offset": "отместване на изгрева", + "sunset_time": "време на залез", + "min_sunset_time": "минимално време на залез", + "max_sunset_time": "максимално време на залез", + "sunset_offset": "отместване на залеза", + "brightness_mode": "режим на яркост", + "brightness_mode_time_dark": "време на режим на яркост при тъмно", + "brightness_mode_time_light": "време на режим на яркост при светло", + "take_over_control": "take_over_control: Деактивира Adaptive Lighting, ако друг източник извика \"light.turn_on\", докато лампите са включени и се адаптират. Имайте предвид, че това извиква \"homeassistant.update_entity\" на всеки \"interval\"! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Открива и спира адаптации за промени в състоянието, които не са \"light.turn_on\". Изисква \"take_over_control\" активиран. 🕵️ Внимание: ⚠️ Някои лампиможе лъжливо да указват 'включено' състояние, което може да доведе до неочаквано включване на лампите. Деактивирайте тази функция, ако се сблъскате с такива проблеми.", + "autoreset_control_seconds": "секунди за автоматично нулиране на контрола", + "only_once": "only_once: Адаптира лампите само когато са включени (\"true\") или продължава да ги адаптира (\"false\"). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: При първоначално включване на лампите. Ако е зададено на \"true\", Адаптивно Осветление се адаптира само ако е извикано \"light.turn_on\" без указване на цвят или яркост. ❌🌈 Това например предотвратява адаптация при активиране на сцена. Ако е \"false\", Адаптивно Осветление се адаптира независимо от наличието на цвят или яркост в първоначалните \"service_data\". Изисква \"take_over_control\" активиран. 🕵️ ", + "separate_turn_on_commands": "separate_turn_on_commands: Използва отделни \"light.turn_on\" команди за цвят и яркост, необходими за някои типове светлини. 🔀", + "send_split_delay": "забавяне при изпращане на разделени", + "adapt_delay": "забавяне при адаптация", + "skip_redundant_commands": "skip_redundant_commands: Пропуска изпращането на команди за адаптация, чиято целева състояние вече е равно на известното състояние на светлината. Минимизира мрежовия трафик и подобрява отговорността на адаптацията в някои ситуации. 📉Деактивирайте, ако физическите състояния на лампите се разминават с записаното състояние на HA.", + "intercept": "intercept: Прихваща и адаптира \"light.turn_on\" повиквания, позволявайки моментална адаптация на цвета и яркостта. 🏎️ Деактивирайте за светлини, които не поддържат \"light.turn_on\" с цвят и яркост.", + "multi_light_intercept": "multi_light_intercept: Прихваща и адаптира \"light.turn_on\" повиквания, които целят множество светлини. ➗⚠️ Това може да доведе до разделяне на едно \"light.turn_on\" повикване на множество повиквания, например когато лампите са в различни превключватели. Изисква \"intercept\" да бъде активиран.", + "include_config_in_attributes": "include_config_in_attributes: Показва всички опции като атрибути на ключа в Home Assistant, когато е зададено на \"true\". 📝" + }, + "data_description": { + "initial_transition": "Продължителност на първия преход, когато лампите преминават от \"off\" на \"on\" в секунди. ⏲️", + "sleep_rgb_or_color_temp": "Използвайте или \"\"rgb_color\"\" или \"\"color_temp\"\" в режим на сън. 🌙", + "sleep_rgb_color": "RGB цвят в режим на сън (използва се, когато \"sleep_rgb_or_color_temp\" е \"rgb_color\"). 🌈", + "sleep_transition": "Продължителност на прехода, когато се превключва \"режим на сън\" в секунди. 😴", + "sunrise_time": "Задайте фиксирано време (HH:MM:SS) за изгрев. 🌅", + "min_sunrise_time": "Задайте най-ранното виртуално време за изгрев (HH:MM:SS), позволяващо по-късни изгреви. 🌅", + "max_sunrise_time": "Задайте най-късното виртуално време за изгрев (HH:MM:SS), позволяващо по-ранни изгреви. 🌅", + "sunrise_offset": "Регулирайте времето на изгрев с положителен или отрицателен отместване в секунди. ⏰", + "sunset_time": "Задайте фиксирано време (HH:MM:SS) за залез. 🌇", + "min_sunset_time": "Задайте най-ранното виртуално време за залез (HH:MM:SS), позволяващо по-късни залези. 🌇", + "max_sunset_time": "Задайте най-късното виртуално време за залез (HH:MM:SS), позволяващо по-ранни залези. 🌇", + "sunset_offset": "Регулирайте времето на залез с положителен или отрицателен отместване в секунди. ⏰", + "brightness_mode": "Режим на яркост за използване. Възможни стойности са \"default\", \"linear\" и \"tanh\" (използва \"brightness_mode_time_dark\" и \"brightness_mode_time_light\"). 📈", + "brightness_mode_time_dark": "(Игнорира се, ако \"brightness_mode='default'\") Продължителност в секунди за увеличаване/намаляване на яркостта преди/след изгрев/залез. 📈📉", + "brightness_mode_time_light": "(Игнорира се, ако \"brightness_mode='default'\") Продължителност в секунди за увеличаване/намаляване на яркостта след/преди изгрев/залез. 📈📉.", + "autoreset_control_seconds": "Автоматично нулиране на ръчния контрол след определен брой секунди. Задайте на 0 за деактивиране. ⏲️", + "send_split_delay": "Забавяне (ms) между \"separate_turn_on_commands\" за светлини, които не поддържат едновременна настройка на яркост и цвят. ⏲️", + "adapt_delay": "Време за изчакване (секунди) между включване на светлината и прилагане на промени от Адаптивно Осветление. Може да помогне за избягване на трептене. ⏲️" + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/ca.json b/custom_components/adaptive_lighting/translations/ca.json index 4291d8c2..9042b60e 100644 --- a/custom_components/adaptive_lighting/translations/ca.json +++ b/custom_components/adaptive_lighting/translations/ca.json @@ -4,49 +4,57 @@ "step": { "init": { "data_description": { - "initial_transition": "Durada de la primera transició quan els llums canvien de `off` a `on` en segons. ⏲️", - "sunset_offset": "Ajusta l'hora de la posta del sol amb una compensació positiva o negativa en segons. ⏰", - "send_split_delay": "Retard (ms) entre `separate_turn_on_commands` per als llums que no admeten la configuració simultània de brillantor i color. ⏲️", - "sunrise_offset": "Ajusta l'hora de sortida del sol amb una compensació positiva o negativa en segons. ⏰", - "autoreset_control_seconds": "Restableix automàticament el control manual al cap d'uns segons. Posar a 0 per desactivar. ⏲️", - "brightness_mode": "Mode de brillantor a utilitzar. Els valors possibles són `default`, \"linear\" i \"tanh\" (utilitza `brightness_mode_time_dark` i `brightness_mode_time_light`). 📈", - "sleep_color_temp": "Temperatura de color en mode nocturn (s'utilitza quan `sleep_rgb_or_color_temp` és `color_temp`) en Kelvin. 😴", - "sleep_brightness": "Percentatge de brillantor dels llums en mode nocturn. 😴", "interval": "Freqüència d'adaptació de les llums, en segons. 🔄", - "sleep_transition": "Durada de la transició en commutar el \"mode nocturn\", en segons. 🕑", - "sleep_rgb_color": "Color RGB en mode nocturn (s'utilitza quan `sleep_rgb_or_color_temp` és \"rgb_color\"). 🌈", "transition": "Durada de la transició en canviar les llums, en segons. 🕑", - "sunrise_time": "Indica una hora fixa (HH:MM:SS) per a la sortida del sol. 🌅", - "sleep_rgb_or_color_temp": "Utilitza `\"rgb_color\"` o `\"color_temp\"` durant el mode nocturn. 🌙", - "sunset_time": "Indica una hora fixa (HH:MM:SS) per a la posta de sol. 🌇", - "brightness_mode_time_dark": "(S'ignora si `brightness_mode='default'`) La durada en segons de la variació de la brillantor abans/despres de la sortida/posta de sol. 📈📉.", - "brightness_mode_time_light": "(S'ignora si `brightness_mode='default'`) La durada en segons de la variació de la brillantor abans/despres de la sortida/posta de sol. 📈📉.", - "adapt_delay": "Temps d'espera (en segons) entre l'encesa de la llum i els canvis per part d'Adaptive Lighting. Pot ajudar a evitar els parpalleigs. ⏲️", - "min_sunrise_time": "Defineix la sortida de sol virtual més primerenca (HH:MM:SS), tot permetent sortides de sol posteriors. 🌅", - "max_sunrise_time": "Defineix la sortida de sol virtual més tardana (HH:MM:SS), tot permetent sortides de sol abans. 🌅", - "max_sunset_time": "Defineix la sortida virtual de sol virtual més tardana (HH:MM:SS), tot permetent sortides de sol abans. 🌇", - "min_sunset_time": "Defineix la posta de sol virtual més primerenca (HH:MM:SS), tot permetent postes de sol més tard. 🌇" + "sleep_brightness": "Percentatge de brillantor dels llums en mode nocturn. 😴", + "sleep_color_temp": "Temperatura de color en mode nocturn (s'utilitza quan `sleep_rgb_or_color_temp` és `color_temp`) en Kelvin. 😴" }, "title": "Opcions Il·luminació Adaptativa", "data": { - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quan s'encenen les llums inicialment. Si el valor és `true`, AL adapta només si s'ha cridat `light.turn_on` sense especificar color o brillantor. ❌🌈 Això impedeix l'adaptació quan s'activa una escena. Si el valor és `false`, AL adapta independentment de la presencia de color o brillantor en les dades inicials `service_data`. Necessita `take_over_control` habilitat. 🕵️", - "detect_non_ha_changes": "detect_non_ha_changes: Detecta i atura les adaptacions per als canvis d'estat diferents a `light.turn_on`. Necessita que `take_over_control` estigui habilitat. 🕵️ Precaució: ⚠️ Alguns llums poden indicar falsament un estat \"encès\", cosa que podria provocar que els llums s'encenguin inesperadament. Desactiva aquesta funció si trobes aquests problemes.", "lights": "lights: Llista d'entity_ids dels llums a controlar (pot estar buida). 🌟", "min_brightness": "min_brightness: Percentatge mínim de brillantor. 💡", "max_brightness": "max_brightness: Percentatge màxim de brillantor. 💡", "min_color_temp": "min_color_temp: Temperatura de color més càlida en graus Kelvin. 🔥", - "max_color_temp": "max_color_temp: Temperatura de color més freda en graus Kelvin. ❄️", - "prefer_rgb_color": "prefer_rgb_color: Si prefereixes l'ajustament del color RGB en lloc de la temperatura de color, quan sigui possible. 🌈", - "take_over_control": "take_over_control: Inhabilita Adaptive Lighting si una altra font crida `light.turn_on` quan les llums estan enceses i en procés d'adaptació. Tingues present que això cridarà `homeassistant.update_entity` cada `interval`! 🔒", - "only_once": "only_once: Adapta els llums només quan s'encenen (`true`) o segueix adaptant-les (`false`). 🔄", - "separate_turn_on_commands": "separate_turn_on_commands: Separa les crides de `light.turn_on` per a color i brillantor; necessari per alguns tipus de llums. 🔀", - "include_config_in_attributes": "include_config_in_attributes: Mostra totes les opcions com atributs a l'interruptor de Home Assistant quan s'estableix com a `true`. 📝", - "multi_light_intercept": "multi_light_intercept: Intercepta i adapta les crides `light.turn_on` dirigides a múltiples llums. ➗⚠️ Pot provocar la divisió d'una crida única `light.turn_on` en múltiples crides, com ara, quan les llums són en interruptors diferents. Necessita que `intercept` estigui habilitat.", - "transition_until_sleep": "transition_until_sleep: Si s'activa, Adaptive Lighting considerarà els ajustaments del mode nocturn com a mínims, fent una transició cap aquests valors després de la posta de sol. 🌙", - "intercept": "intercept: Intercepta i adapta les crides `light.turn_on` per permetre canvis instantanis de color i brillantor. 🏎️ Inhabilita-ho per a llums que no admeten `light.turn_on` amb color i brillantor.", - "skip_redundant_commands": "skip_redundant_commands: Evita l'enviament de d'ordres d'adaptació als objectius on el seu estat ja és el conegut del llum. Minimitza el trànsit de la xarxa i millora la resposta de l'adaptació en alguns casos. 📉 Inhabilita-ho si l'estat físic del llum queda desincronitzat amb l'estat registrat a Home Assistant." + "max_color_temp": "max_color_temp: Temperatura de color més freda en graus Kelvin. ❄️" }, - "description": "Configura un component d'Adaptive Lighting. Els noms de les opcions s'alineen amb la configuració de YAML. Si has definit aquesta entrada a YAML, aquí no apareixerà cap opció. Per veure gràfics interactius que demostren efectes de paràmetres, visita [aquesta aplicació web]({webapp_url}). Per a més detalls, pots veure la [documentació oficial]({docs_url})." + "description": "Configura un component d'Adaptive Lighting. Els noms de les opcions s'alineen amb la configuració de YAML. Si has definit aquesta entrada a YAML, aquí no apareixerà cap opció. Per veure gràfics interactius que demostren efectes de paràmetres, visita [aquesta aplicació web]({webapp_url}). Per a més detalls, pots veure la [documentació oficial]({docs_url}).", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: Si prefereixes l'ajustament del color RGB en lloc de la temperatura de color, quan sigui possible. 🌈", + "transition_until_sleep": "transition_until_sleep: Si s'activa, Adaptive Lighting considerarà els ajustaments del mode nocturn com a mínims, fent una transició cap aquests valors després de la posta de sol. 🌙", + "take_over_control": "take_over_control: Inhabilita Adaptive Lighting si una altra font crida `light.turn_on` quan les llums estan enceses i en procés d'adaptació. Tingues present que això cridarà `homeassistant.update_entity` cada `interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Detecta i atura les adaptacions per als canvis d'estat diferents a `light.turn_on`. Necessita que `take_over_control` estigui habilitat. 🕵️ Precaució: ⚠️ Alguns llums poden indicar falsament un estat \"encès\", cosa que podria provocar que els llums s'encenguin inesperadament. Desactiva aquesta funció si trobes aquests problemes.", + "only_once": "only_once: Adapta els llums només quan s'encenen (`true`) o segueix adaptant-les (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quan s'encenen les llums inicialment. Si el valor és `true`, AL adapta només si s'ha cridat `light.turn_on` sense especificar color o brillantor. ❌🌈 Això impedeix l'adaptació quan s'activa una escena. Si el valor és `false`, AL adapta independentment de la presencia de color o brillantor en les dades inicials `service_data`. Necessita `take_over_control` habilitat. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Separa les crides de `light.turn_on` per a color i brillantor; necessari per alguns tipus de llums. 🔀", + "skip_redundant_commands": "skip_redundant_commands: Evita l'enviament de d'ordres d'adaptació als objectius on el seu estat ja és el conegut del llum. Minimitza el trànsit de la xarxa i millora la resposta de l'adaptació en alguns casos. 📉 Inhabilita-ho si l'estat físic del llum queda desincronitzat amb l'estat registrat a Home Assistant.", + "intercept": "intercept: Intercepta i adapta les crides `light.turn_on` per permetre canvis instantanis de color i brillantor. 🏎️ Inhabilita-ho per a llums que no admeten `light.turn_on` amb color i brillantor.", + "multi_light_intercept": "multi_light_intercept: Intercepta i adapta les crides `light.turn_on` dirigides a múltiples llums. ➗⚠️ Pot provocar la divisió d'una crida única `light.turn_on` en múltiples crides, com ara, quan les llums són en interruptors diferents. Necessita que `intercept` estigui habilitat.", + "include_config_in_attributes": "include_config_in_attributes: Mostra totes les opcions com atributs a l'interruptor de Home Assistant quan s'estableix com a `true`. 📝" + }, + "data_description": { + "initial_transition": "Durada de la primera transició quan els llums canvien de `off` a `on` en segons. ⏲️", + "sleep_rgb_or_color_temp": "Utilitza `\"rgb_color\"` o `\"color_temp\"` durant el mode nocturn. 🌙", + "sleep_rgb_color": "Color RGB en mode nocturn (s'utilitza quan `sleep_rgb_or_color_temp` és \"rgb_color\"). 🌈", + "sleep_transition": "Durada de la transició en commutar el \"mode nocturn\", en segons. 🕑", + "sunrise_time": "Indica una hora fixa (HH:MM:SS) per a la sortida del sol. 🌅", + "min_sunrise_time": "Defineix la sortida de sol virtual més primerenca (HH:MM:SS), tot permetent sortides de sol posteriors. 🌅", + "max_sunrise_time": "Defineix la sortida de sol virtual més tardana (HH:MM:SS), tot permetent sortides de sol abans. 🌅", + "sunrise_offset": "Ajusta l'hora de sortida del sol amb una compensació positiva o negativa en segons. ⏰", + "sunset_time": "Indica una hora fixa (HH:MM:SS) per a la posta de sol. 🌇", + "min_sunset_time": "Defineix la posta de sol virtual més primerenca (HH:MM:SS), tot permetent postes de sol més tard. 🌇", + "max_sunset_time": "Defineix la sortida virtual de sol virtual més tardana (HH:MM:SS), tot permetent sortides de sol abans. 🌇", + "sunset_offset": "Ajusta l'hora de la posta del sol amb una compensació positiva o negativa en segons. ⏰", + "brightness_mode": "Mode de brillantor a utilitzar. Els valors possibles són `default`, \"linear\" i \"tanh\" (utilitza `brightness_mode_time_dark` i `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(S'ignora si `brightness_mode='default'`) La durada en segons de la variació de la brillantor abans/despres de la sortida/posta de sol. 📈📉.", + "brightness_mode_time_light": "(S'ignora si `brightness_mode='default'`) La durada en segons de la variació de la brillantor abans/despres de la sortida/posta de sol. 📈📉.", + "autoreset_control_seconds": "Restableix automàticament el control manual al cap d'uns segons. Posar a 0 per desactivar. ⏲️", + "send_split_delay": "Retard (ms) entre `separate_turn_on_commands` per als llums que no admeten la configuració simultània de brillantor i color. ⏲️", + "adapt_delay": "Temps d'espera (en segons) entre l'encesa de la llum i els canvis per part d'Adaptive Lighting. Pot ajudar a evitar els parpalleigs. ⏲️" + } + } + } } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/cs.json b/custom_components/adaptive_lighting/translations/cs.json index 71a93590..0ac5c4a0 100644 --- a/custom_components/adaptive_lighting/translations/cs.json +++ b/custom_components/adaptive_lighting/translations/cs.json @@ -25,61 +25,69 @@ "description": "Nakonfigurujte komponentu Adaptive Lighting. Názvy voleb odpovídají nastavení YAML. Pokud je tato položka definována v YAML, žádné volby se zde nezobrazí. Interaktivní grafy znázorňující vliv parametrů najdete v [této webové aplikaci]({webapp_url}). Další podrobnosti najdete v [oficiální dokumentaci]({docs_url}).", "data": { "lights": "lights: Seznam světel (entity_id), které mají být ovládané (může být prázdný). 🌟", - "initial_transition": "initial_transition: Prodlení pro změnu z 'vypnuto' do 'zapnuto' (sekundy)", - "sleep_transition": "sleep_transition: Prodleva pro přepnutí do „režimu spánku“ (sekundy)", "interval": "interval: Prodleva pro změny osvětlení (v sekundách)", - "max_brightness": "max_brightness: Nejvyšší jas osvětlení během cyklu. (%)", - "max_color_temp": "max_color_temp: Nejchladnější odstín cyklu teploty barev. (Kelvin)", - "min_brightness": "min_brightness: Nejnižší jas osvětlení během cyklu. (%)", - "min_color_temp": "min_color_temp, Nejteplejší odstín cyklu teploty barev. (Kelvin)", - "only_once": "only_once: Přizpůsobení osvětlení pouze při rozsvícení.", - "prefer_rgb_color": "prefer_rgb_color: Upřednostněte použití 'rgb_color' před 'color_temp'.", - "separate_turn_on_commands": "separate_turn_on_commands: Oddělení příkazů pro každý atribut (barva, jas, atd.) v atributu 'light.turn_on' (vyžadováno pro některá světla).", - "send_split_delay": "send_split_delay: prodleva mezi příkazy (milisekundy), když je použit atribut 'separate_turn_on_commands'. Může zajistit správné zpracování obou příkazů.", - "sleep_brightness": "sleep_brightness, Nastavení jasu pro režim spánku. (%)", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, použijte 'rgb_color' nebo 'color_temp'", - "sleep_rgb_color": "sleep_rgb_color, v RGB", - "sleep_color_temp": "sleep_color_temp: Nastavení teploty barev pro režim spánku. (v Kelvinech)", - "sunrise_offset": "sunrise_offset: Jak dlouho před (-) nebo po (+) definovat bod cyklu východu slunce (+/- v sekundách)", - "sunrise_time": "sunrise_time: Manuální přepsání času východu slunce, pokud je „None“, použije se skutečný čas východu slunce ve vaší lokalitě (HH:MM:SS)", - "max_sunrise_time": "max_sunrise_time: Ruční přepsání nejpozdějšího času východu slunce, pokud je „None“, použije se skutečný čas východu slunce vaší lokality (HH:MM:SS)", - "sunset_offset": "sunset_offset: Jak dlouho před (-) nebo po (+) definovat bod cyklu západu slunce (+/- v sekundách)", - "sunset_time": "sunset_time: Ruční přepsání času západu slunce, pokud je „None“, použije se skutečný čas západu slunce vaší lokality (HH:MM:SS)", - "min_sunset_time": "min_sunset_time: Ruční přepsání nejdřívějšího času západu slunce, pokud je „None“, použije se skutečný čas západu slunce vaší lokality (HH:MM:SS)", - "take_over_control": "take_over_control: Je-li volán 'light.turn_on' z jiného zdroje, než Adaptivním osvětlením, když je světlo již rozsvíceno, přestaňte toto světlo ovládat, dokud není vypnuto -> zapnuto (nebo i vypínačem).", - "detect_non_ha_changes": "detect_non_ha_changes: detekuje všechny změny >10% provedených pro osvětlení (také mimo HA), vyžaduje povolení atributu 'take_over_control' (každý 'interval' spouští 'homeassistant.update_entity'!)", "transition": "", - "adapt_delay": "", - "transition_until_sleep": "transition_until_sleep: Pokud je zapnuto, Adaptive Lighting bude zacházet s nastavením spánku jako s minimem, na tyto hodnoty přejde po západu slunce. 🌙", - "multi_light_intercept": "multi_light_intercept: Zachytí a přizpůsobí volání `light.turn_on`, která se zaměřují na více světel. ➗⚠️ To může vést k rozdělení jednoho volání `light.turn_on` na více volání, např. když jsou světla v různých vypínačích. Vyžaduje, aby bylo povoleno `intercept`.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Jenom při prvním zapnutí světel. Je-li nastaveno na `true`, AL udělá přizpůsobení pouze tehdy, je-li vyvoláno `light.turn_on` bez zadání barvy nebo jasu. ❌🌈 Tím se zabrání přizpůsobení např. při aktivaci scény. Pokud je `false`, AL udělá přizpůsobení bez ohledu na přítomnost barvy nebo jasu `service_data` volání. Vyžaduje zapnutí `take_over_control`. 🕵️ ", - "skip_redundant_commands": "skip_redundant_commands: Přeskočí odesílání adaptačních příkazů, jejichž cílový stav se již rovná známému stavu světla. Minimalizuje síťový provoz a v některých situacích zlepšuje odezvu adaptace. 📉Zakažte, pokud se fyzické stavy světel dostanou mimo synchronizaci se zaznamenaným stavem HA.", - "intercept": "intercept: Zachytit a přizpůsobit volání `light.turn_on` a umožnit tak okamžité přizpůsobení barev a jasu. 🏎️ Zakažte pro světla, která nepodporují `light.turn_on` s barvou a jasem najednou.", - "include_config_in_attributes": "include_config_in_attributes: Zobrazit všechny možnosti jako atributy přepínače v Home Assistant, pokud je nastaveno na `true`. 📝" + "min_brightness": "min_brightness: Nejnižší jas osvětlení během cyklu. (%)", + "max_brightness": "max_brightness: Nejvyšší jas osvětlení během cyklu. (%)", + "min_color_temp": "min_color_temp, Nejteplejší odstín cyklu teploty barev. (Kelvin)", + "max_color_temp": "max_color_temp: Nejchladnější odstín cyklu teploty barev. (Kelvin)", + "sleep_brightness": "sleep_brightness, Nastavení jasu pro režim spánku. (%)", + "sleep_color_temp": "sleep_color_temp: Nastavení teploty barev pro režim spánku. (v Kelvinech)" }, "data_description": { - "sleep_rgb_or_color_temp": "V režimu spánku se použije buď `\"rgb_color\"`, nebo `\"color_temp\"`. 🌙", - "sleep_color_temp": "Teplota barev v režimu spánku (používá se, když `sleep_rgb_or_color_temp` je `color_temp`) v Kelvinech. 😴", - "sleep_transition": "Doba trvání přechodu do režimu spánku v sekundách. 😴", - "autoreset_control_seconds": "Automatické resetování ručního ovládání po určitém počtu sekund. Nastavením na 0 se vypne. ⏲️", - "min_sunset_time": "Nastavte nejbližší virtuální čas západu slunce (HH:MM:SS), abyste mohli nastavit pozdější západ slunce. 🌅", - "sleep_brightness": "Jas světel během režimu spánku (v %). 😴", - "min_sunrise_time": "Nastavte nejbližší virtuální čas východu slunce (HH:MM:SS), abyste mohli nastavit pozdější východ slunce. 🌅", "interval": "Frekvence přizpůsobení světel v sekundách. 🔄", - "adapt_delay": "Doba čekání (v sekundách) mezi zapnutím světla a změnou adaptivního osvětlení. Mohlo by to pomoci zabránit blikání. ⏲️", - "sleep_rgb_color": "RGB barva v režimu spánku (používané když `sleep_rgb_or_color_temp` je \"rgb_color\"). 🌈", - "sunrise_offset": "Upravte čas východu slunce o sekundy dopředu nebo dozadu. ⏰", "transition": "Doba trvání přechodu změny světel v sekundách. 🕑", - "brightness_mode": "Výběr režimu jasu. Možné hodnoty jsou `default`, `linear` a `tanh` (používá `brightness_mode_time_dark` a `brightness_mode_time_light`). 📈", - "brightness_mode_time_light": "(Ignorováno, pokud `brightness_mode='default'`) Doba trvání v sekundách pro zvýšení/snížení jasu po/před východem/západem slunce. 📈📉.", - "sunset_offset": "Upravte čas západu slunce o sekundy dopředu nebo dozadu. ⏰", - "sunset_time": "Nastavit pevný čas (HH:MM:SS) pro západ slunce. 🌅", - "max_sunset_time": "Nastavte nejpozdější virtuální čas západu slunce (HH:MM:SS), což umožňuje dřívější západ slunce. 🌅", - "sunrise_time": "Nastavit pevný čas (HH:MM:SS) pro východ slunce. 🌅", - "initial_transition": "Doba trvání prvního přechodu, kdy se světla změní z `vypnuto` na `zapnuto`, v sekundách. ⏲️", - "brightness_mode_time_dark": "(Ignorováno, pokud `brightness_mode='default'`) Doba trvání v sekundách pro zvýšení/snížení jasu po/před východem/západem slunce. 📈📉.", - "max_sunrise_time": "Nastavte nejpozdější virtuální čas východu slunce (HH:MM:SS), což umožňuje dřívější východ slunce. 🌅", - "send_split_delay": "Zpoždění (ms) mezi příkazy `separate_turn_on_commands` pro světla, která nepodporují současné nastavení jasu a barvy. ⏲️" + "sleep_brightness": "Jas světel během režimu spánku (v %). 😴", + "sleep_color_temp": "Teplota barev v režimu spánku (používá se, když `sleep_rgb_or_color_temp` je `color_temp`) v Kelvinech. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition: Prodlení pro změnu z 'vypnuto' do 'zapnuto' (sekundy)", + "prefer_rgb_color": "prefer_rgb_color: Upřednostněte použití 'rgb_color' před 'color_temp'.", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, použijte 'rgb_color' nebo 'color_temp'", + "sleep_rgb_color": "sleep_rgb_color, v RGB", + "sleep_transition": "sleep_transition: Prodleva pro přepnutí do „režimu spánku“ (sekundy)", + "transition_until_sleep": "transition_until_sleep: Pokud je zapnuto, Adaptive Lighting bude zacházet s nastavením spánku jako s minimem, na tyto hodnoty přejde po západu slunce. 🌙", + "sunrise_time": "sunrise_time: Manuální přepsání času východu slunce, pokud je „None“, použije se skutečný čas východu slunce ve vaší lokalitě (HH:MM:SS)", + "max_sunrise_time": "max_sunrise_time: Ruční přepsání nejpozdějšího času východu slunce, pokud je „None“, použije se skutečný čas východu slunce vaší lokality (HH:MM:SS)", + "sunrise_offset": "sunrise_offset: Jak dlouho před (-) nebo po (+) definovat bod cyklu východu slunce (+/- v sekundách)", + "sunset_time": "sunset_time: Ruční přepsání času západu slunce, pokud je „None“, použije se skutečný čas západu slunce vaší lokality (HH:MM:SS)", + "min_sunset_time": "min_sunset_time: Ruční přepsání nejdřívějšího času západu slunce, pokud je „None“, použije se skutečný čas západu slunce vaší lokality (HH:MM:SS)", + "sunset_offset": "sunset_offset: Jak dlouho před (-) nebo po (+) definovat bod cyklu západu slunce (+/- v sekundách)", + "take_over_control": "take_over_control: Je-li volán 'light.turn_on' z jiného zdroje, než Adaptivním osvětlením, když je světlo již rozsvíceno, přestaňte toto světlo ovládat, dokud není vypnuto -> zapnuto (nebo i vypínačem).", + "detect_non_ha_changes": "detect_non_ha_changes: detekuje všechny změny >10% provedených pro osvětlení (také mimo HA), vyžaduje povolení atributu 'take_over_control' (každý 'interval' spouští 'homeassistant.update_entity'!)", + "only_once": "only_once: Přizpůsobení osvětlení pouze při rozsvícení.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Jenom při prvním zapnutí světel. Je-li nastaveno na `true`, AL udělá přizpůsobení pouze tehdy, je-li vyvoláno `light.turn_on` bez zadání barvy nebo jasu. ❌🌈 Tím se zabrání přizpůsobení např. při aktivaci scény. Pokud je `false`, AL udělá přizpůsobení bez ohledu na přítomnost barvy nebo jasu `service_data` volání. Vyžaduje zapnutí `take_over_control`. 🕵️ ", + "separate_turn_on_commands": "separate_turn_on_commands: Oddělení příkazů pro každý atribut (barva, jas, atd.) v atributu 'light.turn_on' (vyžadováno pro některá světla).", + "send_split_delay": "send_split_delay: prodleva mezi příkazy (milisekundy), když je použit atribut 'separate_turn_on_commands'. Může zajistit správné zpracování obou příkazů.", + "adapt_delay": "", + "skip_redundant_commands": "skip_redundant_commands: Přeskočí odesílání adaptačních příkazů, jejichž cílový stav se již rovná známému stavu světla. Minimalizuje síťový provoz a v některých situacích zlepšuje odezvu adaptace. 📉Zakažte, pokud se fyzické stavy světel dostanou mimo synchronizaci se zaznamenaným stavem HA.", + "intercept": "intercept: Zachytit a přizpůsobit volání `light.turn_on` a umožnit tak okamžité přizpůsobení barev a jasu. 🏎️ Zakažte pro světla, která nepodporují `light.turn_on` s barvou a jasem najednou.", + "multi_light_intercept": "multi_light_intercept: Zachytí a přizpůsobí volání `light.turn_on`, která se zaměřují na více světel. ➗⚠️ To může vést k rozdělení jednoho volání `light.turn_on` na více volání, např. když jsou světla v různých vypínačích. Vyžaduje, aby bylo povoleno `intercept`.", + "include_config_in_attributes": "include_config_in_attributes: Zobrazit všechny možnosti jako atributy přepínače v Home Assistant, pokud je nastaveno na `true`. 📝" + }, + "data_description": { + "initial_transition": "Doba trvání prvního přechodu, kdy se světla změní z `vypnuto` na `zapnuto`, v sekundách. ⏲️", + "sleep_rgb_or_color_temp": "V režimu spánku se použije buď `\"rgb_color\"`, nebo `\"color_temp\"`. 🌙", + "sleep_rgb_color": "RGB barva v režimu spánku (používané když `sleep_rgb_or_color_temp` je \"rgb_color\"). 🌈", + "sleep_transition": "Doba trvání přechodu do režimu spánku v sekundách. 😴", + "sunrise_time": "Nastavit pevný čas (HH:MM:SS) pro východ slunce. 🌅", + "min_sunrise_time": "Nastavte nejbližší virtuální čas východu slunce (HH:MM:SS), abyste mohli nastavit pozdější východ slunce. 🌅", + "max_sunrise_time": "Nastavte nejpozdější virtuální čas východu slunce (HH:MM:SS), což umožňuje dřívější východ slunce. 🌅", + "sunrise_offset": "Upravte čas východu slunce o sekundy dopředu nebo dozadu. ⏰", + "sunset_time": "Nastavit pevný čas (HH:MM:SS) pro západ slunce. 🌅", + "min_sunset_time": "Nastavte nejbližší virtuální čas západu slunce (HH:MM:SS), abyste mohli nastavit pozdější západ slunce. 🌅", + "max_sunset_time": "Nastavte nejpozdější virtuální čas západu slunce (HH:MM:SS), což umožňuje dřívější západ slunce. 🌅", + "sunset_offset": "Upravte čas západu slunce o sekundy dopředu nebo dozadu. ⏰", + "brightness_mode": "Výběr režimu jasu. Možné hodnoty jsou `default`, `linear` a `tanh` (používá `brightness_mode_time_dark` a `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Ignorováno, pokud `brightness_mode='default'`) Doba trvání v sekundách pro zvýšení/snížení jasu po/před východem/západem slunce. 📈📉.", + "brightness_mode_time_light": "(Ignorováno, pokud `brightness_mode='default'`) Doba trvání v sekundách pro zvýšení/snížení jasu po/před východem/západem slunce. 📈📉.", + "autoreset_control_seconds": "Automatické resetování ručního ovládání po určitém počtu sekund. Nastavením na 0 se vypne. ⏲️", + "send_split_delay": "Zpoždění (ms) mezi příkazy `separate_turn_on_commands` pro světla, která nepodporují současné nastavení jasu a barvy. ⏲️", + "adapt_delay": "Doba čekání (v sekundách) mezi zapnutím světla a změnou adaptivního osvětlení. Mohlo by to pomoci zabránit blikání. ⏲️" + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/da.json b/custom_components/adaptive_lighting/translations/da.json index 3d6d5951..5925dbc7 100644 --- a/custom_components/adaptive_lighting/translations/da.json +++ b/custom_components/adaptive_lighting/translations/da.json @@ -26,54 +26,62 @@ "description": "Alle indstillinger tilhørende en Adaptiv Belysnings komponent. Indstillingernes navne svarer til YAML indstillingernes. Ingen indstillinger vises hvis du allerede har konfigureret den i YAML. For interaktive grafer, der viser parametereffekter, besøg [denne webapp]({webapp_url}). Yderligere detaljer finder du i den [officielle dokumentation]({docs_url}).", "data": { "lights": "lights: lyskilder", - "initial_transition": "initial_transition: Hvor lang overgang når lyset går fra 'off' til 'on' eller når 'sleep_state' skiftes. (i sekunder)", "interval": "interval: Tid imellem opdateringer (i sekunder)", - "max_brightness": "max_brightness: Højeste lysstyrke i cyklussen. (%)", - "max_color_temp": "max_color_temp: Koldeste lystemperatur i cyklussen. (Kelvin)", - "min_brightness": "min_brightness: Laveste lysstyrke i cyklussen. (%)", - "min_color_temp": "min_color_temp: Varmeste lystemperatur i cyklussen. (Kelvin)", - "only_once": "only_once: Juster udelukkende lysene adaptivt i øjeblikket de tændes.", - "prefer_rgb_color": "prefer_rgb_color: Brug 'rgb_color' istedet for 'color_temp' når muligt.", - "separate_turn_on_commands": "separate_turn_on_commands: Adskil kommandoerne for hver attribut (color, brightness, etc.) ved 'light.turn_on' (nødvendigt for bestemte lys).", - "sleep_brightness": "sleep_brightness, Lysstyrke for Sleep Mode. (%)", - "sleep_color_temp": "sleep_color_temp: Farvetemperatur under Sleep Mode. (Kelvin)", - "sunrise_offset": "sunrise_offset: Hvor længe før (-) eller efter (+) at definere solopgangen i cyklussen (+/- sekunder)", - "sunrise_time": "sunrise_time: Manuel overstyring af solopgangstidspunktet, hvis 'None', bruges det egentlige tidspunkt din lokation. (HH:MM:SS)", - "sunset_offset": "sunset_offset: Hvor længe før (-) eller efter (+) at definere solnedgangen i cyklussen (+/- sekunder)", - "sunset_time": "sunset_time: Manuel overstyring af solnedgangstidspunktet, hvis 'None', bruges det egentlige tidspunkt for din lokation. (HH:MM:SS)", - "take_over_control": "take_over_control: Hvis andet end Adaptiv Belysning kalder 'light.turn_on' på et lys der allerede er tændt, afbryd adaptering af lyset indtil at det tændes igen.", - "detect_non_ha_changes": "detect_non_ha_changes: Registrer alle ændringer på >10% på et lys (også udenfor HA), kræver at 'take_over_control' er slået til (kalder 'homeassistant.update_entity' hvert 'interval'!)", "transition": "Overgangsperiode når en ændring i lyset udføres (i sekunder)", - "transition_until_sleep": "overgang_til_sove: Når aktiveret, vil adaptiv belysning behandle søvnindstillinger som minimum, og overgår til disse værdier efter solnedgang. 🌙", - "adapt_only_on_bare_turn_on": "tilpas_kun_ved_enkelt_tænd: Når du tænder lys for første gang. Hvis indstillet til 'true', tilpasser AL kun, hvis 'lys.tænd' er kaldt uden at angive farve eller lysstyrke. ❌🌈 Dette forhindrer f.eks. tilpasning, når du aktiverer en scene. Hvis indstillet til 'false' tilpasser AL sig uanset tilstanden af farve eller lysstyrke i den oprindelige 'service_data'. Har brug for at 'take_over_control' er aktiveret. 🕵️", - "include_config_in_attributes": "include_config_in_attributes: Vis alle indstillinger som attributter for kontakten når dette er sat til »true«. 📝", - "skip_redundant_commands": "skip_redundant_commands: Undlad at sende tilpasningskommando, hvis lampens kendte tilstand allerede er lig den ønskede tilstand. Mindsker mængden af netværkstrafik og forbedrer tilpasningens responsivitet i visse situationer. 📉 Slå fra, hvis lampens faktiske tilstand kommer ud af takt med den tilstand, som HA rapporterer.", - "intercept": "intercept: Indfang og tilpas »light.turn_on«-kald for at muliggøre øjeblikkelig farve- og lysstyrketilpasning. 🏎️ Slå fra for lyskilder, som ikke understøtter »light.turn_on« med farve og lysstyrke.", - "multi_light_intercept": "multi_light_intercept: Indfang og tilpas »light.turn_on«-kald til mere end en enkelt lyskilde. ➗⚠️ Dette kan bevirke at et enkelt »light.turn_on«-kald deles op i flere, f.eks. hvis lyskilderne er forbundet til forskellige kontakter. Forudsætter at »intercept« er slået til." + "min_brightness": "min_brightness: Laveste lysstyrke i cyklussen. (%)", + "max_brightness": "max_brightness: Højeste lysstyrke i cyklussen. (%)", + "min_color_temp": "min_color_temp: Varmeste lystemperatur i cyklussen. (Kelvin)", + "max_color_temp": "max_color_temp: Koldeste lystemperatur i cyklussen. (Kelvin)", + "sleep_brightness": "sleep_brightness, Lysstyrke for Sleep Mode. (%)", + "sleep_color_temp": "sleep_color_temp: Farvetemperatur under Sleep Mode. (Kelvin)" }, "data_description": { "interval": "Frekvens til at tilpasse lysene, i sekunder. 🔄", - "sleep_brightness": "Lysstyrkeprocent af lys i søvntilstand. 😴", "transition": "Varighed af overgang, når lys ændres, i sekunder. 🕑", - "sleep_rgb_or_color_temp": "Brug enten `\"rgb_farve\"` eller `\"farve_temp\"` i søvntilstand. 🌙", - "sleep_transition": "Varigheden af overgangen, når \"sovetilstand\" skiftes, i sekunder. 😴", - "sunrise_time": "Sæt en fast tid (HH:MM:SS) for solopgang. 🌅", - "sunset_time": "Sæt en fast tid (HH:MM:SS) for solnedgang. 🌇", - "min_sunrise_time": "Indstil den tidligste virtuelle solopgangstid (HH:MM:SS), hvilket giver mulighed for senere solopgange. 🌅", - "max_sunrise_time": "Indstil den seneste virtuelle solopgangstid (HH:MM:SS), hvilket giver mulighed for tidligere solopgange. 🌅", - "autoreset_control_seconds": "Nulstil automatisk den manuelle styring efter et antal sekunder. Indstil til 0 for at deaktivere. ⏲️", - "min_sunset_time": "Indstil den tidligste virtuelle solnedgangstid (HH:MM:SS), hvilket giver mulighed for senere solnedgange. 🌇", - "adapt_delay": "Ventetid (sekunder) mellem lyset tændes og Adaptive Lighting anvender ændringer. Kan hjælpe med at undgå flimren. ⏲️", - "sunset_offset": "Juster solnedgang tid med et positivt eller negativt offset, i sekunder. ⏰", - "sunrise_offset": "Juster solopgangstiden med en positiv eller negativ offset på få sekunder. ⏰", - "max_sunset_time": "Indstil den seneste virtuelle solnedgangstid (HH:MM:SS), hvilket giver mulighed for tidligere solnedgange. 🌇", - "sleep_color_temp": "Farvetemperatur i søvntilstand (bruges når `sleep_rgb_or_color_temp` er `color_temp`) i Kelvin. 😴", - "brightness_mode": "Lysstyrketilstand til brug. Mulige værdier er \"default\", \"linear\" og \"tanh\" (bruger \"brightness_mode_time_dark\" og \"brightness_mode_time_light\"). 📈", - "send_split_delay": "Forsinkelse (ms) mellem »separate_turn_on_commands« for lyskilder som ikke understøtter simultane styrke- og farveindstillinger. ⏲️", - "initial_transition": "Den første overgangs varighed når lysene ændres fra »off« til »on« i sekunder. ⏲️", - "sleep_rgb_color": "RGB-farve i søvntilstand (anvendes når »sleep_rgb_or_color_temp« er sat til »rgb_color«). 🌈", - "brightness_mode_time_dark": "(Ignoreres hvis »brightness_mode='default'«) Varigheden i sekunder for tilpasningen af lysstyrken ved solopgang eller -nedgang. 📈📉", - "brightness_mode_time_light": "(Ignoreres hvis »brightness_mode='default'«) Varigheden i sekunder for tilpasningen af lysstyrken ved solopgang eller -nedgang. 📈📉" + "sleep_brightness": "Lysstyrkeprocent af lys i søvntilstand. 😴", + "sleep_color_temp": "Farvetemperatur i søvntilstand (bruges når `sleep_rgb_or_color_temp` er `color_temp`) i Kelvin. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition: Hvor lang overgang når lyset går fra 'off' til 'on' eller når 'sleep_state' skiftes. (i sekunder)", + "prefer_rgb_color": "prefer_rgb_color: Brug 'rgb_color' istedet for 'color_temp' når muligt.", + "transition_until_sleep": "overgang_til_sove: Når aktiveret, vil adaptiv belysning behandle søvnindstillinger som minimum, og overgår til disse værdier efter solnedgang. 🌙", + "sunrise_time": "sunrise_time: Manuel overstyring af solopgangstidspunktet, hvis 'None', bruges det egentlige tidspunkt din lokation. (HH:MM:SS)", + "sunrise_offset": "sunrise_offset: Hvor længe før (-) eller efter (+) at definere solopgangen i cyklussen (+/- sekunder)", + "sunset_time": "sunset_time: Manuel overstyring af solnedgangstidspunktet, hvis 'None', bruges det egentlige tidspunkt for din lokation. (HH:MM:SS)", + "sunset_offset": "sunset_offset: Hvor længe før (-) eller efter (+) at definere solnedgangen i cyklussen (+/- sekunder)", + "take_over_control": "take_over_control: Hvis andet end Adaptiv Belysning kalder 'light.turn_on' på et lys der allerede er tændt, afbryd adaptering af lyset indtil at det tændes igen.", + "detect_non_ha_changes": "detect_non_ha_changes: Registrer alle ændringer på >10% på et lys (også udenfor HA), kræver at 'take_over_control' er slået til (kalder 'homeassistant.update_entity' hvert 'interval'!)", + "only_once": "only_once: Juster udelukkende lysene adaptivt i øjeblikket de tændes.", + "adapt_only_on_bare_turn_on": "tilpas_kun_ved_enkelt_tænd: Når du tænder lys for første gang. Hvis indstillet til 'true', tilpasser AL kun, hvis 'lys.tænd' er kaldt uden at angive farve eller lysstyrke. ❌🌈 Dette forhindrer f.eks. tilpasning, når du aktiverer en scene. Hvis indstillet til 'false' tilpasser AL sig uanset tilstanden af farve eller lysstyrke i den oprindelige 'service_data'. Har brug for at 'take_over_control' er aktiveret. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Adskil kommandoerne for hver attribut (color, brightness, etc.) ved 'light.turn_on' (nødvendigt for bestemte lys).", + "skip_redundant_commands": "skip_redundant_commands: Undlad at sende tilpasningskommando, hvis lampens kendte tilstand allerede er lig den ønskede tilstand. Mindsker mængden af netværkstrafik og forbedrer tilpasningens responsivitet i visse situationer. 📉 Slå fra, hvis lampens faktiske tilstand kommer ud af takt med den tilstand, som HA rapporterer.", + "intercept": "intercept: Indfang og tilpas »light.turn_on«-kald for at muliggøre øjeblikkelig farve- og lysstyrketilpasning. 🏎️ Slå fra for lyskilder, som ikke understøtter »light.turn_on« med farve og lysstyrke.", + "multi_light_intercept": "multi_light_intercept: Indfang og tilpas »light.turn_on«-kald til mere end en enkelt lyskilde. ➗⚠️ Dette kan bevirke at et enkelt »light.turn_on«-kald deles op i flere, f.eks. hvis lyskilderne er forbundet til forskellige kontakter. Forudsætter at »intercept« er slået til.", + "include_config_in_attributes": "include_config_in_attributes: Vis alle indstillinger som attributter for kontakten når dette er sat til »true«. 📝" + }, + "data_description": { + "initial_transition": "Den første overgangs varighed når lysene ændres fra »off« til »on« i sekunder. ⏲️", + "sleep_rgb_or_color_temp": "Brug enten `\"rgb_farve\"` eller `\"farve_temp\"` i søvntilstand. 🌙", + "sleep_rgb_color": "RGB-farve i søvntilstand (anvendes når »sleep_rgb_or_color_temp« er sat til »rgb_color«). 🌈", + "sleep_transition": "Varigheden af overgangen, når \"sovetilstand\" skiftes, i sekunder. 😴", + "sunrise_time": "Sæt en fast tid (HH:MM:SS) for solopgang. 🌅", + "min_sunrise_time": "Indstil den tidligste virtuelle solopgangstid (HH:MM:SS), hvilket giver mulighed for senere solopgange. 🌅", + "max_sunrise_time": "Indstil den seneste virtuelle solopgangstid (HH:MM:SS), hvilket giver mulighed for tidligere solopgange. 🌅", + "sunrise_offset": "Juster solopgangstiden med en positiv eller negativ offset på få sekunder. ⏰", + "sunset_time": "Sæt en fast tid (HH:MM:SS) for solnedgang. 🌇", + "min_sunset_time": "Indstil den tidligste virtuelle solnedgangstid (HH:MM:SS), hvilket giver mulighed for senere solnedgange. 🌇", + "max_sunset_time": "Indstil den seneste virtuelle solnedgangstid (HH:MM:SS), hvilket giver mulighed for tidligere solnedgange. 🌇", + "sunset_offset": "Juster solnedgang tid med et positivt eller negativt offset, i sekunder. ⏰", + "brightness_mode": "Lysstyrketilstand til brug. Mulige værdier er \"default\", \"linear\" og \"tanh\" (bruger \"brightness_mode_time_dark\" og \"brightness_mode_time_light\"). 📈", + "brightness_mode_time_dark": "(Ignoreres hvis »brightness_mode='default'«) Varigheden i sekunder for tilpasningen af lysstyrken ved solopgang eller -nedgang. 📈📉", + "brightness_mode_time_light": "(Ignoreres hvis »brightness_mode='default'«) Varigheden i sekunder for tilpasningen af lysstyrken ved solopgang eller -nedgang. 📈📉", + "autoreset_control_seconds": "Nulstil automatisk den manuelle styring efter et antal sekunder. Indstil til 0 for at deaktivere. ⏲️", + "send_split_delay": "Forsinkelse (ms) mellem »separate_turn_on_commands« for lyskilder som ikke understøtter simultane styrke- og farveindstillinger. ⏲️", + "adapt_delay": "Ventetid (sekunder) mellem lyset tændes og Adaptive Lighting anvender ændringer. Kan hjælpe med at undgå flimren. ⏲️" + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json index c7592dc5..a03c238f 100644 --- a/custom_components/adaptive_lighting/translations/de.json +++ b/custom_components/adaptive_lighting/translations/de.json @@ -28,61 +28,69 @@ "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde. Interaktive Diagramme zur Veranschaulichung der Auswirkungen der Parameter finden Sie unter [dieser Webanwendung]({webapp_url}). Weitere Details finden Sie in der [offiziellen Dokumentation]({docs_url}).", "data": { "lights": "Lichter", - "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt", - "sleep_transition": "sleep_transition: Wenn 'sleep_state' sich ändert. (Sekunden)", "interval": "interval, Zeit zwischen Updates des Switches", - "max_brightness": "max_brightness: Maximale Helligkeit in Prozent. 💡", - "max_color_temp": "max_color_temp: Kälteste Farbtemperatur in Kelvin. ❄️", - "min_brightness": "min_brightness: Minimale Helligkeit in Prozent. 💡", - "min_color_temp": "min_color_temp: Wärmste Farbtemperatur in Kelvin. 🔥", - "only_once": "only_once: Lichter nur einmalig anpassen, wenn sie eingeschaltet werden (`true`) oder sie immer wieder anpassen (`false`). 🔄", - "prefer_rgb_color": "prefer_rgb_color: Ob die RGB-Farbanpassung der Farbtemperaturanpassung vorgezogen werden soll, wenn möglich. 🌈", - "separate_turn_on_commands": "separate_turn_on_commands: Verwende getrennte `light.turn_on`-Aufrufe für Farbe und Helligkeit, erforderlich für einige Lichttypen. 🔀", - "send_split_delay": "send_split_delay: Wartezeit zwischen dem Senden der Befehle (Millisekunden), wenn separate_turn_on_commands genutzt wird. Kann helfen, wenn die Leuchtmittel die separaten Befehle nicht korrekt umsetzen.", - "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, nutze 'rgb_color' oder 'color_temp'", - "sleep_rgb_color": "sleep_rgb_color, in RGB", - "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperatur in Kelvin", - "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- Sekunden", - "sunrise_time": "sunrise_time, Sonnenaufgangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenaufgangs an deiner Position verwendet)", - "max_sunrise_time": "max_sunrise_time: Manuelles Überschreiben der max. sunrise_time. Falls 'None', wird die tatsächliche sunrise_time an deiner Position verwendet (HH:MM:SS)", - "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- Sekunden", - "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)", - "min_sunset_time": "min_sunset_time: Manuelles Überschreiben der min. sunset_time. Falls 'None', wird die tatsächliche sunset_time an deiner Position verwendet (HH:MM:SS)", - "take_over_control": "take_over_control: Deaktiviere die adaptive Beleuchtung, wenn eine andere Quelle `light.turn_on` aufruft, während die Beleuchtung eingeschaltet ist und angepasst wird. Beachte, dass dies `homeassistant.update_entity` jedes `Intervall` aufruft! 🔒", - "detect_non_ha_changes": "detect_non_ha_changes: Erkennt und stoppt Anpassungen für nicht-`light.turn_on`-Zustandsänderungen. Benötigt, dass `take_over_control` aktiviert ist. 🕵️ Vorsicht: ⚠️ Einige Lichter können fälschlicherweise einen 'an'-Zustand anzeigen, was dazu führen kann, dass Lichter unerwartet eingeschaltet werden. Deaktiviere diese Funktion, wenn solche Probleme auftreten.", "transition": "transition, Wechselzeit in Sekunden", - "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.", - "skip_redundant_commands": "skip_redundant_commands: Überspringt das Senden von Anpassungsbefehlen, deren Zielzustand bereits mit dem bekannten Zustand der Leuchte übereinstimmt. Minimiert den Netzwerkverkehr und verbessert die Anpassungsreaktion in einigen Situationen. 📉 Deaktivieren, falls der physikalische Zustand der Lichter nicht mehr mit dem Zustand in HA übereinstimmt.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Beim ersten Einschalten des Lichts. Wenn auf `true` gesetzt, passt AL das Licht nur an, wenn `light.turn_on` ohne eine Angabe von Farbe oder Helligkeit aufgerufen wird. ❌🌈 Dies verhindert z.B. die Anpassung durch AL beim Aktivieren einer Szene. Wenn auf \"false\" gesetzt, passt AL das licht unabhängig von der Angabe von Farbe oder Helligkeit in den ursprünglichen `service_data` an. Benötigt das `take_over_control` aktiviert ist. 🕵️", - "include_config_in_attributes": "include_config_in_attributes: Alle Optionen als Attribute auf dem Schalter im Home Assistant anzeigen, wenn auf `true` gesetzt. 📝", - "multi_light_intercept": "multi_light_intercept: Abfangen und Anpassen von `light.turn_on`-Aufrufen, die auf mehrere Lichter aufrufen. ➗⚠️ Dies kann dazu führen, dass ein einzelner `light.turn_on`-Aufruf in mehrere Aufrufe aufgeteilt wird, z.B. wenn Lichter in verschiedenen Schaltern sind. Erfordert, dass `intercept` aktiviert ist.", - "transition_until_sleep": "transition_until_sleep: Wenn diese Option aktiviert ist, behandelt die adaptive Beleuchtung die Schlafeinstellungen als Minimum und geht nach Sonnenuntergang zu diesen Werten über. 🌙", - "intercept": "intercept: Abfangen und Anpassen von `light.turn_on`-Aufrufen, um eine sofortige Anpassung von Farbe und Helligkeit zu ermöglichen. 🏎️ Deaktivieren für Leuchten, die `light.turn_on` mit Farbe und Helligkeit nicht unterstützen." + "min_brightness": "min_brightness: Minimale Helligkeit in Prozent. 💡", + "max_brightness": "max_brightness: Maximale Helligkeit in Prozent. 💡", + "min_color_temp": "min_color_temp: Wärmste Farbtemperatur in Kelvin. 🔥", + "max_color_temp": "max_color_temp: Kälteste Farbtemperatur in Kelvin. ❄️", + "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %", + "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperatur in Kelvin" }, "data_description": { - "sunrise_offset": "Anpassung der Sonnenaufgangszeit mit positivem oder negativem Versatz in Sekunden. ⏰", - "sunset_offset": "Anpassung der Sonnenuntergangszeit mit positivem oder negativem Versatz in Sekunden. ⏰", - "brightness_mode": "Helligkeitsmodus, der verwendet werden soll. Mögliche Werte sind `default`, `linear` und `tanh` (verwendet `brightness_mode_time_dark` und `brightness_mode_time_light`). 📈", - "send_split_delay": "Verzögerung (ms) zwischen `separate_turn_on_commands` für Leuchten, die keine gleichzeitige Einstellung von Helligkeit und Farbe unterstützen. ⏲️", - "transition": "Dauer des Übergangs beim Lichtwechsel in Sekunden. 🕑", - "sleep_rgb_color": "RGB-Farbe im Schlafmodus (wird verwendet, wenn `sleep_rgb_or_color_temp` `rgb_color` ist). 🌈", - "sunset_time": "Stelle eine feste Zeit (HH:MM:SS) für den Sonnenuntergang ein. 🌇", - "max_sunrise_time": "Lege die späteste virtuelle Sonnenaufgangszeit (HH:MM:SS) fest, um einen früheren Sonnenaufgang zu ermöglichen. 🌅", - "min_sunset_time": "Lege die früheste virtuelle Sonnenuntergangszeit (HH:MM:SS) fest, um spätere Sonnenuntergänge zu ermöglichen. 🌇", - "max_sunset_time": "Lege die späteste virtuelle Sonnenuntergangszeit (HH:MM:SS) fest, um frühere Sonnenuntergänge zu ermöglichen. 🌇", - "adapt_delay": "Wartezeit (Sekunden) zwischen dem Einschalten des Lichts und der Anwendung der adaptiven Beleuchtung. Könnte helfen, Flackern zu vermeiden. ⏲️", - "min_sunrise_time": "Lege die früheste virtuelle Sonnenaufgangszeit (HH:MM:SS) fest, um einen späteren Sonnenaufgang zu ermöglichen. 🌅", "interval": "Häufigkeit der Lichtanpassung in Sekunden. 🔄", - "brightness_mode_time_light": "(Wird ignoriert, wenn `brightness_mode='default'`) Die Dauer in Sekunden, um die Helligkeit nach/vor Sonnenaufgang/Sonnenuntergang hoch/runter zu fahren. 📈📉.", - "brightness_mode_time_dark": "(Wird ignoriert, wenn `brightness_mode='default'`) Die Dauer in Sekunden, um die Helligkeit vor/nach Sonnenaufgang/Sonnenuntergang hoch/runter zu fahren. 📈📉.", - "autoreset_control_seconds": "Setzt die manuelle Steuerung nach einer bestimmten Anzahl von Sekunden automatisch zurück. Zum Deaktivieren auf 0 setzen. ⏲️", + "transition": "Dauer des Übergangs beim Lichtwechsel in Sekunden. 🕑", "sleep_brightness": "Helligkeit der Lichter im Schlafmodus in Prozent. 😴", - "sleep_color_temp": "Farbtemperatur im Schlafmodus in Kelvin (wird verwendet, wenn `sleep_rgb_or_color_temp` `color_temp` ist) . 😴", - "initial_transition": "Dauer des ersten Übergangs, wenn das Licht von `off` auf `on` schaltet, in Sekunden. ⏲️", - "sleep_rgb_or_color_temp": "Verwende entweder `rgb_color` oder `color_temp` im Schlafmodus. 🌙", - "sleep_transition": "Dauer des Übergangs, wenn der \"Schlafmodus\" umgeschaltet wird, in Sekunden. 😴", - "sunrise_time": "Stelle eine feste Zeit (HH:MM:SS) für den Sonnenaufgang ein. 🌅" + "sleep_color_temp": "Farbtemperatur im Schlafmodus in Kelvin (wird verwendet, wenn `sleep_rgb_or_color_temp` `color_temp` ist) . 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt", + "prefer_rgb_color": "prefer_rgb_color: Ob die RGB-Farbanpassung der Farbtemperaturanpassung vorgezogen werden soll, wenn möglich. 🌈", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, nutze 'rgb_color' oder 'color_temp'", + "sleep_rgb_color": "sleep_rgb_color, in RGB", + "sleep_transition": "sleep_transition: Wenn 'sleep_state' sich ändert. (Sekunden)", + "transition_until_sleep": "transition_until_sleep: Wenn diese Option aktiviert ist, behandelt die adaptive Beleuchtung die Schlafeinstellungen als Minimum und geht nach Sonnenuntergang zu diesen Werten über. 🌙", + "sunrise_time": "sunrise_time, Sonnenaufgangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenaufgangs an deiner Position verwendet)", + "max_sunrise_time": "max_sunrise_time: Manuelles Überschreiben der max. sunrise_time. Falls 'None', wird die tatsächliche sunrise_time an deiner Position verwendet (HH:MM:SS)", + "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- Sekunden", + "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)", + "min_sunset_time": "min_sunset_time: Manuelles Überschreiben der min. sunset_time. Falls 'None', wird die tatsächliche sunset_time an deiner Position verwendet (HH:MM:SS)", + "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- Sekunden", + "take_over_control": "take_over_control: Deaktiviere die adaptive Beleuchtung, wenn eine andere Quelle `light.turn_on` aufruft, während die Beleuchtung eingeschaltet ist und angepasst wird. Beachte, dass dies `homeassistant.update_entity` jedes `Intervall` aufruft! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Erkennt und stoppt Anpassungen für nicht-`light.turn_on`-Zustandsänderungen. Benötigt, dass `take_over_control` aktiviert ist. 🕵️ Vorsicht: ⚠️ Einige Lichter können fälschlicherweise einen 'an'-Zustand anzeigen, was dazu führen kann, dass Lichter unerwartet eingeschaltet werden. Deaktiviere diese Funktion, wenn solche Probleme auftreten.", + "only_once": "only_once: Lichter nur einmalig anpassen, wenn sie eingeschaltet werden (`true`) oder sie immer wieder anpassen (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Beim ersten Einschalten des Lichts. Wenn auf `true` gesetzt, passt AL das Licht nur an, wenn `light.turn_on` ohne eine Angabe von Farbe oder Helligkeit aufgerufen wird. ❌🌈 Dies verhindert z.B. die Anpassung durch AL beim Aktivieren einer Szene. Wenn auf \"false\" gesetzt, passt AL das licht unabhängig von der Angabe von Farbe oder Helligkeit in den ursprünglichen `service_data` an. Benötigt das `take_over_control` aktiviert ist. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Verwende getrennte `light.turn_on`-Aufrufe für Farbe und Helligkeit, erforderlich für einige Lichttypen. 🔀", + "send_split_delay": "send_split_delay: Wartezeit zwischen dem Senden der Befehle (Millisekunden), wenn separate_turn_on_commands genutzt wird. Kann helfen, wenn die Leuchtmittel die separaten Befehle nicht korrekt umsetzen.", + "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.", + "skip_redundant_commands": "skip_redundant_commands: Überspringt das Senden von Anpassungsbefehlen, deren Zielzustand bereits mit dem bekannten Zustand der Leuchte übereinstimmt. Minimiert den Netzwerkverkehr und verbessert die Anpassungsreaktion in einigen Situationen. 📉 Deaktivieren, falls der physikalische Zustand der Lichter nicht mehr mit dem Zustand in HA übereinstimmt.", + "intercept": "intercept: Abfangen und Anpassen von `light.turn_on`-Aufrufen, um eine sofortige Anpassung von Farbe und Helligkeit zu ermöglichen. 🏎️ Deaktivieren für Leuchten, die `light.turn_on` mit Farbe und Helligkeit nicht unterstützen.", + "multi_light_intercept": "multi_light_intercept: Abfangen und Anpassen von `light.turn_on`-Aufrufen, die auf mehrere Lichter aufrufen. ➗⚠️ Dies kann dazu führen, dass ein einzelner `light.turn_on`-Aufruf in mehrere Aufrufe aufgeteilt wird, z.B. wenn Lichter in verschiedenen Schaltern sind. Erfordert, dass `intercept` aktiviert ist.", + "include_config_in_attributes": "include_config_in_attributes: Alle Optionen als Attribute auf dem Schalter im Home Assistant anzeigen, wenn auf `true` gesetzt. 📝" + }, + "data_description": { + "initial_transition": "Dauer des ersten Übergangs, wenn das Licht von `off` auf `on` schaltet, in Sekunden. ⏲️", + "sleep_rgb_or_color_temp": "Verwende entweder `rgb_color` oder `color_temp` im Schlafmodus. 🌙", + "sleep_rgb_color": "RGB-Farbe im Schlafmodus (wird verwendet, wenn `sleep_rgb_or_color_temp` `rgb_color` ist). 🌈", + "sleep_transition": "Dauer des Übergangs, wenn der \"Schlafmodus\" umgeschaltet wird, in Sekunden. 😴", + "sunrise_time": "Stelle eine feste Zeit (HH:MM:SS) für den Sonnenaufgang ein. 🌅", + "min_sunrise_time": "Lege die früheste virtuelle Sonnenaufgangszeit (HH:MM:SS) fest, um einen späteren Sonnenaufgang zu ermöglichen. 🌅", + "max_sunrise_time": "Lege die späteste virtuelle Sonnenaufgangszeit (HH:MM:SS) fest, um einen früheren Sonnenaufgang zu ermöglichen. 🌅", + "sunrise_offset": "Anpassung der Sonnenaufgangszeit mit positivem oder negativem Versatz in Sekunden. ⏰", + "sunset_time": "Stelle eine feste Zeit (HH:MM:SS) für den Sonnenuntergang ein. 🌇", + "min_sunset_time": "Lege die früheste virtuelle Sonnenuntergangszeit (HH:MM:SS) fest, um spätere Sonnenuntergänge zu ermöglichen. 🌇", + "max_sunset_time": "Lege die späteste virtuelle Sonnenuntergangszeit (HH:MM:SS) fest, um frühere Sonnenuntergänge zu ermöglichen. 🌇", + "sunset_offset": "Anpassung der Sonnenuntergangszeit mit positivem oder negativem Versatz in Sekunden. ⏰", + "brightness_mode": "Helligkeitsmodus, der verwendet werden soll. Mögliche Werte sind `default`, `linear` und `tanh` (verwendet `brightness_mode_time_dark` und `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Wird ignoriert, wenn `brightness_mode='default'`) Die Dauer in Sekunden, um die Helligkeit vor/nach Sonnenaufgang/Sonnenuntergang hoch/runter zu fahren. 📈📉.", + "brightness_mode_time_light": "(Wird ignoriert, wenn `brightness_mode='default'`) Die Dauer in Sekunden, um die Helligkeit nach/vor Sonnenaufgang/Sonnenuntergang hoch/runter zu fahren. 📈📉.", + "autoreset_control_seconds": "Setzt die manuelle Steuerung nach einer bestimmten Anzahl von Sekunden automatisch zurück. Zum Deaktivieren auf 0 setzen. ⏲️", + "send_split_delay": "Verzögerung (ms) zwischen `separate_turn_on_commands` für Leuchten, die keine gleichzeitige Einstellung von Helligkeit und Farbe unterstützen. ⏲️", + "adapt_delay": "Wartezeit (Sekunden) zwischen dem Einschalten des Lichts und der Anwendung der adaptiven Beleuchtung. Könnte helfen, Flackern zu vermeiden. ⏲️" + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/el.json b/custom_components/adaptive_lighting/translations/el.json index 8794a46a..e9dbbe41 100644 --- a/custom_components/adaptive_lighting/translations/el.json +++ b/custom_components/adaptive_lighting/translations/el.json @@ -3,7 +3,15 @@ "options": { "step": { "init": { - "title": "Επιλογές Adaptive Lighting" + "title": "Επιλογές Adaptive Lighting", + "sections": { + "advanced": { + "data": {}, + "data_description": {} + } + }, + "data": {}, + "data_description": {} } } }, diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 9d218f62..688bb984 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -30,68 +30,78 @@ "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟", "interval": "interval", "transition": "transition", - "initial_transition": "initial_transition", "min_brightness": "min_brightness: Minimum brightness percentage. 💡", "max_brightness": "max_brightness: Maximum brightness percentage. 💡", "min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥", "max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️", - "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", "sleep_brightness": "sleep_brightness", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp", - "sleep_color_temp": "sleep_color_temp", - "sleep_rgb_color": "sleep_rgb_color", - "sleep_transition": "sleep_transition", - "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", - "sunrise_time": "sunrise_time", - "min_sunrise_time": "min_sunrise_time", - "max_sunrise_time": "max_sunrise_time", - "sunrise_offset": "sunrise_offset", - "sunset_time": "sunset_time", - "min_sunset_time": "min_sunset_time", - "max_sunset_time": "max_sunset_time", - "sunset_offset": "sunset_offset", - "brightness_mode": "brightness_mode", - "brightness_mode_time_dark": "brightness_mode_time_dark", - "brightness_mode_time_light": "brightness_mode_time_light", - "take_over_control": "take_over_control: Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒", - "take_over_control_mode": "take_over_control_mode", - "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", - "autoreset_control_seconds": "autoreset_control_seconds", - "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", - "reset_manual_control_on_sleep_mode_change": "reset_manual_control_on_sleep_mode_change: Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴", - "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", - "send_split_delay": "send_split_delay", - "adapt_delay": "adapt_delay", - "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.", - "intercept": "intercept: Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness.", - "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled.", - "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝" + "sleep_color_temp": "sleep_color_temp" }, "data_description": { "interval": "Frequency to adapt the lights, in seconds. 🔄", "transition": "Duration of transition when lights change, in seconds. 🕑", - "initial_transition": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", "sleep_brightness": "Brightness percentage of lights in sleep mode. 😴", - "sleep_rgb_or_color_temp": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", - "sleep_color_temp": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", - "sleep_rgb_color": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", - "sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", - "sunrise_time": "Set a fixed time (HH:MM:SS) for sunrise. 🌅", - "min_sunrise_time": "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅", - "max_sunrise_time": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", - "sunrise_offset": "Adjust sunrise time with a positive or negative offset in seconds. ⏰", - "sunset_time": "Set a fixed time (HH:MM:SS) for sunset. 🌇", - "min_sunset_time": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", - "max_sunset_time": "Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇", - "sunset_offset": "Adjust sunset time with a positive or negative offset in seconds. ⏰", - "brightness_mode": "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", - "brightness_mode_time_dark": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", - "brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.", - "take_over_control_mode": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.", - "autoreset_control_seconds": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", - "send_split_delay": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", - "adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️" + "sleep_color_temp": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴" + }, + "sections": { + "advanced": { + "name": "Advanced settings", + "description": "Additional settings for fine-tuning Adaptive Lighting.", + "data": { + "initial_transition": "initial_transition", + "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp", + "sleep_rgb_color": "sleep_rgb_color", + "sleep_transition": "sleep_transition", + "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙", + "sunrise_time": "sunrise_time", + "min_sunrise_time": "min_sunrise_time", + "max_sunrise_time": "max_sunrise_time", + "sunrise_offset": "sunrise_offset", + "sunset_time": "sunset_time", + "min_sunset_time": "min_sunset_time", + "max_sunset_time": "max_sunset_time", + "sunset_offset": "sunset_offset", + "brightness_mode": "brightness_mode", + "brightness_mode_time_dark": "brightness_mode_time_dark", + "brightness_mode_time_light": "brightness_mode_time_light", + "take_over_control": "take_over_control: Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒", + "take_over_control_mode": "take_over_control_mode", + "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", + "autoreset_control_seconds": "autoreset_control_seconds", + "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", + "reset_manual_control_on_sleep_mode_change": "reset_manual_control_on_sleep_mode_change: Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴", + "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", + "send_split_delay": "send_split_delay", + "adapt_delay": "adapt_delay", + "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.", + "intercept": "intercept: Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness.", + "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled.", + "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝" + }, + "data_description": { + "initial_transition": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "sleep_rgb_or_color_temp": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", + "sleep_rgb_color": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "sunrise_time": "Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "min_sunrise_time": "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅", + "max_sunrise_time": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅", + "sunrise_offset": "Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "sunset_time": "Set a fixed time (HH:MM:SS) for sunset. 🌇", + "min_sunset_time": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇", + "max_sunset_time": "Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇", + "sunset_offset": "Adjust sunset time with a positive or negative offset in seconds. ⏰", + "brightness_mode": "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉", + "brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.", + "take_over_control_mode": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.", + "autoreset_control_seconds": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "send_split_delay": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", + "adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️" + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/es.json b/custom_components/adaptive_lighting/translations/es.json index 600187d3..5944193c 100644 --- a/custom_components/adaptive_lighting/translations/es.json +++ b/custom_components/adaptive_lighting/translations/es.json @@ -5,49 +5,57 @@ "init": { "title": "Configuración de la Iluminación Adaptativa", "data_description": { - "sunset_offset": "Define la hora de la puesta del sol con un desfase (positivo o negativo) en segundos. ⏰", - "sunrise_offset": "Define la hora de la salida del sol con un desfase (positivo o negativo) en segundos. ⏰", - "sleep_color_temp": "Temperatura de color en modo noche (usado cuando`sleep_rgb_or_color_temp` es `color_temp`) en grados Kelvin. 😴", - "send_split_delay": "Retraso (ms) entre `separate_turn_on_commands` para luces que no soportan ajustes simultáneos de brillo y color. ⏲️", - "transition": "Duración de la transición cuando las luces se adaptan, en segundos. ⏲️", - "initial_transition": "Duración de la primera transición cuando las luces pasan de `off` a `on` en segundos. ⏲️", - "sleep_transition": "Duración de la transición cuando el \"modo noche\" se activa o desactiva, en segundos. 😴", - "max_sunrise_time": "Define el amanecer virtual más tardío (HH:MM:SS), permitiendo amaneceres más tempranos. 🌅", - "max_sunset_time": "Define el atardecer virtual más tardío (HH:MM:SS), permitiendo atardeceres más tempranos. 🌇", - "sleep_brightness": "Porcentaje de brillo de las luces en el modo noche. 😴", "interval": "Frecuencia de adaptación de las luces, en segundos. 🔄", - "sleep_rgb_color": "Color RGB en modo noche(usado cuando `sleep_rgb_or_color_temp` es \"rgb_color\"). 🌈", - "sunrise_time": "Fijar una hora (HH:MM:SS) para el amanecer. 🌅", - "min_sunrise_time": "Define el amanecer virtual más temprano (HH:MM:SS), permitiendo amaneceres más tardíos. 🌅", - "sleep_rgb_or_color_temp": "Usar el modo`\"rgb_color\"` o `\"color_temp\"` en el modo noche. 🌙", - "autoreset_control_seconds": "Resetear automáticamente el control manual tras `X` segundos. Poner a 0 para deshabilitar.", - "brightness_mode": "Modo de brillo a usar. Valores posibles son: `default`, `linear` y `tanh` (usa`brightness_mode_time_dark` y `brightness_mode_time_light`). 📈", - "brightness_mode_time_light": "(Ignorado si `brightness_mode='default'`) La duración, en segundos, de la transición del brillo después/antes del amanecer/atardecer. 📈📉.", - "brightness_mode_time_dark": "(Ignorado si `brightness_mode='default'`) La duración, en segundos, de la transición del brillo después/antes del amanecer/atardecer. 📈📉.", - "sunset_time": "Fijar una hora (HH:MM:SS) para el atardecer. 🌇", - "min_sunset_time": "Define el atardecer virtual más temprano (HH:MM:SS), permitiendo atardeceres más tardíos. 🌇", - "adapt_delay": "Tiempo de espera (segundos) entre el encendido de la luz y Adaptive Lighting aplicando cambios. Puede ayudar a evitar parpadeos. ⏲️", - "take_over_control_mode": "El modo de pausa de adaptación cuando otras fuentes cambian el brillo y/o el color de las luces. `pause_all` siempre pausa tanto la adaptación de brillo como la de color. `pause_changed` pausa la adaptación solo de los atributos cambiados y continúa adaptando los atributos sin cambios, por ejemplo, continúa la adaptación de color cuando solo se cambió el brillo." + "transition": "Duración de la transición cuando las luces se adaptan, en segundos. ⏲️", + "sleep_brightness": "Porcentaje de brillo de las luces en el modo noche. 😴", + "sleep_color_temp": "Temperatura de color en modo noche (usado cuando`sleep_rgb_or_color_temp` es `color_temp`) en grados Kelvin. 😴" }, "data": { - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Al encender las luces. Si el valor es `true`, AL adapta sólo si se llama `light.turn_on` sin especificar el color o brillo. ❌🌈 Esto, por ejemplo, previene la adaptación al activar una escena. Si el valor es `false`, AL adapta independientemente de la presencia de color o brillo en `service_data` inicial. Necesita `take_over_control` habilitado. 🕵️", - "detect_non_ha_changes": "detect_non_ha_changes: Detecta e interrumpe adaptaciones para cambios de estado no `light.turn_on`. Necesita `take_over_control` habilitado. 🕵️ Precaución: ⚠️ Algunas luces pueden indicar de forma errónea un estado 'on', que puede resultar en luces que se enciendan de forma no esperada. Deshabilita esta función si encuentras dichos problemas.", - "intercept": "intercept: Intercepta y adapta llamadas a `light.turn_on` para habilitar adaptaciones instantáneas de color y brillo. 🏎️ Deshabilitar para luces que no soporten `light.turn_on` con color y brillo.", - "min_color_temp": "min_color_temp: Temperatura de color más cálida en grados Kelvin. 🔥", "lights": "lights: Lista de entity_ids de luces a controlar (puede estar vacía). 🌟", - "max_brightness": "max_brightness: Porcentaje máximo de brillo. 💡", - "max_color_temp": "max_color_temp: Temperatura de color más fría en grados Kelvin. ❄️", "min_brightness": "min_brightness: Porcentaje mínimo de brillo. 💡", - "prefer_rgb_color": "prefer_rgb_color: Preferir ajustar el color RGB a la temperatura de color cuando sea posible. 🌈", - "transition_until_sleep": "transition_until_sleep: Cuando habilitado, Adaptive Lighting tratará los ajustes del modo noche como los valores mínimos, transicionando a esos valores tras la puesta del sol. 🌙", - "include_config_in_attributes": "include_config_in_attributes: Muestra todas las opciones como atributes del interruptor en Home Assistant cuando sea `true`. 📝", - "multi_light_intercept": "multi_light_intercept: Intercepta y adapta llamadas a `light.turn_on` que apuntan a múltiples luces. ➗⚠️ Esto puede resultar en dividir una única llamada a `light.turn_on` en múltiples llamadas, por ejemplo, cuando las luces están vinculadas a distintos interruptores. Requiere que `intercept` esté habilitado.", - "only_once": "only_once: Adapta las luces sólo cuando se encienden (`true`) o mantener adaptadas (`false`). 🔄", - "separate_turn_on_commands": "separate_turn_on_commands: Usar llamadas independientes a `light.turn_on` para color y brillo, necesario para ciertos tipos de luces. 🔀", - "skip_redundant_commands": "skip_redundant_commands: Evitar mandar comandos de adaptación a luces cuyo estado ya sea el esperado. Reduce tráfico en la red y mejora la respuesta de la adaptación en ciertas situaciones. 📉Deshabilitar si el estado real de las luces se desincroniza con el estado registrado en Home Assistant.", - "take_over_control": "take_over_control: Deshabilita Adaptive Lighting si otra fuente llama`light.turn_on` mientras las luces están encendidas y adaptándose. Cuidado, esto llama`homeassistant.update_entity` cada `interval`! 🔒" + "max_brightness": "max_brightness: Porcentaje máximo de brillo. 💡", + "min_color_temp": "min_color_temp: Temperatura de color más cálida en grados Kelvin. 🔥", + "max_color_temp": "max_color_temp: Temperatura de color más fría en grados Kelvin. ❄️" }, - "description": "Configura un componente Adaptive Lighting. Los nombres de las opciones se asemejan a las disponibles en la configuración YAML. Si has definido esta entrada en YAML, no aparecerá ninguna opción aquí. Para gráficos interactivos que demuestran los efectos de los parámetros, visita [esta web app]({webapp_url}). Para más detalles, ver la [documentación oficial]({docs_url})." + "description": "Configura un componente Adaptive Lighting. Los nombres de las opciones se asemejan a las disponibles en la configuración YAML. Si has definido esta entrada en YAML, no aparecerá ninguna opción aquí. Para gráficos interactivos que demuestran los efectos de los parámetros, visita [esta web app]({webapp_url}). Para más detalles, ver la [documentación oficial]({docs_url}).", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: Preferir ajustar el color RGB a la temperatura de color cuando sea posible. 🌈", + "transition_until_sleep": "transition_until_sleep: Cuando habilitado, Adaptive Lighting tratará los ajustes del modo noche como los valores mínimos, transicionando a esos valores tras la puesta del sol. 🌙", + "take_over_control": "take_over_control: Deshabilita Adaptive Lighting si otra fuente llama`light.turn_on` mientras las luces están encendidas y adaptándose. Cuidado, esto llama`homeassistant.update_entity` cada `interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Detecta e interrumpe adaptaciones para cambios de estado no `light.turn_on`. Necesita `take_over_control` habilitado. 🕵️ Precaución: ⚠️ Algunas luces pueden indicar de forma errónea un estado 'on', que puede resultar en luces que se enciendan de forma no esperada. Deshabilita esta función si encuentras dichos problemas.", + "only_once": "only_once: Adapta las luces sólo cuando se encienden (`true`) o mantener adaptadas (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Al encender las luces. Si el valor es `true`, AL adapta sólo si se llama `light.turn_on` sin especificar el color o brillo. ❌🌈 Esto, por ejemplo, previene la adaptación al activar una escena. Si el valor es `false`, AL adapta independientemente de la presencia de color o brillo en `service_data` inicial. Necesita `take_over_control` habilitado. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Usar llamadas independientes a `light.turn_on` para color y brillo, necesario para ciertos tipos de luces. 🔀", + "skip_redundant_commands": "skip_redundant_commands: Evitar mandar comandos de adaptación a luces cuyo estado ya sea el esperado. Reduce tráfico en la red y mejora la respuesta de la adaptación en ciertas situaciones. 📉Deshabilitar si el estado real de las luces se desincroniza con el estado registrado en Home Assistant.", + "intercept": "intercept: Intercepta y adapta llamadas a `light.turn_on` para habilitar adaptaciones instantáneas de color y brillo. 🏎️ Deshabilitar para luces que no soporten `light.turn_on` con color y brillo.", + "multi_light_intercept": "multi_light_intercept: Intercepta y adapta llamadas a `light.turn_on` que apuntan a múltiples luces. ➗⚠️ Esto puede resultar en dividir una única llamada a `light.turn_on` en múltiples llamadas, por ejemplo, cuando las luces están vinculadas a distintos interruptores. Requiere que `intercept` esté habilitado.", + "include_config_in_attributes": "include_config_in_attributes: Muestra todas las opciones como atributes del interruptor en Home Assistant cuando sea `true`. 📝" + }, + "data_description": { + "initial_transition": "Duración de la primera transición cuando las luces pasan de `off` a `on` en segundos. ⏲️", + "sleep_rgb_or_color_temp": "Usar el modo`\"rgb_color\"` o `\"color_temp\"` en el modo noche. 🌙", + "sleep_rgb_color": "Color RGB en modo noche(usado cuando `sleep_rgb_or_color_temp` es \"rgb_color\"). 🌈", + "sleep_transition": "Duración de la transición cuando el \"modo noche\" se activa o desactiva, en segundos. 😴", + "sunrise_time": "Fijar una hora (HH:MM:SS) para el amanecer. 🌅", + "min_sunrise_time": "Define el amanecer virtual más temprano (HH:MM:SS), permitiendo amaneceres más tardíos. 🌅", + "max_sunrise_time": "Define el amanecer virtual más tardío (HH:MM:SS), permitiendo amaneceres más tempranos. 🌅", + "sunrise_offset": "Define la hora de la salida del sol con un desfase (positivo o negativo) en segundos. ⏰", + "sunset_time": "Fijar una hora (HH:MM:SS) para el atardecer. 🌇", + "min_sunset_time": "Define el atardecer virtual más temprano (HH:MM:SS), permitiendo atardeceres más tardíos. 🌇", + "max_sunset_time": "Define el atardecer virtual más tardío (HH:MM:SS), permitiendo atardeceres más tempranos. 🌇", + "sunset_offset": "Define la hora de la puesta del sol con un desfase (positivo o negativo) en segundos. ⏰", + "brightness_mode": "Modo de brillo a usar. Valores posibles son: `default`, `linear` y `tanh` (usa`brightness_mode_time_dark` y `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Ignorado si `brightness_mode='default'`) La duración, en segundos, de la transición del brillo después/antes del amanecer/atardecer. 📈📉.", + "brightness_mode_time_light": "(Ignorado si `brightness_mode='default'`) La duración, en segundos, de la transición del brillo después/antes del amanecer/atardecer. 📈📉.", + "take_over_control_mode": "El modo de pausa de adaptación cuando otras fuentes cambian el brillo y/o el color de las luces. `pause_all` siempre pausa tanto la adaptación de brillo como la de color. `pause_changed` pausa la adaptación solo de los atributos cambiados y continúa adaptando los atributos sin cambios, por ejemplo, continúa la adaptación de color cuando solo se cambió el brillo.", + "autoreset_control_seconds": "Resetear automáticamente el control manual tras `X` segundos. Poner a 0 para deshabilitar.", + "send_split_delay": "Retraso (ms) entre `separate_turn_on_commands` para luces que no soportan ajustes simultáneos de brillo y color. ⏲️", + "adapt_delay": "Tiempo de espera (segundos) entre el encendido de la luz y Adaptive Lighting aplicando cambios. Puede ayudar a evitar parpadeos. ⏲️" + } + } + } } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/et.json b/custom_components/adaptive_lighting/translations/et.json index b2599f2a..58c1c54d 100644 --- a/custom_components/adaptive_lighting/translations/et.json +++ b/custom_components/adaptive_lighting/translations/et.json @@ -21,25 +21,33 @@ "description": "Kohanduva valguse suvandid. Valikute nimetused ühtuvad YAML kirjes olevatega. Valikuid ei kuvata kui seadistus on tehtud YAML kirjes.", "data": { "lights": "valgustid", - "initial_transition": "Algne üleminek kui valgustid lülituvad sisse/välja või unerežiim muutub", "interval": "Intervall, aeg muutuste vahel sekundites", - "max_brightness": "Suurim heledus %", - "max_color_temp": "Suurim värvustemperatuur Kelvinites", + "transition": "Üleminekud, sekundites", "min_brightness": "Vähim heledus %", + "max_brightness": "Suurim heledus %", "min_color_temp": "Vähim värvustemperatuur Kelvinites", - "only_once": "Ainult üks kord, rakendub ainult valgusti sisselülitamisel", - "prefer_rgb_color": "Eelista RGB värve, võimalusel kasuta RGB sätteid värvustemperatuuri asemel", - "separate_turn_on_commands": "Eraldi lülitused iga valiku (värvus, heledus jne.) sisselülitamiseks, mõned valgustid vajavad seda.", + "max_color_temp": "Suurim värvustemperatuur Kelvinites", "sleep_brightness": "Unerežiimi heledus %", - "sleep_color_temp": "Uneržiimi värvus Kelvinites", - "sunrise_offset": "Nihe päikesetõusust, +/- sekundit", - "sunrise_time": "Päikesetõusu aeg 'HH:MM:SS' vormingus. (Kui jätta tühjaks kasutatakse asukohajärgset)", - "sunset_offset": "Nihe päikeseloojangust, +/- sekundit", - "sunset_time": "Päikeseloojangu aeg 'HH:MM:SS' vormingus. (Kui jätta tühjaks kasutatakse asukohajärgset)", - "take_over_control": "Käsitsi juhtimine: kui miski peale kohanduva valguse enda lültiab valgusti sisse ja see juba põleb, katkesta kohandamine kuni järgmise välise lülitamiseni.", - "detect_non_ha_changes": "Märka väliseid lülitusi: kui mõni säte muutub üle 10% (isegi väljaspoolt HA juhituna) siis peab käsitsi juhtimine olema lubatud (kutsutakse 'homeassistant.update_entity')'interval'!)", - "transition": "Üleminekud, sekundites" - } + "sleep_color_temp": "Uneržiimi värvus Kelvinites" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "Algne üleminek kui valgustid lülituvad sisse/välja või unerežiim muutub", + "prefer_rgb_color": "Eelista RGB värve, võimalusel kasuta RGB sätteid värvustemperatuuri asemel", + "sunrise_time": "Päikesetõusu aeg 'HH:MM:SS' vormingus. (Kui jätta tühjaks kasutatakse asukohajärgset)", + "sunrise_offset": "Nihe päikesetõusust, +/- sekundit", + "sunset_time": "Päikeseloojangu aeg 'HH:MM:SS' vormingus. (Kui jätta tühjaks kasutatakse asukohajärgset)", + "sunset_offset": "Nihe päikeseloojangust, +/- sekundit", + "take_over_control": "Käsitsi juhtimine: kui miski peale kohanduva valguse enda lültiab valgusti sisse ja see juba põleb, katkesta kohandamine kuni järgmise välise lülitamiseni.", + "detect_non_ha_changes": "Märka väliseid lülitusi: kui mõni säte muutub üle 10% (isegi väljaspoolt HA juhituna) siis peab käsitsi juhtimine olema lubatud (kutsutakse 'homeassistant.update_entity')'interval'!)", + "only_once": "Ainult üks kord, rakendub ainult valgusti sisselülitamisel", + "separate_turn_on_commands": "Eraldi lülitused iga valiku (värvus, heledus jne.) sisselülitamiseks, mõned valgustid vajavad seda." + }, + "data_description": {} + } + }, + "data_description": {} } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/fi.json b/custom_components/adaptive_lighting/translations/fi.json index 65233d1b..b8310c7c 100644 --- a/custom_components/adaptive_lighting/translations/fi.json +++ b/custom_components/adaptive_lighting/translations/fi.json @@ -138,49 +138,57 @@ "step": { "init": { "data_description": { - "autoreset_control_seconds": "Resetoi manuaalisen ohjauksen automaattisesti määritetyn sekuntimäärän jälkeen. Aseta arvoon 0 jos et halua käyttää asetusta.", - "sleep_brightness": "Valojen kirkkausmäärä prosenteissa unitilassa (sleep mode).", - "sunrise_offset": "Muuta auringonnousun aikaa positiivisella tai negatiivisella korjauksella määritettynä sekunneissa.", - "transition": "Valojen siirtymän kesto sekunneissa, kun valaistusta muutetaan.", - "brightness_mode": "Kirkkaus-moodi jota käytetään. Mahdolliset arvot ovat `default`, `linear`, and `tanh` (käyttää arvoja `brightness_mode_time_dark` ja `brightness_mode_time_light`).", - "sunset_offset": "Muuta auringonlaskun aikaa positiivisella tai negatiivisella korjauksella määritettynä sekunneissa.", - "initial_transition": "Ensimmäisen siirtymän kesto sekunneissa, kun valot kytketään 'off'-tilasta 'on'-tilaan.", - "send_split_delay": "Viive (ms) `separate_turn_on_commands` välillä valoille, jotka eivät tue yhtäaikaista kirkkauden ja värilämpötilan säätöä.", - "sleep_color_temp": "Värilämpötila lepotilassa (käytetään, kun `sleep_rgb_or_color_temp` on `color_temp`) kelvineinä. 😴", - "brightness_mode_time_dark": "(Ohitetaan, jos `brightness_mode='default'`) Kesto sekunteina kirkkauden lisäämiseen/vähentämiseen auringonnousun/auringonlaskun jälkeen/ennen. 📈📉.", - "adapt_delay": "Odotusaika (sekunteina) valon syttymisen ja Adaptive Lightingin muutosten käyttöönoton välillä. Saattaa auttaa välttämään välkkymistä. ⏲️", - "sleep_transition": "Siirtymän kesto, kun \"lepotila\" vaihdetaan sekunneiksi. 😴", "interval": "Tiheys valojen mukauttamiseen sekunneissa. 🔄", - "brightness_mode_time_light": "(Ohitetaan, jos `brightness_mode='default'`) Kesto sekunteina kirkkauden lisäämiseen/vähentämiseen auringonnousun/auringonlaskun jälkeen/ennen. 📈📉.", - "sleep_rgb_color": "RGB-väri lepotilassa (käytetään, kun `sleep_rgb_or_color_temp` on \"rgb_color\"). 🌈", - "sunrise_time": "Aseta kiinteä aika (TT:MM:SS) auringonnousulle. 🌅", - "sunset_time": "Aseta kiinteä aika (TT:MM:SS) auringonlaskulle. 🌇", - "min_sunset_time": "Aseta aikaisin virtuaalinen auringonlaskuaika (TT:MM:SS), myöhempiä auringonlaskuja sallien. 🌅", - "min_sunrise_time": "Aseta aikaisin virtuaalinen auringonnousuaika (TT:MM:SS), myöhempiä auringonnousuja sallien. 🌅", - "max_sunrise_time": "Aseta aikaisin virtuaalinen auringonnousuaika (TT:MM:SS), aikaisempia auringonnousuja sallien. 🌅", - "sleep_rgb_or_color_temp": "Käytä joko `\"rgb_color\"` tai `\"color_temp\"` lepotilassa. 🌙", - "max_sunset_time": "Aseta viimeisin virtuaalinen auringonlaskuaika (TT:MM:SS), jotta aikaisemmat auringonlaskut ovat mahdollisia. 🌇" + "transition": "Valojen siirtymän kesto sekunneissa, kun valaistusta muutetaan.", + "sleep_brightness": "Valojen kirkkausmäärä prosenteissa unitilassa (sleep mode).", + "sleep_color_temp": "Värilämpötila lepotilassa (käytetään, kun `sleep_rgb_or_color_temp` on `color_temp`) kelvineinä. 😴" }, "description": "Määritä Adaptive Lighting -komponentti. Vaihtoehtojen nimet vastaavat YAML-asetuksia. Jos olet määrittänyt tämän merkinnän YAML:ssa, tässä ei näy vaihtoehtoja. Interaktiiviset kaaviot, jotka esittelevät parametrien vaikutuksia, on [tässä verkkosovelluksessa]({webapp_url}). Lisätietoja löytyy [virallisesta dokumentaatiosta]({docs_url}).", "data": { - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Kun valot sytytetään ensimmäisen kerran. Jos asetuksena on \"true\", Adaptive Lighting mukautuu vain, jos \"light.turn_on\" kutsutaan määrittelemättä väriä tai kirkkautta. ❌🌈 Tämä esimerkiksi estää mukautumisen näkymää aktivoitaessa. Jos \"false\", Adaptive Lighting mukautuu riippumatta siitä, onko alkuperäisessä \"service_data\"-arvossa väri tai kirkkaus. Vaatii \"take_over_control\":n käyttöönoton. 🕵️", - "multi_light_intercept": "multi_light_intercept: sieppaa ja mukauta light.turn_on-kutsut, jotka kohdistuvat useisiin valoihin. ➗⚠️ Tämä saattaa johtaa yksittäisen light.turn_on-kutsun jakamiseen useiksi kutsuiksi, esimerkiksi kun valot ovat eri kytkimissä. Vaadi `intercept`:n käyttöönotto.", - "only_once": "only_once: Mukauta valot vain, kun ne ovat päällä (\"true\") tai mukauta niitä jatkuvasti (\"false\"). 🔄", - "skip_redundant_commands": "skip_redundant_commands: Ohita mukautuskomentojen lähettäminen, joiden kohdetila on jo yhtä suuri kuin valon tunnettu tila. Minimoi verkkoliikenteen ja parantaa mukautumisvastetta joissain tilanteissa. 📉 Poista käytöstä, jos fyysiset valotilat eivät ole synkronoitu kotiavustajan tallennetun tilan kanssa.", - "take_over_control": "take_over_control: Poista mukautuva valaistus käytöstä, jos toinen lähde kutsuu `light.turn_on`, kun valot ovat päällä ja niitä mukautetaan. Huomaa, että tämä kutsuu `homeassistant.update_entity` joka `interval`! 🔒", - "detect_non_ha_changes": "detect_non_ha_changes: Havaitsee ja pysäyttää mukautukset ei-\"light.turn_on\" -tilanmuutoksille. Vaatii \"take_over_control\" käyttöönoton. 🕵️ Varoitus: ⚠️ Jotkut valot saatavat osoittaa virheellisesti 'on'-tilan, mikä voi johtaa valojen syttymiseen odottamatta. Poista tämä ominaisuus käytöstä, jos kohtaat tällaisia ongelmia.", "lights": "lights: Luettelo ohjattavista valon entity_ids:stä (voi olla tyhjä). 🌟", - "max_brightness": "max_brightness: Enimmäiskirkkausprosentti. 💡", - "max_color_temp": "max_color_temp: Kylmin värilämpötila kelvineinä. ❄️", "min_brightness": "min_brightness: Vähittäiskirkkausprosentti. 💡", + "max_brightness": "max_brightness: Enimmäiskirkkausprosentti. 💡", "min_color_temp": "min_color_temp: Lämpimin värilämpötila kelvineinä. 🔥", - "prefer_rgb_color": "prefer_rgb_color: valitaanko RGB-värien säätö valon värilämpötilan sijaan, kun mahdollista. 🌈", - "transition_until_sleep": "shift_until_sleep: Kun käytössä, Adaptive Lighting käsittelee lepoasetukset miniminä ja siirtyy näihin arvoihin auringonlaskun jälkeen. 🌙", - "include_config_in_attributes": "include_config_in_attributes: Näytä kaikki vaihtoehdot attribuutteina Kotiavustajan kytkimessä, kun asetuksena on \"true\". 📝", - "intercept": "intercept: sieppaa ja mukauta \"light.turn_on\"-kutsut mahdollistamaan välitön värin ja kirkkauden mukauttaminen. 🏎️ Poista käytöstä valot, jotka eivät tue \"light.turn_on\" värin ja kirkkauden kanssa.", - "separate_turn_on_commands": "separate_turn_on_commands: Käytä erillisiä `light.turn_on`-kutsuja värin ja kirkkauden määrittämiseksi, joita tarvitaan joissakin valotyypeissä. 🔀" + "max_color_temp": "max_color_temp: Kylmin värilämpötila kelvineinä. ❄️" }, - "title": "Adaptive Lightingin vaihtoehdot" + "title": "Adaptive Lightingin vaihtoehdot", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: valitaanko RGB-värien säätö valon värilämpötilan sijaan, kun mahdollista. 🌈", + "transition_until_sleep": "shift_until_sleep: Kun käytössä, Adaptive Lighting käsittelee lepoasetukset miniminä ja siirtyy näihin arvoihin auringonlaskun jälkeen. 🌙", + "take_over_control": "take_over_control: Poista mukautuva valaistus käytöstä, jos toinen lähde kutsuu `light.turn_on`, kun valot ovat päällä ja niitä mukautetaan. Huomaa, että tämä kutsuu `homeassistant.update_entity` joka `interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Havaitsee ja pysäyttää mukautukset ei-\"light.turn_on\" -tilanmuutoksille. Vaatii \"take_over_control\" käyttöönoton. 🕵️ Varoitus: ⚠️ Jotkut valot saatavat osoittaa virheellisesti 'on'-tilan, mikä voi johtaa valojen syttymiseen odottamatta. Poista tämä ominaisuus käytöstä, jos kohtaat tällaisia ongelmia.", + "only_once": "only_once: Mukauta valot vain, kun ne ovat päällä (\"true\") tai mukauta niitä jatkuvasti (\"false\"). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Kun valot sytytetään ensimmäisen kerran. Jos asetuksena on \"true\", Adaptive Lighting mukautuu vain, jos \"light.turn_on\" kutsutaan määrittelemättä väriä tai kirkkautta. ❌🌈 Tämä esimerkiksi estää mukautumisen näkymää aktivoitaessa. Jos \"false\", Adaptive Lighting mukautuu riippumatta siitä, onko alkuperäisessä \"service_data\"-arvossa väri tai kirkkaus. Vaatii \"take_over_control\":n käyttöönoton. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Käytä erillisiä `light.turn_on`-kutsuja värin ja kirkkauden määrittämiseksi, joita tarvitaan joissakin valotyypeissä. 🔀", + "skip_redundant_commands": "skip_redundant_commands: Ohita mukautuskomentojen lähettäminen, joiden kohdetila on jo yhtä suuri kuin valon tunnettu tila. Minimoi verkkoliikenteen ja parantaa mukautumisvastetta joissain tilanteissa. 📉 Poista käytöstä, jos fyysiset valotilat eivät ole synkronoitu kotiavustajan tallennetun tilan kanssa.", + "intercept": "intercept: sieppaa ja mukauta \"light.turn_on\"-kutsut mahdollistamaan välitön värin ja kirkkauden mukauttaminen. 🏎️ Poista käytöstä valot, jotka eivät tue \"light.turn_on\" värin ja kirkkauden kanssa.", + "multi_light_intercept": "multi_light_intercept: sieppaa ja mukauta light.turn_on-kutsut, jotka kohdistuvat useisiin valoihin. ➗⚠️ Tämä saattaa johtaa yksittäisen light.turn_on-kutsun jakamiseen useiksi kutsuiksi, esimerkiksi kun valot ovat eri kytkimissä. Vaadi `intercept`:n käyttöönotto.", + "include_config_in_attributes": "include_config_in_attributes: Näytä kaikki vaihtoehdot attribuutteina Kotiavustajan kytkimessä, kun asetuksena on \"true\". 📝" + }, + "data_description": { + "initial_transition": "Ensimmäisen siirtymän kesto sekunneissa, kun valot kytketään 'off'-tilasta 'on'-tilaan.", + "sleep_rgb_or_color_temp": "Käytä joko `\"rgb_color\"` tai `\"color_temp\"` lepotilassa. 🌙", + "sleep_rgb_color": "RGB-väri lepotilassa (käytetään, kun `sleep_rgb_or_color_temp` on \"rgb_color\"). 🌈", + "sleep_transition": "Siirtymän kesto, kun \"lepotila\" vaihdetaan sekunneiksi. 😴", + "sunrise_time": "Aseta kiinteä aika (TT:MM:SS) auringonnousulle. 🌅", + "min_sunrise_time": "Aseta aikaisin virtuaalinen auringonnousuaika (TT:MM:SS), myöhempiä auringonnousuja sallien. 🌅", + "max_sunrise_time": "Aseta aikaisin virtuaalinen auringonnousuaika (TT:MM:SS), aikaisempia auringonnousuja sallien. 🌅", + "sunrise_offset": "Muuta auringonnousun aikaa positiivisella tai negatiivisella korjauksella määritettynä sekunneissa.", + "sunset_time": "Aseta kiinteä aika (TT:MM:SS) auringonlaskulle. 🌇", + "min_sunset_time": "Aseta aikaisin virtuaalinen auringonlaskuaika (TT:MM:SS), myöhempiä auringonlaskuja sallien. 🌅", + "max_sunset_time": "Aseta viimeisin virtuaalinen auringonlaskuaika (TT:MM:SS), jotta aikaisemmat auringonlaskut ovat mahdollisia. 🌇", + "sunset_offset": "Muuta auringonlaskun aikaa positiivisella tai negatiivisella korjauksella määritettynä sekunneissa.", + "brightness_mode": "Kirkkaus-moodi jota käytetään. Mahdolliset arvot ovat `default`, `linear`, and `tanh` (käyttää arvoja `brightness_mode_time_dark` ja `brightness_mode_time_light`).", + "brightness_mode_time_dark": "(Ohitetaan, jos `brightness_mode='default'`) Kesto sekunteina kirkkauden lisäämiseen/vähentämiseen auringonnousun/auringonlaskun jälkeen/ennen. 📈📉.", + "brightness_mode_time_light": "(Ohitetaan, jos `brightness_mode='default'`) Kesto sekunteina kirkkauden lisäämiseen/vähentämiseen auringonnousun/auringonlaskun jälkeen/ennen. 📈📉.", + "autoreset_control_seconds": "Resetoi manuaalisen ohjauksen automaattisesti määritetyn sekuntimäärän jälkeen. Aseta arvoon 0 jos et halua käyttää asetusta.", + "send_split_delay": "Viive (ms) `separate_turn_on_commands` välillä valoille, jotka eivät tue yhtäaikaista kirkkauden ja värilämpötilan säätöä.", + "adapt_delay": "Odotusaika (sekunteina) valon syttymisen ja Adaptive Lightingin muutosten käyttöönoton välillä. Saattaa auttaa välttämään välkkymistä. ⏲️" + } + } + } } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json index 495c5dae..4feaa9e8 100644 --- a/custom_components/adaptive_lighting/translations/fr.json +++ b/custom_components/adaptive_lighting/translations/fr.json @@ -25,55 +25,63 @@ "description": "Configurer un composant d'éclairage adaptatif. Les noms correspondent aux paramètres YAML. Si vous avez défini cette entrée en YAML, aucune option n'apparaît ici. Pour les graphiques interactifs qui montrent les effets des paramètres, visiter [cette application web]({webapp_url}). Pour plus de détail, voir la [documentation]({docs_url})", "data": { "lights": "lights : Liste d'\"entity_ids\" de lumières à controller (peu être vide). 🌟", - "initial_transition": "initial_transition : Transition (en secondes) lorsque l'état d'une lampe passe d'« éteinte » à « allumée ».", - "sleep_transition": "sleep_transition : Transition (en secondes) lorsque « sleep_state » est commuté.", "interval": "interval : Temps (en secondes) entre deux mises à jour du commutateur.", - "max_brightness": "max_brightness : Luminosité maximum (en pourcentage). 💡", - "max_color_temp": "max_color_temp : Couleur la plus froide (en Kelvins). ❄️", - "min_brightness": "min_brightness : Luminosité minimale en pourcentage. 💡", - "min_color_temp": "min_color_temp : Couleur de température la plus chaude en kelvins. 🔥", - "only_once": "only_once : Adapter les lampes uniquement au moment où elles sont allumées. 🔄", - "prefer_rgb_color": "prefer_rgb_color : Indique s'il est préférable d'utiliser le réglage de couleur RBG plutôt que la température de couleur lorsque cela est possible. 🌈", - "separate_turn_on_commands": "separate_turn_on_commands : Utiliser des appels \"light.turn_on\" séparés pour la couleur et la luminosité, nécessaires pour certains types de lumière. 🔀", - "sleep_brightness": "sleep_brightness : Luminosité (en pourcentage) du mode nuit.", - "sleep_color_temp": "sleep_color_temp : Température de couleur (en kelvins) du mode nuit.", - "sunrise_offset": "sunrise_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au lever du soleil.", - "sunrise_time": "sunrise_time : Heure (HH:MM:SS) du lever du soleil. Si « None », utilise l'heure correspondant à votre emplacement.", - "sunset_offset": "sunset_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au coucher du soleil.", - "sunset_time": "sunset_time : Heure (HH:MM:SS) du coucher du soleil. Si « None », utilise l'heure correspondant à votre emplacement.", - "take_over_control": "take_over_control : Désactive l'éclairage adaptatif si une autre source appelle \"light.turn_on\" pendant que la lumière est allumée ou adoptée. Notez que cela appelle \"homeassistant.update_entity\" chaque \"interval\"! 🔒", - "detect_non_ha_changes": "detect_non_ha_changes : Détecter et arrête les changement d'états autre que \"light.turn_on\". Nécessite que \"take_over_control\" soit activé. 🕵️ Attention : ⚠️ Certaines lumière peuvent faussement indiqué un état \"on\", ce qui pourrait occasionner des lumières s'allumant de façon inattendu. Désactivez cette fonctionnalité si vous rencontrez de tels problème.", "transition": "transition : Durée de la transition (en secondes) des changements appliqués aux lampes.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Lors de l'allumage initiale des lumières. Si le paramètre est \"vrai\", AL s'adapte uniquement si l'on invoque \"light.turn_on\" sans préciser la couleur ou la luminosité. ❌🌈 Ceci, par exemple, empêche l'adaptation lors de l'activation d'une scène. Si \"false\", AL adapte indépendamment de la présence de couleur ou de luminosité dans le \"service_data\" initial. \"take_over_control\" doit être activé. 🕵", - "multi_light_intercept": "multi_light_intercept : Intercepte et adapte les appels à \"light.turn_on\" qui ciblent plusieurs lumières. ➗⚠️ Cela peut entraîner la division d'un seul appel \"light.turn_on\" en plusieurs appels, par exemple, lorsque les lumières sont dans différents interrupteurs. Nécessite que \"intercept\" soit activé.", - "intercept": "intercept : Intercepter et adapter les appels à \"light.turn_on\" pour permettre une adaptation instantanée de la couleur et de la luminosité. 🏎️ Désactivez cette option pour les lumières qui ne prennent pas en charge \"light.turn_on\" avec couleur et luminosité.", - "include_config_in_attributes": "include_config_in_attributes : Afficher toutes les options en tant qu'attributs sur l'interrupteur dans Home Assistant lorsqu'il est défini sur \"true\". 📝", - "skip_redundant_commands": "skip_redundant_commands : Évite d'envoyer des commandes d'adaptation lorsque l'état cible est déjà égal à l'état connu de la lumière. Minimise le trafic réseau et améliore la réactivité de l'adaptation dans certaines situations. 📉 Désactivez si les états physiques des lumières ne correspondent pas à l'état enregistré de Home Assistant.", - "transition_until_sleep": "transition_until_sleep : Lorsque cela est activée, l'éclairage Adaptatif considérera les paramètres du mode nuit comme le minimum, effectuant la transition vers ces valeurs après le coucher du soleil. 🌙" + "min_brightness": "min_brightness : Luminosité minimale en pourcentage. 💡", + "max_brightness": "max_brightness : Luminosité maximum (en pourcentage). 💡", + "min_color_temp": "min_color_temp : Couleur de température la plus chaude en kelvins. 🔥", + "max_color_temp": "max_color_temp : Couleur la plus froide (en Kelvins). ❄️", + "sleep_brightness": "sleep_brightness : Luminosité (en pourcentage) du mode nuit.", + "sleep_color_temp": "sleep_color_temp : Température de couleur (en kelvins) du mode nuit." }, "data_description": { "interval": "Fréquence d'adaptation des lumières, en secondes. 🔄", - "sleep_brightness": "Pourcentage de luminosité des lumières en mode nuit. 😴", - "autoreset_control_seconds": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲️", - "sunset_offset": "Réglez le l'heure de coucher du soleil avec un décalage positif ou négatif en quelques secondes. ⏰", - "brightness_mode": "Mode de luminosité à utiliser. Les valeurs possibles sont \"défaut\" , \"linear\" (linéaire) et \"tanh\" (tangente) utilise \"brightness_mode_time_dark\" et \"brightness_mode_time_light\". 📈", - "send_split_delay": "Délai (ms) entre \"separate_turn_on_commands\" pour les lumières qui ne supportent pas la commande de luminosité et le réglage de couleur en même temps. ⏲️", - "sleep_color_temp": "Température de couleur en mode nuit en Kelvin (utilisée lorsque \"sleep_rgb_or_color_temp\" est égaler à \"color_temp\") . 😴", - "sunrise_offset": "Ajuster l'heure du lever de soleil avec un décalage positif ou négatif en secondes. ⏰", "transition": "Durée de la transition des changements lumineux, en secondes. 🕑", - "initial_transition": "Durée de la première transition des lampes passent de \"off\" à \"on\" (en secondes). ⏲️", - "sleep_transition": "Durée de la transition quand le \"mode nuit\" est déclenché. (en secondes) 😴", - "min_sunset_time": "Définir l'heure virtuelle de coucher du soleil la plus précoce (HH:MM:SS), permettant des couchers de soleil tardifs. 🌇", - "sleep_rgb_color": "Couleur RGB en mode nuit (utilisée lorsque \"sleep_rgb_or_color_temp\" est \"rgb_color\"). 🌈", - "brightness_mode_time_light": "(Ignoré si \"brightness_mode='default'\") La durée en secondes pour augmenter/diminuer progressivement la luminosité après/avant le lever/coucher du soleil. 📈📉.", - "sunset_time": "Définir une heure fixe (HH:MM:SS) pour le coucher du soleil. 🌇", - "sunrise_time": "Définir une heure fixe (HH:MM:SS) pour le lever du soleil. 🌅", - "brightness_mode_time_dark": "(Ignoré si \"brightness_mode='default'\") La durée en secondes pour augmenter/diminuer progressivement la luminosité après/avant le lever/coucher du soleil. 📈📉.", - "sleep_rgb_or_color_temp": "Utilisez soit \"rgb_color\" soit \"color_temp\" en mode nuit. 🌙", - "min_sunrise_time": "Définir l'heure virtuelle de lever du soleil la plus précoce (HH:MM:SS), permettant des levers de soleil tardifs. 🌅", - "adapt_delay": "Temps d'attente (en secondes) entre l'allumage de la lumière et l'application des changements par l'Éclairage Adaptatif. Peut aider à éviter les scintillements. ⏲️", - "max_sunset_time": "Définir l'heure virtuelle de coucher du soleil la plus tardive (HH:MM:SS), permettant des couchers de soleil plus précoces. 🌇", - "max_sunrise_time": "Définir l'heure virtuelle de lever du soleil la plus tardive (HH:MM:SS), permettant des levers de soleil plus tôt. 🌅" + "sleep_brightness": "Pourcentage de luminosité des lumières en mode nuit. 😴", + "sleep_color_temp": "Température de couleur en mode nuit en Kelvin (utilisée lorsque \"sleep_rgb_or_color_temp\" est égaler à \"color_temp\") . 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition : Transition (en secondes) lorsque l'état d'une lampe passe d'« éteinte » à « allumée ».", + "prefer_rgb_color": "prefer_rgb_color : Indique s'il est préférable d'utiliser le réglage de couleur RBG plutôt que la température de couleur lorsque cela est possible. 🌈", + "sleep_transition": "sleep_transition : Transition (en secondes) lorsque « sleep_state » est commuté.", + "transition_until_sleep": "transition_until_sleep : Lorsque cela est activée, l'éclairage Adaptatif considérera les paramètres du mode nuit comme le minimum, effectuant la transition vers ces valeurs après le coucher du soleil. 🌙", + "sunrise_time": "sunrise_time : Heure (HH:MM:SS) du lever du soleil. Si « None », utilise l'heure correspondant à votre emplacement.", + "sunrise_offset": "sunrise_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au lever du soleil.", + "sunset_time": "sunset_time : Heure (HH:MM:SS) du coucher du soleil. Si « None », utilise l'heure correspondant à votre emplacement.", + "sunset_offset": "sunset_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au coucher du soleil.", + "take_over_control": "take_over_control : Désactive l'éclairage adaptatif si une autre source appelle \"light.turn_on\" pendant que la lumière est allumée ou adoptée. Notez que cela appelle \"homeassistant.update_entity\" chaque \"interval\"! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes : Détecter et arrête les changement d'états autre que \"light.turn_on\". Nécessite que \"take_over_control\" soit activé. 🕵️ Attention : ⚠️ Certaines lumière peuvent faussement indiqué un état \"on\", ce qui pourrait occasionner des lumières s'allumant de façon inattendu. Désactivez cette fonctionnalité si vous rencontrez de tels problème.", + "only_once": "only_once : Adapter les lampes uniquement au moment où elles sont allumées. 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Lors de l'allumage initiale des lumières. Si le paramètre est \"vrai\", AL s'adapte uniquement si l'on invoque \"light.turn_on\" sans préciser la couleur ou la luminosité. ❌🌈 Ceci, par exemple, empêche l'adaptation lors de l'activation d'une scène. Si \"false\", AL adapte indépendamment de la présence de couleur ou de luminosité dans le \"service_data\" initial. \"take_over_control\" doit être activé. 🕵", + "separate_turn_on_commands": "separate_turn_on_commands : Utiliser des appels \"light.turn_on\" séparés pour la couleur et la luminosité, nécessaires pour certains types de lumière. 🔀", + "skip_redundant_commands": "skip_redundant_commands : Évite d'envoyer des commandes d'adaptation lorsque l'état cible est déjà égal à l'état connu de la lumière. Minimise le trafic réseau et améliore la réactivité de l'adaptation dans certaines situations. 📉 Désactivez si les états physiques des lumières ne correspondent pas à l'état enregistré de Home Assistant.", + "intercept": "intercept : Intercepter et adapter les appels à \"light.turn_on\" pour permettre une adaptation instantanée de la couleur et de la luminosité. 🏎️ Désactivez cette option pour les lumières qui ne prennent pas en charge \"light.turn_on\" avec couleur et luminosité.", + "multi_light_intercept": "multi_light_intercept : Intercepte et adapte les appels à \"light.turn_on\" qui ciblent plusieurs lumières. ➗⚠️ Cela peut entraîner la division d'un seul appel \"light.turn_on\" en plusieurs appels, par exemple, lorsque les lumières sont dans différents interrupteurs. Nécessite que \"intercept\" soit activé.", + "include_config_in_attributes": "include_config_in_attributes : Afficher toutes les options en tant qu'attributs sur l'interrupteur dans Home Assistant lorsqu'il est défini sur \"true\". 📝" + }, + "data_description": { + "initial_transition": "Durée de la première transition des lampes passent de \"off\" à \"on\" (en secondes). ⏲️", + "sleep_rgb_or_color_temp": "Utilisez soit \"rgb_color\" soit \"color_temp\" en mode nuit. 🌙", + "sleep_rgb_color": "Couleur RGB en mode nuit (utilisée lorsque \"sleep_rgb_or_color_temp\" est \"rgb_color\"). 🌈", + "sleep_transition": "Durée de la transition quand le \"mode nuit\" est déclenché. (en secondes) 😴", + "sunrise_time": "Définir une heure fixe (HH:MM:SS) pour le lever du soleil. 🌅", + "min_sunrise_time": "Définir l'heure virtuelle de lever du soleil la plus précoce (HH:MM:SS), permettant des levers de soleil tardifs. 🌅", + "max_sunrise_time": "Définir l'heure virtuelle de lever du soleil la plus tardive (HH:MM:SS), permettant des levers de soleil plus tôt. 🌅", + "sunrise_offset": "Ajuster l'heure du lever de soleil avec un décalage positif ou négatif en secondes. ⏰", + "sunset_time": "Définir une heure fixe (HH:MM:SS) pour le coucher du soleil. 🌇", + "min_sunset_time": "Définir l'heure virtuelle de coucher du soleil la plus précoce (HH:MM:SS), permettant des couchers de soleil tardifs. 🌇", + "max_sunset_time": "Définir l'heure virtuelle de coucher du soleil la plus tardive (HH:MM:SS), permettant des couchers de soleil plus précoces. 🌇", + "sunset_offset": "Réglez le l'heure de coucher du soleil avec un décalage positif ou négatif en quelques secondes. ⏰", + "brightness_mode": "Mode de luminosité à utiliser. Les valeurs possibles sont \"défaut\" , \"linear\" (linéaire) et \"tanh\" (tangente) utilise \"brightness_mode_time_dark\" et \"brightness_mode_time_light\". 📈", + "brightness_mode_time_dark": "(Ignoré si \"brightness_mode='default'\") La durée en secondes pour augmenter/diminuer progressivement la luminosité après/avant le lever/coucher du soleil. 📈📉.", + "brightness_mode_time_light": "(Ignoré si \"brightness_mode='default'\") La durée en secondes pour augmenter/diminuer progressivement la luminosité après/avant le lever/coucher du soleil. 📈📉.", + "autoreset_control_seconds": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲️", + "send_split_delay": "Délai (ms) entre \"separate_turn_on_commands\" pour les lumières qui ne supportent pas la commande de luminosité et le réglage de couleur en même temps. ⏲️", + "adapt_delay": "Temps d'attente (en secondes) entre l'allumage de la lumière et l'application des changements par l'Éclairage Adaptatif. Peut aider à éviter les scintillements. ⏲️" + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/gl.json b/custom_components/adaptive_lighting/translations/gl.json index 5a1f211c..17d23d0a 100644 --- a/custom_components/adaptive_lighting/translations/gl.json +++ b/custom_components/adaptive_lighting/translations/gl.json @@ -3,13 +3,21 @@ "step": { "init": { "data_description": { - "sleep_brightness": "Porcentaxe de brillo das luces en modo durmir. 😴", - "send_split_delay": "Retraso (ms) entre `separate_turn_on_commands`", - "sunrise_offset": "Axusta a hora do amencer cun desfasamento positivo ou negativo en segundos. ⏰", - "sunset_offset": "Axusta a hora da posta de sol cun desfasamento positivo ou negativo en segundos. ⏰", - "interval": "Frecuencia para adaptar as luces, en segundos. 🔄" + "interval": "Frecuencia para adaptar as luces, en segundos. 🔄", + "sleep_brightness": "Porcentaxe de brillo das luces en modo durmir. 😴" }, - "title": "Configuración de Iluminación Adaptativa" + "title": "Configuración de Iluminación Adaptativa", + "sections": { + "advanced": { + "data": {}, + "data_description": { + "sunrise_offset": "Axusta a hora do amencer cun desfasamento positivo ou negativo en segundos. ⏰", + "sunset_offset": "Axusta a hora da posta de sol cun desfasamento positivo ou negativo en segundos. ⏰", + "send_split_delay": "Retraso (ms) entre `separate_turn_on_commands`" + } + } + }, + "data": {} } } }, diff --git a/custom_components/adaptive_lighting/translations/hr.json b/custom_components/adaptive_lighting/translations/hr.json index 2f8e65ca..84301681 100644 --- a/custom_components/adaptive_lighting/translations/hr.json +++ b/custom_components/adaptive_lighting/translations/hr.json @@ -3,12 +3,18 @@ "step": { "init": { "title": "Opcije prilagodljivog osvjetljenja", - "data_description": { - "sunrise_offset": "Podesite vrijeme izlaska sunca s pozitivnim ili negativnim pomakom u sekundama. ⏰", - "sunset_offset": "Podesite vrijeme izlaska sunca s pozitivnim ili negativnim pomakom u sekundama. ⏰" - }, - "data": { - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Prilikom početnog paljenja svjetla. Ako je postavljeno na \"true\", AL se prilagođava samo ako se \"light.turn_on\" pozove bez navođenja boje ili svjetline. ❌🌈 Ovo npr. sprječava prilagodbu prilikom aktiviranja scene. Ako je \"false\", AL se prilagođava bez obzira na prisutnost boje ili svjetline u početnim \"service_data\". Potrebno je omogućiti `take_over_control`. 🕵️ " + "data_description": {}, + "data": {}, + "sections": { + "advanced": { + "data": { + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Prilikom početnog paljenja svjetla. Ako je postavljeno na \"true\", AL se prilagođava samo ako se \"light.turn_on\" pozove bez navođenja boje ili svjetline. ❌🌈 Ovo npr. sprječava prilagodbu prilikom aktiviranja scene. Ako je \"false\", AL se prilagođava bez obzira na prisutnost boje ili svjetline u početnim \"service_data\". Potrebno je omogućiti `take_over_control`. 🕵️ " + }, + "data_description": { + "sunrise_offset": "Podesite vrijeme izlaska sunca s pozitivnim ili negativnim pomakom u sekundama. ⏰", + "sunset_offset": "Podesite vrijeme izlaska sunca s pozitivnim ili negativnim pomakom u sekundama. ⏰" + } + } } } } diff --git a/custom_components/adaptive_lighting/translations/hu.json b/custom_components/adaptive_lighting/translations/hu.json index d6f49af9..deda5379 100644 --- a/custom_components/adaptive_lighting/translations/hu.json +++ b/custom_components/adaptive_lighting/translations/hu.json @@ -3,49 +3,57 @@ "step": { "init": { "data_description": { - "sleep_color_temp": "Színhőmérséklet alvó üzemmódban (amikor a `sleep_rgb_or_color_temp` értéke `color_temp`) Kelvinben megadva. 😴", - "sleep_rgb_or_color_temp": "Az `\"rgb_color\" vagy a `\"color_temp\" használata alvó üzemmódban. 🌙", - "sleep_transition": "Az transition időtartama az \"alvó üzemmód\" kapcsolásakor másodpercben. 😴", - "autoreset_control_seconds": "Automatikusan visszaállítja a kézi vezérlést néhány másodperc után. A letiltáshoz állítsa 0-ra. ⏲️", - "min_sunset_time": "Állítsa be a legkorábbi virtuális naplemente időpontját (HH:MM:SS), lehetővé téve a későbbi naplementéket. 🌇", - "sleep_brightness": "Az alvó üzemmódban lévő lights fényerejének százalékos értéke. 😴", - "min_sunrise_time": "Állítsa be a legkorábbi virtuális napfelkelte időpontját (HH:MM:SS), lehetővé téve a későbbi napfelkeltét. 🌅", "interval": "Gyakoriság a lights illesztéséhez, másodpercekben. 🔄", - "adapt_delay": "Várakozási idő (másodpercben) a világítás bekapcsolása és az Adaptív világítás alkalmazása között. Segíthet elkerülni a villódzást. ⏲️", - "sleep_rgb_color": "RGB szín alvó üzemmódban (akkor érvényes, ha a `sleep_rgb_or_color_temp` értéke \"rgb_color\"). 🌈", - "sunrise_offset": "A napfelkelte idejének beállítása pozitív vagy negatív eltolással másodpercekben. ⏰", "transition": "Az transition időtartama, amikor a lights változnak, másodpercben. 🕑", - "brightness_mode": "Használandó fényerő üzemmód. A lehetséges értékek: `default`, `linear` és `tanh` (a `brightness_mode_time_dark` és `brightness_mode_time_light` értékeket használja). 📈", - "brightness_mode_time_light": "(Figyelmen kívül hagyva, ha `brightness_mode='default'`) A fényerő növelésének/csökkentésének időtartama másodpercben napfelkelte/napnyugta után/előtt. 📈📉.", - "sunset_offset": "A naplemente idejének beállítása pozitív vagy negatív eltolással másodpercekben. ⏰", - "sunset_time": "Állítson be egy fix időpontot (HH:MM:SS) a naplementéhez. 🌇", - "max_sunset_time": "A legkésőbbi virtuális napnyugta időpontjának beállítása (HH:MM:SS), amely lehetővé teszi a korábbi naplementéket. 🌇", - "sunrise_time": "Állítson be egy fix időpontot (HH:MM:SS) a napfelkeltéhez. 🌅", - "initial_transition": "Az első transition időtartama, amikor a lights \"kikapcsolt\" állapotból \"bekapcsolt\" állapotba váltanak, másodpercben. ⏲️", - "brightness_mode_time_dark": "(Figyelmen kívül hagyva, ha `brightness_mode='default'`) A fényerő növelésének/csökkentésének időtartama másodpercben napfelkelte/napnyugta után/előtt. 📈📉.", - "max_sunrise_time": "A legkésőbbi virtuális napfelkelte időpontjának beállítása (HH:MM:SS), amely lehetővé teszi a korábbi napfelkeltét. 🌅", - "send_split_delay": "Késleltetés (ms-ban) a `separate_turn_on_commands` (különálló_bekapcsolási_parancsok) között olyan lights esetében, amelyek nem támogatják a fényerő és a szín egyidejű beállítását. ⏲️" + "sleep_brightness": "Az alvó üzemmódban lévő lights fényerejének százalékos értéke. 😴", + "sleep_color_temp": "Színhőmérséklet alvó üzemmódban (amikor a `sleep_rgb_or_color_temp` értéke `color_temp`) Kelvinben megadva. 😴" }, "data": { - "max_brightness": "max_brightness: Maximális fényerő százalékban megadva. 💡", - "detect_non_ha_changes": "detect_non_ha_changes: `Világítás: Bekapcsolás`- szolgáltatás meghívástól eltérő állapotváltozások esetén észleli és leállítja az illesztéseket. A `take_over_control` beállítás engedélyezése szükséges. 🕵️ Vigyázat: ⚠️ Egyes lights tévesen jelezhetik a \"bekapcsolt\" állapotot, ami váratlanul bekapcsolódó lámpákhoz vezethet. Ha ilyen problémákat tapasztal, tiltsa le ezt a funkciót.", - "multi_light_intercept": "multi_light_intercept: `Világítás: Bekapcsolás` szolgáltatás hívások elfogása és adaptálása, amelyek több fényt céloznak meg. ➗⚠️ Ez azt eredményezheti, hogy egyetlen `Világítás: Bekapcsolás` szolgáltatás hívás több hívásra oszlik fel, pl. ha a lights különböző kapcsolókban vannak. Az `elfogás` engedélyezése szükséges.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Kizárólag a bekapcsoláskor érvényes. A beállítást \"igaz\"-ra állítva, az AL csak akkor végzi az illesztést, amennyiben a \"Világítás: Bekapcsolás\" szolgáltatás meghívása a szín és fényerő paraméterek megadása nélkül történik.❌🌈 Ez pl. alkalmas az illesztés felfüggesztésére egy jelenet aktiválásakor. \"Hamis\" beállítás esetén az AL elvégzi a kezdeti illesztést a szín és fényerő paraméterek meghívásától függetlenül. A használatához engedélyezve kell lennie a \"take_over_control\" beállításnak. 🕵️", - "skip_redundant_commands": "skip_redundant_commands: Az olyan adaptációs parancsok küldésének kihagyása, amelyek célállapota már megegyezik a fény ismert állapotával. Minimalizálja a hálózati forgalmat, és bizonyos helyzetekben javítja az adaptációs reakciókészséget. 📉Kapcsolja ki, ha a fizikai fényállapotok nem szinkronizálódnak a HA rögzített állapotával.", - "separate_turn_on_commands": "separate_turn_on_commands: Elkülönített `Világítás: Bekapcsolás` hívásokat használ a szín és a fényerő beállításához, ami néhány világítás típusnál szükséges. 🔀", - "max_color_temp": "max_color_temp: A leghidegebb színhőmérséklet kelvinben. ❄️", - "prefer_rgb_color": "prefer_rgb_color: Lehetőség szerint az RGB színbeállítás előnyben részesítése a fény színhőmérsékletével szemben. 🌈", - "intercept": "elfogás: `Világítás: Bekapcsolás` szolgáltatás hívások elfogása és adaptálása a színek és a fényerő azonnali illesztésének lehetővé tétele érdekében. 🏎️ Letiltja az olyan lights esetében, amelyek nem támogatják a `Világítás: Bekapcsolás` szolgáltatás színnel és fényerővel történő szín- és fényerőszabályozását.", - "only_once": "only_once: lights illesztése kizárólag, amikor azok be vannak kapcsolva (`igaz`) vagy tartsa folyamatosan illesztve őket (`false`). 🔄", - "take_over_control": "take_over_control: Adaptív világítás kikapcsolása, amennyiben más forrásból érkező `Világítás: Bekapcsolás` szolgáltatás hívás történik, miközben a fények be vannak kapcsolva és illesztve vannak. Vegye figyelembe, hogy ez minden `interval`-ban meghívja a `homeassistant.update_entity`-t! 🔒", "lights": "lights: Az entity_id-k listája, amelyeket az AL vezéreljen (üresen is maradhat).🌟", "min_brightness": "min_brightness: Minimális fényerő százalékban. 💡", + "max_brightness": "max_brightness: Maximális fényerő százalékban megadva. 💡", "min_color_temp": "min_color_temp: A legmelegebb színhőmérséklet Kelvinben. 🔥", - "transition_until_sleep": "transition_until_sleep: Ha engedélyezve van, az Adaptív világítás az alvó mód beállításokat minimálisnak tekinti, és napnyugta után ezekre az értékekre vált át. 🌙", - "include_config_in_attributes": "include_config_in_attributes: A kapcsoló összes opciójának attribútumként való megjelenítése a Home Assistantben, ha a beállítás értéke `igaz`. 📝" + "max_color_temp": "max_color_temp: A leghidegebb színhőmérséklet kelvinben. ❄️" }, "title": "Adaptív világítás beállításai", - "description": "Egy Adaptív világítás komponens konfigurálása. Az opciók nevei a YAML-beállításokhoz igazodnak. Ha ezt a bejegyzést YAML-ben definiálta, itt nem jelennek meg beállítások. A paraméterek hatásait bemutató interaktív grafikonokért látogasson el [erre a webes alkalmazásra]({webapp_url}). További részletekért olvasd el a [hivatalos dokumentációt]({docs_url})." + "description": "Egy Adaptív világítás komponens konfigurálása. Az opciók nevei a YAML-beállításokhoz igazodnak. Ha ezt a bejegyzést YAML-ben definiálta, itt nem jelennek meg beállítások. A paraméterek hatásait bemutató interaktív grafikonokért látogasson el [erre a webes alkalmazásra]({webapp_url}). További részletekért olvasd el a [hivatalos dokumentációt]({docs_url}).", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: Lehetőség szerint az RGB színbeállítás előnyben részesítése a fény színhőmérsékletével szemben. 🌈", + "transition_until_sleep": "transition_until_sleep: Ha engedélyezve van, az Adaptív világítás az alvó mód beállításokat minimálisnak tekinti, és napnyugta után ezekre az értékekre vált át. 🌙", + "take_over_control": "take_over_control: Adaptív világítás kikapcsolása, amennyiben más forrásból érkező `Világítás: Bekapcsolás` szolgáltatás hívás történik, miközben a fények be vannak kapcsolva és illesztve vannak. Vegye figyelembe, hogy ez minden `interval`-ban meghívja a `homeassistant.update_entity`-t! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: `Világítás: Bekapcsolás`- szolgáltatás meghívástól eltérő állapotváltozások esetén észleli és leállítja az illesztéseket. A `take_over_control` beállítás engedélyezése szükséges. 🕵️ Vigyázat: ⚠️ Egyes lights tévesen jelezhetik a \"bekapcsolt\" állapotot, ami váratlanul bekapcsolódó lámpákhoz vezethet. Ha ilyen problémákat tapasztal, tiltsa le ezt a funkciót.", + "only_once": "only_once: lights illesztése kizárólag, amikor azok be vannak kapcsolva (`igaz`) vagy tartsa folyamatosan illesztve őket (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Kizárólag a bekapcsoláskor érvényes. A beállítást \"igaz\"-ra állítva, az AL csak akkor végzi az illesztést, amennyiben a \"Világítás: Bekapcsolás\" szolgáltatás meghívása a szín és fényerő paraméterek megadása nélkül történik.❌🌈 Ez pl. alkalmas az illesztés felfüggesztésére egy jelenet aktiválásakor. \"Hamis\" beállítás esetén az AL elvégzi a kezdeti illesztést a szín és fényerő paraméterek meghívásától függetlenül. A használatához engedélyezve kell lennie a \"take_over_control\" beállításnak. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Elkülönített `Világítás: Bekapcsolás` hívásokat használ a szín és a fényerő beállításához, ami néhány világítás típusnál szükséges. 🔀", + "skip_redundant_commands": "skip_redundant_commands: Az olyan adaptációs parancsok küldésének kihagyása, amelyek célállapota már megegyezik a fény ismert állapotával. Minimalizálja a hálózati forgalmat, és bizonyos helyzetekben javítja az adaptációs reakciókészséget. 📉Kapcsolja ki, ha a fizikai fényállapotok nem szinkronizálódnak a HA rögzített állapotával.", + "intercept": "elfogás: `Világítás: Bekapcsolás` szolgáltatás hívások elfogása és adaptálása a színek és a fényerő azonnali illesztésének lehetővé tétele érdekében. 🏎️ Letiltja az olyan lights esetében, amelyek nem támogatják a `Világítás: Bekapcsolás` szolgáltatás színnel és fényerővel történő szín- és fényerőszabályozását.", + "multi_light_intercept": "multi_light_intercept: `Világítás: Bekapcsolás` szolgáltatás hívások elfogása és adaptálása, amelyek több fényt céloznak meg. ➗⚠️ Ez azt eredményezheti, hogy egyetlen `Világítás: Bekapcsolás` szolgáltatás hívás több hívásra oszlik fel, pl. ha a lights különböző kapcsolókban vannak. Az `elfogás` engedélyezése szükséges.", + "include_config_in_attributes": "include_config_in_attributes: A kapcsoló összes opciójának attribútumként való megjelenítése a Home Assistantben, ha a beállítás értéke `igaz`. 📝" + }, + "data_description": { + "initial_transition": "Az első transition időtartama, amikor a lights \"kikapcsolt\" állapotból \"bekapcsolt\" állapotba váltanak, másodpercben. ⏲️", + "sleep_rgb_or_color_temp": "Az `\"rgb_color\" vagy a `\"color_temp\" használata alvó üzemmódban. 🌙", + "sleep_rgb_color": "RGB szín alvó üzemmódban (akkor érvényes, ha a `sleep_rgb_or_color_temp` értéke \"rgb_color\"). 🌈", + "sleep_transition": "Az transition időtartama az \"alvó üzemmód\" kapcsolásakor másodpercben. 😴", + "sunrise_time": "Állítson be egy fix időpontot (HH:MM:SS) a napfelkeltéhez. 🌅", + "min_sunrise_time": "Állítsa be a legkorábbi virtuális napfelkelte időpontját (HH:MM:SS), lehetővé téve a későbbi napfelkeltét. 🌅", + "max_sunrise_time": "A legkésőbbi virtuális napfelkelte időpontjának beállítása (HH:MM:SS), amely lehetővé teszi a korábbi napfelkeltét. 🌅", + "sunrise_offset": "A napfelkelte idejének beállítása pozitív vagy negatív eltolással másodpercekben. ⏰", + "sunset_time": "Állítson be egy fix időpontot (HH:MM:SS) a naplementéhez. 🌇", + "min_sunset_time": "Állítsa be a legkorábbi virtuális naplemente időpontját (HH:MM:SS), lehetővé téve a későbbi naplementéket. 🌇", + "max_sunset_time": "A legkésőbbi virtuális napnyugta időpontjának beállítása (HH:MM:SS), amely lehetővé teszi a korábbi naplementéket. 🌇", + "sunset_offset": "A naplemente idejének beállítása pozitív vagy negatív eltolással másodpercekben. ⏰", + "brightness_mode": "Használandó fényerő üzemmód. A lehetséges értékek: `default`, `linear` és `tanh` (a `brightness_mode_time_dark` és `brightness_mode_time_light` értékeket használja). 📈", + "brightness_mode_time_dark": "(Figyelmen kívül hagyva, ha `brightness_mode='default'`) A fényerő növelésének/csökkentésének időtartama másodpercben napfelkelte/napnyugta után/előtt. 📈📉.", + "brightness_mode_time_light": "(Figyelmen kívül hagyva, ha `brightness_mode='default'`) A fényerő növelésének/csökkentésének időtartama másodpercben napfelkelte/napnyugta után/előtt. 📈📉.", + "autoreset_control_seconds": "Automatikusan visszaállítja a kézi vezérlést néhány másodperc után. A letiltáshoz állítsa 0-ra. ⏲️", + "send_split_delay": "Késleltetés (ms-ban) a `separate_turn_on_commands` (különálló_bekapcsolási_parancsok) között olyan lights esetében, amelyek nem támogatják a fényerő és a szín egyidejű beállítását. ⏲️", + "adapt_delay": "Várakozási idő (másodpercben) a világítás bekapcsolása és az Adaptív világítás alkalmazása között. Segíthet elkerülni a villódzást. ⏲️" + } + } + } } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/id.json b/custom_components/adaptive_lighting/translations/id.json index 6e670c1a..6d4d8fcf 100644 --- a/custom_components/adaptive_lighting/translations/id.json +++ b/custom_components/adaptive_lighting/translations/id.json @@ -137,49 +137,57 @@ "step": { "init": { "data_description": { - "sleep_rgb_or_color_temp": "Gunakan `\"rgb_color\"` atau `\"color_temp\"` dalam mode tidur. 🌙", - "sleep_color_temp": "Suhu warna dalam mode tidur (digunakan ketika `sleep_rgb_or_color_temp` adalah `color_temp`) dalam Kelvin. 😴", - "sleep_transition": "Durasi transisi ketika \"mode tidur\" diubah, dalam hitungan detik. 😴", - "autoreset_control_seconds": "Secara otomatis mengatur ulang kontrol manual setelah beberapa detik. Setel ke 0 untuk menonaktifkan. ⏲️", - "min_sunset_time": "Tetapkan waktu matahari terbenam virtual paling awal (HH:MM:SS), memungkinkan matahari terbenam di kemudian waktu. 🌇", - "sleep_brightness": "Persentase kecerahan lampu dalam mode tidur. 😴", - "min_sunrise_time": "Tetapkan waktu matahari terbit virtual paling awal (HH:MM:SS), memungkinkan matahari terbit di kemudian waktu. 🌅", "interval": "Frekuensi untuk menyesuaikan lampu, dalam hitungan detik. 🔄", - "adapt_delay": "Waktu tunggu (detik) antara lampu menyala dan penerapan ubahan Pencahayaan Adaptif. Mungkin membantu untuk menghindari kedipan. ⏲️", - "sleep_rgb_color": "Warna RGB dalam mode tidur (digunakan ketika `sleep_rgb_or_color_temp` adalah \"rgb_color\"). 🌈", - "sunrise_offset": "Sesuaikan waktu matahari terbit dengan offset positif atau negatif dalam hitungan detik. ⏰", "transition": "Durasi transisi saat lampu berganti, dalam hitungan detik. 🕑", - "brightness_mode": "Mode kecerahan untuk digunakan. Nilai yang memungkinkan adalah `default`, `linear`, dan `tanh` (menggunakan `brightness_mode_time_dark` dan `brightness_mode_time_light`). 📈", - "brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) Durasi dalam hitungan detik untuk meningkatkan/menurunkan kecerahan setelah/sebelum matahari terbit/terbenam. 📈📉.", - "sunset_offset": "Sesuaikan waktu matahari terbenam dengan offset positif atau negatif dalam hitungan detik. ⏰", - "sunset_time": "Tetapkan waktu tetap (HH:MM:SS) untuk matahari terbenam. 🌇", - "max_sunset_time": "Atur waktu matahari terbenam virtual terkini (HH:MM:SS), memungkinkan matahari terbenam lebih cepat. 🌇", - "sunrise_time": "Tetapkan waktu tetap (HH:MM:SS) untuk matahari terbit. 🌅", - "initial_transition": "Durasi transisi pertama saat lampu berubah dari `mati` ke `hidup` dalam hitungan detik. ⏲️", - "brightness_mode_time_dark": "(Diabaikan jika `brightness_mode='default'`) Durasi dalam hitungan detik untuk meningkatkan/menurunkan kecerahan sebelum/sesudah matahari terbit/terbenam. 📈📉", - "max_sunrise_time": "Atur waktu matahari terbit virtual terkini (HH:MM:SS), memungkinkan matahari terbit lebih cepat. 🌅", - "send_split_delay": "Waktu tunda (ms) antara `separate_turn_on_commands` untuk lampu yang tidak mendukung pengaturan kecerahan dan warna secara bersamaan. ⏲️" + "sleep_brightness": "Persentase kecerahan lampu dalam mode tidur. 😴", + "sleep_color_temp": "Suhu warna dalam mode tidur (digunakan ketika `sleep_rgb_or_color_temp` adalah `color_temp`) dalam Kelvin. 😴" }, "data": { - "detect_non_ha_changes": "detect_non_ha_changes: Mendeteksi dan menghentikan adaptasi untuk perubahan status non-`light.turn_on`. Perlu mengaktifkan `take_over_control`. 🕵️ Perhatian: ⚠️ Beberapa lampu mungkin salah menunjukkan status 'hidup' yang dapat mengakibatkan lampu menyala secara tidak terduga. Nonaktifkan fitur ini jika Anda mengalami masalah seperti itu.", - "multi_light_intercept": "multi_light_intercept: Cegat dan sesuaikan panggilan `light.turn_on` yang menargetkan banyak lampu. ➗⚠️ Hal ini dapat mengakibatkan satu panggilan `light.turn_on` terpecah menjadi beberapa panggilan, misalnya saat lampu berada di sakelar yang berbeda. Membutuhkan `intercept` untuk diaktifkan.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Saat menyalakan lampu pada awalnya. Jika diatur ke `true`, AL hanya beradaptasi jika `light.turn_on` dipanggil tanpa menentukan warna atau kecerahan. ❌🌈 Misalnya, mencegah adaptasi ketika mengaktifkan scene. Jika `false`, AL akan beradaptasi tanpa menghiraukan keberadaan warna atau kecerahan dalam `service_data` awal. Perlu `take_over_control` diaktifkan. 🕵️", - "skip_redundant_commands": "skip_redundant_commands: Lewati pengiriman perintah adaptasi yang status targetnya sudah sama dengan status cahaya yang diketahui. Meminimalkan lalu lintas jaringan dan meningkatkan respons adaptasi dalam beberapa situasi. 📉Nonaktifkan jika status cahaya fisik tidak sinkron dengan status rekaman HA.", - "separate_turn_on_commands": "separate_turn_on_commands: Gunakan panggilan `light.turn_on` terpisah untuk warna dan kecerahan, diperlukan untuk beberapa jenis lampu. 🔀", - "max_color_temp": "max_color_temp: Suhu warna terdingin dalam Kelvin. ❄️", - "prefer_rgb_color": "prefer_rgb_color: Kalau lebih memilih penyesuaian warna RGB dibandingkan suhu warna terang jika memungkinkan. 🌈", - "max_brightness": "max_brightness: Persentase kecerahan maksimum. 💡", - "intercept": "intercept: Cegat dan sesuaikan panggilan `light.turn_on` untuk mengaktifkan adaptasi warna dan kecerahan seketika. 🏎️ Nonaktifkan untuk lampu yang tidak mendukung `light.turn_on` dengan warna dan kecerahan.", - "only_once": "only_once: Sesuaikan lampu hanya saat menyala (`true`) atau terus sesuaikan (`false`). 🔄", - "take_over_control": "take_over_control: Nonaktifkan Pencahayaan Adaptif jika sumber lain memanggil `light.turn_on` saat lampu menyala dan sedang diadaptasi. Perhatikan bahwa ini memanggil `homeassistant.update_entity` setiap `interval`! 🔒", "lights": "lights: Daftar entity_ids lampu yang akan dikontrol (boleh kosong). 🌟", "min_brightness": "min_brightness: Persentase kecerahan minimum. 💡", + "max_brightness": "max_brightness: Persentase kecerahan maksimum. 💡", "min_color_temp": "min_color_temp: Suhu warna terhangat dalam Kelvin. 🔥", - "transition_until_sleep": "transition_until_sleep: Jika diaktifkan, Pencahayaan Adaptif akan menganggap pengaturan tidur sebagai minimum, dan beralih ke nilai ini setelah matahari terbenam. 🌙", - "include_config_in_attributes": "include_config_in_attributes: Tampilkan semua opsi sebagai atribut pada sakelar di Home Assistant ketika diatur ke `true`. 📝" + "max_color_temp": "max_color_temp: Suhu warna terdingin dalam Kelvin. ❄️" }, "title": "Opsi Pencahayaan Adaptif", - "description": "Konfigurasikan komponen Pencahayaan Adaptif. Nama opsi selaras dengan pengaturan YAML. Jika Anda telah menentukan entri ini di YAML, tidak ada opsi yang akan muncul di sini. Untuk grafik interaktif yang menunjukkan efek parameter, kunjungi [aplikasi web ini]({webapp_url}). Untuk detail lebih lanjut, lihat [dokumentasi resmi]({docs_url})." + "description": "Konfigurasikan komponen Pencahayaan Adaptif. Nama opsi selaras dengan pengaturan YAML. Jika Anda telah menentukan entri ini di YAML, tidak ada opsi yang akan muncul di sini. Untuk grafik interaktif yang menunjukkan efek parameter, kunjungi [aplikasi web ini]({webapp_url}). Untuk detail lebih lanjut, lihat [dokumentasi resmi]({docs_url}).", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: Kalau lebih memilih penyesuaian warna RGB dibandingkan suhu warna terang jika memungkinkan. 🌈", + "transition_until_sleep": "transition_until_sleep: Jika diaktifkan, Pencahayaan Adaptif akan menganggap pengaturan tidur sebagai minimum, dan beralih ke nilai ini setelah matahari terbenam. 🌙", + "take_over_control": "take_over_control: Nonaktifkan Pencahayaan Adaptif jika sumber lain memanggil `light.turn_on` saat lampu menyala dan sedang diadaptasi. Perhatikan bahwa ini memanggil `homeassistant.update_entity` setiap `interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Mendeteksi dan menghentikan adaptasi untuk perubahan status non-`light.turn_on`. Perlu mengaktifkan `take_over_control`. 🕵️ Perhatian: ⚠️ Beberapa lampu mungkin salah menunjukkan status 'hidup' yang dapat mengakibatkan lampu menyala secara tidak terduga. Nonaktifkan fitur ini jika Anda mengalami masalah seperti itu.", + "only_once": "only_once: Sesuaikan lampu hanya saat menyala (`true`) atau terus sesuaikan (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Saat menyalakan lampu pada awalnya. Jika diatur ke `true`, AL hanya beradaptasi jika `light.turn_on` dipanggil tanpa menentukan warna atau kecerahan. ❌🌈 Misalnya, mencegah adaptasi ketika mengaktifkan scene. Jika `false`, AL akan beradaptasi tanpa menghiraukan keberadaan warna atau kecerahan dalam `service_data` awal. Perlu `take_over_control` diaktifkan. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Gunakan panggilan `light.turn_on` terpisah untuk warna dan kecerahan, diperlukan untuk beberapa jenis lampu. 🔀", + "skip_redundant_commands": "skip_redundant_commands: Lewati pengiriman perintah adaptasi yang status targetnya sudah sama dengan status cahaya yang diketahui. Meminimalkan lalu lintas jaringan dan meningkatkan respons adaptasi dalam beberapa situasi. 📉Nonaktifkan jika status cahaya fisik tidak sinkron dengan status rekaman HA.", + "intercept": "intercept: Cegat dan sesuaikan panggilan `light.turn_on` untuk mengaktifkan adaptasi warna dan kecerahan seketika. 🏎️ Nonaktifkan untuk lampu yang tidak mendukung `light.turn_on` dengan warna dan kecerahan.", + "multi_light_intercept": "multi_light_intercept: Cegat dan sesuaikan panggilan `light.turn_on` yang menargetkan banyak lampu. ➗⚠️ Hal ini dapat mengakibatkan satu panggilan `light.turn_on` terpecah menjadi beberapa panggilan, misalnya saat lampu berada di sakelar yang berbeda. Membutuhkan `intercept` untuk diaktifkan.", + "include_config_in_attributes": "include_config_in_attributes: Tampilkan semua opsi sebagai atribut pada sakelar di Home Assistant ketika diatur ke `true`. 📝" + }, + "data_description": { + "initial_transition": "Durasi transisi pertama saat lampu berubah dari `mati` ke `hidup` dalam hitungan detik. ⏲️", + "sleep_rgb_or_color_temp": "Gunakan `\"rgb_color\"` atau `\"color_temp\"` dalam mode tidur. 🌙", + "sleep_rgb_color": "Warna RGB dalam mode tidur (digunakan ketika `sleep_rgb_or_color_temp` adalah \"rgb_color\"). 🌈", + "sleep_transition": "Durasi transisi ketika \"mode tidur\" diubah, dalam hitungan detik. 😴", + "sunrise_time": "Tetapkan waktu tetap (HH:MM:SS) untuk matahari terbit. 🌅", + "min_sunrise_time": "Tetapkan waktu matahari terbit virtual paling awal (HH:MM:SS), memungkinkan matahari terbit di kemudian waktu. 🌅", + "max_sunrise_time": "Atur waktu matahari terbit virtual terkini (HH:MM:SS), memungkinkan matahari terbit lebih cepat. 🌅", + "sunrise_offset": "Sesuaikan waktu matahari terbit dengan offset positif atau negatif dalam hitungan detik. ⏰", + "sunset_time": "Tetapkan waktu tetap (HH:MM:SS) untuk matahari terbenam. 🌇", + "min_sunset_time": "Tetapkan waktu matahari terbenam virtual paling awal (HH:MM:SS), memungkinkan matahari terbenam di kemudian waktu. 🌇", + "max_sunset_time": "Atur waktu matahari terbenam virtual terkini (HH:MM:SS), memungkinkan matahari terbenam lebih cepat. 🌇", + "sunset_offset": "Sesuaikan waktu matahari terbenam dengan offset positif atau negatif dalam hitungan detik. ⏰", + "brightness_mode": "Mode kecerahan untuk digunakan. Nilai yang memungkinkan adalah `default`, `linear`, dan `tanh` (menggunakan `brightness_mode_time_dark` dan `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Diabaikan jika `brightness_mode='default'`) Durasi dalam hitungan detik untuk meningkatkan/menurunkan kecerahan sebelum/sesudah matahari terbit/terbenam. 📈📉", + "brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) Durasi dalam hitungan detik untuk meningkatkan/menurunkan kecerahan setelah/sebelum matahari terbit/terbenam. 📈📉.", + "autoreset_control_seconds": "Secara otomatis mengatur ulang kontrol manual setelah beberapa detik. Setel ke 0 untuk menonaktifkan. ⏲️", + "send_split_delay": "Waktu tunda (ms) antara `separate_turn_on_commands` untuk lampu yang tidak mendukung pengaturan kecerahan dan warna secara bersamaan. ⏲️", + "adapt_delay": "Waktu tunggu (detik) antara lampu menyala dan penerapan ubahan Pencahayaan Adaptif. Mungkin membantu untuk menghindari kedipan. ⏲️" + } + } + } } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/it.json b/custom_components/adaptive_lighting/translations/it.json index 015f4f6c..576e7303 100644 --- a/custom_components/adaptive_lighting/translations/it.json +++ b/custom_components/adaptive_lighting/translations/it.json @@ -21,56 +21,64 @@ "description": "Tutte le opzioni per il componente Illuminazione Adattiva. I nomi delle opzioni corrispondono con le impostazioni YAML. Non sono mostrate opzioni se hai la voce adaptive-lighting definita nella tua configurazione YAML.", "data": { "lights": "luci", - "initial_transition": "initial_transition: Quando le luci vengono accese (off -> on). (secondi)", - "sleep_transition": "sleep_transition: Quando 'sleep_state' cambia. (secondi)", "interval": "interval: Tempo tra i cambiamenti dello switch. (secondi)", - "max_brightness": "max_brightness: Luminosità massima delle luci durante un ciclo. (%)", - "max_color_temp": "max_color_temp: Gradazione più fredda del ciclo di temperatura del colore. (Kelvin)", - "min_brightness": "min_brightness: Luminosità minima delle luci durante un ciclo. (%)", - "min_color_temp": "min_color_temp: Gradazione più calda del ciclo di temperatura del colore. (Kelvin)", - "only_once": "only_once: Adatta le luci solo quando vengono accese.", - "prefer_rgb_color": "prefer_rgb_color: Usa 'rgb_color' al posto di 'color_temp' quando possibile.", - "separate_turn_on_commands": "separate_turn_on_commands: Separa i comandi per ogni attributo (color, brightness, etc.) in 'light.turn_on' (richiesto per alcune luci).", - "sleep_brightness": "sleep_brightness: Impostazione della luminosità per la modalità notturna. (%)", - "sleep_color_temp": "sleep_color_temp: Impostazione della temperatura colore per la modalità notturna. (Kelvin)", - "sunrise_offset": "sunrise_offset: Imposta quanto anticipare(-) o ritardare(+) l'alba nel ciclo (+/- secondi)", - "sunrise_time": "sunrise_time: Imposta manualmente l'ora dell'alba, se 'None', usa l'ora effettiva dell'alba alla tua posizione (HH:MM:SS)", - "sunset_offset": "sunset_offset: Imposta quanto anticipare(-) o ritardare(+) il tramonto nel ciclo (+/- secondi)", - "sunset_time": "sunset_time: Imposta manualmente l'ora del tramonto, se 'None', usa l'ora effettiva del tramonto alla tua posizione (HH:MM:SS)", - "take_over_control": "take_over_control: Se viene chiamato il servizio 'lights.turn_on' (non da Illuminazione Adattiva) quando una luce è già accesa, interrompi l'adattamento della luce finquando essa o l'interruttore non vengono riaccesi (off -> on.)", - "detect_non_ha_changes": "detect_non_ha_changes: rileva tutti i cambiamenti >10% applicati alle luci (anche fuori da HA), richiede che 'take_over_control' sia abilitato (chiama 'homeassistant.update_entity' ad ogni 'intervallo'!)", "transition": "Tempo di transizione quando viene applicata una modifica alle luci (secondi)", - "adapt_delay": "Tempo di attesa tra l'accensione della luce, e Illuminazione Adattiva che applica le modifiche allo stato della luce. Potrebbe evitare sfarfallii.", - "transition_until_sleep": "transition_until_sleep: Quando abilitato, Adaptive Lighting tratterà le impostazioni di sleep come valori minimi, facendo la transizione a questi valori dopo il tramonto. 🌙", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quando accendi le luci la prima volta. Se impostato su `true`, AL adatta solo se `light.turn_on è invocato senza specificare il colore o la luminosità. ❌🌈 Questo, per esempio, previene l'adattamento quando si attiva una scena. Se `false`, AL adatta indipendentemente dalla presenza di colore o luminosità nei `service_data` iniziali. Necessita che `take_over_control` sia abilitato. 🕵️ ", - "multi_light_intercept": "multi_light_intercept: Intercetta e adatta le chiamate a `light.turn_on` destinate a più luci. ➗⚠️ Questo potrebbe causare la divisione della singola chiamata `light.turn_on`in più chiamate, ad esempio quando le luci sono su switch diversi. Richiede che l'opzione `intercept` sia abilitata.", - "skip_redundant_commands": "skip_redundant_commands: Salta l'invio di comandi di adattamento rivolti ad entità in cui lo stato desiderato è identico allo stato attuale. Minimizza il traffico sulla rete e migliora la responsività dell'adattamento in alcune situazioni. 📉 Disabilitalo se lo stato reale delle luci va fuori sincrono con quello registrato da HA.", - "intercept": "intercept: Intercetta e adatta alle chiamate a`light.turn_on` per abilitare adattamenti istantanei di colore e luminosità. 🏎️ Disabilita per quelle luci che non supportano l'impostazione di luci e colori a seguito dell'evento `light.turn_on`.", - "include_config_in_attributes": "include_config_in_attributes: Quando impostato come `true`, mostra tutte le opzioni come attributi dello switch in Home Assistant. 📝" + "min_brightness": "min_brightness: Luminosità minima delle luci durante un ciclo. (%)", + "max_brightness": "max_brightness: Luminosità massima delle luci durante un ciclo. (%)", + "min_color_temp": "min_color_temp: Gradazione più calda del ciclo di temperatura del colore. (Kelvin)", + "max_color_temp": "max_color_temp: Gradazione più fredda del ciclo di temperatura del colore. (Kelvin)", + "sleep_brightness": "sleep_brightness: Impostazione della luminosità per la modalità notturna. (%)", + "sleep_color_temp": "sleep_color_temp: Impostazione della temperatura colore per la modalità notturna. (Kelvin)" }, "data_description": { - "sunrise_offset": "Regola il momento dell'alba con un offset positivo o negativo. ⏰", - "sunset_offset": "Modifica l'orario del tramonto con un offset in secondi positivo o negativo. ⏰", - "sleep_rgb_or_color_temp": "Usa uno tra `\"rgb_color\"` or`\"color_temp\"` in modalità notturna. 🌙", - "sleep_color_temp": "Temperatura colore per la modalità notturna (utilizzata quando `sleep_rgb_or_color_temp` vale `color_temp`), espressa in Kelvin. 😴", - "sleep_transition": "Durata della transizione al passaggio da/verso la modalità luce notturna, espressa in secondi. 😴", - "autoreset_control_seconds": "Rimuovi automaticamente il colore impostato manualmente dopo un certo numero di secondi. Imposta 0 per disabilitare. ⏲️", - "min_sunset_time": "Imposta il minimo orario per il tramonto (HH:MM:SS), per eventualmente ritardarlo. 🌅", - "sleep_brightness": "Luminosità percentuale delle luci in modalità notturna. 😴", - "min_sunrise_time": "Imposta il minimo orario per l'alba (HH:MM:SS), per eventualmente ritardarla. 🌅", "interval": "Frequenza di adattamento delle luci, espressa in secondi. 🔄", - "adapt_delay": "Tempo di attesa (in secondi) tra l'accensione della luce e i cambiamenti indotti da Illuminazione Adattativa. Può contribuire a ridurre lo sfarfallio. ⏲️", - "sleep_rgb_color": "Colore RGB in modalità notturna (usato quando `sleep_rgb_or_color_temp` è impostato su \"rgb_color\"). 🌈", "transition": "Durata della transizione quando le luci cambiano, espressa in secondi. 🕑", - "brightness_mode": "Modalità per la luminosità da utilizzare. I valori possibili sono `default`, `linear`, and `tanh` (usa`brightness_mode_time_dark` e `brightness_mode_time_light`). 📈", - "brightness_mode_time_light": "La durata, espressa in secondi, della variazione di luminosità durante le albe/tramonti (ignorato se `brightness_mode='default'`). 📈📉", - "sunset_time": "Imposta un orario fisso (HH:MM:SS) per il tramonto. 🌇", - "max_sunset_time": "Imposta il massimo orario per il tramonto (HH:MM:SS), in modo da eventualmente anticiparlo. 🌇", - "sunrise_time": "Imposta un orario fisso (HH:MM:SS) per l'alba. 🌅", - "initial_transition": "Durata della prima transizione quando le luci passano dallo stato `off` a `on`, espressa in secondi. ⏲️", - "brightness_mode_time_dark": "La durata, espressa in secondi, della variazione di luminosità durante le albe/tramonti (ignorato se `brightness_mode='default'`). 📈📉", - "max_sunrise_time": "Imposta l'orario massimo per l'alba (HH:MM:SS), in modo da eventualmente anticiparla. 🌅", - "send_split_delay": "Ritardo (ms) tra i comandi, per le luci che hanno `separate_turn_on_commands` e che non supportano l'impostazione simultanea di luminosità e colore. ⏲️" + "sleep_brightness": "Luminosità percentuale delle luci in modalità notturna. 😴", + "sleep_color_temp": "Temperatura colore per la modalità notturna (utilizzata quando `sleep_rgb_or_color_temp` vale `color_temp`), espressa in Kelvin. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition: Quando le luci vengono accese (off -> on). (secondi)", + "prefer_rgb_color": "prefer_rgb_color: Usa 'rgb_color' al posto di 'color_temp' quando possibile.", + "sleep_transition": "sleep_transition: Quando 'sleep_state' cambia. (secondi)", + "transition_until_sleep": "transition_until_sleep: Quando abilitato, Adaptive Lighting tratterà le impostazioni di sleep come valori minimi, facendo la transizione a questi valori dopo il tramonto. 🌙", + "sunrise_time": "sunrise_time: Imposta manualmente l'ora dell'alba, se 'None', usa l'ora effettiva dell'alba alla tua posizione (HH:MM:SS)", + "sunrise_offset": "sunrise_offset: Imposta quanto anticipare(-) o ritardare(+) l'alba nel ciclo (+/- secondi)", + "sunset_time": "sunset_time: Imposta manualmente l'ora del tramonto, se 'None', usa l'ora effettiva del tramonto alla tua posizione (HH:MM:SS)", + "sunset_offset": "sunset_offset: Imposta quanto anticipare(-) o ritardare(+) il tramonto nel ciclo (+/- secondi)", + "take_over_control": "take_over_control: Se viene chiamato il servizio 'lights.turn_on' (non da Illuminazione Adattiva) quando una luce è già accesa, interrompi l'adattamento della luce finquando essa o l'interruttore non vengono riaccesi (off -> on.)", + "detect_non_ha_changes": "detect_non_ha_changes: rileva tutti i cambiamenti >10% applicati alle luci (anche fuori da HA), richiede che 'take_over_control' sia abilitato (chiama 'homeassistant.update_entity' ad ogni 'intervallo'!)", + "only_once": "only_once: Adatta le luci solo quando vengono accese.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quando accendi le luci la prima volta. Se impostato su `true`, AL adatta solo se `light.turn_on è invocato senza specificare il colore o la luminosità. ❌🌈 Questo, per esempio, previene l'adattamento quando si attiva una scena. Se `false`, AL adatta indipendentemente dalla presenza di colore o luminosità nei `service_data` iniziali. Necessita che `take_over_control` sia abilitato. 🕵️ ", + "separate_turn_on_commands": "separate_turn_on_commands: Separa i comandi per ogni attributo (color, brightness, etc.) in 'light.turn_on' (richiesto per alcune luci).", + "adapt_delay": "Tempo di attesa tra l'accensione della luce, e Illuminazione Adattiva che applica le modifiche allo stato della luce. Potrebbe evitare sfarfallii.", + "skip_redundant_commands": "skip_redundant_commands: Salta l'invio di comandi di adattamento rivolti ad entità in cui lo stato desiderato è identico allo stato attuale. Minimizza il traffico sulla rete e migliora la responsività dell'adattamento in alcune situazioni. 📉 Disabilitalo se lo stato reale delle luci va fuori sincrono con quello registrato da HA.", + "intercept": "intercept: Intercetta e adatta alle chiamate a`light.turn_on` per abilitare adattamenti istantanei di colore e luminosità. 🏎️ Disabilita per quelle luci che non supportano l'impostazione di luci e colori a seguito dell'evento `light.turn_on`.", + "multi_light_intercept": "multi_light_intercept: Intercetta e adatta le chiamate a `light.turn_on` destinate a più luci. ➗⚠️ Questo potrebbe causare la divisione della singola chiamata `light.turn_on`in più chiamate, ad esempio quando le luci sono su switch diversi. Richiede che l'opzione `intercept` sia abilitata.", + "include_config_in_attributes": "include_config_in_attributes: Quando impostato come `true`, mostra tutte le opzioni come attributi dello switch in Home Assistant. 📝" + }, + "data_description": { + "initial_transition": "Durata della prima transizione quando le luci passano dallo stato `off` a `on`, espressa in secondi. ⏲️", + "sleep_rgb_or_color_temp": "Usa uno tra `\"rgb_color\"` or`\"color_temp\"` in modalità notturna. 🌙", + "sleep_rgb_color": "Colore RGB in modalità notturna (usato quando `sleep_rgb_or_color_temp` è impostato su \"rgb_color\"). 🌈", + "sleep_transition": "Durata della transizione al passaggio da/verso la modalità luce notturna, espressa in secondi. 😴", + "sunrise_time": "Imposta un orario fisso (HH:MM:SS) per l'alba. 🌅", + "min_sunrise_time": "Imposta il minimo orario per l'alba (HH:MM:SS), per eventualmente ritardarla. 🌅", + "max_sunrise_time": "Imposta l'orario massimo per l'alba (HH:MM:SS), in modo da eventualmente anticiparla. 🌅", + "sunrise_offset": "Regola il momento dell'alba con un offset positivo o negativo. ⏰", + "sunset_time": "Imposta un orario fisso (HH:MM:SS) per il tramonto. 🌇", + "min_sunset_time": "Imposta il minimo orario per il tramonto (HH:MM:SS), per eventualmente ritardarlo. 🌅", + "max_sunset_time": "Imposta il massimo orario per il tramonto (HH:MM:SS), in modo da eventualmente anticiparlo. 🌇", + "sunset_offset": "Modifica l'orario del tramonto con un offset in secondi positivo o negativo. ⏰", + "brightness_mode": "Modalità per la luminosità da utilizzare. I valori possibili sono `default`, `linear`, and `tanh` (usa`brightness_mode_time_dark` e `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "La durata, espressa in secondi, della variazione di luminosità durante le albe/tramonti (ignorato se `brightness_mode='default'`). 📈📉", + "brightness_mode_time_light": "La durata, espressa in secondi, della variazione di luminosità durante le albe/tramonti (ignorato se `brightness_mode='default'`). 📈📉", + "autoreset_control_seconds": "Rimuovi automaticamente il colore impostato manualmente dopo un certo numero di secondi. Imposta 0 per disabilitare. ⏲️", + "send_split_delay": "Ritardo (ms) tra i comandi, per le luci che hanno `separate_turn_on_commands` e che non supportano l'impostazione simultanea di luminosità e colore. ⏲️", + "adapt_delay": "Tempo di attesa (in secondi) tra l'accensione della luce e i cambiamenti indotti da Illuminazione Adattativa. Può contribuire a ridurre lo sfarfallio. ⏲️" + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/ja.json b/custom_components/adaptive_lighting/translations/ja.json index 38f13d81..670a127c 100644 --- a/custom_components/adaptive_lighting/translations/ja.json +++ b/custom_components/adaptive_lighting/translations/ja.json @@ -28,14 +28,20 @@ "options": { "step": { "init": { - "data": { - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: 最初に照明オンにするとき。`true`を設定すると、AL(適応型照明)は調色や明るさを指定せずや`light.turn_on`をしたときのみ適応します。❌🌈 例えば、適応型照明をシーンを有効にするときにしないようにする。`false`であれば、`service_data`に最初から、ALはシーンの状態に関係なく調色や明るさを適応する。`take_over_control`を有効にすることが必要。🕵️ " - }, - "data_description": { - "sunrise_offset": "日の出時間を基準に秒単位で正値もしくは負値で調整する。⏰", - "sunset_offset": "日の入時間を基準に秒単位で正値もしくは負値で調整する。⏰" - }, - "title": "明るさの自動調整オプション" + "data": {}, + "data_description": {}, + "title": "明るさの自動調整オプション", + "sections": { + "advanced": { + "data": { + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: 最初に照明オンにするとき。`true`を設定すると、AL(適応型照明)は調色や明るさを指定せずや`light.turn_on`をしたときのみ適応します。❌🌈 例えば、適応型照明をシーンを有効にするときにしないようにする。`false`であれば、`service_data`に最初から、ALはシーンの状態に関係なく調色や明るさを適応する。`take_over_control`を有効にすることが必要。🕵️ " + }, + "data_description": { + "sunrise_offset": "日の出時間を基準に秒単位で正値もしくは負値で調整する。⏰", + "sunset_offset": "日の入時間を基準に秒単位で正値もしくは負値で調整する。⏰" + } + } + } } } } diff --git a/custom_components/adaptive_lighting/translations/ko.json b/custom_components/adaptive_lighting/translations/ko.json index 41807efb..58b0424b 100644 --- a/custom_components/adaptive_lighting/translations/ko.json +++ b/custom_components/adaptive_lighting/translations/ko.json @@ -23,65 +23,73 @@ "lights": "조명: 제어될 조명 entity_ids의 목록 (비어 있을 수 있음). 🌟", "interval": "간격", "transition": "전환", - "initial_transition": "초기 전환", "min_brightness": "최소 밝기: 밝기 최소 퍼센트. 💡", "max_brightness": "최대 밝기: 밝기 최대 퍼센트. 💡", "min_color_temp": "최소 색온도: 켈빈으로 표시된 가장 따뜻한 색온도. 🔥", "max_color_temp": "최대 색온도: 켈빈으로 표시된 가장 차가운 색온도. ❄️", - "prefer_rgb_color": "RGB 색상 선호: 가능할 경우 색온도 조정보다 RGB 색상 조정을 선호하는지 여부. 🌈", "sleep_brightness": "수면 밝기", - "sleep_rgb_or_color_temp": "수면 rgb_or_color_temp", - "sleep_color_temp": "수면 색온도", - "sleep_rgb_color": "수면 RGB 색상", - "sleep_transition": "수면 전환", - "transition_until_sleep": "수면까지 전환: 활성화되면, 적응형 조명은 수면 설정을 최소값으로 취급하고 일몰 후 이 값으로 전환합니다. 🌙", - "sunrise_time": "일출 시간", - "min_sunrise_time": "최소 일출 시간", - "max_sunrise_time": "최대 일출 시간", - "sunrise_offset": "일출 오프셋", - "sunset_time": "일몰 시간", - "min_sunset_time": "최소 일몰 시간", - "max_sunset_time": "최대 일몰 시간", - "sunset_offset": "일몰 오프셋", - "brightness_mode": "밝기 모드", - "brightness_mode_time_dark": "어두울 때 밝기 모드 시간", - "brightness_mode_time_light": "밝을 때 밝기 모드 시간", - "take_over_control": "제어 인계: 다른 소스가 조명이 켜져 있고 조정 중일 때 `light.turn_on`을 호출하면 적응형 조명을 비활성화합니다. 이는 매 `간격`마다 `homeassistant.update_entity`를 호출합니다! 🔒", - "detect_non_ha_changes": "비HA 변경 감지: `light.turn_on`이 아닌 상태 변경을 감지하고 조정을 중단합니다. `take_over_control`이 활성화되어 있어야 합니다. 🕵️ 주의: ⚠️ 일부 조명은 잘못된 '켜짐' 상태를 나타낼 수 있으며, 이로 인해 조명이 예상치 못하게 켜질 수 있습니다. 이러한 문제가 발생하면 이 기능을 비활성화하세요.", - "autoreset_control_seconds": "자동 제어 리셋 초", - "only_once": "한 번만: 조명을 켤 때만 조정 (`true`) 또는 계속해서 조정 (`false`). 🔄", - "adapt_only_on_bare_turn_on": "초기 켜짐 시 조정만: 조명을 처음 켤 때. `true`로 설정하면 `light.turn_on`이 색상이나 밝기를 지정하지 않고 호출될 때만 AL이 조정합니다. ❌🌈 예를 들어, 장면을 활성화할 때 조정을 방지합니다. `false`로 설정하면, AL은 초기 `service_data`에 색상이나 밝기의 존재 여부와 관계없이 조정합니다. `take_over_control`이 활성화되어 있어야 합니다. 🕵️", - "separate_turn_on_commands": "분리된 켜기 명령 사용: 일부 조명 유형에 필요한 색상과 밝기에 대해 별도의 `light.turn_on` 호출을 사용합니다. 🔀", - "send_split_delay": "분할 전송 지연", - "adapt_delay": "조정 지연", - "skip_redundant_commands": "중복 명령 건너뛰기: 목표 상태가 이미 조명의 알려진 상태와 동일한 조정 명령을 보내지 않습니다. 네트워크 트래픽을 최소화하고 일부 상황에서 조정 반응성을 향상시킵니다. 📉 물리적 조명 상태가 HA의 기록된 상태와 동기화되지 않는 경우 비활성화하세요.", - "intercept": "가로채기: 색상과 밝기의 즉각적인 조정을 가능하게 하기 위해 `light.turn_on` 호출을 가로챕니다. 🏎️ 색상과 밝기를 지원하지 않는 조명에 대해 비활성화합니다.", - "multi_light_intercept": "다중 조명 가로채기: 여러 조명을 대상으로 하는 `light.turn_on` 호출을 가로채고 조정합니다. ➗⚠️ 이는 단일 `light.turn_on` 호출을 여러 호출로 분할할 수 있음을 의미합니다. 예를 들어, 조명이 다른 스위치에 있을 때. `intercept`가 활성화되어 있어야 합니다.", - "include_config_in_attributes": "속성에 구성 포함: `true`로 설정하면 Home Assistant에서 스위치의 모든 옵션을 속성으로 표시합니다. 📝" + "sleep_color_temp": "수면 색온도" }, "data_description": { "interval": "조명을 조정하는 빈도, 초 단위. 🔄", "transition": "조명이 변경될 때 전환 기간, 초 단위. 🕑", - "initial_transition": "조명이 `off`에서 `on`으로 바뀔 때 첫 번째 전환의 지속 시간, 초 단위. ⏲️", "sleep_brightness": "수면 모드에서 조명의 밝기 퍼센트. 😴", - "sleep_rgb_or_color_temp": "수면 모드에서 `\"rgb_color\"` 또는 `\"color_temp\"` 사용. 🌙", - "sleep_color_temp": "수면 모드에서 색온도 (sleep_rgb_or_color_temp가 `color_temp`일 때 사용) 켈빈 단위. 😴", - "sleep_rgb_color": "수면 모드에서 RGB 색상 (sleep_rgb_or_color_temp가 \"rgb_color\"일 때 사용). 🌈", - "sleep_transition": "\"수면 모드\"가 전환될 때 전환 기간, 초 단위. 😴", - "sunrise_time": "일출 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌅", - "min_sunrise_time": "가장 이른 가상 일출 시간 (HH:MM:SS)을 설정하여 더 늦은 일출을 허용. 🌅", - "max_sunrise_time": "가장 늦은 가상 일출 시간 (HH:MM:SS)을 설정하여 더 일찍 일출을 허용. 🌅", - "sunrise_offset": "양수 또는 음수 오프셋(초)으로 일출 시간을 조정. ⏰", - "sunset_time": "일몰 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌇", - "min_sunset_time": "가장 이른 가상 일몰 시간 (HH:MM:SS)을 설정하여 더 늦은 일몰을 허용. 🌇", - "max_sunset_time": "가장 늦은 가상 일몰 시간 (HH:MM:SS)을 설정하여 더 일찍 일몰을 허용. 🌇", - "sunset_offset": "양수 또는 음수 오프셋(초)으로 일몰 시간을 조정. ⏰", - "brightness_mode": "사용할 밝기 모드. 가능한 값은 `default`, `linear`, `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", - "brightness_mode_time_dark": "(`brightness_mode='default'`인 경우 무시됨) 일출/일몰 전/후에 밝기를 높이거나 낮추는 데 걸리는 시간, 초 단위. 📈📉", - "brightness_mode_time_light": "(`brightness_mode='default'`인 경우 무시됨) 일출/일몰 후/전에 밝기를 높이거나 낮추는 데 걸리는 시간, 초 단위. 📈📉.", - "autoreset_control_seconds": "특정 초 후에 수동 제어를 자동으로 재설정. 0으로 설정하면 비활성화됩니다. ⏲️", - "send_split_delay": "`separate_turn_on_commands`에 대한 호출 사이의 지연 시간(밀리초)으로, 밝기와 색상을 동시에 설정하지 않는 조명에 대한 지연. ⏲️", - "adapt_delay": "조명을 켠 후 적응형 조명이 변경 사항을 적용하기까지의 대기 시간(초). 깜박임을 피하는 데 도움이 될 수 있습니다. ⏲️" + "sleep_color_temp": "수면 모드에서 색온도 (sleep_rgb_or_color_temp가 `color_temp`일 때 사용) 켈빈 단위. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "초기 전환", + "prefer_rgb_color": "RGB 색상 선호: 가능할 경우 색온도 조정보다 RGB 색상 조정을 선호하는지 여부. 🌈", + "sleep_rgb_or_color_temp": "수면 rgb_or_color_temp", + "sleep_rgb_color": "수면 RGB 색상", + "sleep_transition": "수면 전환", + "transition_until_sleep": "수면까지 전환: 활성화되면, 적응형 조명은 수면 설정을 최소값으로 취급하고 일몰 후 이 값으로 전환합니다. 🌙", + "sunrise_time": "일출 시간", + "min_sunrise_time": "최소 일출 시간", + "max_sunrise_time": "최대 일출 시간", + "sunrise_offset": "일출 오프셋", + "sunset_time": "일몰 시간", + "min_sunset_time": "최소 일몰 시간", + "max_sunset_time": "최대 일몰 시간", + "sunset_offset": "일몰 오프셋", + "brightness_mode": "밝기 모드", + "brightness_mode_time_dark": "어두울 때 밝기 모드 시간", + "brightness_mode_time_light": "밝을 때 밝기 모드 시간", + "take_over_control": "제어 인계: 다른 소스가 조명이 켜져 있고 조정 중일 때 `light.turn_on`을 호출하면 적응형 조명을 비활성화합니다. 이는 매 `간격`마다 `homeassistant.update_entity`를 호출합니다! 🔒", + "detect_non_ha_changes": "비HA 변경 감지: `light.turn_on`이 아닌 상태 변경을 감지하고 조정을 중단합니다. `take_over_control`이 활성화되어 있어야 합니다. 🕵️ 주의: ⚠️ 일부 조명은 잘못된 '켜짐' 상태를 나타낼 수 있으며, 이로 인해 조명이 예상치 못하게 켜질 수 있습니다. 이러한 문제가 발생하면 이 기능을 비활성화하세요.", + "autoreset_control_seconds": "자동 제어 리셋 초", + "only_once": "한 번만: 조명을 켤 때만 조정 (`true`) 또는 계속해서 조정 (`false`). 🔄", + "adapt_only_on_bare_turn_on": "초기 켜짐 시 조정만: 조명을 처음 켤 때. `true`로 설정하면 `light.turn_on`이 색상이나 밝기를 지정하지 않고 호출될 때만 AL이 조정합니다. ❌🌈 예를 들어, 장면을 활성화할 때 조정을 방지합니다. `false`로 설정하면, AL은 초기 `service_data`에 색상이나 밝기의 존재 여부와 관계없이 조정합니다. `take_over_control`이 활성화되어 있어야 합니다. 🕵️", + "separate_turn_on_commands": "분리된 켜기 명령 사용: 일부 조명 유형에 필요한 색상과 밝기에 대해 별도의 `light.turn_on` 호출을 사용합니다. 🔀", + "send_split_delay": "분할 전송 지연", + "adapt_delay": "조정 지연", + "skip_redundant_commands": "중복 명령 건너뛰기: 목표 상태가 이미 조명의 알려진 상태와 동일한 조정 명령을 보내지 않습니다. 네트워크 트래픽을 최소화하고 일부 상황에서 조정 반응성을 향상시킵니다. 📉 물리적 조명 상태가 HA의 기록된 상태와 동기화되지 않는 경우 비활성화하세요.", + "intercept": "가로채기: 색상과 밝기의 즉각적인 조정을 가능하게 하기 위해 `light.turn_on` 호출을 가로챕니다. 🏎️ 색상과 밝기를 지원하지 않는 조명에 대해 비활성화합니다.", + "multi_light_intercept": "다중 조명 가로채기: 여러 조명을 대상으로 하는 `light.turn_on` 호출을 가로채고 조정합니다. ➗⚠️ 이는 단일 `light.turn_on` 호출을 여러 호출로 분할할 수 있음을 의미합니다. 예를 들어, 조명이 다른 스위치에 있을 때. `intercept`가 활성화되어 있어야 합니다.", + "include_config_in_attributes": "속성에 구성 포함: `true`로 설정하면 Home Assistant에서 스위치의 모든 옵션을 속성으로 표시합니다. 📝" + }, + "data_description": { + "initial_transition": "조명이 `off`에서 `on`으로 바뀔 때 첫 번째 전환의 지속 시간, 초 단위. ⏲️", + "sleep_rgb_or_color_temp": "수면 모드에서 `\"rgb_color\"` 또는 `\"color_temp\"` 사용. 🌙", + "sleep_rgb_color": "수면 모드에서 RGB 색상 (sleep_rgb_or_color_temp가 \"rgb_color\"일 때 사용). 🌈", + "sleep_transition": "\"수면 모드\"가 전환될 때 전환 기간, 초 단위. 😴", + "sunrise_time": "일출 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌅", + "min_sunrise_time": "가장 이른 가상 일출 시간 (HH:MM:SS)을 설정하여 더 늦은 일출을 허용. 🌅", + "max_sunrise_time": "가장 늦은 가상 일출 시간 (HH:MM:SS)을 설정하여 더 일찍 일출을 허용. 🌅", + "sunrise_offset": "양수 또는 음수 오프셋(초)으로 일출 시간을 조정. ⏰", + "sunset_time": "일몰 시간을 고정된 시간 (HH:MM:SS)으로 설정. 🌇", + "min_sunset_time": "가장 이른 가상 일몰 시간 (HH:MM:SS)을 설정하여 더 늦은 일몰을 허용. 🌇", + "max_sunset_time": "가장 늦은 가상 일몰 시간 (HH:MM:SS)을 설정하여 더 일찍 일몰을 허용. 🌇", + "sunset_offset": "양수 또는 음수 오프셋(초)으로 일몰 시간을 조정. ⏰", + "brightness_mode": "사용할 밝기 모드. 가능한 값은 `default`, `linear`, `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(`brightness_mode='default'`인 경우 무시됨) 일출/일몰 전/후에 밝기를 높이거나 낮추는 데 걸리는 시간, 초 단위. 📈📉", + "brightness_mode_time_light": "(`brightness_mode='default'`인 경우 무시됨) 일출/일몰 후/전에 밝기를 높이거나 낮추는 데 걸리는 시간, 초 단위. 📈📉.", + "autoreset_control_seconds": "특정 초 후에 수동 제어를 자동으로 재설정. 0으로 설정하면 비활성화됩니다. ⏲️", + "send_split_delay": "`separate_turn_on_commands`에 대한 호출 사이의 지연 시간(밀리초)으로, 밝기와 색상을 동시에 설정하지 않는 조명에 대한 지연. ⏲️", + "adapt_delay": "조명을 켠 후 적응형 조명이 변경 사항을 적용하기까지의 대기 시간(초). 깜박임을 피하는 데 도움이 될 수 있습니다. ⏲️" + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/nb.json b/custom_components/adaptive_lighting/translations/nb.json index 8a8f6e1b..50464143 100644 --- a/custom_components/adaptive_lighting/translations/nb.json +++ b/custom_components/adaptive_lighting/translations/nb.json @@ -21,54 +21,62 @@ "description": "Alle innstillinger for en adaptiv belysning konfigurasjon. Innstillingene er identiske med innstillingene for YAML konfigurasjon. Ingen innstillinger vises dersom du har definert adaptive_lighting i din YAML konfigurasjon.", "data": { "lights": "Lys / Lyskilder", - "initial_transition": "'initial_transition': overgangen (i sekunder) når lysene skrus av eller på - eller når 'sleep_state' endres", "interval": "'interval': tiden mellom oppdateringer (i sekunder)", - "max_brightness": "'max_brightness': den høyeste lysstyrken (i prosent) på lysene i løpet av en syklus", - "max_color_temp": "'max_color_temp': den høyeste fargetemperaturen (i kelvin) på lysene i løpet av en syklus", - "min_brightness": "'min_brightness': den laveste lysstyrken (i prosent) på lysene i løpet av en syklus", - "min_color_temp": "'min_color_temp': den laveste fargetemperaturen (i kelvin) på lysene i løpet av en syklus", - "only_once": "'only_once': anvend innstillingene for adaptiv belysning kun når lysene skrus av eller på", - "prefer_rgb_color": "'prefer_rgb_color': benytt rgb i stedet for fargetemperatur dersom det er mulig", - "separate_turn_on_commands": "'separate_turn_on_commands': separer kommandone i 'light.turn_on' for hver attributt (farge, lysstyrke, osv.). Dette kan være nødvendig for enkelte typer lys / lyskilder", - "sleep_brightness": "'sleep_brightness': lysstyrken på lysene (i prosent) når 'sleep_mode' (søvnmodus) er aktiv", - "sleep_color_temp": "'sleep_color_temp': fargetemperaturen på lysene (i kelvin) når 'sleep_mode' (søvnmodus) er aktiv", - "sunrise_offset": "'sunrise_offset': hvor lenge før (-) eller etter (+) tidspunktet solen står opp (lokalt) skal defineres som soloppgang (i sekunder)", - "sunrise_time": "'sunrise_time': definer tidspunktet for soloppgang manuelt (i følgende format: TT:MM:SS)", - "sunset_offset": "'sunset_offset': hvor lenge før (-) eller etter (+) tidspunktet solen går ned (lokalt) skal defineres som solnedgang (i sekunder)", - "sunset_time": "'sunset_time': definer tidspunktet for solnedgang manuelt (i følgende format: TT:MM:SS - f. eks: '20:30:00' vil definere tidspunktet for solnegang som halv-ni på kvelden)", - "take_over_control": "'take_over_control': dersom en annen tjeneste enn adaptiv belysning skrur lysene av eller på, vil automatisk adaptering av lyset stoppes inntil lyset (eller den tilhørende bryteren for adaptiv belysning) blir slått av - og på igjen", - "detect_non_ha_changes": "'detect_non_ha_changes': registrerer alle endringer i lysstyrke over 10% med opprinnelse utenfor Home Assistant - krever at 'take_over_control' er aktivert (OBS: tilkaller 'homeassistant.update_entity' ved hvert 'interval'!)", "transition": "'transition': varigheten (i sekunder) på overgangen når lysene oppdateres ", - "transition_until_sleep": "transition_until_sleep: Når aktivert, Adaptive lightning vil behandle sove innstillingene som minimum, bevege seg til disse verdiene etter solnedgang.", - "skip_redundant_commands": "skip_redundant_commands: Dropp sending av tilpassnings kommandoer hvor målets tilstand allerede er lik den kjente tilstanden til lyset. Minimerer nettverk trafikk og forbedrer tilpasningens responsitivitet i noen situasjoner. Skru av hvis fysisk tilstand til lyset er ute av synkronisering med HA´s registrere tilstand.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_on: Når lysene skrues på. Hvis satt til \"sann\", AL vil bare hvis \"light.turn_on\" er aktivert uten spesifisert farge og styrke. Dette f.eks. forhindrer aktivering når en scene aktiveres. Hvis \"false\", AL vil aktivere uansett om farge og stryke er satt av i opprinnelig \"service_data\". Trenger \"take_over_control\" er aktivert. ", - "intercept": "Bryt: Bryt og tilpass `light.turn_on` kall for å aktivere umiddelbar farge og styrke tilpassning. Deaktiver for lys som ikke støtter `light.turn_on` med farge og styrke.", - "multi_light_intercept": "multi_light_incept: Avskjære og tilpasse \"light.turn_on\" kall til flere lyskilder. Dette kan medføre oppsplitting av et enkelt \"light.turn_on\" kall til flere kall, f.eks når lys tilhører flere brytere. Dette krever at \"intercept\" er aktivert.", - "include_config_in_attributes": "include_config_in_attributes: Vis alle valg som attributes på bryteren i Home Assistant når satt til `true`." + "min_brightness": "'min_brightness': den laveste lysstyrken (i prosent) på lysene i løpet av en syklus", + "max_brightness": "'max_brightness': den høyeste lysstyrken (i prosent) på lysene i løpet av en syklus", + "min_color_temp": "'min_color_temp': den laveste fargetemperaturen (i kelvin) på lysene i løpet av en syklus", + "max_color_temp": "'max_color_temp': den høyeste fargetemperaturen (i kelvin) på lysene i løpet av en syklus", + "sleep_brightness": "'sleep_brightness': lysstyrken på lysene (i prosent) når 'sleep_mode' (søvnmodus) er aktiv", + "sleep_color_temp": "'sleep_color_temp': fargetemperaturen på lysene (i kelvin) når 'sleep_mode' (søvnmodus) er aktiv" }, "data_description": { - "sunrise_offset": "Juster soloppgang tidspunkt med en positiv eller negativ forskyvning i sekunder. ⏰", - "sunset_offset": "Juster soloppgang tidspunkt med en positiv eller negativ forskyvning i sekunder. ⏰", - "sleep_rgb_or_color_temp": "Bruk enten `\"rgb_color\"` eller `\"color_temp\"` i sove modus.", - "sleep_rgb_color": "RGB farger i sove modus (brukes når \"sleep_rgb_or_color_temp\" er \"rgb_color\")", - "sleep_brightness": "Lysstyrkeprosent på lysene i sove modus.", - "sleep_color_temp": "Fargetemperatur i sove modus (brukes når `sleep_rgb_or_color_temp` er `color_temp`) i Kelvin.", - "initial_transition": "Varighet på første overgang når lysene endres fra `off` til `on` i sekunder.", - "transition": "Varighet på overgang når lysene endres, i sekunder.", "interval": "Frekvens til å tilpasse lys, i sekunder.", - "sunset_time": "Sett et fast tidspunkt (TT:MM:SS) for solnedgang.", - "sleep_transition": "Varighet på overgang når \"sleep mode\" er aktivert i sekunder.", - "sunrise_time": "Sett et fast tidspunkt (TT:MM:SS) for soloppgang.", - "min_sunrise_time": "Sett tidligste virituelle tidspunkt for soloppgang (TT:MM:SS), muliggjør for senere soloppganger", - "max_sunrise_time": "Sett det seneste virituelle tidspunktet for soloppgang (TT:MM:SS), muliggjør for tidligere soloppganger.", - "min_sunset_time": "Sett det tidligste virituelle tidspunktet for solnedgang (TT:MM:SS), muliggjør for senere solnedgang.", - "max_sunset_time": "Sett det seneste virituelle tidspunktet for solnedgang (TT:MM:SS), muliggjør for tidligere solnedgang.", - "brightness_mode": "Hvilken lysstyrke moduse skal brukes. Mulige verdier er `default`, `linear`, and `tanh` (bruker `brightness_mode_time_dark` og `brightness_mode_time_light`).", - "send_split_delay": "Forsinkelse (ms) mellom `separate_turn_on_commands` for lys som ikke støtter simultane styrke og farge innstillinger.", - "adapt_delay": "Ventetid (sekunder) mellom at lyset skrues på og Adaptive Lightning sender endringer. Kan hjelpe til for å unngå blinking.", - "autoreset_control_seconds": "Automatisk reset manuell kontroll etter et gitt antall sekunder. Sett til 0 for å skru av.", - "brightness_mode_time_light": "(Ignorere hvis `brightness_mode='default'`) Varigheten i sekunder for å justere opp/ned lysstyrken før/etter soloppgang/solnedgang.", - "brightness_mode_time_dark": "(Ignorere hvis `brightness_mode='default'`) Varigheten i sekunder for å justere opp/ned lysstyrken før/etter soloppgang/solnedgang." + "transition": "Varighet på overgang når lysene endres, i sekunder.", + "sleep_brightness": "Lysstyrkeprosent på lysene i sove modus.", + "sleep_color_temp": "Fargetemperatur i sove modus (brukes når `sleep_rgb_or_color_temp` er `color_temp`) i Kelvin." + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "'initial_transition': overgangen (i sekunder) når lysene skrus av eller på - eller når 'sleep_state' endres", + "prefer_rgb_color": "'prefer_rgb_color': benytt rgb i stedet for fargetemperatur dersom det er mulig", + "transition_until_sleep": "transition_until_sleep: Når aktivert, Adaptive lightning vil behandle sove innstillingene som minimum, bevege seg til disse verdiene etter solnedgang.", + "sunrise_time": "'sunrise_time': definer tidspunktet for soloppgang manuelt (i følgende format: TT:MM:SS)", + "sunrise_offset": "'sunrise_offset': hvor lenge før (-) eller etter (+) tidspunktet solen står opp (lokalt) skal defineres som soloppgang (i sekunder)", + "sunset_time": "'sunset_time': definer tidspunktet for solnedgang manuelt (i følgende format: TT:MM:SS - f. eks: '20:30:00' vil definere tidspunktet for solnegang som halv-ni på kvelden)", + "sunset_offset": "'sunset_offset': hvor lenge før (-) eller etter (+) tidspunktet solen går ned (lokalt) skal defineres som solnedgang (i sekunder)", + "take_over_control": "'take_over_control': dersom en annen tjeneste enn adaptiv belysning skrur lysene av eller på, vil automatisk adaptering av lyset stoppes inntil lyset (eller den tilhørende bryteren for adaptiv belysning) blir slått av - og på igjen", + "detect_non_ha_changes": "'detect_non_ha_changes': registrerer alle endringer i lysstyrke over 10% med opprinnelse utenfor Home Assistant - krever at 'take_over_control' er aktivert (OBS: tilkaller 'homeassistant.update_entity' ved hvert 'interval'!)", + "only_once": "'only_once': anvend innstillingene for adaptiv belysning kun når lysene skrus av eller på", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_on: Når lysene skrues på. Hvis satt til \"sann\", AL vil bare hvis \"light.turn_on\" er aktivert uten spesifisert farge og styrke. Dette f.eks. forhindrer aktivering når en scene aktiveres. Hvis \"false\", AL vil aktivere uansett om farge og stryke er satt av i opprinnelig \"service_data\". Trenger \"take_over_control\" er aktivert. ", + "separate_turn_on_commands": "'separate_turn_on_commands': separer kommandone i 'light.turn_on' for hver attributt (farge, lysstyrke, osv.). Dette kan være nødvendig for enkelte typer lys / lyskilder", + "skip_redundant_commands": "skip_redundant_commands: Dropp sending av tilpassnings kommandoer hvor målets tilstand allerede er lik den kjente tilstanden til lyset. Minimerer nettverk trafikk og forbedrer tilpasningens responsitivitet i noen situasjoner. Skru av hvis fysisk tilstand til lyset er ute av synkronisering med HA´s registrere tilstand.", + "intercept": "Bryt: Bryt og tilpass `light.turn_on` kall for å aktivere umiddelbar farge og styrke tilpassning. Deaktiver for lys som ikke støtter `light.turn_on` med farge og styrke.", + "multi_light_intercept": "multi_light_incept: Avskjære og tilpasse \"light.turn_on\" kall til flere lyskilder. Dette kan medføre oppsplitting av et enkelt \"light.turn_on\" kall til flere kall, f.eks når lys tilhører flere brytere. Dette krever at \"intercept\" er aktivert.", + "include_config_in_attributes": "include_config_in_attributes: Vis alle valg som attributes på bryteren i Home Assistant når satt til `true`." + }, + "data_description": { + "initial_transition": "Varighet på første overgang når lysene endres fra `off` til `on` i sekunder.", + "sleep_rgb_or_color_temp": "Bruk enten `\"rgb_color\"` eller `\"color_temp\"` i sove modus.", + "sleep_rgb_color": "RGB farger i sove modus (brukes når \"sleep_rgb_or_color_temp\" er \"rgb_color\")", + "sleep_transition": "Varighet på overgang når \"sleep mode\" er aktivert i sekunder.", + "sunrise_time": "Sett et fast tidspunkt (TT:MM:SS) for soloppgang.", + "min_sunrise_time": "Sett tidligste virituelle tidspunkt for soloppgang (TT:MM:SS), muliggjør for senere soloppganger", + "max_sunrise_time": "Sett det seneste virituelle tidspunktet for soloppgang (TT:MM:SS), muliggjør for tidligere soloppganger.", + "sunrise_offset": "Juster soloppgang tidspunkt med en positiv eller negativ forskyvning i sekunder. ⏰", + "sunset_time": "Sett et fast tidspunkt (TT:MM:SS) for solnedgang.", + "min_sunset_time": "Sett det tidligste virituelle tidspunktet for solnedgang (TT:MM:SS), muliggjør for senere solnedgang.", + "max_sunset_time": "Sett det seneste virituelle tidspunktet for solnedgang (TT:MM:SS), muliggjør for tidligere solnedgang.", + "sunset_offset": "Juster soloppgang tidspunkt med en positiv eller negativ forskyvning i sekunder. ⏰", + "brightness_mode": "Hvilken lysstyrke moduse skal brukes. Mulige verdier er `default`, `linear`, and `tanh` (bruker `brightness_mode_time_dark` og `brightness_mode_time_light`).", + "brightness_mode_time_dark": "(Ignorere hvis `brightness_mode='default'`) Varigheten i sekunder for å justere opp/ned lysstyrken før/etter soloppgang/solnedgang.", + "brightness_mode_time_light": "(Ignorere hvis `brightness_mode='default'`) Varigheten i sekunder for å justere opp/ned lysstyrken før/etter soloppgang/solnedgang.", + "autoreset_control_seconds": "Automatisk reset manuell kontroll etter et gitt antall sekunder. Sett til 0 for å skru av.", + "send_split_delay": "Forsinkelse (ms) mellom `separate_turn_on_commands` for lys som ikke støtter simultane styrke og farge innstillinger.", + "adapt_delay": "Ventetid (sekunder) mellom at lyset skrues på og Adaptive Lightning sender endringer. Kan hjelpe til for å unngå blinking." + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json index 9f1abfe5..cfe3e677 100644 --- a/custom_components/adaptive_lighting/translations/nl.json +++ b/custom_components/adaptive_lighting/translations/nl.json @@ -28,62 +28,70 @@ "description": "Alle instellingen voor een Adaptieve verlichting component. De optienamen komen overeen met de YAML-instellingen. Er worden geen opties weergegeven als u het item `adaptive_lighting` hebt gedefinieerd in uw YAML-configuratie.\nVoor een demonstratie met interactieve grafieken, parameters en effecten, bezoek [deze web applicatie]({webapp_url}). Voor verdere details, bekijk de [officiële documentatie]({docs_url}).", "data": { "lights": "Lampen: lijst van `light` entiteiten om te bedienen (kan leeg zijn). 🌟", - "initial_transition": "initial_transition: Wanneer lichten van 'uit' naar 'aan' gaan. (seconden)", - "sleep_transition": "sleep_transition: Wanneer 'sleep_state' verandert. (seconden)", "interval": "interval: Tijd tussen switch-updates. (seconden)", - "max_brightness": "max_brightness: Hoogste helderheid van lichten tijdens een cyclus. (%)", - "max_color_temp": "max_color_temp: Koudste tint van de kleurtemperatuurcyclus. (kelvin)", - "min_brightness": "min_brightness: Laagste helderheid van lichten tijdens een cyclus. (%)", - "min_color_temp": "min_color_temp, Warmste tint van de kleurtemperatuurcyclus. (Kelvin)", - "only_once": "only_once: pas de verlichting alleen aan wanneer u ze aanzet.", - "prefer_rgb_color": "prefer_rgb_color: Gebruik waar mogelijk 'rgb_color' in plaats van 'color_temp'.", - "separate_turn_on_commands": "separate_turn_on_commands: Scheid de commando's voor elk attribuut (kleur, helderheid, enz.) in 'light.turn_on' (vereist voor sommige lampen).", - "send_split_delay": "send_split_delay: wacht tussen commando's (milliseconden), wanneer separate_turn_on_commands wordt gebruikt. Kan ervoor zorgen dat beide commando's correct door de lamp worden afgehandeld.", - "sleep_brightness": "sleep_brightness, helderheidsinstelling voor slaapstand. (%)", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, gebruik 'rgb_color' of 'color_temp'", - "sleep_rgb_color": "sleep_rgb_color, in RGB", - "sleep_color_temp": "sleep_color_temp: Kleurtemperatuurinstelling voor slaapstand. (kelvin)", - "sunrise_offset": "sunrise_offset: Hoe lang voor(-) of na(+) zonsopgang uitvoeren (+/- seconden)", - "sunrise_time": "sunrise_time: Handmatige wijziging van de zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", - "max_sunrise_time": "max_sunrise_time: handmatige aanpassing van de maximale zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", - "sunset_offset": "sunset_offset: Hoe lang voor(-) of na(+) zonsondergang uitvoeren (+/- seconden)", - "sunset_time": "sunset_time: handmatige onderdrukking van de zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", - "min_sunset_time": "min_sunset_time: handmatige onderdrukking van de minimale zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsondergangstijd op uw locatie gebruikt (UU:MM:SS)", - "take_over_control": "take_over_control: Als iets anders dan Adaptieve verlichting 'light.turn_on' roept wanneer een lamp al aan is, stop dan met het aanpassen van het licht totdat het (of de schakelaar) uit -> aan gaat.", - "detect_non_ha_changes": "detect_non_ha_changes: Detecteert en stopt aanpassingen voor`light.turn_on` statuswijzigingen. Vereist dat`take_over_control` is ingeschakeld. 🕵️ Voorzichtig: ⚠️ Sommige lampen kunnen een 'aan' status vals aangeven, wat kan leiden tot onverwacht inschakelen van lampen. Schakel deze functie uit als je dergelijke problemen tegenkomt.", "transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)", - "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Bij het initieel inschakelen van de lampen. Als dit op `true` is ingesteld, past Av alleen aan als `light.turn_on` wordt aangeroepen zonder een kleur of helderheid te specificeren. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Als het `false` is, past Av aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. `take_over_control` moet ingeschakeld zijn. 🕵️", - "transition_until_sleep": "transition_until_sleep: Wanneer ingeschakeld, zal Adaptieve verlichting de slaapinstellingen behandelen als het minimum, overgaand naar deze waarden na zonsondergang. 🌙", - "skip_redundant_commands": "skip_redundant_commands: Sla het verzenden van aanpassingscommando's over waarvan de doelstatus al gelijk is aan de bekende status van de lamp. Minimaliseert netwerkverkeer en verbetert de responsiviteit van de aanpassing in sommige situaties. 📉Schakel uit als de fysieke lichtstatus niet meer synchroon loopt met de door HA geregistreerde status.", - "intercept": "intercept: Onderschep en pas `light.turn_on` oproepen aan om directe kleur- en helderheidsaanpassing mogelijk te maken. 🏎️ Schakel uit voor lampen die `light.turn_on` niet ondersteunen met kleur en helderheid.", - "include_config_in_attributes": "include_config_in_attributes: Toon alle opties als attributen op de schakelaar in Home Assistant wanneer ingesteld op `true`. 📝", - "multi_light_intercept": "multi_light_intercept: Onderschep en pas `light.turn_on` oproepen aan die gericht zijn op meerdere lampen. ➗⚠️ Dit kan resulteren in het opsplitsen van een enkele `light.turn_on` call in meerdere calls, bijvoorbeeld wanneer lampen zich in verschillende schakelaars bevinden. Vereist dat `intercept` is ingeschakeld." + "min_brightness": "min_brightness: Laagste helderheid van lichten tijdens een cyclus. (%)", + "max_brightness": "max_brightness: Hoogste helderheid van lichten tijdens een cyclus. (%)", + "min_color_temp": "min_color_temp, Warmste tint van de kleurtemperatuurcyclus. (Kelvin)", + "max_color_temp": "max_color_temp: Koudste tint van de kleurtemperatuurcyclus. (kelvin)", + "sleep_brightness": "sleep_brightness, helderheidsinstelling voor slaapstand. (%)", + "sleep_color_temp": "sleep_color_temp: Kleurtemperatuurinstelling voor slaapstand. (kelvin)" }, "data_description": { - "sunrise_offset": "Pas de zonsopkomsttijd aan met een positieve of negatieve offset in seconden. ⏰", - "sunset_offset": "Pas de tijd van zonsondergang aan met een positieve of negatieve verschuiving in seconden. ⏰", "interval": "Frequentie om de lampen aan te passen, in seconden. 🔄", - "sleep_transition": "Duur van de overgang in seconden, als slaapstand wordt geactiveerd. 😴", - "autoreset_control_seconds": "Herstel de handmatige bediening automatisch na een aantal seconden. Stel in op 0 om uit te schakelen.", - "sleep_brightness": "Helderheidspercentage van lampen in slaapstand. 😴", - "sleep_color_temp": "Kleurtemperatuur in slaapmodus (gebruikt wanneer `sleep_rgb_or_color_temp` gelijk is aan `color_temp`) in Kelvin. 😴", - "brightness_mode": "Helderheidsmodus om te gebruiken. Mogelijke waarden zijn `default`, `linear` en `tanh` (gebruikt `brightness_mode_time_dark` en `brightness_mode_time_light`). 📈", - "send_split_delay": "Vertraging (ms) tussen `separate_turn_on_commands` voor lampen die geen gelijktijdige helderheids- en kleurinstelling ondersteunen. ⏲️", "transition": "Duur van de overgang, in seconden, als lampen aanpassen. 🕑", - "initial_transition": "Duur van de eerste overgang wanneer de lampen van `uit` naar `aan`gaan, in seconden. ⏲️", - "sleep_rgb_or_color_temp": "Gebruik één van beide `\"rgb_color\"` of `\"color_temp\"` in slaapstand. 🌙", - "min_sunset_time": "Stel de tijd (HH:MM:SS) in voor de meest vroege virtuele zonsondergang, maakt latere zonsondergangen mogelijk. 🌇", - "min_sunrise_time": "Stel de tijd (HH:MM:SS) in voor de meest vroege virtuele zonsopkomst, maakt latere zonsopkomsten mogelijk. 🌅", - "adapt_delay": "Wachttijd in (seconden) tussen het aanzetten van de lamp en het toepassen van Adaptieve verlichting veranderingen. Het kan helpen om knipperen tegen te gaan. ⏲️", - "sleep_rgb_color": "RGB kleur in slaapstand (wordt gebruikt wanneer `sleep_rgb_or_color_temp` gelijk is aan \"rgb_color\"). 🌈", - "brightness_mode_time_light": "(Negeer wanneer `brightness_mode='default'`) De duur in seconden van oplopende/aflopende helderheid na/voor zonsopkomst/zonsondergang. 📈📉.", - "sunset_time": "Stel een vaste tijd (HH:MM:SS) in voor zonsondergang. 🌇", - "max_sunset_time": "Stel de tijd (HH:MM:SS) in voor de laatste virtuele zonsondergang, maakt eerdere zonsondergangen mogelijk. 🌇", - "sunrise_time": "Stel een vaste tijd (HH:MM:SS) in voor zonsopkomst. 🌅", - "brightness_mode_time_dark": "(Negeer wanneer `brightness_mode='default'`) De duur in seconden van oplopende/aflopende helderheid na/voor zonsopkomst/zonsondergang. 📈📉.", - "max_sunrise_time": "Stel de tijd (HH:MM:SS) in voor de laatste virtuele zonsopkomst, maakt eerdere zonsopkomsten mogelijk. 🌅", - "take_over_control_mode": "De adaptie pauzeermodus wanneer andere bronnen de helderheid en/of kleur van lampen veranderen. `pause_all` pauzeert altijd verandering van zowel helderheid als kleur. `pause_changed` pauzeert alleen de verandering van de extern veranderde eigenschappen en blijft onveranderde eigenschappen aanpassen, bijv. doorgaan met kleur veranderen als alleen helderheid extern is veranderd." + "sleep_brightness": "Helderheidspercentage van lampen in slaapstand. 😴", + "sleep_color_temp": "Kleurtemperatuur in slaapmodus (gebruikt wanneer `sleep_rgb_or_color_temp` gelijk is aan `color_temp`) in Kelvin. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition: Wanneer lichten van 'uit' naar 'aan' gaan. (seconden)", + "prefer_rgb_color": "prefer_rgb_color: Gebruik waar mogelijk 'rgb_color' in plaats van 'color_temp'.", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, gebruik 'rgb_color' of 'color_temp'", + "sleep_rgb_color": "sleep_rgb_color, in RGB", + "sleep_transition": "sleep_transition: Wanneer 'sleep_state' verandert. (seconden)", + "transition_until_sleep": "transition_until_sleep: Wanneer ingeschakeld, zal Adaptieve verlichting de slaapinstellingen behandelen als het minimum, overgaand naar deze waarden na zonsondergang. 🌙", + "sunrise_time": "sunrise_time: Handmatige wijziging van de zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", + "max_sunrise_time": "max_sunrise_time: handmatige aanpassing van de maximale zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", + "sunrise_offset": "sunrise_offset: Hoe lang voor(-) of na(+) zonsopgang uitvoeren (+/- seconden)", + "sunset_time": "sunset_time: handmatige onderdrukking van de zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)", + "min_sunset_time": "min_sunset_time: handmatige onderdrukking van de minimale zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsondergangstijd op uw locatie gebruikt (UU:MM:SS)", + "sunset_offset": "sunset_offset: Hoe lang voor(-) of na(+) zonsondergang uitvoeren (+/- seconden)", + "take_over_control": "take_over_control: Als iets anders dan Adaptieve verlichting 'light.turn_on' roept wanneer een lamp al aan is, stop dan met het aanpassen van het licht totdat het (of de schakelaar) uit -> aan gaat.", + "detect_non_ha_changes": "detect_non_ha_changes: Detecteert en stopt aanpassingen voor`light.turn_on` statuswijzigingen. Vereist dat`take_over_control` is ingeschakeld. 🕵️ Voorzichtig: ⚠️ Sommige lampen kunnen een 'aan' status vals aangeven, wat kan leiden tot onverwacht inschakelen van lampen. Schakel deze functie uit als je dergelijke problemen tegenkomt.", + "only_once": "only_once: pas de verlichting alleen aan wanneer u ze aanzet.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Bij het initieel inschakelen van de lampen. Als dit op `true` is ingesteld, past Av alleen aan als `light.turn_on` wordt aangeroepen zonder een kleur of helderheid te specificeren. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Als het `false` is, past Av aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. `take_over_control` moet ingeschakeld zijn. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Scheid de commando's voor elk attribuut (kleur, helderheid, enz.) in 'light.turn_on' (vereist voor sommige lampen).", + "send_split_delay": "send_split_delay: wacht tussen commando's (milliseconden), wanneer separate_turn_on_commands wordt gebruikt. Kan ervoor zorgen dat beide commando's correct door de lamp worden afgehandeld.", + "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.", + "skip_redundant_commands": "skip_redundant_commands: Sla het verzenden van aanpassingscommando's over waarvan de doelstatus al gelijk is aan de bekende status van de lamp. Minimaliseert netwerkverkeer en verbetert de responsiviteit van de aanpassing in sommige situaties. 📉Schakel uit als de fysieke lichtstatus niet meer synchroon loopt met de door HA geregistreerde status.", + "intercept": "intercept: Onderschep en pas `light.turn_on` oproepen aan om directe kleur- en helderheidsaanpassing mogelijk te maken. 🏎️ Schakel uit voor lampen die `light.turn_on` niet ondersteunen met kleur en helderheid.", + "multi_light_intercept": "multi_light_intercept: Onderschep en pas `light.turn_on` oproepen aan die gericht zijn op meerdere lampen. ➗⚠️ Dit kan resulteren in het opsplitsen van een enkele `light.turn_on` call in meerdere calls, bijvoorbeeld wanneer lampen zich in verschillende schakelaars bevinden. Vereist dat `intercept` is ingeschakeld.", + "include_config_in_attributes": "include_config_in_attributes: Toon alle opties als attributen op de schakelaar in Home Assistant wanneer ingesteld op `true`. 📝" + }, + "data_description": { + "initial_transition": "Duur van de eerste overgang wanneer de lampen van `uit` naar `aan`gaan, in seconden. ⏲️", + "sleep_rgb_or_color_temp": "Gebruik één van beide `\"rgb_color\"` of `\"color_temp\"` in slaapstand. 🌙", + "sleep_rgb_color": "RGB kleur in slaapstand (wordt gebruikt wanneer `sleep_rgb_or_color_temp` gelijk is aan \"rgb_color\"). 🌈", + "sleep_transition": "Duur van de overgang in seconden, als slaapstand wordt geactiveerd. 😴", + "sunrise_time": "Stel een vaste tijd (HH:MM:SS) in voor zonsopkomst. 🌅", + "min_sunrise_time": "Stel de tijd (HH:MM:SS) in voor de meest vroege virtuele zonsopkomst, maakt latere zonsopkomsten mogelijk. 🌅", + "max_sunrise_time": "Stel de tijd (HH:MM:SS) in voor de laatste virtuele zonsopkomst, maakt eerdere zonsopkomsten mogelijk. 🌅", + "sunrise_offset": "Pas de zonsopkomsttijd aan met een positieve of negatieve offset in seconden. ⏰", + "sunset_time": "Stel een vaste tijd (HH:MM:SS) in voor zonsondergang. 🌇", + "min_sunset_time": "Stel de tijd (HH:MM:SS) in voor de meest vroege virtuele zonsondergang, maakt latere zonsondergangen mogelijk. 🌇", + "max_sunset_time": "Stel de tijd (HH:MM:SS) in voor de laatste virtuele zonsondergang, maakt eerdere zonsondergangen mogelijk. 🌇", + "sunset_offset": "Pas de tijd van zonsondergang aan met een positieve of negatieve verschuiving in seconden. ⏰", + "brightness_mode": "Helderheidsmodus om te gebruiken. Mogelijke waarden zijn `default`, `linear` en `tanh` (gebruikt `brightness_mode_time_dark` en `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Negeer wanneer `brightness_mode='default'`) De duur in seconden van oplopende/aflopende helderheid na/voor zonsopkomst/zonsondergang. 📈📉.", + "brightness_mode_time_light": "(Negeer wanneer `brightness_mode='default'`) De duur in seconden van oplopende/aflopende helderheid na/voor zonsopkomst/zonsondergang. 📈📉.", + "take_over_control_mode": "De adaptie pauzeermodus wanneer andere bronnen de helderheid en/of kleur van lampen veranderen. `pause_all` pauzeert altijd verandering van zowel helderheid als kleur. `pause_changed` pauzeert alleen de verandering van de extern veranderde eigenschappen en blijft onveranderde eigenschappen aanpassen, bijv. doorgaan met kleur veranderen als alleen helderheid extern is veranderd.", + "autoreset_control_seconds": "Herstel de handmatige bediening automatisch na een aantal seconden. Stel in op 0 om uit te schakelen.", + "send_split_delay": "Vertraging (ms) tussen `separate_turn_on_commands` voor lampen die geen gelijktijdige helderheids- en kleurinstelling ondersteunen. ⏲️", + "adapt_delay": "Wachttijd in (seconden) tussen het aanzetten van de lamp en het toepassen van Adaptieve verlichting veranderingen. Het kan helpen om knipperen tegen te gaan. ⏲️" + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json index 28b22d3a..8957db09 100644 --- a/custom_components/adaptive_lighting/translations/pl.json +++ b/custom_components/adaptive_lighting/translations/pl.json @@ -21,55 +21,63 @@ "description": "Konfiguracja komponentu Adaptacyjnego oświetlenia. Nazwy opcji odpowiadają ustawieniom YAML. Żadne opcje nie są wyświetlane, jeśli są zdefiniowane w konfiguracji YAML. Aby zobaczyć interaktywne wykresy demonstrujące działanie parametrów odwiedź [tą aplikację webową]({webapp_url}). Aby zobaczyć więcej szczegółów odwiedź [oficjalną dokumentację]({docs_url}).", "data": { "lights": "lights: Lista `entity_id`, które mają być kontrolowane (może być pusta). 🌟", - "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (sekund)", - "sleep_transition": "sleep_transition: When 'sleep_state' changes. (sekund)", "interval": "interval: Time between switch updates. (sekund)", - "max_brightness": "max_brightness: Maksymalna jasność (w procentach). 💡", - "max_color_temp": "max_color_temp: Najzimniejsza temperatura barwowa (w Kelwinach). ❄️", - "min_brightness": "min_brightness: Minimalna jasność (w procentach). 💡", - "min_color_temp": "min_color_temp: Najcieplejsza temperatura barwowa (w Kelwinach). 🔥", - "only_once": "only_once: Adaptuj światło tylko podczas włączenia (`true`) lub adaptuj cały czas (`false`). 🔄", - "prefer_rgb_color": "prefer_rgb_color: Czy w miarę możliwości preferować regulację kolorów RGB zamiast regulacji temperatury barwowej światła. 🌈", - "separate_turn_on_commands": "separate_turn_on_commands: Używaj oddzielnych wywołań `light.turn_on` dla koloru i jasności, wymagane dla niektórych typów świateł. 🔀", - "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)", - "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)", - "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- sekund)", - "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", - "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- sekund)", - "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", - "take_over_control": "take_over_control: Wyłącz adaptowanie oświetlenia, kiedy inna usługa wywoła `light.turn_on`, gdy oświetlenie jest już włączone. Zauważ, że to wywołuje `homeassistant.update_entity` co`interval`! 🔒", - "detect_non_ha_changes": "detect_non_ha_changes: Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan \"on\", co może powodować nieoczekiwane włączanie się świateł. Wyłącz to ustawienie, jeżeli doświadczasz takich objawów.", "transition": "Transition time when applying a change to the lights (sekund)", - "transition_until_sleep": "transition_until_sleep: Gdy włączone, Adaptacyjne oświetlenie będzie traktowało ustawienia spania jako minimalne i przejdzie do nich po zachodzie słońca. 🌙", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Gdy włączone (`true`) to adaptacyjne oświetlenie zastosuje adaptacje tylko jeżeli `light.turn_on` jest wywołane bez konkretnego koloru lub jasności. ❌🌈 To ustawienie zapobiega między innymi adaptacji, gdy aktywowana jest scena. Gdy wyłączone (`false`), adaptacyjne oświetlenie zastosuje adaptacje niezależnie czy `service_data` zawiera kolor lub jasność. Potrzebuje włączonej opcji `take_over_control`. 🕵️", - "skip_redundant_commands": "skip_redundant_commands: Pomiń wysyłanie polecenia adaptacji, jeżeli stan światła jest taki sam jak docelowy stan adaptacji. Minimalizuje to ruch sieciowy oraz w niektórych przypadkach poprawia szybkość działania. 📉 Wyłącz, jeżeli faktyczny stan światła się nie pokrywa z tym który widnieje w Home Assistant.", - "include_config_in_attributes": "include_config_in_attributes: Gdy włączone (`true`) pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant. 📝", - "intercept": "intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`, aby błyskawicznie dostosować kolor i jasność. 🏎️ Wyłącz dla świateł, które nie akceptują wywołania `light.turn_on` zawierającego kolor i jasność.", - "multi_light_intercept": "multi_light_intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`, które dotyczą wielu świateł. ➗⚠️ Może to powodować rozdzielenie pojedynczego wywołania `light.turn_on` na wiele wywołań, na przykład gdy światła są przypisane do rożnych instancji. Wymaga włączonej opcji `intercept`." + "min_brightness": "min_brightness: Minimalna jasność (w procentach). 💡", + "max_brightness": "max_brightness: Maksymalna jasność (w procentach). 💡", + "min_color_temp": "min_color_temp: Najcieplejsza temperatura barwowa (w Kelwinach). 🔥", + "max_color_temp": "max_color_temp: Najzimniejsza temperatura barwowa (w Kelwinach). ❄️", + "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)", + "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)" }, "data_description": { "interval": "Częstotliwość adaptacji świateł w sekundach. 🔄", "transition": "Długość przejścia do nowego stanu (w sekundach). 🕑", - "initial_transition": "Długość pierwszego przejścia, gdy światło zostanie przełączone z `off` na `on` (w sekundach). ⏲️", - "sleep_rgb_or_color_temp": "Użyj `\"rgb_color\"` albo `\"color_temp\"` w trybie spania. 🌙", - "sleep_color_temp": "Temperatura barwowa w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `color_temp`) (w Kelwinach). 😴", - "sleep_rgb_color": "Kolor RGB w trybie spania (używane, gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈", - "sleep_transition": "Długość przejścia, gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴", - "sunrise_time": "Ustaw stały czas wschodu słońca (HH:MM:SS). 🌅", "sleep_brightness": "Jasność świateł w trybie spania (w procentach). 😴", - "min_sunrise_time": "Ustaw czas najwcześniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na opóźnienie wschodu słońca. 🌅", - "max_sunrise_time": "Ustaw czas najpóźniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na przyspieszenie wschodu słońca. 🌅", - "sunrise_offset": "Dostosuj czas wschodu słońca - przesunięcie o +/- sekund. ⏰", - "sunset_time": "Ustaw stały czas zachodu słońca (HH:MM:SS). 🌇", - "min_sunset_time": "Ustaw czas najwcześniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na opóźnienie zachodu słońca. 🌇", - "brightness_mode": "Tryb ustawiania jasności. Dostępne opcje to `default`, `linear` i `tanh` (używa `brightness_mode_time_dark` i `brightness_mode_time_light`). 📈", - "max_sunset_time": "Ustaw czas najpóźniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na przyspieszenie zachodu słońca. 🌇", - "sunset_offset": "Dostosuj czas zachodu słońca - przesunięcie o +/- sekund. ⏰", - "brightness_mode_time_dark": "(Pomijany, gdy `brightness_mode='default'`). Czas w sekundach, kiedy jasność będzie zwiększana przed wschodem słońca/zmniejszana po zachodzie słońca. 📈📉", - "brightness_mode_time_light": "(Pomijany, gdy `brightness_mode='default'`). Czas w sekundach, kiedy jasność będzie zwiększana po wschodzie słońca/zmniejszana przed zachodem słońca. 📈📉", - "autoreset_control_seconds": "Czas, po którym manualna kontrola zostanie wyłączona (w sekundach). Ustaw 0, aby wyłączyć. ⏲️", - "send_split_delay": "Opóźnienie (w ms) pomiędzy `separate_turn_on_commands` dla świateł, które nie akceptują jednoczesnego ustawiania jasności i koloru. ⏲️", - "adapt_delay": "Czas (w sekundach) pomiędzy włączeniem światła, a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Może pomóc zredukować migotanie. ⏲️" + "sleep_color_temp": "Temperatura barwowa w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `color_temp`) (w Kelwinach). 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (sekund)", + "prefer_rgb_color": "prefer_rgb_color: Czy w miarę możliwości preferować regulację kolorów RGB zamiast regulacji temperatury barwowej światła. 🌈", + "sleep_transition": "sleep_transition: When 'sleep_state' changes. (sekund)", + "transition_until_sleep": "transition_until_sleep: Gdy włączone, Adaptacyjne oświetlenie będzie traktowało ustawienia spania jako minimalne i przejdzie do nich po zachodzie słońca. 🌙", + "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)", + "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- sekund)", + "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)", + "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- sekund)", + "take_over_control": "take_over_control: Wyłącz adaptowanie oświetlenia, kiedy inna usługa wywoła `light.turn_on`, gdy oświetlenie jest już włączone. Zauważ, że to wywołuje `homeassistant.update_entity` co`interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan \"on\", co może powodować nieoczekiwane włączanie się świateł. Wyłącz to ustawienie, jeżeli doświadczasz takich objawów.", + "only_once": "only_once: Adaptuj światło tylko podczas włączenia (`true`) lub adaptuj cały czas (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Gdy włączone (`true`) to adaptacyjne oświetlenie zastosuje adaptacje tylko jeżeli `light.turn_on` jest wywołane bez konkretnego koloru lub jasności. ❌🌈 To ustawienie zapobiega między innymi adaptacji, gdy aktywowana jest scena. Gdy wyłączone (`false`), adaptacyjne oświetlenie zastosuje adaptacje niezależnie czy `service_data` zawiera kolor lub jasność. Potrzebuje włączonej opcji `take_over_control`. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Używaj oddzielnych wywołań `light.turn_on` dla koloru i jasności, wymagane dla niektórych typów świateł. 🔀", + "skip_redundant_commands": "skip_redundant_commands: Pomiń wysyłanie polecenia adaptacji, jeżeli stan światła jest taki sam jak docelowy stan adaptacji. Minimalizuje to ruch sieciowy oraz w niektórych przypadkach poprawia szybkość działania. 📉 Wyłącz, jeżeli faktyczny stan światła się nie pokrywa z tym który widnieje w Home Assistant.", + "intercept": "intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`, aby błyskawicznie dostosować kolor i jasność. 🏎️ Wyłącz dla świateł, które nie akceptują wywołania `light.turn_on` zawierającego kolor i jasność.", + "multi_light_intercept": "multi_light_intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`, które dotyczą wielu świateł. ➗⚠️ Może to powodować rozdzielenie pojedynczego wywołania `light.turn_on` na wiele wywołań, na przykład gdy światła są przypisane do rożnych instancji. Wymaga włączonej opcji `intercept`.", + "include_config_in_attributes": "include_config_in_attributes: Gdy włączone (`true`) pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant. 📝" + }, + "data_description": { + "initial_transition": "Długość pierwszego przejścia, gdy światło zostanie przełączone z `off` na `on` (w sekundach). ⏲️", + "sleep_rgb_or_color_temp": "Użyj `\"rgb_color\"` albo `\"color_temp\"` w trybie spania. 🌙", + "sleep_rgb_color": "Kolor RGB w trybie spania (używane, gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈", + "sleep_transition": "Długość przejścia, gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴", + "sunrise_time": "Ustaw stały czas wschodu słońca (HH:MM:SS). 🌅", + "min_sunrise_time": "Ustaw czas najwcześniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na opóźnienie wschodu słońca. 🌅", + "max_sunrise_time": "Ustaw czas najpóźniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na przyspieszenie wschodu słońca. 🌅", + "sunrise_offset": "Dostosuj czas wschodu słońca - przesunięcie o +/- sekund. ⏰", + "sunset_time": "Ustaw stały czas zachodu słońca (HH:MM:SS). 🌇", + "min_sunset_time": "Ustaw czas najwcześniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na opóźnienie zachodu słońca. 🌇", + "max_sunset_time": "Ustaw czas najpóźniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na przyspieszenie zachodu słońca. 🌇", + "sunset_offset": "Dostosuj czas zachodu słońca - przesunięcie o +/- sekund. ⏰", + "brightness_mode": "Tryb ustawiania jasności. Dostępne opcje to `default`, `linear` i `tanh` (używa `brightness_mode_time_dark` i `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Pomijany, gdy `brightness_mode='default'`). Czas w sekundach, kiedy jasność będzie zwiększana przed wschodem słońca/zmniejszana po zachodzie słońca. 📈📉", + "brightness_mode_time_light": "(Pomijany, gdy `brightness_mode='default'`). Czas w sekundach, kiedy jasność będzie zwiększana po wschodzie słońca/zmniejszana przed zachodem słońca. 📈📉", + "autoreset_control_seconds": "Czas, po którym manualna kontrola zostanie wyłączona (w sekundach). Ustaw 0, aby wyłączyć. ⏲️", + "send_split_delay": "Opóźnienie (w ms) pomiędzy `separate_turn_on_commands` dla świateł, które nie akceptują jednoczesnego ustawiania jasności i koloru. ⏲️", + "adapt_delay": "Czas (w sekundach) pomiędzy włączeniem światła, a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Może pomóc zredukować migotanie. ⏲️" + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/pt-BR.json b/custom_components/adaptive_lighting/translations/pt-BR.json index 549090e6..76042c8b 100644 --- a/custom_components/adaptive_lighting/translations/pt-BR.json +++ b/custom_components/adaptive_lighting/translations/pt-BR.json @@ -28,50 +28,58 @@ "description": "Todas as configurações de um componente de iluminação adaptativa. Os nomes das opções correspondem às configurações de YAML. Nenhuma opção será exibida se você tiver a entrada adaptive_lighting definida em sua configuração YAML.", "data": { "lights": "luzes", - "initial_transition": "initial_transition: Quando as luzes mudam de 'off' para 'on'. (segundos)", - "sleep_transition": "sleep_transition: Quando 'sleep_state' muda. (segundos)", "interval": "interval: Tempo entre as atualizações do switch. (segundos)", - "max_brightness": "max_brightness: Maior brilho das luzes durante um ciclo. (%)", - "max_color_temp": "max_color_temp: Matiz mais frio do ciclo de temperatura de cor. (Kelvin)", - "min_brightness": "min_brightness: Menor brilho das luzes durante um ciclo. (%)", - "min_color_temp": "min_color_temp, matiz mais quente do ciclo de temperatura de cor. (Kelvin)", - "only_once": "only_once: Apenas adapte as luzes ao ligá-las.", - "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' em vez de 'color_temp' quando possível.", - "separate_turn_on_commands": "separar_turn_on_commands: Separe os comandos para cada atributo (cor, brilho, etc.) em 'light.turn_on' (necessário para algumas luzes).", - "sleep_brightness": "sleep_brightness, configuração de brilho para o modo de suspensão. (%)", - "sleep_color_temp": "sleep_color_temp: configuração de temperatura de cor para o modo de suspensão. (Kelvin)", - "sunrise_offset": "sunrise_offset: Quanto tempo antes (-) ou depois (+) para definir o ponto do nascer do sol do ciclo (+/- segundos)", - "sunrise_time": "sunrise_time: substituição manual do horário do nascer do sol, se 'Nenhum', ele usa o horário real do nascer do sol em sua localização (HH:MM:SS)", - "sunset_offset": "Sunset_offset: Quanto tempo antes (-) ou depois (+) para definir o ponto de pôr do sol do ciclo (+/- segundos)", - "sunset_time": "sunset_time: substituição manual do horário do pôr do sol, se 'Nenhum', ele usa o horário real do nascer do sol em sua localização (HH:MM:SS)", - "take_over_control": "take_over_control: Se qualquer coisa, exceto Adaptive Lighting, chamar 'light.turn_on' quando uma luz já estiver acesa, pare de adaptar essa luz até que ela (ou o interruptor) desligue -> ligue.", - "detect_non_ha_changes": "detect_non_ha_changes: detecta todas as alterações > 10% feitas nas luzes (também fora do HA), requer que 'take_over_control' seja ativado (chama 'homeassistant.update_entity' a cada 'intervalo'!)", "transition": "Tempo de transição ao aplicar uma mudança nas luzes (segundos)", - "skip_redundant_commands": "skip_redundant_commands: Deixar de enviar comandos de adaptação cujo estado alvo já seja igual ao estado atual da luz. Minimiza o tráfego de rede e melhora a responsividade da adaptação em algumas situações. 📉Desative se os estados físicos das luzes podem ficar diferentes do estado registrado no HA.", - "transition_until_sleep": "transition_until_sleep: Quando ativada, a Iluminação Adaptativa considerará as configurações de sono como o valor mínimo, transicionando para esses valores após o pôr do sol. 🌙", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Ao ligar as luzes inicialmente. Se definido como `true`, a Iluminação Adaptativa se adapta somente se o comando `light.turn_on` for chamado sem especificar cor ou brilho. ❌🌈 Isso, por exemplo, impede a adaptação ao ativar uma cena. Se false, a Iluminação Adaptativa se adapta independentemente da presença de cor ou brilho nos dados iniciais do `service_data`. Precisa de `take_over_control` ativado. 🕵️", - "intercept": "intercept: Interceptar e adaptar os chamados `light.turn_on` para ativar a adaptação instantânea de cor e brilho. 🏎️ Desative para luzes que não suportam `light.turn_on` com cor e brilho.", - "include_config_in_attributes": "include_config_in_attributes: Mostra todas as opções como atributos no interruptor do Home Assistant quando está definido para `true`. 📝", - "multi_light_intercept": "multi_light_intercept: Interceptar e adaptar chamadas de 'light.turn_on' que visem múltiplas luzes. ➗⚠️ Isso pode resultar na divisão de uma única chamada de 'light.turn_on' em múltiplas chamadas, por exemplo, quando as luzes estão em interruptores diferentes. Exige que 'intercept' esteja ativado." + "min_brightness": "min_brightness: Menor brilho das luzes durante um ciclo. (%)", + "max_brightness": "max_brightness: Maior brilho das luzes durante um ciclo. (%)", + "min_color_temp": "min_color_temp, matiz mais quente do ciclo de temperatura de cor. (Kelvin)", + "max_color_temp": "max_color_temp: Matiz mais frio do ciclo de temperatura de cor. (Kelvin)", + "sleep_brightness": "sleep_brightness, configuração de brilho para o modo de suspensão. (%)", + "sleep_color_temp": "sleep_color_temp: configuração de temperatura de cor para o modo de suspensão. (Kelvin)" }, "data_description": { "interval": "Frequência, em segundos, para adaptar as luzes. 🔄", - "sunrise_time": "Define um horário fixo (HH:MM:SS) para o nascer do sol. 🌅", - "autoreset_control_seconds": "Redefine automaticamente o controle manual após um período de tempo definido em segundos. Defina 0 para desabilitar. ⏲️", "transition": "Duração da transição, em segundos, quando as luzes mudam. 🕑", "sleep_brightness": "Porcentagem do brilho das luzes no modo dormir. 😴", - "initial_transition": "Duração da primeira transição, em segundos, quando as luzes alternarem de 'desligado' para 'ligado'. ⏲️", - "sleep_transition": "Duração da transição em segundos quando o modo dormir é alterado. 😴", - "sunset_offset": "Ajusta o horário do pôr do sol com um deslocamento positivo ou negativo em segundos. ⏰", - "sunrise_offset": "Ajusta o horário do nascer do sol com um deslocamento positivo ou negativo em segundos. ⏰", - "sunset_time": "Definir um horário fixo (HH:MM:SS) para o pôr do sol. 🌇", - "sleep_color_temp": "Temperatura de Cor no modo dormir (usado quando `sleep_rgb_or_color_temp` é `color_temp`) em Kelvin. 😴", - "sleep_rgb_or_color_temp": "Use `\"rgb_color\"` ou `\"color_temp\"` no modo dormir. 🌙", - "adapt_delay": "Tempo de espera (segundos) entre a luz ligar e a aplicação das mudanças da iluminação adaptativa. Pode ajudar a evitar que a luz pisque. ⏲️", - "min_sunrise_time": "Defina o horário virtual mais cedo do nascer do sol (HH:MM:SS), permitindo nasceres do sol mais tarde. 🌅", - "max_sunrise_time": "Defina o horário virtual mais recente do nascer do sol (HH:MM:SS), permitindo nasceres do sol mais cedo. 🌅", - "max_sunset_time": "Defina o horário virtual mais recente do pôr do sol (HH:MM:SS), permitindo pores do sol mais cedo. 🌇", - "min_sunset_time": "Defina o horário virtual mais cedo do pôr do sol (HH:MM:SS), permitindo pores do sol mais tarde. 🌇" + "sleep_color_temp": "Temperatura de Cor no modo dormir (usado quando `sleep_rgb_or_color_temp` é `color_temp`) em Kelvin. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition: Quando as luzes mudam de 'off' para 'on'. (segundos)", + "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' em vez de 'color_temp' quando possível.", + "sleep_transition": "sleep_transition: Quando 'sleep_state' muda. (segundos)", + "transition_until_sleep": "transition_until_sleep: Quando ativada, a Iluminação Adaptativa considerará as configurações de sono como o valor mínimo, transicionando para esses valores após o pôr do sol. 🌙", + "sunrise_time": "sunrise_time: substituição manual do horário do nascer do sol, se 'Nenhum', ele usa o horário real do nascer do sol em sua localização (HH:MM:SS)", + "sunrise_offset": "sunrise_offset: Quanto tempo antes (-) ou depois (+) para definir o ponto do nascer do sol do ciclo (+/- segundos)", + "sunset_time": "sunset_time: substituição manual do horário do pôr do sol, se 'Nenhum', ele usa o horário real do nascer do sol em sua localização (HH:MM:SS)", + "sunset_offset": "Sunset_offset: Quanto tempo antes (-) ou depois (+) para definir o ponto de pôr do sol do ciclo (+/- segundos)", + "take_over_control": "take_over_control: Se qualquer coisa, exceto Adaptive Lighting, chamar 'light.turn_on' quando uma luz já estiver acesa, pare de adaptar essa luz até que ela (ou o interruptor) desligue -> ligue.", + "detect_non_ha_changes": "detect_non_ha_changes: detecta todas as alterações > 10% feitas nas luzes (também fora do HA), requer que 'take_over_control' seja ativado (chama 'homeassistant.update_entity' a cada 'intervalo'!)", + "only_once": "only_once: Apenas adapte as luzes ao ligá-las.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Ao ligar as luzes inicialmente. Se definido como `true`, a Iluminação Adaptativa se adapta somente se o comando `light.turn_on` for chamado sem especificar cor ou brilho. ❌🌈 Isso, por exemplo, impede a adaptação ao ativar uma cena. Se false, a Iluminação Adaptativa se adapta independentemente da presença de cor ou brilho nos dados iniciais do `service_data`. Precisa de `take_over_control` ativado. 🕵️", + "separate_turn_on_commands": "separar_turn_on_commands: Separe os comandos para cada atributo (cor, brilho, etc.) em 'light.turn_on' (necessário para algumas luzes).", + "skip_redundant_commands": "skip_redundant_commands: Deixar de enviar comandos de adaptação cujo estado alvo já seja igual ao estado atual da luz. Minimiza o tráfego de rede e melhora a responsividade da adaptação em algumas situações. 📉Desative se os estados físicos das luzes podem ficar diferentes do estado registrado no HA.", + "intercept": "intercept: Interceptar e adaptar os chamados `light.turn_on` para ativar a adaptação instantânea de cor e brilho. 🏎️ Desative para luzes que não suportam `light.turn_on` com cor e brilho.", + "multi_light_intercept": "multi_light_intercept: Interceptar e adaptar chamadas de 'light.turn_on' que visem múltiplas luzes. ➗⚠️ Isso pode resultar na divisão de uma única chamada de 'light.turn_on' em múltiplas chamadas, por exemplo, quando as luzes estão em interruptores diferentes. Exige que 'intercept' esteja ativado.", + "include_config_in_attributes": "include_config_in_attributes: Mostra todas as opções como atributos no interruptor do Home Assistant quando está definido para `true`. 📝" + }, + "data_description": { + "initial_transition": "Duração da primeira transição, em segundos, quando as luzes alternarem de 'desligado' para 'ligado'. ⏲️", + "sleep_rgb_or_color_temp": "Use `\"rgb_color\"` ou `\"color_temp\"` no modo dormir. 🌙", + "sleep_transition": "Duração da transição em segundos quando o modo dormir é alterado. 😴", + "sunrise_time": "Define um horário fixo (HH:MM:SS) para o nascer do sol. 🌅", + "min_sunrise_time": "Defina o horário virtual mais cedo do nascer do sol (HH:MM:SS), permitindo nasceres do sol mais tarde. 🌅", + "max_sunrise_time": "Defina o horário virtual mais recente do nascer do sol (HH:MM:SS), permitindo nasceres do sol mais cedo. 🌅", + "sunrise_offset": "Ajusta o horário do nascer do sol com um deslocamento positivo ou negativo em segundos. ⏰", + "sunset_time": "Definir um horário fixo (HH:MM:SS) para o pôr do sol. 🌇", + "min_sunset_time": "Defina o horário virtual mais cedo do pôr do sol (HH:MM:SS), permitindo pores do sol mais tarde. 🌇", + "max_sunset_time": "Defina o horário virtual mais recente do pôr do sol (HH:MM:SS), permitindo pores do sol mais cedo. 🌇", + "sunset_offset": "Ajusta o horário do pôr do sol com um deslocamento positivo ou negativo em segundos. ⏰", + "autoreset_control_seconds": "Redefine automaticamente o controle manual após um período de tempo definido em segundos. Defina 0 para desabilitar. ⏲️", + "adapt_delay": "Tempo de espera (segundos) entre a luz ligar e a aplicação das mudanças da iluminação adaptativa. Pode ajudar a evitar que a luz pisque. ⏲️" + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/pt.json b/custom_components/adaptive_lighting/translations/pt.json index cc94ff24..888f2e1e 100644 --- a/custom_components/adaptive_lighting/translations/pt.json +++ b/custom_components/adaptive_lighting/translations/pt.json @@ -63,13 +63,8 @@ "step": { "init": { "data_description": { - "sunrise_offset": "Ajustar a hora do nascer do sol com um offset positivo ou negativo em segundos. ⏰", - "sunset_offset": "Ajustar a hora do pôr do sol com um offset positivo ou negativo em segundos. ⏰", - "sleep_transition": "Duração da transição quando o \"modo dormir\" é alternado em segundos. 😴", - "autoreset_control_seconds": "Reiniciar o controlo manual automaticamente após um número de segundos. Definir 0 para desativar. ⏲️", "transition": "Duração da transição quando as luzes mudam, em segundos. 🕑", - "sleep_brightness": "Porcentagem do brilho da lâmpadas no modo \"sleep mode\".", - "brightness_mode": "Brilho que irá ser usado. Possíveis valores são `default`, `linear` e `tanh`(usa `brightness_mode_time_dark` e `brightness_mode_time_light`). 📈" + "sleep_brightness": "Porcentagem do brilho da lâmpadas no modo \"sleep mode\"." }, "title": "Opções da Iluminação Adaptativa", "description": "Configure um componente da Iluminação Adaptativa. O nome das opções são as mesmas que as do YML. Se você já definiu essa configuração no YAML, nenhuma opção vai aparecer aqui. Para acessar um gráfico que demonstra o efeito dos parâmetros, acesse [esse app]({webapp_url}). Para mais detalhes, veja a [documentação oficial]({docs_url}).", @@ -78,10 +73,23 @@ "min_brightness": "min_brightness: Percentagem minima de brilho. 💡", "max_brightness": "max_brightness: Percentagem máxima de brilho. 💡", "min_color_temp": "min_color_temp: Cor mais quente em Kelvin. 🔥", - "max_color_temp": "max_color_temp: Cor mais fria em Kelvin. ❄️", - "prefer_rgb_color": "prefer_rgb_color: Quando possível escolher ajuste em RGB em vez de temperatura da cor. 🌈", - "transition_until_sleep": "transition_until_sleep: Quando ativado, Adaptive Lighting usará as definições do modo noturno como os mínimos, passando para esses valores no por do sol. 🌙", - "take_over_control": "take_over_control: Desativa Adaptive Lighting se alguma fonte chamar`light.turn_on` enquanto as luzes estiverem ligadas e a serem controladas. Tomar nota que esta opção chama o serviço `homeassistant.update_entity` a cada `interval`! 🔒" + "max_color_temp": "max_color_temp: Cor mais fria em Kelvin. ❄️" + }, + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: Quando possível escolher ajuste em RGB em vez de temperatura da cor. 🌈", + "transition_until_sleep": "transition_until_sleep: Quando ativado, Adaptive Lighting usará as definições do modo noturno como os mínimos, passando para esses valores no por do sol. 🌙", + "take_over_control": "take_over_control: Desativa Adaptive Lighting se alguma fonte chamar`light.turn_on` enquanto as luzes estiverem ligadas e a serem controladas. Tomar nota que esta opção chama o serviço `homeassistant.update_entity` a cada `interval`! 🔒" + }, + "data_description": { + "sleep_transition": "Duração da transição quando o \"modo dormir\" é alternado em segundos. 😴", + "sunrise_offset": "Ajustar a hora do nascer do sol com um offset positivo ou negativo em segundos. ⏰", + "sunset_offset": "Ajustar a hora do pôr do sol com um offset positivo ou negativo em segundos. ⏰", + "brightness_mode": "Brilho que irá ser usado. Possíveis valores são `default`, `linear` e `tanh`(usa `brightness_mode_time_dark` e `brightness_mode_time_light`). 📈", + "autoreset_control_seconds": "Reiniciar o controlo manual automaticamente após um número de segundos. Definir 0 para desativar. ⏲️" + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/ro.json b/custom_components/adaptive_lighting/translations/ro.json index 2daa6c6c..a128e229 100644 --- a/custom_components/adaptive_lighting/translations/ro.json +++ b/custom_components/adaptive_lighting/translations/ro.json @@ -14,17 +14,24 @@ "step": { "init": { "data_description": { - "brightness_mode_time_light": "(Se ignoră dacă `modul_de_luminozitate='implicit'`) Durată în secunde a modificării luminozităţii în sus/jos cand poziţia sorelui este înainte sau după răsărit/apus.", - "sunrise_offset": "Ajustați ora răsăritului cu un decalaj pozitiv sau negativ în secunde.⏰", - "autoreset_control_seconds": "Resetare automată al controlului manual după un număr de secunde. Setaţi la 0 pentru a dezactiva.", - "brightness_mode": "Mod de luminozitate de utilizat. Valorile posibile sunt: 'implicit', 'liniar' şi 'hiperbolic' ( ultilizează 'mod_luminozitate_timp_de_noapte' şi 'mod_luminozitate_timp_de_zi').", - "sleep_brightness": "Procentul luminozităţii luminilor în modul 'somn'.", "interval": "Frecvenţa adaptării luminilor, în secunde.", - "sunset_offset": "Ajustați ora răsăritului cu un decalaj pozitiv sau negativ în secunde." + "sleep_brightness": "Procentul luminozităţii luminilor în modul 'somn'." }, "title": "Opţiuni Iluminare Adaptivă", - "data": { - "adapt_only_on_bare_turn_on": "adaptează_doar_la_comanda_de_arpindere: La aprinderea iniţială a luminilor. Dacă este activat, IA va adapta luminile doar dacă se invocă 'light.turn_on' fără a specifica culoarea sau luminozitatea. Aceasta, de exemplu, previne adaptarea atunci când se activează o scenă. Dacă este dezactivat,IA va adaptata luminile indiferent de prezența valorilor culorii sau luminozității în service_data. Necesită activarea opţiunii 'preia_controlul. " + "data": {}, + "sections": { + "advanced": { + "data": { + "adapt_only_on_bare_turn_on": "adaptează_doar_la_comanda_de_arpindere: La aprinderea iniţială a luminilor. Dacă este activat, IA va adapta luminile doar dacă se invocă 'light.turn_on' fără a specifica culoarea sau luminozitatea. Aceasta, de exemplu, previne adaptarea atunci când se activează o scenă. Dacă este dezactivat,IA va adaptata luminile indiferent de prezența valorilor culorii sau luminozității în service_data. Necesită activarea opţiunii 'preia_controlul. " + }, + "data_description": { + "sunrise_offset": "Ajustați ora răsăritului cu un decalaj pozitiv sau negativ în secunde.⏰", + "sunset_offset": "Ajustați ora răsăritului cu un decalaj pozitiv sau negativ în secunde.", + "brightness_mode": "Mod de luminozitate de utilizat. Valorile posibile sunt: 'implicit', 'liniar' şi 'hiperbolic' ( ultilizează 'mod_luminozitate_timp_de_noapte' şi 'mod_luminozitate_timp_de_zi').", + "brightness_mode_time_light": "(Se ignoră dacă `modul_de_luminozitate='implicit'`) Durată în secunde a modificării luminozităţii în sus/jos cand poziţia sorelui este înainte sau după răsărit/apus.", + "autoreset_control_seconds": "Resetare automată al controlului manual după un număr de secunde. Setaţi la 0 pentru a dezactiva." + } + } } } } diff --git a/custom_components/adaptive_lighting/translations/ru.json b/custom_components/adaptive_lighting/translations/ru.json index 77800d7e..cdb4b768 100644 --- a/custom_components/adaptive_lighting/translations/ru.json +++ b/custom_components/adaptive_lighting/translations/ru.json @@ -21,65 +21,73 @@ "description": "Все настройки компонента Adaptive Lighting. Названия опций соответствуют настройкам в YAML. Параметры не отображаются, если в конфигурации YAML определена запись adaptive_lighting.", "data": { "lights": "Осветительные приборы: список источников света, которыми нужно управлять (может быть пустым). 🌟", - "initial_transition": "initial_transition: Начальный переход, когда свет переключается с 'off' на 'on'. (секунды)", - "sleep_transition": "sleep_transition: Когда прибор переходит в Режима Сна (Sleep Mode) и 'sleep_state' изменяется. (секунды)", "interval": "interval: Интервал между обновлениями переключателя. (секунды)", - "max_brightness": "max_brightness: Максимальная яркость света во время цикла. (%)", - "max_color_temp": "max_color_temp: Самый холодный оттенок цветовой температуры во время цикла. (Kelvin)", - "min_brightness": "min_brightness: Минимальная яркость света во время цикла. (%)", - "min_color_temp": "min_color_temp: Самый теплый оттенок цветовой температуры во время цикла. (Kelvin)", - "only_once": "only_once: Адаптировать свет только при включении.", - "prefer_rgb_color": "prefer_rgb_color: По возможности использовать 'rgb_color' вместо 'color_temp'.", - "separate_turn_on_commands": "separate_turn_on_commands: Раздельные команды для каждого атрибута (цвет, яркость и т.д.) в 'light.turn_on' (требуется для некоторых источников света).", - "sleep_brightness": "sleep_brightness: Настройка яркости для Режима Сна (Sleep Mode). (%)", - "sleep_color_temp": "sleep_color_temp: Настройка цветовой температуры для Режима Сна (Sleep Mode). (Kelvin)", - "sunrise_offset": "sunrise_offset: За сколько времени до (-) или после (+) переопределить время восхода во время цикла. (+/- секунды)", - "sunrise_time": "sunrise_time: Ручное изменение времени восхода солнца, если указано 'None', используется фактическое время восхода в Вашем местоположении. (ЧЧ:ММ:СС)", - "sunset_offset": "sunset_offset: За сколько времени до (-) или после (+) переопределить время заката во время цикла. (+/- секунды)", - "sunset_time": "sunset_time: Ручное изменение времени заката солнца, если указано 'None', используется фактическое время заката в Вашем местоположении. (ЧЧ:ММ:СС)", - "take_over_control": "take_over_control: Если что-либо, кроме Adaptive Lighting, вызывает службу 'light.turn_on', когда свет уже включен, прекратить адаптацию этого осветительного прибора, пока он (или переключатель) не переключится off -> on.", - "detect_non_ha_changes": "detect_non_ha_changes: Обнаруживает все изменения на >10% примененные к освещению (также и из-за пределов Home Assistant), требует включения 'take_over_control' (вызывает 'homeassistant.update_entity' каждый 'interval'!)", "transition": "Время перехода при применении изменения к источникам света. (секунды)", - "adapt_delay": "Время ожидания между включением света и применением адаптации. Может помочь избежать мерцания. (секунды)", - "multi_light_intercept": "multi_light_intercept: перехватывает и адаптирует вызовы `light.turn_on`, нацеленные на несколько источников света. ➗⚠️ Это может привести к разделению одного вызова `light.turn_on` на несколько вызовов, например, когда освещение включено в разные выключатели. Требуется, чтобы `перехват` был включен.", - "adapt_only_on_bare_turn_on": "Adapt_only_on_bare_turn_on: При первоначальном включении света. Если установлено значение «true», AL адаптируется только в том случае, если «light.turn_on» вызывается без указания цвета или яркости. ❌🌈 Это, например, предотвращает адаптацию при активации сцены. Если false, AL адаптируется независимо от наличия цвета или яркости в исходных service_data. Требуется включить take_over_control. 🕵️", - "skip_redundant_commands": "Skip_redundant_commands: Пропустить отправку команд адаптации, целевое состояние которых уже равно известному состоянию источника света. Минимизирует сетевой трафик и улучшает скорость адаптации в некоторых ситуациях. 📉Отключите, если физические состояния освещения не синхронизируются с записанным состоянием HA.", - "intercept": "intercept: перехватывать и адаптировать вызовы `light.turn_on` для обеспечения мгновенной адаптации цвета и яркости. 🏎️ Отключите источники света, которые не поддерживают `light.turn_on` с цветом и яркостью.", - "include_config_in_attributes": "include_config_in_attributes: отображать все параметры в качестве атрибутов на переключателе в Home Assistant, если установлено значение `true`. 📝", - "transition_until_sleep": "transition_until_sleep: когда включено, адаптивное освещение будет рассматривать настройки сна как минимальные, переходя к этим значениям после захода солнца. 🌙", - "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Использовать либо 'rgb_color', либо 'color_temp' в режиме сна. 🌙", - "sleep_rgb_color": "sleep_rgb_color: Цвет RGB в режиме сна (используется при 'sleep_rgb_or_color_temp' как 'rgb_color'). 🌈", - "min_sunrise_time": "min_sunrise_time: Самое раннее время виртуального восхода (ЧЧ:ММ:СС). 🌅", - "max_sunrise_time": "max_sunrise_time: Самое позднее время виртуального восхода (ЧЧ:ММ:СС). 🌅", - "min_sunset_time": "min_sunset_time: Самое раннее время виртуального заката (ЧЧ:ММ:СС). 🌇", - "max_sunset_time": "max_sunset_time: Самое позднее время виртуального заката (ЧЧ:ММ:СС). 🌇", - "brightness_mode": "brightness_mode: Режим яркости для использования (default, linear, tanh). 📈", - "autoreset_control_seconds": "autoreset_control_seconds: Автосброс ручного управления через X секунд. ⏲️", - "send_split_delay": "send_split_delay: Задержка между отдельными командами включения для источников света. ⏲️" + "min_brightness": "min_brightness: Минимальная яркость света во время цикла. (%)", + "max_brightness": "max_brightness: Максимальная яркость света во время цикла. (%)", + "min_color_temp": "min_color_temp: Самый теплый оттенок цветовой температуры во время цикла. (Kelvin)", + "max_color_temp": "max_color_temp: Самый холодный оттенок цветовой температуры во время цикла. (Kelvin)", + "sleep_brightness": "sleep_brightness: Настройка яркости для Режима Сна (Sleep Mode). (%)", + "sleep_color_temp": "sleep_color_temp: Настройка цветовой температуры для Режима Сна (Sleep Mode). (Kelvin)" }, "data_description": { - "sleep_rgb_or_color_temp": "Используйте либо `\"rgb_color\"`, либо `\"color_temp\"` в спящем режиме. 🌙", - "sleep_color_temp": "Цветовая температура в спящем режиме (используется, когда параметр `sleep_rgb_or_color_temp` имеет значение `color_temp`) в Кельвинах. 😴", - "sleep_transition": "Длительность перехода при переключении \"спящего режима\" в секундах. 😴", - "autoreset_control_seconds": "Автоматический сброс ручного управления через несколько секунд. Установите значение 0, чтобы отключить. ⏲️", - "min_sunset_time": "Устанавливает самое раннее время виртуального заката (ЧЧ:ММ:СС), чтобы обеспечить более поздние закаты. 🌇", - "sleep_brightness": "Процент яркости света в спящем режиме. 😴", - "min_sunrise_time": "Устанавливает самое раннее время виртуального восхода солнца (ЧЧ:ММ:СС), чтобы обеспечить возможность более позднего восхода солнца. 🌅", "interval": "Частота адаптации освещения в секундах. 🔄", - "adapt_delay": "Время ожидания (в секундах) между включением света и применением изменений адаптивного освещения. Возможно поможет избежать мерцания. ⏲️", - "sleep_rgb_color": "Цвет RGB в спящем режиме (используется, когда параметр `sleep_rgb_or_color_temp` имеет значение \"rgb_color\"). 🌈", - "sunrise_offset": "Регулирует время восхода солнца с положительным или отрицательным смещением в секундах. ⏰", "transition": "Продолжительность перехода при смене освещения, в секундах. 🕑", - "brightness_mode": "Режим яркости для использования. Возможные значения: `default`, `linear` и `tanh` (используются `brightness_mode_time_dark` и `brightness_mode_time_light`). 📈", - "brightness_mode_time_light": "(Игнорируется, если `brightness_mode='default'`) Продолжительность в секундах увеличения/уменьшения яркости после/до восхода/заката. 📈📉.", - "sunset_offset": "Регулирует время заката с помощью положительного или отрицательного смещения в секундах. ⏰", - "sunset_time": "Устанавливает фиксированное время (ЧЧ:ММ:СС) для заката. 🌇", - "max_sunset_time": "Устанавливает последнее время виртуального заката (ЧЧ:ММ:СС), чтобы обеспечить более ранние закаты. 🌇", - "sunrise_time": "Устанавливает фиксированное время (ЧЧ:ММ:СС) восхода солнца. 🌅", - "initial_transition": "Продолжительность первого перехода, когда освещение переключается с `выключено` на `включено` в секундах. ⏲️", - "brightness_mode_time_dark": "(Игнорируется, если `brightness_mode='default'`) Продолжительность в секундах увеличения/уменьшения яркости до/после восхода/заката. 📈📉", - "max_sunrise_time": "Устанавливает последнее время виртуального восхода солнца (ЧЧ:ММ:СС), что позволит восходить раньше. 🌅", - "send_split_delay": "Задержка (миллисекунды) между отдельными командами поворота для источников света, которые не поддерживают одновременную настройку яркости и цвета. ⏲️" + "sleep_brightness": "Процент яркости света в спящем режиме. 😴", + "sleep_color_temp": "Цветовая температура в спящем режиме (используется, когда параметр `sleep_rgb_or_color_temp` имеет значение `color_temp`) в Кельвинах. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition: Начальный переход, когда свет переключается с 'off' на 'on'. (секунды)", + "prefer_rgb_color": "prefer_rgb_color: По возможности использовать 'rgb_color' вместо 'color_temp'.", + "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Использовать либо 'rgb_color', либо 'color_temp' в режиме сна. 🌙", + "sleep_rgb_color": "sleep_rgb_color: Цвет RGB в режиме сна (используется при 'sleep_rgb_or_color_temp' как 'rgb_color'). 🌈", + "sleep_transition": "sleep_transition: Когда прибор переходит в Режима Сна (Sleep Mode) и 'sleep_state' изменяется. (секунды)", + "transition_until_sleep": "transition_until_sleep: когда включено, адаптивное освещение будет рассматривать настройки сна как минимальные, переходя к этим значениям после захода солнца. 🌙", + "sunrise_time": "sunrise_time: Ручное изменение времени восхода солнца, если указано 'None', используется фактическое время восхода в Вашем местоположении. (ЧЧ:ММ:СС)", + "min_sunrise_time": "min_sunrise_time: Самое раннее время виртуального восхода (ЧЧ:ММ:СС). 🌅", + "max_sunrise_time": "max_sunrise_time: Самое позднее время виртуального восхода (ЧЧ:ММ:СС). 🌅", + "sunrise_offset": "sunrise_offset: За сколько времени до (-) или после (+) переопределить время восхода во время цикла. (+/- секунды)", + "sunset_time": "sunset_time: Ручное изменение времени заката солнца, если указано 'None', используется фактическое время заката в Вашем местоположении. (ЧЧ:ММ:СС)", + "min_sunset_time": "min_sunset_time: Самое раннее время виртуального заката (ЧЧ:ММ:СС). 🌇", + "max_sunset_time": "max_sunset_time: Самое позднее время виртуального заката (ЧЧ:ММ:СС). 🌇", + "sunset_offset": "sunset_offset: За сколько времени до (-) или после (+) переопределить время заката во время цикла. (+/- секунды)", + "brightness_mode": "brightness_mode: Режим яркости для использования (default, linear, tanh). 📈", + "take_over_control": "take_over_control: Если что-либо, кроме Adaptive Lighting, вызывает службу 'light.turn_on', когда свет уже включен, прекратить адаптацию этого осветительного прибора, пока он (или переключатель) не переключится off -> on.", + "detect_non_ha_changes": "detect_non_ha_changes: Обнаруживает все изменения на >10% примененные к освещению (также и из-за пределов Home Assistant), требует включения 'take_over_control' (вызывает 'homeassistant.update_entity' каждый 'interval'!)", + "autoreset_control_seconds": "autoreset_control_seconds: Автосброс ручного управления через X секунд. ⏲️", + "only_once": "only_once: Адаптировать свет только при включении.", + "adapt_only_on_bare_turn_on": "Adapt_only_on_bare_turn_on: При первоначальном включении света. Если установлено значение «true», AL адаптируется только в том случае, если «light.turn_on» вызывается без указания цвета или яркости. ❌🌈 Это, например, предотвращает адаптацию при активации сцены. Если false, AL адаптируется независимо от наличия цвета или яркости в исходных service_data. Требуется включить take_over_control. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Раздельные команды для каждого атрибута (цвет, яркость и т.д.) в 'light.turn_on' (требуется для некоторых источников света).", + "send_split_delay": "send_split_delay: Задержка между отдельными командами включения для источников света. ⏲️", + "adapt_delay": "Время ожидания между включением света и применением адаптации. Может помочь избежать мерцания. (секунды)", + "skip_redundant_commands": "Skip_redundant_commands: Пропустить отправку команд адаптации, целевое состояние которых уже равно известному состоянию источника света. Минимизирует сетевой трафик и улучшает скорость адаптации в некоторых ситуациях. 📉Отключите, если физические состояния освещения не синхронизируются с записанным состоянием HA.", + "intercept": "intercept: перехватывать и адаптировать вызовы `light.turn_on` для обеспечения мгновенной адаптации цвета и яркости. 🏎️ Отключите источники света, которые не поддерживают `light.turn_on` с цветом и яркостью.", + "multi_light_intercept": "multi_light_intercept: перехватывает и адаптирует вызовы `light.turn_on`, нацеленные на несколько источников света. ➗⚠️ Это может привести к разделению одного вызова `light.turn_on` на несколько вызовов, например, когда освещение включено в разные выключатели. Требуется, чтобы `перехват` был включен.", + "include_config_in_attributes": "include_config_in_attributes: отображать все параметры в качестве атрибутов на переключателе в Home Assistant, если установлено значение `true`. 📝" + }, + "data_description": { + "initial_transition": "Продолжительность первого перехода, когда освещение переключается с `выключено` на `включено` в секундах. ⏲️", + "sleep_rgb_or_color_temp": "Используйте либо `\"rgb_color\"`, либо `\"color_temp\"` в спящем режиме. 🌙", + "sleep_rgb_color": "Цвет RGB в спящем режиме (используется, когда параметр `sleep_rgb_or_color_temp` имеет значение \"rgb_color\"). 🌈", + "sleep_transition": "Длительность перехода при переключении \"спящего режима\" в секундах. 😴", + "sunrise_time": "Устанавливает фиксированное время (ЧЧ:ММ:СС) восхода солнца. 🌅", + "min_sunrise_time": "Устанавливает самое раннее время виртуального восхода солнца (ЧЧ:ММ:СС), чтобы обеспечить возможность более позднего восхода солнца. 🌅", + "max_sunrise_time": "Устанавливает последнее время виртуального восхода солнца (ЧЧ:ММ:СС), что позволит восходить раньше. 🌅", + "sunrise_offset": "Регулирует время восхода солнца с положительным или отрицательным смещением в секундах. ⏰", + "sunset_time": "Устанавливает фиксированное время (ЧЧ:ММ:СС) для заката. 🌇", + "min_sunset_time": "Устанавливает самое раннее время виртуального заката (ЧЧ:ММ:СС), чтобы обеспечить более поздние закаты. 🌇", + "max_sunset_time": "Устанавливает последнее время виртуального заката (ЧЧ:ММ:СС), чтобы обеспечить более ранние закаты. 🌇", + "sunset_offset": "Регулирует время заката с помощью положительного или отрицательного смещения в секундах. ⏰", + "brightness_mode": "Режим яркости для использования. Возможные значения: `default`, `linear` и `tanh` (используются `brightness_mode_time_dark` и `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Игнорируется, если `brightness_mode='default'`) Продолжительность в секундах увеличения/уменьшения яркости до/после восхода/заката. 📈📉", + "brightness_mode_time_light": "(Игнорируется, если `brightness_mode='default'`) Продолжительность в секундах увеличения/уменьшения яркости после/до восхода/заката. 📈📉.", + "autoreset_control_seconds": "Автоматический сброс ручного управления через несколько секунд. Установите значение 0, чтобы отключить. ⏲️", + "send_split_delay": "Задержка (миллисекунды) между отдельными командами поворота для источников света, которые не поддерживают одновременную настройку яркости и цвета. ⏲️", + "adapt_delay": "Время ожидания (в секундах) между включением света и применением изменений адаптивного освещения. Возможно поможет избежать мерцания. ⏲️" + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/sk.json b/custom_components/adaptive_lighting/translations/sk.json index 0dbc8d7b..733dff85 100644 --- a/custom_components/adaptive_lighting/translations/sk.json +++ b/custom_components/adaptive_lighting/translations/sk.json @@ -3,49 +3,57 @@ "step": { "init": { "data": { - "detect_non_ha_changes": "detect_non_ha_changes: Deteguje a zastaví prispôbovanie pre zmeny mimo `light.turn_on`. Vyžaduje zapnutie `take_over_control`. 🕵️ Upozornenie: ⚠️ Niektoré svetlá môžu falošne indikovať zapnutý stav, čo spôsobí, že sa svetlo neočakávane zapne. Ak narazíte na tento problém, funkciu vypnite.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Len pri čistom zapnutí svetiel. Pri nastavení `true` prispôsobí Adaptívne osvetlenie svetlá len pri zavolaní služby `light.turn_on` bez parametrov jasu alebo teploty svetla. ❌🌈 Napríklad: zamedzí to prispôsobovaniu ak je aktivovaná scéna. Pri nastavení `false` dôjde k prispôsobeniu nezávisle na tom či sú parametre jasu alebo teploty svetla prítomné v`service_data`. Vyžaduje zapnutie `take_over_control`. 🕵️ ", - "separate_turn_on_commands": "separate_turn_on_commands: Použiť samostatné volania služby `light.turn_on` pre nastavenie teploty svetla a jasu (môže byť potrebné pre niektoré typy svetiel). 🔀", - "max_color_temp": "max_color_temp: Najvyššia teplota svetla (v ˚K). ❄️", - "prefer_rgb_color": "prefer_rgb_color: Či preferovať nastavenie cez RGB než nastavením teploty svetla, ak je to možné. 🌈", - "max_brightness": "max_brightness: Najvyšší jas (v %). 💡", - "only_once": "only_once: Prispôsobiť svetlá iba pri zapnutí (`true`) alebo prispôsobovať ich priebežne (`false`). 🔄", - "take_over_control": "take_over_control: Ak sú svetlá zapnuté a prispôsobované a niečo zavolá službu `light.turn_on`, dôjde k vypnutiu Adaptívneho osvetlenia. Poznámka: Zapnutie tejto voľby spôsobí volanie služby `homeassistant.update_entity` každý `interval`! 🔒", "lights": "svetlá: Zoznam svetiel (entity_id), ktoré majú byť ovládané (môže byť prázdny). 🌟", "min_brightness": "min_brightness: Najnižší jas (v %). 💡", + "max_brightness": "max_brightness: Najvyšší jas (v %). 💡", "min_color_temp": "min_color_temp: Najnižšia teplota svetla (v ˚K). 🔥", - "transition_until_sleep": "transition_until_sleep: Keď je funkcia povolená, Adaptívne osvetlenie bude považovať nastavenia režimu spánku ako minimum, a na tieto hodnoty prejde po západe slnka. 🌙", - "multi_light_intercept": "multi_light_intercept: Zachytiť a prispôsobiť volanie služby`light.turn_on`, ktoré ovlyvňuje viacero svetiel. ➗⚠️ Toto môže spôsobiť rozdelenie jedného volania `light.turn_on`na viacero volaní, napr. ak sú svetlá pod rôznymi prepínačmi adaptívneho osvetlenia. Vyžaduje zapnutie `intercept`.", - "skip_redundant_commands": "skip_redundant_commands: Preskočiť odoslanie prispôsobovacích príkazov, ktorých cieľový stav je zhodný s posledným známym stavom. Nastavenie minimalizuje sieťovú prevádzku a v niektorých prípadoch môže zlepšiť odozvu prispôsobovania. 📉 Vypnite, pokiaľ skutočný stav svetiel prestáva odpovedať stavu zaznamenanom v HA.", - "intercept": "intercept: Zachytiť a prispôsobiť volania `light.turn_on`, aby došlo k okamžitému prispôsobeniu jasu a teploty svetla. 🏎️ Vypnite pre svetlá, ktoré nepodporujú `light.turn_on` s teplotou svetla a jasom zároveň.", - "include_config_in_attributes": "include_config_in_attributes: Zobraziť všetky nastavenia ako atribúty prepínača v Home Assistant. 📝" + "max_color_temp": "max_color_temp: Najvyššia teplota svetla (v ˚K). ❄️" }, "data_description": { - "sunset_time": "Nastaviť pevný čas (HH:MM:SS) pre západ slnka. 🌇", - "sunrise_time": "Nastaviť pevný čas (HH:MM:SS) pre východ slnka. 🌅", - "sleep_rgb_or_color_temp": "V režime spánku použiť `\"rgb_color\"` alebo `\"color_temp\"`. 🌙", - "sleep_color_temp": "Teplota svetla (v ˚K) v režime spánku (pokiaľ `sleep_rgb_or_color_temp` je `color_temp`). 😴", - "sleep_transition": "Trvanie prechodu do alebo z režimu spánku (v sekundách). 😴", - "autoreset_control_seconds": "Automaticky ukončiť manuálne ovládanie po zadanom množtve sekúnd. Pre vypnutie nastavte 0. ⏲️", - "min_sunset_time": "Nastavte najskorší možný virtuálny západ slnka (HH:MM:SS). Umožňuje neskorší západ slnka. 🌅", - "sleep_brightness": "Jas svetiel pri režime spánku (v %). 😴", - "min_sunrise_time": "Nastavte najskorší možný virtuálny východ slnka (HH:MM:SS). Umožňuje neskorší východ slnka. 🌅", "interval": "Frekvencia s akou prispôsobovať svetlá (v sekundách). 🔄", - "adapt_delay": "Pauza (v sekundách) medzi zapnutím svetla a aplikáciou zmien Adaptívneho osvetlenia. Môže pomôcť zabrániť blikaniu. ⏲️", - "sleep_rgb_color": "Farba svetla RGB v režime spánku (pokiaľ `sleep_rgb_or_color_temp` je `rgb_color `). 🌈", - "sunrise_offset": "Upravte čas východu slnka o sekundy vpred alebo vzad. ⏰", "transition": "Trvanie prechodu, keď sú svetlá zmenené (v sekundách). ⏲️", - "brightness_mode": "Výber režimu jasu. Možné hodnotu sú `default`, `linear` a `tanh` (používa `brightness_mode_time_dark` a `brightness_mode_time_light`). 📈", - "brightness_mode_time_light": "(Ignorovaný, ak `brightness_mode = 'predvolené') Trvanie v sekundách na rampu / vypnutie jasu po / pred východ slnka/sunset. 📈📉.", - "sunset_offset": "Upravte čas západu slnka o sekundy vpred alebo vzad. ⏰", - "max_sunset_time": "Nastavte najneskorší možný virtuálny západ slnka (HH:MM:SS). Umožňuje skorší západ slnka. 🌅", - "initial_transition": "Trvanie prvého prechodu, keď sú svetlá zapnuté z `off` na `on` (v sekundách). ⏲️", - "brightness_mode_time_dark": "(Ignorované ak `brightness_mode='default'`) Čas na zvýšenie/zníženie jasu po udalosti/pred udalosťou východu/západu slnka. 📈📉", - "max_sunrise_time": "Nastavte najneskorší možný virtuálny východ slnka (HH:MM:SS). Umožňuje skorší východ slnka. 🌅", - "send_split_delay": "Pauza (v ms) medzi príkazmi pri zapnutom `separate_turn_on_commands` pre svetlá, ktoré nepodporujú súčasné nastavenie jasu a teploty svetla. ⏲️" + "sleep_brightness": "Jas svetiel pri režime spánku (v %). 😴", + "sleep_color_temp": "Teplota svetla (v ˚K) v režime spánku (pokiaľ `sleep_rgb_or_color_temp` je `color_temp`). 😴" }, "title": "Nastavenia Adaptívneho osvetlenia", - "description": "Nastavte komponentu Adaptívneho osvetlenia. Názvy nastavení sú zhodné s názvami v súbore YAML. Ak ste túto položku nastavili už v YAML, tak tu sa nezobrazia žiadne možnosti nastavenia. Interaktívne grafy, ktoré zobrazujú vplyv nastavení, nájdete na [tejto webovej aplikácii]({webapp_url}). Ďalšie informácie nájdete v [oficiálnej dokumentácii]({docs_url})." + "description": "Nastavte komponentu Adaptívneho osvetlenia. Názvy nastavení sú zhodné s názvami v súbore YAML. Ak ste túto položku nastavili už v YAML, tak tu sa nezobrazia žiadne možnosti nastavenia. Interaktívne grafy, ktoré zobrazujú vplyv nastavení, nájdete na [tejto webovej aplikácii]({webapp_url}). Ďalšie informácie nájdete v [oficiálnej dokumentácii]({docs_url}).", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: Či preferovať nastavenie cez RGB než nastavením teploty svetla, ak je to možné. 🌈", + "transition_until_sleep": "transition_until_sleep: Keď je funkcia povolená, Adaptívne osvetlenie bude považovať nastavenia režimu spánku ako minimum, a na tieto hodnoty prejde po západe slnka. 🌙", + "take_over_control": "take_over_control: Ak sú svetlá zapnuté a prispôsobované a niečo zavolá službu `light.turn_on`, dôjde k vypnutiu Adaptívneho osvetlenia. Poznámka: Zapnutie tejto voľby spôsobí volanie služby `homeassistant.update_entity` každý `interval`! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: Deteguje a zastaví prispôbovanie pre zmeny mimo `light.turn_on`. Vyžaduje zapnutie `take_over_control`. 🕵️ Upozornenie: ⚠️ Niektoré svetlá môžu falošne indikovať zapnutý stav, čo spôsobí, že sa svetlo neočakávane zapne. Ak narazíte na tento problém, funkciu vypnite.", + "only_once": "only_once: Prispôsobiť svetlá iba pri zapnutí (`true`) alebo prispôsobovať ich priebežne (`false`). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Len pri čistom zapnutí svetiel. Pri nastavení `true` prispôsobí Adaptívne osvetlenie svetlá len pri zavolaní služby `light.turn_on` bez parametrov jasu alebo teploty svetla. ❌🌈 Napríklad: zamedzí to prispôsobovaniu ak je aktivovaná scéna. Pri nastavení `false` dôjde k prispôsobeniu nezávisle na tom či sú parametre jasu alebo teploty svetla prítomné v`service_data`. Vyžaduje zapnutie `take_over_control`. 🕵️ ", + "separate_turn_on_commands": "separate_turn_on_commands: Použiť samostatné volania služby `light.turn_on` pre nastavenie teploty svetla a jasu (môže byť potrebné pre niektoré typy svetiel). 🔀", + "skip_redundant_commands": "skip_redundant_commands: Preskočiť odoslanie prispôsobovacích príkazov, ktorých cieľový stav je zhodný s posledným známym stavom. Nastavenie minimalizuje sieťovú prevádzku a v niektorých prípadoch môže zlepšiť odozvu prispôsobovania. 📉 Vypnite, pokiaľ skutočný stav svetiel prestáva odpovedať stavu zaznamenanom v HA.", + "intercept": "intercept: Zachytiť a prispôsobiť volania `light.turn_on`, aby došlo k okamžitému prispôsobeniu jasu a teploty svetla. 🏎️ Vypnite pre svetlá, ktoré nepodporujú `light.turn_on` s teplotou svetla a jasom zároveň.", + "multi_light_intercept": "multi_light_intercept: Zachytiť a prispôsobiť volanie služby`light.turn_on`, ktoré ovlyvňuje viacero svetiel. ➗⚠️ Toto môže spôsobiť rozdelenie jedného volania `light.turn_on`na viacero volaní, napr. ak sú svetlá pod rôznymi prepínačmi adaptívneho osvetlenia. Vyžaduje zapnutie `intercept`.", + "include_config_in_attributes": "include_config_in_attributes: Zobraziť všetky nastavenia ako atribúty prepínača v Home Assistant. 📝" + }, + "data_description": { + "initial_transition": "Trvanie prvého prechodu, keď sú svetlá zapnuté z `off` na `on` (v sekundách). ⏲️", + "sleep_rgb_or_color_temp": "V režime spánku použiť `\"rgb_color\"` alebo `\"color_temp\"`. 🌙", + "sleep_rgb_color": "Farba svetla RGB v režime spánku (pokiaľ `sleep_rgb_or_color_temp` je `rgb_color `). 🌈", + "sleep_transition": "Trvanie prechodu do alebo z režimu spánku (v sekundách). 😴", + "sunrise_time": "Nastaviť pevný čas (HH:MM:SS) pre východ slnka. 🌅", + "min_sunrise_time": "Nastavte najskorší možný virtuálny východ slnka (HH:MM:SS). Umožňuje neskorší východ slnka. 🌅", + "max_sunrise_time": "Nastavte najneskorší možný virtuálny východ slnka (HH:MM:SS). Umožňuje skorší východ slnka. 🌅", + "sunrise_offset": "Upravte čas východu slnka o sekundy vpred alebo vzad. ⏰", + "sunset_time": "Nastaviť pevný čas (HH:MM:SS) pre západ slnka. 🌇", + "min_sunset_time": "Nastavte najskorší možný virtuálny západ slnka (HH:MM:SS). Umožňuje neskorší západ slnka. 🌅", + "max_sunset_time": "Nastavte najneskorší možný virtuálny západ slnka (HH:MM:SS). Umožňuje skorší západ slnka. 🌅", + "sunset_offset": "Upravte čas západu slnka o sekundy vpred alebo vzad. ⏰", + "brightness_mode": "Výber režimu jasu. Možné hodnotu sú `default`, `linear` a `tanh` (používa `brightness_mode_time_dark` a `brightness_mode_time_light`). 📈", + "brightness_mode_time_dark": "(Ignorované ak `brightness_mode='default'`) Čas na zvýšenie/zníženie jasu po udalosti/pred udalosťou východu/západu slnka. 📈📉", + "brightness_mode_time_light": "(Ignorovaný, ak `brightness_mode = 'predvolené') Trvanie v sekundách na rampu / vypnutie jasu po / pred východ slnka/sunset. 📈📉.", + "autoreset_control_seconds": "Automaticky ukončiť manuálne ovládanie po zadanom množtve sekúnd. Pre vypnutie nastavte 0. ⏲️", + "send_split_delay": "Pauza (v ms) medzi príkazmi pri zapnutom `separate_turn_on_commands` pre svetlá, ktoré nepodporujú súčasné nastavenie jasu a teploty svetla. ⏲️", + "adapt_delay": "Pauza (v sekundách) medzi zapnutím svetla a aplikáciou zmien Adaptívneho osvetlenia. Môže pomôcť zabrániť blikaniu. ⏲️" + } + } + } } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/sl.json b/custom_components/adaptive_lighting/translations/sl.json index 011f1d89..ae4e9a6e 100644 --- a/custom_components/adaptive_lighting/translations/sl.json +++ b/custom_components/adaptive_lighting/translations/sl.json @@ -3,48 +3,56 @@ "step": { "init": { "data": { - "prefer_rgb_color": "prefer_rgb_color: Ali v primeru možnosti raje uporabiti prilagoditev RGB barve kot barvno temperaturo luči. 🌈", - "transition_until_sleep": "transition_until_sleep: Če je omogočeno, bo Adaptive Lighting obravnaval nastavitve spanja kot minimalne vrednosti in bo po zahodu sonca prehajal na te vrednosti. 🌙", - "take_over_control": "take_over_control: Onemogoči Adaptive Lighting, če drug vir pokliče \"light.turn_on\", ko so luči prižgane in se prilagajajo. Opozorilo: to ob vsakem intervalu kliče \"homeassistant.update_entity\"! 🔒", - "detect_non_ha_changes": "„detect_non_ha_changes: Zazna in ustavi prilagoditve za spremembe stanja, ki niso posledica \"light.turn_on\". Zahteva omogočeno \"take_over_control\". 🕵️ Pozor: ⚠️ Nekatere luči lahko nepravilno poročajo, da so prižgane, kar lahko povzroči nepričakovano vklapljanje. Onemogočite to funkcijo, če naletite na takšne težave.", "lights": "lights: Seznam entity_id-jev luči za nadzor (lahko je prazen). 🌟", "min_brightness": "min_brightness: Odstotek najmanjše svetlosti. 💡", "max_brightness": "max_brightness: Odstotek največeje svetlosti. 💡", "min_color_temp": "min_color_temp: Najtoplejša barvna temperatura v Kelvinih. 🔥", - "max_color_temp": "max_color_temp: Najhladnejša barvna temperatura v kelvinih. ❄️", - "separate_turn_on_commands": "separate_turn_on_commands: Uporabi ločene klice \"light.turn_on\" za barvo in jakost, kar je potrebno za nekatere tipe luči. 🔀", - "skip_redundant_commands": "skip_redundant_commands: Preskoči pošiljanje prilagoditvenih ukazov, če je ciljano stanje že enako poznanemu stanju luči. Zmanjšuje omrežni promet in izboljšuje odzivnost prilagajanja v določenih situacijah. 📉 Onemogočite, če se fizična stanja luči ne ujemajo z zabeleženim stanjem v HA.", - "intercept": "intercept: Prestreza in prilagaja klice \"light.turn_on\" za takojšnjo prilagoditev barve in jakosti. 🏎️ Onemogočite za luči, ki ne podpirajo \"light.turn_on\" z barvo in svetlostjo.", - "multi_light_intercept": "multi_light_intercept: Prestreza in prilagaja klice \"light.turn_on\", ki ciljajo več luči. ➗⚠️ To lahko privede do razdelitve enega klica \"light.turn_on\" v več klicev, npr. ko so luči na različnih stikalih. Zahteva omogočeno \"intercept\".", - "include_config_in_attributes": "include_config_in_attributes: Ko je nastavljeno na \"true\", prikaže vse možnosti kot atribute stikala v Home Assistantu. 📝", - "only_once": "only_once: Prilagodi luči samo ob vklopu (true) ali pa jih še naprej prilagajaj (false). 🔄", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Ob začetnem vklopu luči. Če je nastavljeno na \"true\", AL prilagodi samo, če je \"light.turn_on\" klic brez podanih parametrov barve ali jakosti. ❌🌈 S tem se npr. prepreči prilagajanje pri aktivaciji scene. Če je \"false\", AL prilagodi ne glede na prisotnost barve ali jakosti v začetnih \"service_data\". Zahteva omogočeno \"take_over_control\". 🕵️" + "max_color_temp": "max_color_temp: Najhladnejša barvna temperatura v kelvinih. ❄️" }, "data_description": { - "sunrise_offset": "Prilagodite čas sončnega vzhoda z pozitivnim ali negativnim zamikom v sekundah. ⏰", - "send_split_delay": "Zamik (ms) med \"separate_turn_on_commands\" za luči, ki ne podpirajo hkratne nastavitve jakosti in barve. ⏲️", - "transition": "Trajanje prehoda pri spreminjanju luči, v sekundah. 🕑", "interval": "Pogostost prilagajanja luči, v sekundah. 🔄", + "transition": "Trajanje prehoda pri spreminjanju luči, v sekundah. 🕑", "sleep_brightness": "Odstotek svetlosti luči v načinu spanja. 😴", - "sleep_rgb_or_color_temp": "V načinu spanja uporabi \"rgb_color\" ali \"color_temp\". 🌙", - "sleep_color_temp": "Barvna temperatura v načinu spanja (uporabljena, ko je \"sleep_rgb_or_color_temp\" nastavljena na \"color_temp\") v Kelvinih. 😴", - "sleep_transition": "Trajanje prehoda, ko se preklopi način spanja, v sekundah. 😴", - "sunrise_time": "Nastavi fiksni čas (HH:MM:SS) sončnega vzhoda. 🌅", - "min_sunrise_time": "Nastavi najzgodnejši navidezni sončni vzhod (HH:MM:SS), dovoljuje kasnejše vzhode. 🌅", - "sunset_time": "Nastavite fiksni čas (HH:MM:SS) za sončni zahod. 🌇", - "min_sunset_time": "Nastavite najzgodnejši navidezni čas sončnega zahoda (HH:MM:SS), dovoljuje kasnejše sončne zahode. 🌇", - "sunset_offset": "Prilagodite čas sončnega zahoda s pozitivnim ali negativnim zamikom v sekundah. ⏰", - "brightness_mode_time_dark": "(Prezrto, če je \"brightness_mode='default'\") Trajanje v sekundah za postopno povečanje ali zmanjšanje svetlosti pred/po sončnem vzhodu/zahodu. 📈📉", - "brightness_mode_time_light": "(Prezrto, če je \"brightness_mode='default'\") Trajanje v sekundah za postopno povečanje ali zmanjšanje svetlosti po/pred sončnem vzhodu/zahodu. 📈📉", - "autoreset_control_seconds": "Samodejno ponastavi ročni nadzor po določenem številu sekund. Nastavite na 0, da onemogočite. ⏲️", - "max_sunrise_time": "Nastavi najkasnejši virtualni sončni vzhod (HH:MM:SS), dovoljuje zgodnejše vzhode. 🌅", - "max_sunset_time": "Nastavite najpoznejši navidezni čas sončnega zahoda (HH:MM:SS), dovoljuje zgodnejše sončne zahode. 🌇", - "brightness_mode": "Način upravljanja svetlosti. Možne vrednosti so \"default\", \"linear\" in \"tanh\" (uporablja \"brightness_mode_time_dark\" in \"brightness_mode_time_light\"). 📈", - "initial_transition": "Trajanje prvega prehoda, ko se luči prižgejo (iz \"off\" v \"on\"), v sekundah. ⏲️", - "sleep_rgb_color": "RGB barva v načinu spanja (uporabljena, ko je \"sleep_rgb_or_color_temp\" nastavljeno na \"rgb_color\"). 🌈" + "sleep_color_temp": "Barvna temperatura v načinu spanja (uporabljena, ko je \"sleep_rgb_or_color_temp\" nastavljena na \"color_temp\") v Kelvinih. 😴" }, "title": "Nastavitve prilagodljive osvetlitve", - "description": "Konfigurirajte komponento Adaptive Lighting. Imena možnosti so usklajena z nastavitvami v YAML. Če ste ta vnos definirali v YAML, tukaj ne bodo prikazane nobene možnosti. Za interaktivne grafe, ki ponazarjajo učinke parametrov, obiščite to [spletno aplikacijo]({webapp_url}). Za dodatne podrobnosti glejte [uradno dokumentacijo]({docs_url})." + "description": "Konfigurirajte komponento Adaptive Lighting. Imena možnosti so usklajena z nastavitvami v YAML. Če ste ta vnos definirali v YAML, tukaj ne bodo prikazane nobene možnosti. Za interaktivne grafe, ki ponazarjajo učinke parametrov, obiščite to [spletno aplikacijo]({webapp_url}). Za dodatne podrobnosti glejte [uradno dokumentacijo]({docs_url}).", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: Ali v primeru možnosti raje uporabiti prilagoditev RGB barve kot barvno temperaturo luči. 🌈", + "transition_until_sleep": "transition_until_sleep: Če je omogočeno, bo Adaptive Lighting obravnaval nastavitve spanja kot minimalne vrednosti in bo po zahodu sonca prehajal na te vrednosti. 🌙", + "take_over_control": "take_over_control: Onemogoči Adaptive Lighting, če drug vir pokliče \"light.turn_on\", ko so luči prižgane in se prilagajajo. Opozorilo: to ob vsakem intervalu kliče \"homeassistant.update_entity\"! 🔒", + "detect_non_ha_changes": "„detect_non_ha_changes: Zazna in ustavi prilagoditve za spremembe stanja, ki niso posledica \"light.turn_on\". Zahteva omogočeno \"take_over_control\". 🕵️ Pozor: ⚠️ Nekatere luči lahko nepravilno poročajo, da so prižgane, kar lahko povzroči nepričakovano vklapljanje. Onemogočite to funkcijo, če naletite na takšne težave.", + "only_once": "only_once: Prilagodi luči samo ob vklopu (true) ali pa jih še naprej prilagajaj (false). 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Ob začetnem vklopu luči. Če je nastavljeno na \"true\", AL prilagodi samo, če je \"light.turn_on\" klic brez podanih parametrov barve ali jakosti. ❌🌈 S tem se npr. prepreči prilagajanje pri aktivaciji scene. Če je \"false\", AL prilagodi ne glede na prisotnost barve ali jakosti v začetnih \"service_data\". Zahteva omogočeno \"take_over_control\". 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Uporabi ločene klice \"light.turn_on\" za barvo in jakost, kar je potrebno za nekatere tipe luči. 🔀", + "skip_redundant_commands": "skip_redundant_commands: Preskoči pošiljanje prilagoditvenih ukazov, če je ciljano stanje že enako poznanemu stanju luči. Zmanjšuje omrežni promet in izboljšuje odzivnost prilagajanja v določenih situacijah. 📉 Onemogočite, če se fizična stanja luči ne ujemajo z zabeleženim stanjem v HA.", + "intercept": "intercept: Prestreza in prilagaja klice \"light.turn_on\" za takojšnjo prilagoditev barve in jakosti. 🏎️ Onemogočite za luči, ki ne podpirajo \"light.turn_on\" z barvo in svetlostjo.", + "multi_light_intercept": "multi_light_intercept: Prestreza in prilagaja klice \"light.turn_on\", ki ciljajo več luči. ➗⚠️ To lahko privede do razdelitve enega klica \"light.turn_on\" v več klicev, npr. ko so luči na različnih stikalih. Zahteva omogočeno \"intercept\".", + "include_config_in_attributes": "include_config_in_attributes: Ko je nastavljeno na \"true\", prikaže vse možnosti kot atribute stikala v Home Assistantu. 📝" + }, + "data_description": { + "initial_transition": "Trajanje prvega prehoda, ko se luči prižgejo (iz \"off\" v \"on\"), v sekundah. ⏲️", + "sleep_rgb_or_color_temp": "V načinu spanja uporabi \"rgb_color\" ali \"color_temp\". 🌙", + "sleep_rgb_color": "RGB barva v načinu spanja (uporabljena, ko je \"sleep_rgb_or_color_temp\" nastavljeno na \"rgb_color\"). 🌈", + "sleep_transition": "Trajanje prehoda, ko se preklopi način spanja, v sekundah. 😴", + "sunrise_time": "Nastavi fiksni čas (HH:MM:SS) sončnega vzhoda. 🌅", + "min_sunrise_time": "Nastavi najzgodnejši navidezni sončni vzhod (HH:MM:SS), dovoljuje kasnejše vzhode. 🌅", + "max_sunrise_time": "Nastavi najkasnejši virtualni sončni vzhod (HH:MM:SS), dovoljuje zgodnejše vzhode. 🌅", + "sunrise_offset": "Prilagodite čas sončnega vzhoda z pozitivnim ali negativnim zamikom v sekundah. ⏰", + "sunset_time": "Nastavite fiksni čas (HH:MM:SS) za sončni zahod. 🌇", + "min_sunset_time": "Nastavite najzgodnejši navidezni čas sončnega zahoda (HH:MM:SS), dovoljuje kasnejše sončne zahode. 🌇", + "max_sunset_time": "Nastavite najpoznejši navidezni čas sončnega zahoda (HH:MM:SS), dovoljuje zgodnejše sončne zahode. 🌇", + "sunset_offset": "Prilagodite čas sončnega zahoda s pozitivnim ali negativnim zamikom v sekundah. ⏰", + "brightness_mode": "Način upravljanja svetlosti. Možne vrednosti so \"default\", \"linear\" in \"tanh\" (uporablja \"brightness_mode_time_dark\" in \"brightness_mode_time_light\"). 📈", + "brightness_mode_time_dark": "(Prezrto, če je \"brightness_mode='default'\") Trajanje v sekundah za postopno povečanje ali zmanjšanje svetlosti pred/po sončnem vzhodu/zahodu. 📈📉", + "brightness_mode_time_light": "(Prezrto, če je \"brightness_mode='default'\") Trajanje v sekundah za postopno povečanje ali zmanjšanje svetlosti po/pred sončnem vzhodu/zahodu. 📈📉", + "autoreset_control_seconds": "Samodejno ponastavi ročni nadzor po določenem številu sekund. Nastavite na 0, da onemogočite. ⏲️", + "send_split_delay": "Zamik (ms) med \"separate_turn_on_commands\" za luči, ki ne podpirajo hkratne nastavitve jakosti in barve. ⏲️" + } + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/sv.json b/custom_components/adaptive_lighting/translations/sv.json index 6ce670b2..e0b254e1 100644 --- a/custom_components/adaptive_lighting/translations/sv.json +++ b/custom_components/adaptive_lighting/translations/sv.json @@ -28,55 +28,63 @@ "description": "Alla inställningar för en Adaptiv Ljussättning komponent. Titeln på inställningarna är desamma som i YAML konfigurationen. Inga inställningar visas om enheten redan är konfigurerad i YAML.", "data": { "lights": "lights, ljuskällor", - "initial_transition": "initial_transition, när ljuskällorna går från 'av' till 'på' eller när 'sleep_state' ändras", "interval": "interval, Tid mellan uppdateringar i sekunder", - "max_brightness": "max_brightness, i procent %", - "max_color_temp": "max_color_temp, i Kelvin", - "min_brightness": "min_brightness, i %", - "min_color_temp": "min_color_temp, i Kelvin", - "only_once": "only_once, Adaptivt justera endast ljuskällorna när de sätts från 'av' till 'på'", - "prefer_rgb_color": "prefer_rgb_color, Använd 'rgb_color' över 'color_temp' om möjligt", - "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.", - "sleep_brightness": "sleep_brightness, i %", - "sleep_color_temp": "sleep_color_temp, i Kelvin", - "sunrise_offset": "sunrise_offset, i +/- sekunder", - "sunrise_time": "sunrise_time, i 'HH:MM:SS' format (om 'None', används den faktiskta soluppgången för din position)", - "sunset_offset": "sunset_offset, i +/- sekunder", - "sunset_time": "sunset_time, i 'HH:MM:SS' format (om 'None', används den faktiskta solnedgången för din position)", - "take_over_control": "take_over_control, om något utöver 'Adaptiv Ljussättning' komponenten kallar på 'light.turn_on' när en ljuskälla redan är på, stängs den adaptiva justeringen av tills ljuskällan stängs av -> på igen, alternativt switchen för konfigurationen", - "detect_non_ha_changes": "detect_non_ha_changes, Upptäcker alla ändringar större än 5% gjorda på ljuskällorna som inte kommer från HA. Kräver att 'take_over_control' är påslaget.(Kallar på 'homeassistant.update_entity' vid varje 'interval'!)", "transition": "transition, i sekunder", - "multi_light_intercept": "multi_light_intercept: Fånga upp och anpassa \"light.turn_on\"-anrop som riktar sig mot flera lampor. ➗⚠️ Detta kan resultera i att ett enda `light.turn_on`-anrop delas upp i flera anrop, t.ex. när lamporna är kopplade till olika strömbrytare. Kräver att \"intercept\" är aktiverat.", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: När lampor först tänds. Om satt till \"true\", anpassar AL endast om \"light.turn_on\" anropas utan att ange färg eller ljusstyrka. ❌🌈 Detta förhindrar t.ex. anpassning när en scen aktiveras. Om \"false\" anpassas AL oavsett förekomsten av färg eller ljusstyrka i den initiala \"service_data\". \"takeover_control\" måste vara aktiverat. 🕵️", - "skip_redundant_commands": "skip_redundant_commands: Hoppa över att skicka anpassningskommandon vars måltillstånd redan är lika med lampans kända tillstånd. Minimerar nätverkstrafik och förbättrar anpassningsförmågan i vissa situationer. 📉 Inaktivera om lampans tillstånd blir osynkroniserade med HA:s registrerade tillstånd.", - "intercept": "intercept: Fånga upp och anpassa `light.turn_on`-anrop för att möjliggöra omedelbar anpassning av färg och ljusstyrka. 🏎️ Inaktivera för lampor som inte stöder `light.turn_on` med färg och ljusstyrka.", - "transition_until_sleep": "transition_until_sleep: När aktiverat kommer Adaptive Lighting att behandla sömninställningarna som ett minimum och övergå till dessa värden efter solnedgången. 🌙", - "include_config_in_attributes": "include_config_in_attributes: Visa alla alternativ som attribut på strömbrytaren i Home Assistant när den är inställd på \"true\". 📝" + "min_brightness": "min_brightness, i %", + "max_brightness": "max_brightness, i procent %", + "min_color_temp": "min_color_temp, i Kelvin", + "max_color_temp": "max_color_temp, i Kelvin", + "sleep_brightness": "sleep_brightness, i %", + "sleep_color_temp": "sleep_color_temp, i Kelvin" }, "data_description": { - "sleep_color_temp": "Färgtemperatur i sovläge (används när `sleep_rgb_or_color_temp` är `color_temp`) i Kelvin. 😴", - "sleep_transition": "Dröjsmål för övergång när \"sov läge\" slås på/av, i sekunder. 🕑", - "autoreset_control_seconds": "Nollställ automatiskt manuell kontroll efter ett antal sekunder. Sätt till 0 för at avaktivera. ⏲️", - "sleep_brightness": "Procent ljusstyrka för lampor i sovläge. 😴", "interval": "Frekvens för att anpassa lamporna, i sekunder. 🔄", - "sunrise_offset": "Justera soluppgångstiden med positiv och negativ förskutning is sekunder. ⏰", "transition": "Dröjsmål för övergång när lampor ändras, i sekunder. 🕑", - "sunset_offset": "Justera solnedgångstiden med positiv och negativ förskutning is sekunder. ⏰", - "send_split_delay": "Dröjsmål (ms) mellan `separate_turn_on_commands` för lampor som inte stödjer samtidiga ljussyrke och färg inställningar. ⏲️", - "sleep_rgb_or_color_temp": "Använd antingen`\"rgb_color\"` eller `\"color_temp\"` i sovläge. 🌙", - "min_sunset_time": "Ställ in den tidigaste virtuella solnedgångstiden (TT: MM: SS), vilket möjliggör senare solnedgångar. 🌇", - "min_sunrise_time": "Ställ in den tidigaste virtuella soluppgångstiden (TT: MM: SS), vilket möjliggör senare soluppgångar. 🌅", - "adapt_delay": "Väntetid (sekunder) mellan lamptändning och Adaptiv Ljussättning tillämpar ändringar. Kan hjälpa till att undvika flimmer. ⏲️", - "sleep_rgb_color": "RGB-färg i sovläge (används när \"sleep_rgb_or_color_temp\" är \"rgb_color\"). 🌈", - "sunset_time": "Ställ in en fast tid (TT:MM:SS) för solnedgången. 🌇", - "max_sunset_time": "Ställ in den senaste virtuella solnedgångstiden (TT: MM: SS), vilket möjliggör tidigare solnedgångar. 🌇", - "sunrise_time": "Ställ in en fast tid (TT:MM:SS) för soluppgången. 🌅", - "initial_transition": "Den första övergångens varaktighet när lampan slås från ”av” till ”på” i sekunder. ⏲️", - "max_sunrise_time": "Ställ in den senaste virtuella soluppgångstiden (TT: MM: SS), vilket möjliggör tidigare soluppgångar. 🌅", - "brightness_mode": "Ljusstyrkeinställing att använda. Möjliga värden är \"default\", \"linear\" och \"tanh\" (använder \"brightness_mode_time_dark\" och \"brightness_mode_time_light\"). 📈", - "brightness_mode_time_light": "(Ignoreras om `brightness_mode='default'`) Varaktigheten i sekunder för att öka/minska ljusstyrkan efter/före soluppgång/solnedgång. 📈📉.", - "brightness_mode_time_dark": "(Ignoreras om `brightness_mode='default'`) Varaktigheten i sekunder för att öka/minska ljusstyrkan efter/före soluppgång/solnedgång. 📈📉.", - "take_over_control_mode": "Anpassningspausläget när andra källor ändrar ljusstyrka och/eller färg på belysningen. `pause_all` pausar alltid både ljusstyrka och färganpassning. `pause_changed` pausar endast anpassningen av de ändrade attributen och fortsätter att anpassa oförändrade attribut, t.ex. fortsätter färganpassningen när endast ljusstyrkan har ändrats." + "sleep_brightness": "Procent ljusstyrka för lampor i sovläge. 😴", + "sleep_color_temp": "Färgtemperatur i sovläge (används när `sleep_rgb_or_color_temp` är `color_temp`) i Kelvin. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition, när ljuskällorna går från 'av' till 'på' eller när 'sleep_state' ändras", + "prefer_rgb_color": "prefer_rgb_color, Använd 'rgb_color' över 'color_temp' om möjligt", + "transition_until_sleep": "transition_until_sleep: När aktiverat kommer Adaptive Lighting att behandla sömninställningarna som ett minimum och övergå till dessa värden efter solnedgången. 🌙", + "sunrise_time": "sunrise_time, i 'HH:MM:SS' format (om 'None', används den faktiskta soluppgången för din position)", + "sunrise_offset": "sunrise_offset, i +/- sekunder", + "sunset_time": "sunset_time, i 'HH:MM:SS' format (om 'None', används den faktiskta solnedgången för din position)", + "sunset_offset": "sunset_offset, i +/- sekunder", + "take_over_control": "take_over_control, om något utöver 'Adaptiv Ljussättning' komponenten kallar på 'light.turn_on' när en ljuskälla redan är på, stängs den adaptiva justeringen av tills ljuskällan stängs av -> på igen, alternativt switchen för konfigurationen", + "detect_non_ha_changes": "detect_non_ha_changes, Upptäcker alla ändringar större än 5% gjorda på ljuskällorna som inte kommer från HA. Kräver att 'take_over_control' är påslaget.(Kallar på 'homeassistant.update_entity' vid varje 'interval'!)", + "only_once": "only_once, Adaptivt justera endast ljuskällorna när de sätts från 'av' till 'på'", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: När lampor först tänds. Om satt till \"true\", anpassar AL endast om \"light.turn_on\" anropas utan att ange färg eller ljusstyrka. ❌🌈 Detta förhindrar t.ex. anpassning när en scen aktiveras. Om \"false\" anpassas AL oavsett förekomsten av färg eller ljusstyrka i den initiala \"service_data\". \"takeover_control\" måste vara aktiverat. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.", + "skip_redundant_commands": "skip_redundant_commands: Hoppa över att skicka anpassningskommandon vars måltillstånd redan är lika med lampans kända tillstånd. Minimerar nätverkstrafik och förbättrar anpassningsförmågan i vissa situationer. 📉 Inaktivera om lampans tillstånd blir osynkroniserade med HA:s registrerade tillstånd.", + "intercept": "intercept: Fånga upp och anpassa `light.turn_on`-anrop för att möjliggöra omedelbar anpassning av färg och ljusstyrka. 🏎️ Inaktivera för lampor som inte stöder `light.turn_on` med färg och ljusstyrka.", + "multi_light_intercept": "multi_light_intercept: Fånga upp och anpassa \"light.turn_on\"-anrop som riktar sig mot flera lampor. ➗⚠️ Detta kan resultera i att ett enda `light.turn_on`-anrop delas upp i flera anrop, t.ex. när lamporna är kopplade till olika strömbrytare. Kräver att \"intercept\" är aktiverat.", + "include_config_in_attributes": "include_config_in_attributes: Visa alla alternativ som attribut på strömbrytaren i Home Assistant när den är inställd på \"true\". 📝" + }, + "data_description": { + "initial_transition": "Den första övergångens varaktighet när lampan slås från ”av” till ”på” i sekunder. ⏲️", + "sleep_rgb_or_color_temp": "Använd antingen`\"rgb_color\"` eller `\"color_temp\"` i sovläge. 🌙", + "sleep_rgb_color": "RGB-färg i sovläge (används när \"sleep_rgb_or_color_temp\" är \"rgb_color\"). 🌈", + "sleep_transition": "Dröjsmål för övergång när \"sov läge\" slås på/av, i sekunder. 🕑", + "sunrise_time": "Ställ in en fast tid (TT:MM:SS) för soluppgången. 🌅", + "min_sunrise_time": "Ställ in den tidigaste virtuella soluppgångstiden (TT: MM: SS), vilket möjliggör senare soluppgångar. 🌅", + "max_sunrise_time": "Ställ in den senaste virtuella soluppgångstiden (TT: MM: SS), vilket möjliggör tidigare soluppgångar. 🌅", + "sunrise_offset": "Justera soluppgångstiden med positiv och negativ förskutning is sekunder. ⏰", + "sunset_time": "Ställ in en fast tid (TT:MM:SS) för solnedgången. 🌇", + "min_sunset_time": "Ställ in den tidigaste virtuella solnedgångstiden (TT: MM: SS), vilket möjliggör senare solnedgångar. 🌇", + "max_sunset_time": "Ställ in den senaste virtuella solnedgångstiden (TT: MM: SS), vilket möjliggör tidigare solnedgångar. 🌇", + "sunset_offset": "Justera solnedgångstiden med positiv och negativ förskutning is sekunder. ⏰", + "brightness_mode": "Ljusstyrkeinställing att använda. Möjliga värden är \"default\", \"linear\" och \"tanh\" (använder \"brightness_mode_time_dark\" och \"brightness_mode_time_light\"). 📈", + "brightness_mode_time_dark": "(Ignoreras om `brightness_mode='default'`) Varaktigheten i sekunder för att öka/minska ljusstyrkan efter/före soluppgång/solnedgång. 📈📉.", + "brightness_mode_time_light": "(Ignoreras om `brightness_mode='default'`) Varaktigheten i sekunder för att öka/minska ljusstyrkan efter/före soluppgång/solnedgång. 📈📉.", + "take_over_control_mode": "Anpassningspausläget när andra källor ändrar ljusstyrka och/eller färg på belysningen. `pause_all` pausar alltid både ljusstyrka och färganpassning. `pause_changed` pausar endast anpassningen av de ändrade attributen och fortsätter att anpassa oförändrade attribut, t.ex. fortsätter färganpassningen när endast ljusstyrkan har ändrats.", + "autoreset_control_seconds": "Nollställ automatiskt manuell kontroll efter ett antal sekunder. Sätt till 0 för at avaktivera. ⏲️", + "send_split_delay": "Dröjsmål (ms) mellan `separate_turn_on_commands` för lampor som inte stödjer samtidiga ljussyrke och färg inställningar. ⏲️", + "adapt_delay": "Väntetid (sekunder) mellan lamptändning och Adaptiv Ljussättning tillämpar ändringar. Kan hjälpa till att undvika flimmer. ⏲️" + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/ta.json b/custom_components/adaptive_lighting/translations/ta.json index 50d5ecbc..39c6c918 100644 --- a/custom_components/adaptive_lighting/translations/ta.json +++ b/custom_components/adaptive_lighting/translations/ta.json @@ -155,42 +155,50 @@ "min_brightness": "min_brightness: குறைந்தபட்ச ஒளி விழுக்காடு. .", "max_brightness": "அதிகபட்ச பிரகாசம்: அதிகபட்ச ஒளி விழுக்காடு. .", "min_color_temp": "min_color_temp: கெல்வினில் வெப்பமான வண்ண வெப்பநிலை. .", - "max_color_temp": "MAX_COLOR_TEMP: கெல்வினில் குளிரான வண்ண வெப்பநிலை. .", - "prefer_rgb_color": "bey_rgb_color: முடிந்தவரை ஒளி வண்ண வெப்பநிலையை விட RGB வண்ண சரிசெய்தலை விரும்பலாமா. .", - "transition_until_sleep": "Transition_until_sleep: இயக்கப்பட்டால், தகவமைப்பு விளக்குகள் தூக்க அமைப்புகளை குறைந்தபட்சமாகக் கருதும், சூரிய அச்தமனத்திற்குப் பிறகு இந்த மதிப்புகளுக்கு மாறும். .", - "take_over_control": "Take_over_control: விளக்குகள் இயக்கத்தில் இருக்கும்போது மற்றொரு சான்று `லைட்.டர்ன்_ஓஎன்` என்று அழைத்தால் தகவமைப்பு விளக்குகளை முடக்கு. இது `ஓமாசிச்டன்ட்.பிடேட்_என்டிட்டி` ஒவ்வொரு` இடைவெளியையும் 'என்று அழைக்கிறது என்பதை நினைவில் கொள்க! .", - "detect_non_ha_changes": "கண்டறிதல்_நான்_ஆ_சேஞ்ச்ச்: `விளக்கு அல்லாத. டர்ன்_ஓஎன்` மாநில மாற்றங்களுக்கான தழுவல்களைக் கண்டறிந்து நிறுத்துகிறது. `Take_over_control` இயக்கப்பட்டது. 🕵œ எச்சரிக்கை: ⚠œ சில விளக்குகள் ஒரு 'ஆன்' நிலையை பொய்யாகக் குறிக்கக்கூடும், இதனால் விளக்குகள் எதிர்பாராத விதமாக இயக்கப்படலாம். இதுபோன்ற சிக்கல்களை நீங்கள் சந்தித்தால் இந்த அம்சத்தை முடக்கு.", - "only_once": "மட்டும்_இன்: விளக்குகள் இயக்கப்படும்போது மட்டுமே (`உண்மை`) மாற்றியமைக்கும்போது அல்லது அவற்றைத் தழுவிக்கொள்ளுங்கள் (` தவறு`). .", - "adapt_only_on_bare_turn_on": "சரிசெய்_only_on_bare_turn_on: ஆரம்பத்தில் விளக்குகளை இயக்கும்போது. `உண்மை` என அமைக்கப்பட்டால், வண்ணம் அல்லது பிரகாசத்தைக் குறிப்பிடாமல்` லைட்.டர்ன்_ஓஎன்` செயல்படுத்தப்பட்டால் மட்டுமே அல் மாற்றியமைக்கிறது. ❌🌈 இது எ.கா., ஒரு காட்சியைச் செயல்படுத்தும்போது தழுவலைத் தடுக்கிறது. `தவறு` என்றால், ஆரம்ப` சேவை_டா` இல் நிறம் அல்லது ஒளி இருப்பதைப் பொருட்படுத்தாமல் AL மாற்றியமைக்கிறது. `Take_over_control` இயக்கப்பட்டது. . 🕵️", - "separate_turn_on_commands": "தனித்தனி_டர்ன்_ஆன்_காமண்ட்ச்: சில ஒளி வகைகளுக்கு தேவைப்படும் வண்ணம் மற்றும் பிரகாசத்திற்கான தனித்தனி `லைட்.டர்ன்_ஓஎன்` அழைப்புகளைப் பயன்படுத்தவும். .", - "skip_redundant_commands": "Skip_redundant_commands: தழுவல் கட்டளைகளை அனுப்புவதைத் தவிர்க்கவும், அதன் இலக்கு நிலை ஏற்கனவே ஒளியின் அறியப்பட்ட நிலைக்கு சமம். பிணையம் போக்குவரத்தை குறைக்கிறது மற்றும் சில சூழ்நிலைகளில் தழுவல் மறுமொழியை மேம்படுத்துகிறது. ஆ இன் பதிவு செய்யப்பட்ட நிலையுடன் இயற்பியல் ஒளி நிலைகள் ஒத்திசைவிலிருந்து வெளியேறினால் அது காணக்கூடியது.", - "intercept": "இடைமறிப்பு: உடனடி வண்ணம் மற்றும் பிரகாசமான தழுவலை செயல்படுத்த `ஒளி. Color வண்ணம் மற்றும் பிரகாசத்துடன் `ஒளி.", - "multi_light_intercept": "Mulli_light_intect: பல விளக்குகளை குறிவைக்கும் `light.turn_on` அழைப்புகளை இடைமறிக்கவும் மாற்றவும். ➗⚠œ இது ஒரு `லைட்.டர்ன்_ஒன்` அழைப்பை பல அழைப்புகளாக பிரிக்கக்கூடும், எ.கா., விளக்குகள் வெவ்வேறு சுவிட்சுகளில் இருக்கும்போது. இயக்கப்பட வேண்டும் `இடைமறிப்பு` தேவை.", - "include_config_in_attributes": "அடங்கும்_கான்ஃபிக்_இன்_அட்ரிபியூட்: `உண்மை` என அமைக்கப்பட்டிருக்கும் போது வீட்டு உதவியாளரின் சுவிட்சில் உள்ள பண்புகளாக அனைத்து விருப்பங்களையும் காட்டுங்கள். ." + "max_color_temp": "MAX_COLOR_TEMP: கெல்வினில் குளிரான வண்ண வெப்பநிலை. ." }, "data_description": { "interval": "விளக்குகளை மாற்றியமைக்க அதிர்வெண், நொடிகளில். .", "transition": "விளக்குகள் மாறும்போது, நொடிகளில் மாற்றத்தின் காலம். .", - "initial_transition": "விளக்குகள் `ஆஃப்` முதல்` ஆன் `வரை நொடிகளில் மாறும் போது முதல் மாற்றத்தின் காலம். .", "sleep_brightness": "தூக்க பயன்முறையில் விளக்குகளின் ஒளி விழுக்காடு. .", - "sleep_rgb_or_color_temp": "தூக்க பயன்முறையில் `\" rgb_color \"` அல்லது `\" Color_Temp \"key ஐப் பயன்படுத்தவும். .", - "sleep_color_temp": "ச்லீப் பயன்முறையில் வண்ண வெப்பநிலை (கெல்வினில் `SLEEP_RGB_OR_COLOR_TEMP` என்பது` color_temp` ஆக இருக்கும்போது பயன்படுத்தப்படுகிறது). .", - "sleep_rgb_color": "தூக்க பயன்முறையில் RGB வண்ணம் (`SLEEP_RGB_OR_COLOR_TEMP`\" RGB_COLOR \"ஆக இருக்கும்போது பயன்படுத்தப்படுகிறது). .", - "sleep_transition": "\"தூக்க பயன்முறை\" நொடிகளில் மாற்றப்படும்போது மாற்றத்தின் காலம். .", - "sunrise_time": "சூரிய உதயத்திற்கு ஒரு நிலையான நேரத்தை (HH: MM: SS) அமைக்கவும். .", - "min_sunrise_time": "ஆரம்பகால மெய்நிகர் சூரிய உதய நேரத்தை (HH: MM: SS) அமைக்கவும், பின்னர் சூரிய உதயங்களை அனுமதிக்கிறது. .", - "max_sunrise_time": "ஆரம்பகால சூரிய உதயத்தை அனுமதிக்கும் அண்மைக் கால மெய்நிகர் சூரிய தோன்றுகை நேரத்தை (HH: MM: SS) அமைக்கவும். .", - "sunrise_offset": "விநாடிகளில் நேர்மறை அல்லது எதிர்மறை ஆஃப்செட் மூலம் சூரிய தோன்றுகை நேரத்தை சரிசெய்யவும். .", - "sunset_time": "சூரிய அச்தமனத்திற்கு ஒரு நிலையான நேரத்தை (HH: MM: SS) அமைக்கவும். .", - "min_sunset_time": "ஆரம்பகால மெய்நிகர் சூரிய மறைவு நேரத்தை (HH: MM: SS) அமைக்கவும், பின்னர் சூரிய அச்தமனங்களை அனுமதிக்கிறது. .", - "max_sunset_time": "முந்தைய சூரிய அச்தமனங்களை அனுமதிக்கும் அண்மைக் கால மெய்நிகர் சன்செட் நேரத்தை (HH: MM: SS) அமைக்கவும். .", - "sunset_offset": "விநாடிகளில் நேர்மறை அல்லது எதிர்மறை ஆஃப்செட் மூலம் சூரிய மறைவு நேரத்தை சரிசெய்யவும். .", - "send_split_delay": "ஒரே நேரத்தில் ஒளி மற்றும் வண்ண அமைப்பை ஆதரிக்காத விளக்குகளுக்கு `தனி_டர்ன்_ஆன்_காமண்ட்ச்` இடையே நேரந்தவறுகை (எம்.எச்). .", - "adapt_delay": "லைட் டர்ன் மற்றும் தகவமைப்பு விளக்குகள் இடையே காத்திருப்பு நேரம் (விநாடிகள்) மாற்றங்களைப் பயன்படுத்துகிறது. ஒளிரும் தவிர்க்க உதவலாம். .", - "brightness_mode": "பயன்படுத்த பிரகாசமான முறை. சாத்தியமான மதிப்புகள் `இயல்புநிலை`,` லீனியர்`, மற்றும் `டான்` (` பிரகாசம்_மோட்_ நேரம்_டார்க்` மற்றும் `பிரகாசம்_மோட்_மட்_லிட்` ஆகியவற்றைப் பயன்படுத்துகின்றன). .", - "brightness_mode_time_dark": ". .", - "brightness_mode_time_light": ". ..", - "autoreset_control_seconds": "பல விநாடிகளுக்குப் பிறகு தானாகவே கையேடு கட்டுப்பாட்டை மீட்டமைக்கவும். முடக்க 0 என அமைக்கவும். ." + "sleep_color_temp": "ச்லீப் பயன்முறையில் வண்ண வெப்பநிலை (கெல்வினில் `SLEEP_RGB_OR_COLOR_TEMP` என்பது` color_temp` ஆக இருக்கும்போது பயன்படுத்தப்படுகிறது). ." + }, + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "bey_rgb_color: முடிந்தவரை ஒளி வண்ண வெப்பநிலையை விட RGB வண்ண சரிசெய்தலை விரும்பலாமா. .", + "transition_until_sleep": "Transition_until_sleep: இயக்கப்பட்டால், தகவமைப்பு விளக்குகள் தூக்க அமைப்புகளை குறைந்தபட்சமாகக் கருதும், சூரிய அச்தமனத்திற்குப் பிறகு இந்த மதிப்புகளுக்கு மாறும். .", + "take_over_control": "Take_over_control: விளக்குகள் இயக்கத்தில் இருக்கும்போது மற்றொரு சான்று `லைட்.டர்ன்_ஓஎன்` என்று அழைத்தால் தகவமைப்பு விளக்குகளை முடக்கு. இது `ஓமாசிச்டன்ட்.பிடேட்_என்டிட்டி` ஒவ்வொரு` இடைவெளியையும் 'என்று அழைக்கிறது என்பதை நினைவில் கொள்க! .", + "detect_non_ha_changes": "கண்டறிதல்_நான்_ஆ_சேஞ்ச்ச்: `விளக்கு அல்லாத. டர்ன்_ஓஎன்` மாநில மாற்றங்களுக்கான தழுவல்களைக் கண்டறிந்து நிறுத்துகிறது. `Take_over_control` இயக்கப்பட்டது. 🕵œ எச்சரிக்கை: ⚠œ சில விளக்குகள் ஒரு 'ஆன்' நிலையை பொய்யாகக் குறிக்கக்கூடும், இதனால் விளக்குகள் எதிர்பாராத விதமாக இயக்கப்படலாம். இதுபோன்ற சிக்கல்களை நீங்கள் சந்தித்தால் இந்த அம்சத்தை முடக்கு.", + "only_once": "மட்டும்_இன்: விளக்குகள் இயக்கப்படும்போது மட்டுமே (`உண்மை`) மாற்றியமைக்கும்போது அல்லது அவற்றைத் தழுவிக்கொள்ளுங்கள் (` தவறு`). .", + "adapt_only_on_bare_turn_on": "சரிசெய்_only_on_bare_turn_on: ஆரம்பத்தில் விளக்குகளை இயக்கும்போது. `உண்மை` என அமைக்கப்பட்டால், வண்ணம் அல்லது பிரகாசத்தைக் குறிப்பிடாமல்` லைட்.டர்ன்_ஓஎன்` செயல்படுத்தப்பட்டால் மட்டுமே அல் மாற்றியமைக்கிறது. ❌🌈 இது எ.கா., ஒரு காட்சியைச் செயல்படுத்தும்போது தழுவலைத் தடுக்கிறது. `தவறு` என்றால், ஆரம்ப` சேவை_டா` இல் நிறம் அல்லது ஒளி இருப்பதைப் பொருட்படுத்தாமல் AL மாற்றியமைக்கிறது. `Take_over_control` இயக்கப்பட்டது. . 🕵️", + "separate_turn_on_commands": "தனித்தனி_டர்ன்_ஆன்_காமண்ட்ச்: சில ஒளி வகைகளுக்கு தேவைப்படும் வண்ணம் மற்றும் பிரகாசத்திற்கான தனித்தனி `லைட்.டர்ன்_ஓஎன்` அழைப்புகளைப் பயன்படுத்தவும். .", + "skip_redundant_commands": "Skip_redundant_commands: தழுவல் கட்டளைகளை அனுப்புவதைத் தவிர்க்கவும், அதன் இலக்கு நிலை ஏற்கனவே ஒளியின் அறியப்பட்ட நிலைக்கு சமம். பிணையம் போக்குவரத்தை குறைக்கிறது மற்றும் சில சூழ்நிலைகளில் தழுவல் மறுமொழியை மேம்படுத்துகிறது. ஆ இன் பதிவு செய்யப்பட்ட நிலையுடன் இயற்பியல் ஒளி நிலைகள் ஒத்திசைவிலிருந்து வெளியேறினால் அது காணக்கூடியது.", + "intercept": "இடைமறிப்பு: உடனடி வண்ணம் மற்றும் பிரகாசமான தழுவலை செயல்படுத்த `ஒளி. Color வண்ணம் மற்றும் பிரகாசத்துடன் `ஒளி.", + "multi_light_intercept": "Mulli_light_intect: பல விளக்குகளை குறிவைக்கும் `light.turn_on` அழைப்புகளை இடைமறிக்கவும் மாற்றவும். ➗⚠œ இது ஒரு `லைட்.டர்ன்_ஒன்` அழைப்பை பல அழைப்புகளாக பிரிக்கக்கூடும், எ.கா., விளக்குகள் வெவ்வேறு சுவிட்சுகளில் இருக்கும்போது. இயக்கப்பட வேண்டும் `இடைமறிப்பு` தேவை.", + "include_config_in_attributes": "அடங்கும்_கான்ஃபிக்_இன்_அட்ரிபியூட்: `உண்மை` என அமைக்கப்பட்டிருக்கும் போது வீட்டு உதவியாளரின் சுவிட்சில் உள்ள பண்புகளாக அனைத்து விருப்பங்களையும் காட்டுங்கள். ." + }, + "data_description": { + "initial_transition": "விளக்குகள் `ஆஃப்` முதல்` ஆன் `வரை நொடிகளில் மாறும் போது முதல் மாற்றத்தின் காலம். .", + "sleep_rgb_or_color_temp": "தூக்க பயன்முறையில் `\" rgb_color \"` அல்லது `\" Color_Temp \"key ஐப் பயன்படுத்தவும். .", + "sleep_rgb_color": "தூக்க பயன்முறையில் RGB வண்ணம் (`SLEEP_RGB_OR_COLOR_TEMP`\" RGB_COLOR \"ஆக இருக்கும்போது பயன்படுத்தப்படுகிறது). .", + "sleep_transition": "\"தூக்க பயன்முறை\" நொடிகளில் மாற்றப்படும்போது மாற்றத்தின் காலம். .", + "sunrise_time": "சூரிய உதயத்திற்கு ஒரு நிலையான நேரத்தை (HH: MM: SS) அமைக்கவும். .", + "min_sunrise_time": "ஆரம்பகால மெய்நிகர் சூரிய உதய நேரத்தை (HH: MM: SS) அமைக்கவும், பின்னர் சூரிய உதயங்களை அனுமதிக்கிறது. .", + "max_sunrise_time": "ஆரம்பகால சூரிய உதயத்தை அனுமதிக்கும் அண்மைக் கால மெய்நிகர் சூரிய தோன்றுகை நேரத்தை (HH: MM: SS) அமைக்கவும். .", + "sunrise_offset": "விநாடிகளில் நேர்மறை அல்லது எதிர்மறை ஆஃப்செட் மூலம் சூரிய தோன்றுகை நேரத்தை சரிசெய்யவும். .", + "sunset_time": "சூரிய அச்தமனத்திற்கு ஒரு நிலையான நேரத்தை (HH: MM: SS) அமைக்கவும். .", + "min_sunset_time": "ஆரம்பகால மெய்நிகர் சூரிய மறைவு நேரத்தை (HH: MM: SS) அமைக்கவும், பின்னர் சூரிய அச்தமனங்களை அனுமதிக்கிறது. .", + "max_sunset_time": "முந்தைய சூரிய அச்தமனங்களை அனுமதிக்கும் அண்மைக் கால மெய்நிகர் சன்செட் நேரத்தை (HH: MM: SS) அமைக்கவும். .", + "sunset_offset": "விநாடிகளில் நேர்மறை அல்லது எதிர்மறை ஆஃப்செட் மூலம் சூரிய மறைவு நேரத்தை சரிசெய்யவும். .", + "brightness_mode": "பயன்படுத்த பிரகாசமான முறை. சாத்தியமான மதிப்புகள் `இயல்புநிலை`,` லீனியர்`, மற்றும் `டான்` (` பிரகாசம்_மோட்_ நேரம்_டார்க்` மற்றும் `பிரகாசம்_மோட்_மட்_லிட்` ஆகியவற்றைப் பயன்படுத்துகின்றன). .", + "brightness_mode_time_dark": ". .", + "brightness_mode_time_light": ". ..", + "autoreset_control_seconds": "பல விநாடிகளுக்குப் பிறகு தானாகவே கையேடு கட்டுப்பாட்டை மீட்டமைக்கவும். முடக்க 0 என அமைக்கவும். .", + "send_split_delay": "ஒரே நேரத்தில் ஒளி மற்றும் வண்ண அமைப்பை ஆதரிக்காத விளக்குகளுக்கு `தனி_டர்ன்_ஆன்_காமண்ட்ச்` இடையே நேரந்தவறுகை (எம்.எச்). .", + "adapt_delay": "லைட் டர்ன் மற்றும் தகவமைப்பு விளக்குகள் இடையே காத்திருப்பு நேரம் (விநாடிகள்) மாற்றங்களைப் பயன்படுத்துகிறது. ஒளிரும் தவிர்க்க உதவலாம். ." + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/tr.json b/custom_components/adaptive_lighting/translations/tr.json index 00f1459f..d6f929f6 100644 --- a/custom_components/adaptive_lighting/translations/tr.json +++ b/custom_components/adaptive_lighting/translations/tr.json @@ -5,48 +5,56 @@ "init": { "title": "Akıllı Aydınlatma seçenekleri", "data": { - "adapt_only_on_bare_turn_on": "Işıklar açıldığında geçerlidir. `true` olarak ayarlanırsa, eklenti yalnızca `light.turn_on` işlemi renk veya parlaklık belirtilmeden çağrıldığında uyarlama yapar (örneğin sahne etkinleştirmelerinde uyarlama yapılmaz). ❌🌈\n`false` olarak ayarlanırsa, renk veya parlaklık belirtilmiş olsa bile uyarlama yapılır. Bu ayarın çalışması için `take_over_control` etkin olmalıdır. 🕵️", - "detect_non_ha_changes": "`detect_non_ha_changes`: `light.turn_on` dışındaki durum değişikliklerini algılar ve uyarlamayı durdurur. Bu ayarın çalışması için `take_over_control` etkin olmalıdır. 🕵️\nDikkat: ⚠️ Bazı ışıklar yanlışlıkla “açık” durumunu bildirebilir, bu da ışıkların beklenmedik şekilde açılmasına yol açabilir. Böyle bir durumla karşılaşırsanız bu özelliği devre dışı bırakın.", - "include_config_in_attributes": "`include_config_in_attributes`: `true` olarak ayarlandığında, tüm seçenekleri Home Assistant’ta anahtarın attribute’ları olarak gösterir. 📝", - "intercept": "`intercept`: `light.turn_on` çağrılarını yakalar ve renk ile parlaklığın anında uyarlanmasını sağlar. 🏎️ Renk ve parlaklığı desteklemeyen ışıklar için devre dışı bırakın.", "lights": "`lights`: Kontrol edilecek ışıkların entity_id listesi (boş bırakılabilir). 🌟", - "max_brightness": "`max_brightness`: Maksimum parlaklık yüzdesi. 💡", - "max_color_temp": "`max_color_temp`: En düşük renk sıcaklığı (Kelvin cinsinden). ❄️", "min_brightness": "min_brightness: Minimum parlaklık yüzdesi.💡", + "max_brightness": "`max_brightness`: Maksimum parlaklık yüzdesi. 💡", "min_color_temp": "`min_color_temp`: En yüksek (sıcak) renk sıcaklığı (Kelvin cinsinden). 🔥", - "multi_light_intercept": "`multi_light_intercept`: Birden fazla ışığı hedefleyen `light.turn_on` çağrılarını yakalar ve uyarlama yapar. ➗⚠️ Bu, örneğin ışıklar farklı anahtarlardaysa tek bir `light.turn_on` çağrısının birden fazla çağrıya bölünmesine yol açabilir. `intercept` etkin olmalıdır.", - "only_once": "`only_once`: Işıkları yalnızca açıldıklarında mı uyarlasın (`true`), yoksa sürekli uyarlamaya devam mı etsin (`false`). 🔄", - "prefer_rgb_color": "`prefer_rgb_color`: Mümkünse ışık renk sıcaklığı yerine RGB renk ayarını tercih edip etmeyeceğini belirler. 🌈", - "separate_turn_on_commands": "`separate_turn_on_commands`: Renk ve parlaklık için ayrı `light.turn_on` çağrıları kullanır; bazı ışık türleri için gereklidir. 🔀", - "skip_redundant_commands": "`skip_redundant_commands`: Hedef durumu ışığın bilinen durumu ile aynı olan uyarlama komutlarını atlar. Ağ trafiğini azaltır ve bazı durumlarda uyarlamanın yanıt hızını artırır. 📉 \nFiziksel ışık durumları HA’daki kaydedilen durumla senkronize değilse devre dışı bırakın.", - "take_over_control": "`take_over_control`: Işıklar açıkken ve uyarlanırken başka bir kaynaktan `light.turn_on` çağrılırsa Adaptive Lighting’i devre dışı bırakır. Dikkat: Bu işlem her `interval` süresinde `homeassistant.update_entity` çağrısı yapar! 🔒", - "transition_until_sleep": "`transition_until_sleep`: Etkinleştirildiğinde, Adaptive Lighting uyku ayarlarını minimum değer olarak kabul eder ve gün batımından sonra bu değerlere geçiş yapar. 🌙" + "max_color_temp": "`max_color_temp`: En düşük renk sıcaklığı (Kelvin cinsinden). ❄️" }, "data_description": { - "sunrise_offset": "Gün doğumu saatini, saniye cinsinden pozitif veya negatif bir kaydırma ile ayarlayın. ⏰", - "sunset_offset": "Gün batımı saatini, saniye cinsinden pozitif veya negatif bir kaydırma ile ayarlayın. ⏰", - "autoreset_control_seconds": "Manuel kontrolü belirtilen saniye sonunda otomatik olarak sıfırlar. Devre dışı bırakmak için 0 olarak ayarlayın. ⏲️", - "brightness_mode": "Kullanılacak parlaklık modunu belirtir. Olası değerler: `default`, `linear` ve `tanh` (`brightness_mode_time_dark` ve `brightness_mode_time_light` ayarlarını kullanır). 📈", - "sleep_brightness": "Uyku modundayken ışıkların parlaklık yüzdesi 😴", - "sleep_color_temp": "Uyku modunda renk sıcaklığı ( `sleep_rgb_or_color_temp` `color_temp` olarak ayarlandığında kullanılır) Kelvin cinsinden. 😴", - "send_split_delay": "Parlaklık ve renk ayarını aynı anda desteklemeyen ışıklar için `separate_turn_on_commands` arasındaki gecikme (ms). ⏲️", - "initial_transition": "Işıklar `off` durumundan `on` durumuna geçerken ilk geçişin süresi (saniye cinsinden). ⏲️", - "transition": "Işıklar değişirken geçiş süresi (saniye cinsinden). 🕑", - "sleep_transition": "“Uyku modu” açılıp kapatıldığında geçiş süresi (saniye cinsinden). 😴", "interval": "Işıkların uyarlanma sıklığı (saniye cinsinden). 🔄", - "brightness_mode_time_light": "(`brightness_mode='default'` ise göz ardı edilir) Gün doğumu/gün batımı öncesi/sonrası parlaklığı kademeli olarak artırma/azaltma süresi (saniye cinsinden). 📈📉", - "brightness_mode_time_dark": "(`brightness_mode='default'` ise göz ardı edilir) Gün doğumu/gün batımı öncesi/sonrası parlaklığı kademeli olarak artırma/azaltma süresi (saniye cinsinden). 📈📉", - "sleep_rgb_color": "Uyku modunda RGB renk ( `sleep_rgb_or_color_temp` \"rgb_color\" olarak ayarlandığında kullanılır). 🌈", - "sunrise_time": "Gün doğumu için sabit bir saat (SS:DD:YY) belirleyin. 🌅", - "sunset_time": "Gün batımı için sabit bir saat (SS:DD:YY) belirleyin. 🌇", - "min_sunrise_time": "En erken sanal gün doğumu saatini (SS:DD:YY) belirleyin; daha geç gün doğumlarına izin verir. 🌅", - "min_sunset_time": "En erken sanal gün batımı saatini (SS:DD:YY) belirleyin; daha geç gün batımlarına izin verir. 🌇", - "max_sunrise_time": "En geç sanal gün doğumu saatini (SS:DD:YY) belirleyin; daha erken gün doğumlarına izin verir. 🌅", - "max_sunset_time": "En geç sanal gün batımı saatini (SS:DD:YY) belirleyin; daha erken gün batımlarına izin verir. 🌇", - "sleep_rgb_or_color_temp": "Uyku modunda `\"rgb_color\"` veya `\"color_temp\"` kullanın. 🌙", - "adapt_delay": "Işık açıldıktan sonra Adaptive Lighting’in değişiklikleri uygulamasına kadar bekleme süresi (saniye cinsinden). Titremeyi önlemeye yardımcı olabilir. ⏲️" + "transition": "Işıklar değişirken geçiş süresi (saniye cinsinden). 🕑", + "sleep_brightness": "Uyku modundayken ışıkların parlaklık yüzdesi 😴", + "sleep_color_temp": "Uyku modunda renk sıcaklığı ( `sleep_rgb_or_color_temp` `color_temp` olarak ayarlandığında kullanılır) Kelvin cinsinden. 😴" }, - "description": "Bir Adaptive Lighting bileşenini yapılandırın. Seçenek adları YAML ayarlarıyla uyumludur. Eğer bu girdiyi YAML’da tanımladıysanız, burada seçenekler görünmez. \nParametrelerin etkilerini gösteren etkileşimli grafikler için [bu web uygulamasını]({webapp_url}) ziyaret edebilirsiniz. Daha fazla bilgi için [resmi dokümantasyona]({docs_url}) bakın." + "description": "Bir Adaptive Lighting bileşenini yapılandırın. Seçenek adları YAML ayarlarıyla uyumludur. Eğer bu girdiyi YAML’da tanımladıysanız, burada seçenekler görünmez. \nParametrelerin etkilerini gösteren etkileşimli grafikler için [bu web uygulamasını]({webapp_url}) ziyaret edebilirsiniz. Daha fazla bilgi için [resmi dokümantasyona]({docs_url}) bakın.", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "`prefer_rgb_color`: Mümkünse ışık renk sıcaklığı yerine RGB renk ayarını tercih edip etmeyeceğini belirler. 🌈", + "transition_until_sleep": "`transition_until_sleep`: Etkinleştirildiğinde, Adaptive Lighting uyku ayarlarını minimum değer olarak kabul eder ve gün batımından sonra bu değerlere geçiş yapar. 🌙", + "take_over_control": "`take_over_control`: Işıklar açıkken ve uyarlanırken başka bir kaynaktan `light.turn_on` çağrılırsa Adaptive Lighting’i devre dışı bırakır. Dikkat: Bu işlem her `interval` süresinde `homeassistant.update_entity` çağrısı yapar! 🔒", + "detect_non_ha_changes": "`detect_non_ha_changes`: `light.turn_on` dışındaki durum değişikliklerini algılar ve uyarlamayı durdurur. Bu ayarın çalışması için `take_over_control` etkin olmalıdır. 🕵️\nDikkat: ⚠️ Bazı ışıklar yanlışlıkla “açık” durumunu bildirebilir, bu da ışıkların beklenmedik şekilde açılmasına yol açabilir. Böyle bir durumla karşılaşırsanız bu özelliği devre dışı bırakın.", + "only_once": "`only_once`: Işıkları yalnızca açıldıklarında mı uyarlasın (`true`), yoksa sürekli uyarlamaya devam mı etsin (`false`). 🔄", + "adapt_only_on_bare_turn_on": "Işıklar açıldığında geçerlidir. `true` olarak ayarlanırsa, eklenti yalnızca `light.turn_on` işlemi renk veya parlaklık belirtilmeden çağrıldığında uyarlama yapar (örneğin sahne etkinleştirmelerinde uyarlama yapılmaz). ❌🌈\n`false` olarak ayarlanırsa, renk veya parlaklık belirtilmiş olsa bile uyarlama yapılır. Bu ayarın çalışması için `take_over_control` etkin olmalıdır. 🕵️", + "separate_turn_on_commands": "`separate_turn_on_commands`: Renk ve parlaklık için ayrı `light.turn_on` çağrıları kullanır; bazı ışık türleri için gereklidir. 🔀", + "skip_redundant_commands": "`skip_redundant_commands`: Hedef durumu ışığın bilinen durumu ile aynı olan uyarlama komutlarını atlar. Ağ trafiğini azaltır ve bazı durumlarda uyarlamanın yanıt hızını artırır. 📉 \nFiziksel ışık durumları HA’daki kaydedilen durumla senkronize değilse devre dışı bırakın.", + "intercept": "`intercept`: `light.turn_on` çağrılarını yakalar ve renk ile parlaklığın anında uyarlanmasını sağlar. 🏎️ Renk ve parlaklığı desteklemeyen ışıklar için devre dışı bırakın.", + "multi_light_intercept": "`multi_light_intercept`: Birden fazla ışığı hedefleyen `light.turn_on` çağrılarını yakalar ve uyarlama yapar. ➗⚠️ Bu, örneğin ışıklar farklı anahtarlardaysa tek bir `light.turn_on` çağrısının birden fazla çağrıya bölünmesine yol açabilir. `intercept` etkin olmalıdır.", + "include_config_in_attributes": "`include_config_in_attributes`: `true` olarak ayarlandığında, tüm seçenekleri Home Assistant’ta anahtarın attribute’ları olarak gösterir. 📝" + }, + "data_description": { + "initial_transition": "Işıklar `off` durumundan `on` durumuna geçerken ilk geçişin süresi (saniye cinsinden). ⏲️", + "sleep_rgb_or_color_temp": "Uyku modunda `\"rgb_color\"` veya `\"color_temp\"` kullanın. 🌙", + "sleep_rgb_color": "Uyku modunda RGB renk ( `sleep_rgb_or_color_temp` \"rgb_color\" olarak ayarlandığında kullanılır). 🌈", + "sleep_transition": "“Uyku modu” açılıp kapatıldığında geçiş süresi (saniye cinsinden). 😴", + "sunrise_time": "Gün doğumu için sabit bir saat (SS:DD:YY) belirleyin. 🌅", + "min_sunrise_time": "En erken sanal gün doğumu saatini (SS:DD:YY) belirleyin; daha geç gün doğumlarına izin verir. 🌅", + "max_sunrise_time": "En geç sanal gün doğumu saatini (SS:DD:YY) belirleyin; daha erken gün doğumlarına izin verir. 🌅", + "sunrise_offset": "Gün doğumu saatini, saniye cinsinden pozitif veya negatif bir kaydırma ile ayarlayın. ⏰", + "sunset_time": "Gün batımı için sabit bir saat (SS:DD:YY) belirleyin. 🌇", + "min_sunset_time": "En erken sanal gün batımı saatini (SS:DD:YY) belirleyin; daha geç gün batımlarına izin verir. 🌇", + "max_sunset_time": "En geç sanal gün batımı saatini (SS:DD:YY) belirleyin; daha erken gün batımlarına izin verir. 🌇", + "sunset_offset": "Gün batımı saatini, saniye cinsinden pozitif veya negatif bir kaydırma ile ayarlayın. ⏰", + "brightness_mode": "Kullanılacak parlaklık modunu belirtir. Olası değerler: `default`, `linear` ve `tanh` (`brightness_mode_time_dark` ve `brightness_mode_time_light` ayarlarını kullanır). 📈", + "brightness_mode_time_dark": "(`brightness_mode='default'` ise göz ardı edilir) Gün doğumu/gün batımı öncesi/sonrası parlaklığı kademeli olarak artırma/azaltma süresi (saniye cinsinden). 📈📉", + "brightness_mode_time_light": "(`brightness_mode='default'` ise göz ardı edilir) Gün doğumu/gün batımı öncesi/sonrası parlaklığı kademeli olarak artırma/azaltma süresi (saniye cinsinden). 📈📉", + "autoreset_control_seconds": "Manuel kontrolü belirtilen saniye sonunda otomatik olarak sıfırlar. Devre dışı bırakmak için 0 olarak ayarlayın. ⏲️", + "send_split_delay": "Parlaklık ve renk ayarını aynı anda desteklemeyen ışıklar için `separate_turn_on_commands` arasındaki gecikme (ms). ⏲️", + "adapt_delay": "Işık açıldıktan sonra Adaptive Lighting’in değişiklikleri uygulamasına kadar bekleme süresi (saniye cinsinden). Titremeyi önlemeye yardımcı olabilir. ⏲️" + } + } + } } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/uk.json b/custom_components/adaptive_lighting/translations/uk.json index 7c271fcc..f3d66617 100644 --- a/custom_components/adaptive_lighting/translations/uk.json +++ b/custom_components/adaptive_lighting/translations/uk.json @@ -28,55 +28,63 @@ "description": "Всі налаштування компонента адаптивного освітлення. Назви опцій відповідають налаштуванням у YAML. Опції не відображаються, якщо ви вже визначили їх у компоненті adaptive_lighting вашої YAML-конфігурації.", "data": { "lights": "прилади", - "initial_transition": "initial_transition: Коли прилад вимикається (off), вмикається (on), або змінює 'sleep_state'. (секунди)", "interval": "interval: Час між оновленнями перемикача. (секунди)", - "max_brightness": "max_brightness: Найвища яскравість світла під час циклу. (%)", - "max_color_temp": "max_color_temp: Найхолодніший відтінок циклу кольорової температури. (Кельвін)", - "min_brightness": "min_brightness: Найнижча яскравість світла під час циклу. (%)", - "min_color_temp": "min_color_temp: Найтепліший відтінок циклу кольорової температури. (%)", - "only_once": "only_once: Адаптувати світло лише після початкового увімкнення.", - "prefer_rgb_color": "prefer_rgb_color: Використовувати 'rgb_color' замість 'color_temp', коли можливо.", - "separate_turn_on_commands": "separate_turn_on_commands: Окремі команди для кожного атрибута (колір, яскравість, тощо.) в 'light.turn_on' (необхідні для деяких приладів).", - "sleep_brightness": "sleep_brightness: Налаштування яскравості для Режиму сну. (%)", - "sleep_color_temp": "sleep_color_temp: Температура кольору для Режиму сну. (Кельвін)", - "sunrise_offset": "sunrise_offset: Як за довго до(-) або після(+) визначати точку сходу сонця для циклу (+/- секунд)", - "sunrise_time": "sunrise_time: Ручний перезапис часу сходу сонця, якщо 'None', тоді використовується час сходу сонця у вашій локації (HH:MM:SS)", - "sunset_offset": "sunset_offset: Як за довго до(-) або після(+) визначати точку заходу сонця для циклу (+/- секунд)", - "sunset_time": "sunset_time: Ручний перезапис часу заходу сонця, якщо 'None', тоді використовується час заходу сонця у вашій локації (HH:MM:SS)", - "take_over_control": "take_over_control: Якщо що-небудь, окрім Адаптивного освітлення, викликає 'light.turn_on', коли світло вже увімкнено, чи адаптувати освітлення допоки світло (або перемикач) перемкнеться (off -> on).", - "detect_non_ha_changes": "detect_non_ha_changes: виявляти всі зміни >10% до освітлення (включаючи ті, що зроблені поза HA), вимагає, щоб 'take_over_control' був включений (виклик 'homeassistant.update_entity' кожного оновлення 'interval'!)", "transition": "Час переходу, який застосовується до освітлення (секунди)", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: На початку вмикання світла. Якщо `true`, освітлення адаптується лише якщо `light.turn_on` викликано без вказання кольору чи яскравості. ❌🌈 Це, наприклад, запобігає адаптації, коли сцена активується. Якщо `false`, освітлення адаптується незалежно від наявності кольору чи яскравості у початковому `service_data`. Потребує ввімкнення `take_over_control`. 🕵️", - "transition_until_sleep": "transition_until_sleep: Коли активовано, адаптивне освітлення буде ставитись до налаштування сну як мінімум, переходячи до цих значень після заходу сонця. 🌙", - "intercept": "intercept: Перехоплювати та адаптувати виклики увімкнення світла (`light.turn_on`), щоб увімкнути миттєву адаптацію кольору та яскравості. 🏎️ Вимкніть для світла, що не підтримує увімкнення світла (`light.turn_on`) з кольором та яскравістю.", - "include_config_in_attributes": "Включити конфігурацію в атрибути (`include_config_in_attributes`): Показувати всі опції як атрибути на перемикачі в Home Assistant, якщо встановлено значення `true`. 📝", - "multi_light_intercept": "multi_light_intercept: Перехоплення та адаптація викликів `light.turn_on`, які спрямовані на кілька світильників. ➗⚠️ Це може призвести до розділення одного виклику `light.turn_on` на кілька викликів, наприклад, коли світильники підключені до різних вимикачів. Потрібно ввімкнути `intercept`.", - "skip_redundant_commands": "skip_redundant_commands: Пропускати надсилання команд адаптації, цільовий стан яких вже дорівнює відомому стану освітлення. Мінімізує мережевий трафік і покращує швидкість реагування адаптації в деяких ситуаціях. 📉Вимкнути, якщо фізичний стан освітлення не синхронізується із записаним станом HA." + "min_brightness": "min_brightness: Найнижча яскравість світла під час циклу. (%)", + "max_brightness": "max_brightness: Найвища яскравість світла під час циклу. (%)", + "min_color_temp": "min_color_temp: Найтепліший відтінок циклу кольорової температури. (%)", + "max_color_temp": "max_color_temp: Найхолодніший відтінок циклу кольорової температури. (Кельвін)", + "sleep_brightness": "sleep_brightness: Налаштування яскравості для Режиму сну. (%)", + "sleep_color_temp": "sleep_color_temp: Температура кольору для Режиму сну. (Кельвін)" }, "data_description": { - "sunrise_offset": "Змінити час сходу сонця на +/- секунд. ⏰", - "sunset_offset": "Змінити час заходу сонця на +/- секунд. ⏰", - "autoreset_control_seconds": "Самочинно скидати ручне керування після кількох секунд. Встановіть 0, щоб вимкнути.", - "initial_transition": "Тривалість першого переходу, коли світло перемикається зі стану вимкнено `off` на увімкнено `on`, у секундах. ⏲️", - "brightness_mode": "Режим яскравості для використання. Можливі значення: default (стандартний) , linear (лінійний) та tanh (гіперболічний тангенс) (використовує значення brightness_mode_time_dark та brightness_mode_time_light).", - "send_split_delay": "Затримка (мс) між `separate_turn_on_commands` (окремі команди увімкнення) для світла, що не підтримує одночасне налаштування яскравості та кольору. ⏲️", - "brightness_mode_time_dark": "(Ігнорується, якщо `brightness_mode='default'`) Тривалість у секундах для збільшення/зменшення яскравості до/після сходу/заходу сонця. 📈📉", - "brightness_mode_time_light": "(Ігнорується, якщо brightness_mode='default') Тривалість у секундах для збільшення/зменшення яскравості після/до сходу/заходу сонця. 📈📉.", - "transition": "Тривалість переходу, коли світло змінюється, у секундах. 🕑", "interval": "Частота адаптації освітлення, у секундах. 🔄", + "transition": "Тривалість переходу, коли світло змінюється, у секундах. 🕑", "sleep_brightness": "Відсоток яскравості світла в режимі сну. 😴", - "sleep_color_temp": "Колірна температура в режимі сну (використовується, коли `sleep_rgb_or_color_temp` має значення `color_temp`) у Кельвінах. 😴", - "sleep_transition": "Тривалість переходу, коли режим сну \"sleep mode\" увімкнено, у секундах. 😴", - "sleep_rgb_color": "Колір RGB у режимі сну (використовується, коли `sleep_rgb_or_color_temp` має значення \"rgb_color\"). 🌈", - "sunrise_time": "Встановіть фіксований час (ГГ:ХХ:СС) для сходу сонця. 🌅", - "sunset_time": "Встановіть фіксований час (ГГ:ХХ:СС) для заходу сонця. 🌇", - "min_sunrise_time": "Встановіть найраніший час віртуального сходу сонця (ГГ:ХХ:СС), враховуючи пізніші сходи. 🌅", - "min_sunset_time": "Встановіть найраніший час віртуального заходу сонця (ГГ:ХХ:СС), враховуючи пізніші заходи сонця. 🌇", - "max_sunrise_time": "Встановіть найпізніший час віртуального сходу сонця (ГГ:ХХ:СС), враховуючи більш ранні сходи сонця. 🌅", - "max_sunset_time": "Встановіть найновіший час віртуального заходу сонця (ГГ:ХХ:СС), враховуючи більш ранні заходи сонця. 🌇", - "sleep_rgb_or_color_temp": "Використовуйте `\"rgb_color\"` або `\"color_temp\"` у режимі сну. 🌙", - "adapt_delay": "Час очікування (секунди) між увімкненням світла та застосуванням змін системою адаптивного освітлення. Може допомогти уникнути мерехтіння. ⏲️", - "take_over_control_mode": "Режим призупинення адаптації, коли інші джерела змінюють яскравість та/або колір світла. `pause_all` завжди призупиняє адаптацію як яскравості, так і кольору. `pause_changed` призупиняє адаптацію лише змінених атрибутів та продовжує адаптацію незмінних атрибутів, наприклад, продовжує адаптацію кольору, коли змінювалася лише яскравість." + "sleep_color_temp": "Колірна температура в режимі сну (використовується, коли `sleep_rgb_or_color_temp` має значення `color_temp`) у Кельвінах. 😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "initial_transition: Коли прилад вимикається (off), вмикається (on), або змінює 'sleep_state'. (секунди)", + "prefer_rgb_color": "prefer_rgb_color: Використовувати 'rgb_color' замість 'color_temp', коли можливо.", + "transition_until_sleep": "transition_until_sleep: Коли активовано, адаптивне освітлення буде ставитись до налаштування сну як мінімум, переходячи до цих значень після заходу сонця. 🌙", + "sunrise_time": "sunrise_time: Ручний перезапис часу сходу сонця, якщо 'None', тоді використовується час сходу сонця у вашій локації (HH:MM:SS)", + "sunrise_offset": "sunrise_offset: Як за довго до(-) або після(+) визначати точку сходу сонця для циклу (+/- секунд)", + "sunset_time": "sunset_time: Ручний перезапис часу заходу сонця, якщо 'None', тоді використовується час заходу сонця у вашій локації (HH:MM:SS)", + "sunset_offset": "sunset_offset: Як за довго до(-) або після(+) визначати точку заходу сонця для циклу (+/- секунд)", + "take_over_control": "take_over_control: Якщо що-небудь, окрім Адаптивного освітлення, викликає 'light.turn_on', коли світло вже увімкнено, чи адаптувати освітлення допоки світло (або перемикач) перемкнеться (off -> on).", + "detect_non_ha_changes": "detect_non_ha_changes: виявляти всі зміни >10% до освітлення (включаючи ті, що зроблені поза HA), вимагає, щоб 'take_over_control' був включений (виклик 'homeassistant.update_entity' кожного оновлення 'interval'!)", + "only_once": "only_once: Адаптувати світло лише після початкового увімкнення.", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: На початку вмикання світла. Якщо `true`, освітлення адаптується лише якщо `light.turn_on` викликано без вказання кольору чи яскравості. ❌🌈 Це, наприклад, запобігає адаптації, коли сцена активується. Якщо `false`, освітлення адаптується незалежно від наявності кольору чи яскравості у початковому `service_data`. Потребує ввімкнення `take_over_control`. 🕵️", + "separate_turn_on_commands": "separate_turn_on_commands: Окремі команди для кожного атрибута (колір, яскравість, тощо.) в 'light.turn_on' (необхідні для деяких приладів).", + "skip_redundant_commands": "skip_redundant_commands: Пропускати надсилання команд адаптації, цільовий стан яких вже дорівнює відомому стану освітлення. Мінімізує мережевий трафік і покращує швидкість реагування адаптації в деяких ситуаціях. 📉Вимкнути, якщо фізичний стан освітлення не синхронізується із записаним станом HA.", + "intercept": "intercept: Перехоплювати та адаптувати виклики увімкнення світла (`light.turn_on`), щоб увімкнути миттєву адаптацію кольору та яскравості. 🏎️ Вимкніть для світла, що не підтримує увімкнення світла (`light.turn_on`) з кольором та яскравістю.", + "multi_light_intercept": "multi_light_intercept: Перехоплення та адаптація викликів `light.turn_on`, які спрямовані на кілька світильників. ➗⚠️ Це може призвести до розділення одного виклику `light.turn_on` на кілька викликів, наприклад, коли світильники підключені до різних вимикачів. Потрібно ввімкнути `intercept`.", + "include_config_in_attributes": "Включити конфігурацію в атрибути (`include_config_in_attributes`): Показувати всі опції як атрибути на перемикачі в Home Assistant, якщо встановлено значення `true`. 📝" + }, + "data_description": { + "initial_transition": "Тривалість першого переходу, коли світло перемикається зі стану вимкнено `off` на увімкнено `on`, у секундах. ⏲️", + "sleep_rgb_or_color_temp": "Використовуйте `\"rgb_color\"` або `\"color_temp\"` у режимі сну. 🌙", + "sleep_rgb_color": "Колір RGB у режимі сну (використовується, коли `sleep_rgb_or_color_temp` має значення \"rgb_color\"). 🌈", + "sleep_transition": "Тривалість переходу, коли режим сну \"sleep mode\" увімкнено, у секундах. 😴", + "sunrise_time": "Встановіть фіксований час (ГГ:ХХ:СС) для сходу сонця. 🌅", + "min_sunrise_time": "Встановіть найраніший час віртуального сходу сонця (ГГ:ХХ:СС), враховуючи пізніші сходи. 🌅", + "max_sunrise_time": "Встановіть найпізніший час віртуального сходу сонця (ГГ:ХХ:СС), враховуючи більш ранні сходи сонця. 🌅", + "sunrise_offset": "Змінити час сходу сонця на +/- секунд. ⏰", + "sunset_time": "Встановіть фіксований час (ГГ:ХХ:СС) для заходу сонця. 🌇", + "min_sunset_time": "Встановіть найраніший час віртуального заходу сонця (ГГ:ХХ:СС), враховуючи пізніші заходи сонця. 🌇", + "max_sunset_time": "Встановіть найновіший час віртуального заходу сонця (ГГ:ХХ:СС), враховуючи більш ранні заходи сонця. 🌇", + "sunset_offset": "Змінити час заходу сонця на +/- секунд. ⏰", + "brightness_mode": "Режим яскравості для використання. Можливі значення: default (стандартний) , linear (лінійний) та tanh (гіперболічний тангенс) (використовує значення brightness_mode_time_dark та brightness_mode_time_light).", + "brightness_mode_time_dark": "(Ігнорується, якщо `brightness_mode='default'`) Тривалість у секундах для збільшення/зменшення яскравості до/після сходу/заходу сонця. 📈📉", + "brightness_mode_time_light": "(Ігнорується, якщо brightness_mode='default') Тривалість у секундах для збільшення/зменшення яскравості після/до сходу/заходу сонця. 📈📉.", + "take_over_control_mode": "Режим призупинення адаптації, коли інші джерела змінюють яскравість та/або колір світла. `pause_all` завжди призупиняє адаптацію як яскравості, так і кольору. `pause_changed` призупиняє адаптацію лише змінених атрибутів та продовжує адаптацію незмінних атрибутів, наприклад, продовжує адаптацію кольору, коли змінювалася лише яскравість.", + "autoreset_control_seconds": "Самочинно скидати ручне керування після кількох секунд. Встановіть 0, щоб вимкнути.", + "send_split_delay": "Затримка (мс) між `separate_turn_on_commands` (окремі команди увімкнення) для світла, що не підтримує одночасне налаштування яскравості та кольору. ⏲️", + "adapt_delay": "Час очікування (секунди) між увімкненням світла та застосуванням змін системою адаптивного освітлення. Може допомогти уникнути мерехтіння. ⏲️" + } + } } } }, diff --git a/custom_components/adaptive_lighting/translations/ur.json b/custom_components/adaptive_lighting/translations/ur.json index 1f04e5f6..f61e47b1 100644 --- a/custom_components/adaptive_lighting/translations/ur.json +++ b/custom_components/adaptive_lighting/translations/ur.json @@ -137,49 +137,57 @@ "step": { "init": { "data_description": { - "sleep_rgb_or_color_temp": "نیند کے موڈ میں \"rgb_color\" یا \"color_temp\" کا استعمال کریں۔ 🌙", - "sleep_color_temp": "کیلون میں نیند کے موڈ میں رنگ کا درجہ حرارت (جب 'sleep_rgb_or_color_temp' 'color_temp' ہوتا ہے) میں استعمال ہوتا ہے۔ 😴", - "sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", - "autoreset_control_seconds": "کئی سیکنڈ کے بعد دستی کنٹرول کو خود بخود ری سیٹ کریں۔ غیر فعال کرنے کے لئے 0 پر سیٹ کریں۔ ⏲️", - "min_sunset_time": "سب سے پہلے مجازی غروب آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں ، جس سے بعد میں غروب آفتاب کی اجازت ملتی ہے۔ 🌇", - "sleep_brightness": "نیند کے موڈ میں روشنی کی چمک کا فیصد. 😴", - "min_sunrise_time": "ابتدائی مجازی طلوع آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں ، جس سے بعد میں طلوع آفتاب کی اجازت ملتی ہے۔ 🌅", "interval": "روشنیوں کو سیکنڈوں میں ڈھالنے کی فریکوئنسی۔ 🔄", - "adapt_delay": "لائٹ آن ہونے اور ایڈاپٹو لائٹنگ کے درمیان انتظار کا وقت (سیکنڈ) تبدیلیاں لاگو کرتا ہے۔ جھلکنے سے بچنے میں مدد مل سکتی ہے۔ ⏲️", - "sleep_rgb_color": "نیند کے موڈ میں آر جی بی رنگ (جب 'sleep_rgb_or_color_temp' \"rgb_color\" ہوتا ہے تو استعمال کیا جاتا ہے). 🌈", - "sunrise_offset": "طلوع آفتاب کے وقت کو سیکنڈوں میں مثبت یا منفی آفسیٹ کے ساتھ ایڈجسٹ کریں۔ ⏰", "transition": "جب روشنیاں تبدیل ہوتی ہیں تو منتقلی کا دورانیہ ، سیکنڈوں میں۔ 🕑", - "brightness_mode": "استعمال کرنے کے لئے چمک کا موڈ۔ ممکنہ قدریں 'ڈیفالٹ'، 'لکیری' اور 'تن' ہیں ('brightness_mode_time_dark' اور 'brightness_mode_time_light' کا استعمال کرتی ہیں)۔ 📈", - "brightness_mode_time_light": "(اگر 'brightness_mode='ڈیفالٹ') سورج طلوع ہونے کے بعد / اس سے پہلے / غروب آفتاب سے پہلے چمک کو بڑھانے کے لئے سیکنڈوں میں دورانیہ۔ 📈📉.", - "sunset_offset": "غروب آفتاب کے وقت کو سیکنڈوں میں مثبت یا منفی آفسیٹ کے ساتھ ایڈجسٹ کریں۔ ⏰", - "sunset_time": "غروب آفتاب کے لئے ایک مقررہ وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں۔ 🌇", - "max_sunset_time": "تازہ ترین مجازی غروب آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) سیٹ کریں ، جس سے قبل غروب آفتاب کی اجازت ملتی ہے۔ 🌇", - "sunrise_time": "طلوع آفتاب کے لئے ایک مقررہ وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں۔ 🌅", - "initial_transition": "پہلی منتقلی کا دورانیہ جب لائٹس سیکنڈوں میں 'بند' سے 'آن' میں تبدیل ہوجاتی ہیں۔ ⏲️", - "brightness_mode_time_dark": "(اگر 'brightness_mode='ڈیفالٹ') سورج طلوع ہونے سے پہلے / غروب آفتاب سے پہلے / بعد میں چمک کو بڑھانے کے لئے سیکنڈ میں دورانیہ۔ 📈📉", - "max_sunrise_time": "تازہ ترین مجازی طلوع آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) سیٹ کریں ، جس سے قبل طلوع آفتاب کی اجازت ملتی ہے۔ 🌅", - "send_split_delay": "ان روشنیوں کے لئے 'separate_turn_on_commands' کے درمیان تاخیر (ایم ایس) جو بیک وقت چمک اور رنگ کی ترتیب کی حمایت نہیں کرتی ہیں۔ ⏲️" + "sleep_brightness": "نیند کے موڈ میں روشنی کی چمک کا فیصد. 😴", + "sleep_color_temp": "کیلون میں نیند کے موڈ میں رنگ کا درجہ حرارت (جب 'sleep_rgb_or_color_temp' 'color_temp' ہوتا ہے) میں استعمال ہوتا ہے۔ 😴" }, "data": { - "detect_non_ha_changes": "detect_non_ha_changes: غیر light.turn_on ریاست کی تبدیلیوں کے لئے موافقت کا پتہ لگاتا ہے اور روکتا ہے۔ 'take_over_control' کو فعال کرنے کی ضرورت ہے۔ 🕵️ احتیاط: ⚠️ کچھ لائٹس غلط طور پر 'آن' حالت کی نشاندہی کر سکتی ہیں ، جس کے نتیجے میں لائٹس غیر متوقع طور پر آن ہوسکتی ہیں۔ اگر آپ کو اس طرح کے مسائل کا سامنا کرنا پڑتا ہے تو اس خصوصیت کو غیر فعال کریں۔", - "multi_light_intercept": "multi_light_intercept: 'light.turn_on' کالز کو روکیں اور ان کے مطابق ڈھالیں جو متعدد روشنیوں کو نشانہ بناتی ہیں۔ ➗⚠️ اس کے نتیجے میں ایک ہی 'light.turn_on' کال کو متعدد کالز میں تقسیم کیا جاسکتا ہے ، مثال کے طور پر ، جب لائٹس مختلف سوئچوں میں ہوتی ہیں۔ 'انٹرسیپٹ' کو فعال کرنے کی ضرورت ہے۔", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: شروع میں لائٹس آن کرتے وقت۔ اگر 'true' پر سیٹ کیا جاتا ہے، AL صرف اس صورت میں موافق ہوتا ہے جب رنگ یا چمک کی وضاحت کیے بغیر 'light.turn_on' کو مدعو کیا جاتا ہے۔ ❌🌈 یہ مثال کے طور پر، کسی منظر کو چالو کرتے وقت موافقت کو روکتا ہے۔ اگر 'غلط'، AL ابتدائی `سروس_ڈیٹا` میں رنگ یا چمک کی موجودگی سے قطع نظر موافقت کرتا ہے۔ 'ٹیک_اوور_کنٹرول' کو فعال کرنے کی ضرورت ہے۔ 🕵️ ", - "skip_redundant_commands": "skip_redundant_commands: موافقت کے احکامات بھیجنے سے گریز کریں جن کی ہدف کی حالت پہلے سے ہی روشنی کی معلوم حالت کے برابر ہے۔ نیٹ ورک ٹریفک کو کم سے کم کرتا ہے اور کچھ حالات میں موافقت کی ذمہ داری کو بہتر بناتا ہے۔ 📉اگر جسمانی روشنی کی حالت یں ایچ اے کی ریکارڈ شدہ حالت کے ساتھ مطابقت سے باہر ہوجاتی ہیں تو غیر فعال کریں۔", - "separate_turn_on_commands": "separate_turn_on_commands: رنگ اور چمک کے لئے الگ الگ 'light.turn_on' کا استعمال کریں، جو کچھ روشنی کی اقسام کے لئے ضروری ہے. 🔀", - "max_color_temp": "max_color_temp: کیلون میں سرد ترین رنگ کا درجہ حرارت۔ ❄️", - "prefer_rgb_color": "prefer_rgb_color: جب ممکن ہو تو روشنی کے رنگ کے درجہ حرارت پر آر جی بی رنگ ایڈجسٹمنٹ کو ترجیح دیں یا نہیں۔ 🌈", - "max_brightness": "max_brightness: زیادہ سے زیادہ چمک کا فیصد. 💡", - "intercept": "انٹرسیپٹ: 'light.turn_on' کالز کو فوری طور پر رنگ اور چمک کے مطابقت پذیری کو قابل بنانے کے لئے روکیں اور اپنائیں۔ 🏎️ ایسی روشنیوں کو غیر فعال کریں جو رنگ اور چمک کے ساتھ 'light.turn_on' کی حمایت نہیں کرتی ہیں۔", - "only_once": "only_once: لائٹس کو صرف اس وقت ڈھالیں جب وہ آن ہوں ('سچ') یا انہیں اپناتے رہیں ('جھوٹ')۔ 🔄", - "take_over_control": "take_over_control: اگر کوئی دوسرا ذریعہ 'light.turn_on' کا نام دیتا ہے تو ایڈاپٹو لائٹنگ کو غیر فعال کریں جب لائٹس آن ہیں اور اسے اپنایا جارہا ہے۔ نوٹ کریں کہ یہ ہر 'وقفے' کو 'homeassistant.update_entity' کہتا ہے! 🔒", "lights": "لائٹس: کنٹرول کی جانے والی روشنی کے entity_ids کی فہرست (خالی ہوسکتی ہے). 🌟", "min_brightness": "min_brightness: کم سے کم چمک کا فیصد. 💡", + "max_brightness": "max_brightness: زیادہ سے زیادہ چمک کا فیصد. 💡", "min_color_temp": "min_color_temp: کیلون میں گرم ترین رنگ کا درجہ حرارت. 🔥", - "transition_until_sleep": "transition_until_sleep: جب فعال کیا جاتا ہے تو ، ایڈاپٹو لائٹنگ نیند کی ترتیبات کو کم سے کم تصور کرے گی ، غروب آفتاب کے بعد ان اقدار میں منتقل ہوگی۔ 🌙", - "include_config_in_attributes": "include_config_in_attributes: 'سچ' پر سیٹ ہونے پر ہوم اسسٹنٹ میں سوئچ پر خصوصیات کے طور پر تمام اختیارات دکھائیں۔ 📝" + "max_color_temp": "max_color_temp: کیلون میں سرد ترین رنگ کا درجہ حرارت۔ ❄️" }, "title": "مطابقت پذیر روشنی کے اختیارات", - "description": "ایک مطابقت پذیر لائٹنگ جزو تشکیل دیں۔ آپشن کے نام YAML کی ترتیبات کے ساتھ مطابقت رکھتے ہیں۔ اگر آپ نے YAML میں اس اندراج کی وضاحت کی ہے تو ، یہاں کوئی آپشن ظاہر نہیں ہوگا۔ انٹرایکٹو گراف کے لئے جو پیرامیٹر کے اثرات کو ظاہر کرتے ہیں ، ملاحظہ کریں [اس ویب ایپ]({webapp_url})۔ مزید تفصیلات کے لئے ، [سرکاری دستاویزات]({docs_url}) ملاحظہ کریں۔" + "description": "ایک مطابقت پذیر لائٹنگ جزو تشکیل دیں۔ آپشن کے نام YAML کی ترتیبات کے ساتھ مطابقت رکھتے ہیں۔ اگر آپ نے YAML میں اس اندراج کی وضاحت کی ہے تو ، یہاں کوئی آپشن ظاہر نہیں ہوگا۔ انٹرایکٹو گراف کے لئے جو پیرامیٹر کے اثرات کو ظاہر کرتے ہیں ، ملاحظہ کریں [اس ویب ایپ]({webapp_url})۔ مزید تفصیلات کے لئے ، [سرکاری دستاویزات]({docs_url}) ملاحظہ کریں۔", + "sections": { + "advanced": { + "data": { + "prefer_rgb_color": "prefer_rgb_color: جب ممکن ہو تو روشنی کے رنگ کے درجہ حرارت پر آر جی بی رنگ ایڈجسٹمنٹ کو ترجیح دیں یا نہیں۔ 🌈", + "transition_until_sleep": "transition_until_sleep: جب فعال کیا جاتا ہے تو ، ایڈاپٹو لائٹنگ نیند کی ترتیبات کو کم سے کم تصور کرے گی ، غروب آفتاب کے بعد ان اقدار میں منتقل ہوگی۔ 🌙", + "take_over_control": "take_over_control: اگر کوئی دوسرا ذریعہ 'light.turn_on' کا نام دیتا ہے تو ایڈاپٹو لائٹنگ کو غیر فعال کریں جب لائٹس آن ہیں اور اسے اپنایا جارہا ہے۔ نوٹ کریں کہ یہ ہر 'وقفے' کو 'homeassistant.update_entity' کہتا ہے! 🔒", + "detect_non_ha_changes": "detect_non_ha_changes: غیر light.turn_on ریاست کی تبدیلیوں کے لئے موافقت کا پتہ لگاتا ہے اور روکتا ہے۔ 'take_over_control' کو فعال کرنے کی ضرورت ہے۔ 🕵️ احتیاط: ⚠️ کچھ لائٹس غلط طور پر 'آن' حالت کی نشاندہی کر سکتی ہیں ، جس کے نتیجے میں لائٹس غیر متوقع طور پر آن ہوسکتی ہیں۔ اگر آپ کو اس طرح کے مسائل کا سامنا کرنا پڑتا ہے تو اس خصوصیت کو غیر فعال کریں۔", + "only_once": "only_once: لائٹس کو صرف اس وقت ڈھالیں جب وہ آن ہوں ('سچ') یا انہیں اپناتے رہیں ('جھوٹ')۔ 🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: شروع میں لائٹس آن کرتے وقت۔ اگر 'true' پر سیٹ کیا جاتا ہے، AL صرف اس صورت میں موافق ہوتا ہے جب رنگ یا چمک کی وضاحت کیے بغیر 'light.turn_on' کو مدعو کیا جاتا ہے۔ ❌🌈 یہ مثال کے طور پر، کسی منظر کو چالو کرتے وقت موافقت کو روکتا ہے۔ اگر 'غلط'، AL ابتدائی `سروس_ڈیٹا` میں رنگ یا چمک کی موجودگی سے قطع نظر موافقت کرتا ہے۔ 'ٹیک_اوور_کنٹرول' کو فعال کرنے کی ضرورت ہے۔ 🕵️ ", + "separate_turn_on_commands": "separate_turn_on_commands: رنگ اور چمک کے لئے الگ الگ 'light.turn_on' کا استعمال کریں، جو کچھ روشنی کی اقسام کے لئے ضروری ہے. 🔀", + "skip_redundant_commands": "skip_redundant_commands: موافقت کے احکامات بھیجنے سے گریز کریں جن کی ہدف کی حالت پہلے سے ہی روشنی کی معلوم حالت کے برابر ہے۔ نیٹ ورک ٹریفک کو کم سے کم کرتا ہے اور کچھ حالات میں موافقت کی ذمہ داری کو بہتر بناتا ہے۔ 📉اگر جسمانی روشنی کی حالت یں ایچ اے کی ریکارڈ شدہ حالت کے ساتھ مطابقت سے باہر ہوجاتی ہیں تو غیر فعال کریں۔", + "intercept": "انٹرسیپٹ: 'light.turn_on' کالز کو فوری طور پر رنگ اور چمک کے مطابقت پذیری کو قابل بنانے کے لئے روکیں اور اپنائیں۔ 🏎️ ایسی روشنیوں کو غیر فعال کریں جو رنگ اور چمک کے ساتھ 'light.turn_on' کی حمایت نہیں کرتی ہیں۔", + "multi_light_intercept": "multi_light_intercept: 'light.turn_on' کالز کو روکیں اور ان کے مطابق ڈھالیں جو متعدد روشنیوں کو نشانہ بناتی ہیں۔ ➗⚠️ اس کے نتیجے میں ایک ہی 'light.turn_on' کال کو متعدد کالز میں تقسیم کیا جاسکتا ہے ، مثال کے طور پر ، جب لائٹس مختلف سوئچوں میں ہوتی ہیں۔ 'انٹرسیپٹ' کو فعال کرنے کی ضرورت ہے۔", + "include_config_in_attributes": "include_config_in_attributes: 'سچ' پر سیٹ ہونے پر ہوم اسسٹنٹ میں سوئچ پر خصوصیات کے طور پر تمام اختیارات دکھائیں۔ 📝" + }, + "data_description": { + "initial_transition": "پہلی منتقلی کا دورانیہ جب لائٹس سیکنڈوں میں 'بند' سے 'آن' میں تبدیل ہوجاتی ہیں۔ ⏲️", + "sleep_rgb_or_color_temp": "نیند کے موڈ میں \"rgb_color\" یا \"color_temp\" کا استعمال کریں۔ 🌙", + "sleep_rgb_color": "نیند کے موڈ میں آر جی بی رنگ (جب 'sleep_rgb_or_color_temp' \"rgb_color\" ہوتا ہے تو استعمال کیا جاتا ہے). 🌈", + "sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "sunrise_time": "طلوع آفتاب کے لئے ایک مقررہ وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں۔ 🌅", + "min_sunrise_time": "ابتدائی مجازی طلوع آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں ، جس سے بعد میں طلوع آفتاب کی اجازت ملتی ہے۔ 🌅", + "max_sunrise_time": "تازہ ترین مجازی طلوع آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) سیٹ کریں ، جس سے قبل طلوع آفتاب کی اجازت ملتی ہے۔ 🌅", + "sunrise_offset": "طلوع آفتاب کے وقت کو سیکنڈوں میں مثبت یا منفی آفسیٹ کے ساتھ ایڈجسٹ کریں۔ ⏰", + "sunset_time": "غروب آفتاب کے لئے ایک مقررہ وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں۔ 🌇", + "min_sunset_time": "سب سے پہلے مجازی غروب آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) مقرر کریں ، جس سے بعد میں غروب آفتاب کی اجازت ملتی ہے۔ 🌇", + "max_sunset_time": "تازہ ترین مجازی غروب آفتاب کا وقت (ایچ ایچ: ایم ایم: ایس ایس) سیٹ کریں ، جس سے قبل غروب آفتاب کی اجازت ملتی ہے۔ 🌇", + "sunset_offset": "غروب آفتاب کے وقت کو سیکنڈوں میں مثبت یا منفی آفسیٹ کے ساتھ ایڈجسٹ کریں۔ ⏰", + "brightness_mode": "استعمال کرنے کے لئے چمک کا موڈ۔ ممکنہ قدریں 'ڈیفالٹ'، 'لکیری' اور 'تن' ہیں ('brightness_mode_time_dark' اور 'brightness_mode_time_light' کا استعمال کرتی ہیں)۔ 📈", + "brightness_mode_time_dark": "(اگر 'brightness_mode='ڈیفالٹ') سورج طلوع ہونے سے پہلے / غروب آفتاب سے پہلے / بعد میں چمک کو بڑھانے کے لئے سیکنڈ میں دورانیہ۔ 📈📉", + "brightness_mode_time_light": "(اگر 'brightness_mode='ڈیفالٹ') سورج طلوع ہونے کے بعد / اس سے پہلے / غروب آفتاب سے پہلے چمک کو بڑھانے کے لئے سیکنڈوں میں دورانیہ۔ 📈📉.", + "autoreset_control_seconds": "کئی سیکنڈ کے بعد دستی کنٹرول کو خود بخود ری سیٹ کریں۔ غیر فعال کرنے کے لئے 0 پر سیٹ کریں۔ ⏲️", + "send_split_delay": "ان روشنیوں کے لئے 'separate_turn_on_commands' کے درمیان تاخیر (ایم ایس) جو بیک وقت چمک اور رنگ کی ترتیب کی حمایت نہیں کرتی ہیں۔ ⏲️", + "adapt_delay": "لائٹ آن ہونے اور ایڈاپٹو لائٹنگ کے درمیان انتظار کا وقت (سیکنڈ) تبدیلیاں لاگو کرتا ہے۔ جھلکنے سے بچنے میں مدد مل سکتی ہے۔ ⏲️" + } + } + } } }, "error": { diff --git a/custom_components/adaptive_lighting/translations/zh-Hans.json b/custom_components/adaptive_lighting/translations/zh-Hans.json index 1fd9d3da..1cebfdd0 100644 --- a/custom_components/adaptive_lighting/translations/zh-Hans.json +++ b/custom_components/adaptive_lighting/translations/zh-Hans.json @@ -23,65 +23,73 @@ "lights": "lights:要控制的灯光实体ID列表(可以为空)。🌟", "interval": "频率(interval)", "transition": "过渡(transition)", - "initial_transition": "初始过渡(initial_transition)", "min_brightness": "min_brightness:最小亮度百分比。💡", "max_brightness": "max_brightness:最大亮度百分比。💡", "min_color_temp": "min_color_temp:最暖的色温,以开尔文为单位。🔥", "max_color_temp": "max_color_temp:最冷的色温,以开尔文为单位。❄️", - "prefer_rgb_color": "prefer_rgb_color:在可能时是否优先使用RGB颜色调整而不是灯光色温。🌈", "sleep_brightness": "睡眠模式亮度(sleep_brightness)", - "sleep_rgb_or_color_temp": "睡眠模式RGB或色温(sleep_rgb_or_color_temp)", - "sleep_color_temp": "睡眠模式中的色温(sleep_color_temp)", - "sleep_rgb_color": "睡眠模式中的RGB颜色(sleep_rgb_color)", - "sleep_transition": "睡眠模式过渡时间(sleep_transition)", - "transition_until_sleep": "transition_until_sleep:启用时,自适应照明将将睡眠设置视为最小值,在日落后过渡到这些值。🌙", - "sunrise_time": "日出时间(sunrise_time)", - "min_sunrise_time": "最早日出时间(min_sunrise_time)", - "max_sunrise_time": "最晚日出时间(max_sunrise_time)", - "sunrise_offset": "日出时间偏移(sunrise_offset)", - "sunset_time": "日落时间(sunset_time)", - "min_sunset_time": "最早日落时间(min_sunset_time)", - "max_sunset_time": "最晚日落时间(max_sunset_time)", - "sunset_offset": "日落时间偏移(sunset_offset)", - "brightness_mode": "亮度模式(brightness_mode)", - "brightness_mode_time_dark": "变暗时间(brightness_mode_time_dark)", - "brightness_mode_time_light": "变亮时间(brightness_mode_time_light)", - "take_over_control": "take_over_control: 如果在灯光处于开启并处于适应照明的状态时,另一个来源调用`light.turn_on`,则禁用自适应照明。请注意,这会在每个`interval`调用`homeassistant.update_entity`!🔒", - "detect_non_ha_changes": "detect_non_ha_changes: 检测非`light.turn_on`的状态更改,并停止自适应照明。需要启用`take_over_control`。🕵️ 注意:⚠️ 一些灯光可能错误地显示为“开启”状态,这可能会导致灯光意外打开。如果遇到此类问题,请禁用此功能。", - "autoreset_control_seconds": "自动重置时间(autoreset_control_seconds)", - "only_once": "only_once:仅在打开时调整灯光(`true`)或始终调整灯光(`false`)。🔄", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on:当首次打开灯光时。如果设置为`true`,仅在没有指定颜色或亮度的情况下,AL才进行适应。❌🌈 例如,这可以防止在激活场景时进行适应。如果为`false`,则不考虑初始`service_data`中是否存在颜色或亮度,AL都会适应。需要启用`take_over_control`。🕵️", - "separate_turn_on_commands": "separate_turn_on_commands:为某些灯光类型需要使用单独的`light.turn_on`调用来设置颜色和亮度。🔀", - "send_split_delay": "指令发送间隔延迟(send_split_delay)", - "adapt_delay": "自适应照明延迟(adapt_delay)", - "skip_redundant_commands": "skip_redundant_commands:跳过目标状态已经等于灯光已知状态的自适应命令。在某些情况下,可以减少网络流量并提高适应响应性。📉如果物理灯光状态与HA的记录状态不同步,请禁用此功能。", - "intercept": "intercept:拦截并适应`light.turn_on`调用,以实现即时的颜色和亮度适应。🏎️ 对于不支持使用颜色和亮度进行`light.turn_on`的灯光,禁用此功能。", - "multi_light_intercept": "multi_light_intercept:拦截和适应针对多个灯光的`light.turn_on`调用。➗⚠️ 这可能会将单个`light.turn_on`调用拆分为多个调用,例如当灯光位于不同的开关中时。需要启用`intercept`。", - "include_config_in_attributes": "include_config_in_attributes:在Home Assistant中将所有选项显示为开关的属性时,设置为`true`。📝" + "sleep_color_temp": "睡眠模式中的色温(sleep_color_temp)" }, "data_description": { "interval": "调整灯光的频率,以秒为单位。🔄", "transition": "灯光变化时的过渡持续时间,以秒为单位。🕑", - "initial_transition": "灯光从“关闭”到“开启”时的第一个过渡持续时间,以秒为单位。⏲️", "sleep_brightness": "睡眠模式中的亮度百分比。😴", - "sleep_rgb_or_color_temp": "在睡眠模式中使用“rgb_color”或“color_temp”。🌙", - "sleep_color_temp": "睡眠模式中的色温(当`sleep_rgb_or_color_temp`为`color_temp`时使用),以开尔文为单位。😴", - "sleep_rgb_color": "睡眠模式中的RGB颜色(当`sleep_rgb_or_color_temp`为“rgb_color”时使用)。🌈", - "sleep_transition": "切换“睡眠模式”时的过渡持续时间,以秒为单位。😴", - "sunrise_time": "设置固定的日出时间(HH:MM:SS)。🌅", - "min_sunrise_time": "设置最早的虚拟日出时间(HH:MM:SS),允许更晚的日出。🌅", - "max_sunrise_time": "设置最晚的虚拟日出时间(HH:MM:SS),允许更早的日出。🌅", - "sunrise_offset": "以秒为单位的正负偏移调整日出时间。⏰", - "sunset_time": "设置固定的日落时间(HH:MM:SS)。🌇", - "min_sunset_time": "设置最早的虚拟日落时间(HH:MM:SS),允许更晚的日落。🌇", - "max_sunset_time": "设置最晚的虚拟日落时间(HH:MM:SS),允许更早的日落。🌇", - "sunset_offset": "以秒为单位的正负偏移调整日落时间。⏰", - "brightness_mode": "要使用的亮度模式。可能的值为`default`、`linear`和`tanh`(使用`brightness_mode_time_dark`和`brightness_mode_time_light`)。📈", - "brightness_mode_time_dark": "(如果`brightness_mode='default'`将被忽略)日出/日落之前/之后亮度逐渐增加/减少的持续时间,以秒为单位。📈📉", - "brightness_mode_time_light": "(如果`brightness_mode='default'`将被忽略)日出/日落之后/之前亮度逐渐增加/减少的持续时间,以秒为单位。📈📉。", - "autoreset_control_seconds": "在若干秒后自动重置手动控制。设置为0以禁用。⏲️", - "send_split_delay": "对于不支持同时设置亮度和颜色的灯光,`separate_turn_on_commands`之间的延迟时间(毫秒)。⏲️", - "adapt_delay": "灯光打开和自适应照明应用更改之间的等待时间(秒)。可能有助于避免闪烁。⏲️" + "sleep_color_temp": "睡眠模式中的色温(当`sleep_rgb_or_color_temp`为`color_temp`时使用),以开尔文为单位。😴" + }, + "sections": { + "advanced": { + "data": { + "initial_transition": "初始过渡(initial_transition)", + "prefer_rgb_color": "prefer_rgb_color:在可能时是否优先使用RGB颜色调整而不是灯光色温。🌈", + "sleep_rgb_or_color_temp": "睡眠模式RGB或色温(sleep_rgb_or_color_temp)", + "sleep_rgb_color": "睡眠模式中的RGB颜色(sleep_rgb_color)", + "sleep_transition": "睡眠模式过渡时间(sleep_transition)", + "transition_until_sleep": "transition_until_sleep:启用时,自适应照明将将睡眠设置视为最小值,在日落后过渡到这些值。🌙", + "sunrise_time": "日出时间(sunrise_time)", + "min_sunrise_time": "最早日出时间(min_sunrise_time)", + "max_sunrise_time": "最晚日出时间(max_sunrise_time)", + "sunrise_offset": "日出时间偏移(sunrise_offset)", + "sunset_time": "日落时间(sunset_time)", + "min_sunset_time": "最早日落时间(min_sunset_time)", + "max_sunset_time": "最晚日落时间(max_sunset_time)", + "sunset_offset": "日落时间偏移(sunset_offset)", + "brightness_mode": "亮度模式(brightness_mode)", + "brightness_mode_time_dark": "变暗时间(brightness_mode_time_dark)", + "brightness_mode_time_light": "变亮时间(brightness_mode_time_light)", + "take_over_control": "take_over_control: 如果在灯光处于开启并处于适应照明的状态时,另一个来源调用`light.turn_on`,则禁用自适应照明。请注意,这会在每个`interval`调用`homeassistant.update_entity`!🔒", + "detect_non_ha_changes": "detect_non_ha_changes: 检测非`light.turn_on`的状态更改,并停止自适应照明。需要启用`take_over_control`。🕵️ 注意:⚠️ 一些灯光可能错误地显示为“开启”状态,这可能会导致灯光意外打开。如果遇到此类问题,请禁用此功能。", + "autoreset_control_seconds": "自动重置时间(autoreset_control_seconds)", + "only_once": "only_once:仅在打开时调整灯光(`true`)或始终调整灯光(`false`)。🔄", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on:当首次打开灯光时。如果设置为`true`,仅在没有指定颜色或亮度的情况下,AL才进行适应。❌🌈 例如,这可以防止在激活场景时进行适应。如果为`false`,则不考虑初始`service_data`中是否存在颜色或亮度,AL都会适应。需要启用`take_over_control`。🕵️", + "separate_turn_on_commands": "separate_turn_on_commands:为某些灯光类型需要使用单独的`light.turn_on`调用来设置颜色和亮度。🔀", + "send_split_delay": "指令发送间隔延迟(send_split_delay)", + "adapt_delay": "自适应照明延迟(adapt_delay)", + "skip_redundant_commands": "skip_redundant_commands:跳过目标状态已经等于灯光已知状态的自适应命令。在某些情况下,可以减少网络流量并提高适应响应性。📉如果物理灯光状态与HA的记录状态不同步,请禁用此功能。", + "intercept": "intercept:拦截并适应`light.turn_on`调用,以实现即时的颜色和亮度适应。🏎️ 对于不支持使用颜色和亮度进行`light.turn_on`的灯光,禁用此功能。", + "multi_light_intercept": "multi_light_intercept:拦截和适应针对多个灯光的`light.turn_on`调用。➗⚠️ 这可能会将单个`light.turn_on`调用拆分为多个调用,例如当灯光位于不同的开关中时。需要启用`intercept`。", + "include_config_in_attributes": "include_config_in_attributes:在Home Assistant中将所有选项显示为开关的属性时,设置为`true`。📝" + }, + "data_description": { + "initial_transition": "灯光从“关闭”到“开启”时的第一个过渡持续时间,以秒为单位。⏲️", + "sleep_rgb_or_color_temp": "在睡眠模式中使用“rgb_color”或“color_temp”。🌙", + "sleep_rgb_color": "睡眠模式中的RGB颜色(当`sleep_rgb_or_color_temp`为“rgb_color”时使用)。🌈", + "sleep_transition": "切换“睡眠模式”时的过渡持续时间,以秒为单位。😴", + "sunrise_time": "设置固定的日出时间(HH:MM:SS)。🌅", + "min_sunrise_time": "设置最早的虚拟日出时间(HH:MM:SS),允许更晚的日出。🌅", + "max_sunrise_time": "设置最晚的虚拟日出时间(HH:MM:SS),允许更早的日出。🌅", + "sunrise_offset": "以秒为单位的正负偏移调整日出时间。⏰", + "sunset_time": "设置固定的日落时间(HH:MM:SS)。🌇", + "min_sunset_time": "设置最早的虚拟日落时间(HH:MM:SS),允许更晚的日落。🌇", + "max_sunset_time": "设置最晚的虚拟日落时间(HH:MM:SS),允许更早的日落。🌇", + "sunset_offset": "以秒为单位的正负偏移调整日落时间。⏰", + "brightness_mode": "要使用的亮度模式。可能的值为`default`、`linear`和`tanh`(使用`brightness_mode_time_dark`和`brightness_mode_time_light`)。📈", + "brightness_mode_time_dark": "(如果`brightness_mode='default'`将被忽略)日出/日落之前/之后亮度逐渐增加/减少的持续时间,以秒为单位。📈📉", + "brightness_mode_time_light": "(如果`brightness_mode='default'`将被忽略)日出/日落之后/之前亮度逐渐增加/减少的持续时间,以秒为单位。📈📉。", + "autoreset_control_seconds": "在若干秒后自动重置手动控制。设置为0以禁用。⏲️", + "send_split_delay": "对于不支持同时设置亮度和颜色的灯光,`separate_turn_on_commands`之间的延迟时间(毫秒)。⏲️", + "adapt_delay": "灯光打开和自适应照明应用更改之间的等待时间(秒)。可能有助于避免闪烁。⏲️" + } + } } } }, diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 8a09265e..04cbed5b 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,6 +1,17 @@ """Test Adaptive Lighting config flow.""" +import json + +import pytest +import voluptuous as vol + +try: + from probatio import to_field_list +except ImportError: + from voluptuous_serialize import convert as to_field_list from homeassistant.components.adaptive_lighting.const import ( + BASIC_OPTIONS, + CONF_INITIAL_TRANSITION, CONF_SUNRISE_TIME, CONF_SUNSET_TIME, DEFAULT_NAME, @@ -10,12 +21,34 @@ from homeassistant.components.adaptive_lighting.const import ( ) from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_NAME -from homeassistant.data_entry_flow import FlowResultType +from homeassistant.data_entry_flow import FlowResultType, section +from homeassistant.helpers import config_validation as cv from tests.common import MockConfigEntry DEFAULT_DATA = {key: default for key, default, _ in VALIDATION_TUPLES} +# Split DEFAULT_DATA into basic and advanced for section-based input +BASIC_DATA = {key: value for key, value in DEFAULT_DATA.items() if key in BASIC_OPTIONS} +ADVANCED_DATA = { + key: value for key, value in DEFAULT_DATA.items() if key not in BASIC_OPTIONS +} + + +def _schema_defaults(schema: vol.Schema) -> dict[str, object]: + """Return the defaults from a voluptuous schema.""" + return { + key.schema: key.default() if callable(key.default) else key.default + for key in schema.schema + } + + +def _advanced_section(result) -> section: + """Return the advanced options section from a flow result.""" + advanced = result["data_schema"].schema["advanced"] + assert isinstance(advanced, section) + return advanced + async def test_flow_manual_configuration(hass): """Test that config flow works.""" @@ -53,7 +86,7 @@ async def test_import_success(hass): async def test_options(hass): - """Test updating options.""" + """Test updating options with collapsible sections.""" entry = MockConfigEntry( domain=DOMAIN, title=DEFAULT_NAME, @@ -68,20 +101,79 @@ async def test_options(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" - data = DEFAULT_DATA.copy() - data[CONF_SUNRISE_TIME] = NONE_STR - data[CONF_SUNSET_TIME] = NONE_STR + # Build input with advanced options nested in "advanced" section + advanced_data = ADVANCED_DATA.copy() + advanced_data[CONF_INITIAL_TRANSITION] = 23 + advanced_data[CONF_SUNRISE_TIME] = NONE_STR + advanced_data[CONF_SUNSET_TIME] = NONE_STR + basic_data = {**BASIC_DATA, "min_brightness": 12} + user_input = { + **basic_data, + "advanced": advanced_data, + } result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input=data, + user_input=user_input, ) assert result["type"] == FlowResultType.CREATE_ENTRY - for key, value in data.items(): + + # Verify flattened data is saved correctly + expected_data = {**basic_data, **advanced_data} + for key, value in expected_data.items(): assert result["data"][key] == value + assert "advanced" not in result["data"] -async def test_incorrect_options(hass): - """Test updating incorrect options.""" + # Starting the flow again must load the saved flat options into both parts + # of the sectioned form. + result = await hass.config_entries.options.async_init(entry.entry_id) + assert _schema_defaults(result["data_schema"])["min_brightness"] == 12 + assert ( + _schema_defaults(_advanced_section(result).schema)[CONF_INITIAL_TRANSITION] + == 23 + ) + + +async def test_options_schema_has_each_setting_once(hass): + """Test that basic and advanced options partition all settings.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_NAME, + data={CONF_NAME: DEFAULT_NAME, "interval": 120, "min_brightness": 7}, + options={"min_brightness": 12}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + schema = result["data_schema"].schema + advanced = _advanced_section(result) + + assert advanced.options == {"collapsed": True} + assert {key.schema for key in schema if key.schema != "advanced"} == BASIC_OPTIONS + assert {key.schema for key in advanced.schema.schema} == set( + DEFAULT_DATA, + ) - BASIC_OPTIONS + assert _schema_defaults(result["data_schema"])["interval"] == 120 + assert _schema_defaults(result["data_schema"])["min_brightness"] == 12 + + serialized_schema = to_field_list( + result["data_schema"], + custom_serializer=cv.custom_serializer, + ) + json.dumps(serialized_schema) + serialized_advanced = next( + field for field in serialized_schema if field["name"] == "advanced" + ) + assert serialized_advanced["type"] == "expandable" + assert serialized_advanced["expanded"] is False + assert {field["name"] for field in serialized_advanced["schema"]} == set( + DEFAULT_DATA, + ) - BASIC_OPTIONS + + +@pytest.mark.parametrize("lights", [[], ["light.missing"]]) +async def test_incorrect_options(hass, lights): + """Test updating incorrect options in advanced section.""" entry = MockConfigEntry( domain=DOMAIN, title=DEFAULT_NAME, @@ -93,12 +185,30 @@ async def test_incorrect_options(hass): await hass.config_entries.async_setup(entry.entry_id) result = await hass.config_entries.options.async_init(entry.entry_id) - data = DEFAULT_DATA.copy() - data[CONF_SUNRISE_TIME] = "yolo" - data[CONF_SUNSET_TIME] = "yolo" + + # Build input with invalid advanced options nested in section + advanced_data = ADVANCED_DATA.copy() + advanced_data[CONF_SUNRISE_TIME] = "yolo" + advanced_data[CONF_SUNSET_TIME] = "yolo" + basic_data = {**BASIC_DATA, "min_brightness": 12, "lights": lights} + user_input = { + **basic_data, + "advanced": advanced_data, + } result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input=data, + user_input=user_input, + ) + # Should show form with errors + assert result["type"] == FlowResultType.FORM + expected_errors = {"base": "option_error"} + if lights: + expected_errors["lights"] = "entity_missing" + assert result["errors"] == expected_errors + assert _schema_defaults(result["data_schema"])["lights"] == lights + assert _schema_defaults(result["data_schema"])["min_brightness"] == 12 + assert ( + _schema_defaults(_advanced_section(result).schema)[CONF_SUNRISE_TIME] == "yolo" ) @@ -145,6 +255,10 @@ async def test_options_flow_for_yaml_import(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result.get("data_schema") is None + assert result["description_placeholders"] == { + "docs_url": "https://github.com/basnijholt/adaptive-lighting#readme", + "webapp_url": "https://basnijholt.github.io/adaptive-lighting", + } async def test_menu_shown_when_entries_exist(hass): From 87486f9eb30767b32b3924d5bb2d10afa77b984f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 17:18:47 +0200 Subject: [PATCH 1052/1077] Set conservative line and branch coverage floors for stable HA versions (#1566) --- .github/workflows/pytest.yaml | 9 +++++++++ tests/README.md | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 8a0ebf82..137cfaf9 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -103,6 +103,15 @@ jobs: exit 1 fi + if [[ "${CORE_VERSION}" != "dev" && "${PYTEST_OUTCOME}" == "success" ]]; then + echo "Required coverage: 89% lines and 80% branches." >> "${GITHUB_STEP_SUMMARY}" + if ! jq -e '.totals | .covered_lines * 100 >= .num_statements * 89 + and .covered_branches * 100 >= .num_branches * 80' core/coverage.json > /dev/null; then + echo "::error::Coverage must be at least 89% lines and 80% branches." + exit 1 + fi + fi + - name: Upload coverage reports if: ${{ !cancelled() }} uses: actions/upload-artifact@v7.0.1 diff --git a/tests/README.md b/tests/README.md index 761c67f7..97c609c9 100644 --- a/tests/README.md +++ b/tests/README.md @@ -5,7 +5,7 @@ Alternatively, you can use the provided Docker image to run the tests locally or ## Coverage reports -Open a `pytest` workflow run in GitHub Actions to see line and branch coverage in each job's summary. Download its `coverage--py` artifact for the XML and JSON reports and the browsable HTML report. After extracting it, open `htmlcov/index.html` to inspect missing lines and branches. +Open a `pytest` workflow run in GitHub Actions to see line and branch coverage in each job's summary. Download its `coverage--py` artifact for the XML and JSON reports and the browsable HTML report. After extracting it, open `htmlcov/index.html` to inspect missing lines and branches. Supported stable Home Assistant versions require at least 89% line coverage and 80% branch coverage. The `dev` job reports coverage without enforcing these floors. Coverage measures executed code, not whether assertions would catch a bug. Add tests for observable behavior: emitted light commands, final states, manual-control events, and timer expiry. The integration suite runs inside Home Assistant with simulated lights; it does not establish physical-device behavior. It also does not execute every documentation generator included in the package's coverage total. From 1d50165eb1bb288dbc69edee420f0006408f381a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 17:20:07 +0200 Subject: [PATCH 1053/1077] Cancel transition timers when the last profile unloads (#1567) --- custom_components/adaptive_lighting/switch.py | 5 +- tests/test_switch.py | 47 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index f5f0e938..f240a334 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1878,12 +1878,15 @@ class AdaptiveLightingManager: ) def disable(self) -> None: - """Disable listeners and pending automatic manual-control resets.""" + """Disable listeners and pending manual-reset and transition timers.""" for remove in self.listener_removers: remove() for timer in self.auto_reset_manual_control_timers.values(): timer.cancel() self.auto_reset_manual_control_timers.clear() + for timer in self.transition_timers.values(): + timer.cancel() + self.transition_timers.clear() def set_proactively_adapting(self, context_id: str, entity_id: str) -> None: """Declare the adaptation with context_id as proactively adapting, diff --git a/tests/test_switch.py b/tests/test_switch.py index 8b354c71..03f0066c 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -2384,6 +2384,53 @@ async def test_unload_switch(hass): assert not timer.is_running() +async def test_unload_cancels_pending_light_transition(hass): + """A real transition must not keep its timer alive after last profile unload.""" + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_LIGHTS: [ENTITY_LIGHT_3], + CONF_INITIAL_TRANSITION: 60, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + calls = [] + remove_listener = hass.bus.async_listen(EVENT_CALL_SERVICE, calls.append) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_3}, + blocking=True, + ) + await hass.async_block_till_done() + transition_calls = [ + event.data["service_data"] + for event in calls + if event.data["domain"] == LIGHT_DOMAIN + and event.data["service"] == SERVICE_TURN_ON + and ATTR_TRANSITION in event.data["service_data"] + ] + assert len(transition_calls) == 1 + assert transition_calls[0][ATTR_TRANSITION] == 60 + timer = switch.manager.transition_timers[ENTITY_LIGHT_3] + assert timer.is_running() + entry = hass.config_entries.async_entries(DOMAIN)[0] + try: + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert DOMAIN not in hass.data + assert not timer.is_running() + assert not switch.manager.transition_timers + state = hass.states.get(ENTITY_LIGHT_3) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + finally: + remove_listener() + timer.cancel() + await asyncio.gather(timer.task, return_exceptions=True) + + @pytest.mark.parametrize("state", [STATE_ON, STATE_OFF, None]) async def test_restore_off_state(hass, state): """Test that the 'off' and 'on' states are propoperly restored.""" From cda1c2db3392f353befa219588a8eaa4c42bf748 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 19:09:26 +0200 Subject: [PATCH 1054/1077] Track manual changes across shared light profiles (#1569) --- custom_components/adaptive_lighting/switch.py | 30 ++- tests/test_switch.py | 220 ++++++++++++++++++ 2 files changed, 234 insertions(+), 16 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index f240a334..0a340c6c 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2612,22 +2612,20 @@ class AdaptiveLightingManager: # Fix for https://github.com/basnijholt/adaptive-lighting/issues/1378 state = self.hass.states.get(eid) if state is not None and state.state == STATE_ON: - try: - switch = _switch_with_lights( - self.hass, - [eid], - expand_light_groups=False, - ) - await self.update_manually_controlled_from_event( - switch, - eid, - force=False, - ) - except NoSwitchFoundError: - _LOGGER.debug( - "No switch found for entity_id='%s' in 'on' event listener", - eid, - ) + switches = _switches_with_lights( + self.hass, + [eid], + expand_light_groups=False, + ) + for switch in switches: + # Preserve tracking for a lone profile, including when off. + # Shared lights notify each enabled owner using its takeover policy. + if switch.is_on or len(switches) == 1: + await self.update_manually_controlled_from_event( + switch, + eid, + force=False, + ) timer = self.auto_reset_manual_control_timers.get(eid) if ( diff --git a/tests/test_switch.py b/tests/test_switch.py index 03f0066c..733b1ef3 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -40,6 +40,7 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_INITIAL_TRANSITION, CONF_MANUAL_CONTROL, CONF_MAX_BRIGHTNESS, + CONF_MAX_COLOR_TEMP, CONF_MIN_BRIGHTNESS, CONF_MIN_COLOR_TEMP, CONF_MULTI_LIGHT_INTERCEPT, @@ -1760,6 +1761,154 @@ async def test_manual_control_state_updates_shared_switches(hass): assert attrs["manual_control_color"] == [] +@pytest.mark.parametrize("intercept", [False, True]) +@pytest.mark.parametrize( + ( + "brightness_takeover", + "color_takeover", + "color_enabled", + "color_mode", + "expected_brightness", + "expected_color_calls", + "event_profiles", + ), + [ + ( + True, + True, + True, + TakeOverControlMode.PAUSE_CHANGED, + 77, + 1, + ["brightness", "color"], + ), + ( + True, + True, + True, + TakeOverControlMode.PAUSE_ALL, + 77, + 0, + ["brightness", "color"], + ), + (False, True, True, TakeOverControlMode.PAUSE_CHANGED, 77, 1, ["color"]), + (True, False, True, TakeOverControlMode.PAUSE_CHANGED, 77, 1, ["brightness"]), + (False, False, True, TakeOverControlMode.PAUSE_CHANGED, 128, 1, []), + (False, True, False, TakeOverControlMode.PAUSE_CHANGED, 128, 0, []), + (True, True, False, TakeOverControlMode.PAUSE_CHANGED, 77, 0, ["brightness"]), + ], +) +async def test_shared_profiles_track_manual_brightness( + hass, + intercept, + brightness_takeover, + color_takeover, + color_enabled, + color_mode, + expected_brightness, + expected_color_calls, + event_profiles, +): + """Shared owners track manual service calls and apply each profile's pause mode.""" + await setup_lights(hass) + profiles = {} + for name, takeover, mode in ( + ("brightness", brightness_takeover, TakeOverControlMode.PAUSE_CHANGED), + ("color", color_takeover, color_mode), + ): + _, switch = await setup_switch( + hass, + { + CONF_NAME: name, + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_INTERCEPT: intercept, + CONF_TAKE_OVER_CONTROL: takeover, + CONF_TAKE_OVER_CONTROL_MODE: mode, + CONF_DETECT_NON_HA_CHANGES: False, + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + CONF_MIN_COLOR_TEMP: 4000, + CONF_MAX_COLOR_TEMP: 4000, + }, + ) + other_axis = ( + switch.adapt_color_switch + if name == "brightness" + else switch.adapt_brightness_switch + ) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: other_axis.entity_id}, + blocking=True, + ) + profiles[name] = switch + if not color_enabled: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: profiles["color"].entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + events = [] + remove_events = hass.bus.async_listen(f"{DOMAIN}.manual_control", events.append) + context = Context() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_BRIGHTNESS: 77}, + context=context, + blocking=True, + ) + await hass.async_block_till_done() + calls = [] + remove_calls = hass.bus.async_listen(EVENT_CALL_SERVICE, calls.append) + for switch in profiles.values(): + if switch.is_on: + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + remove_calls() + remove_events() + + light_calls = [ + event.data["service_data"] + for event in calls + if event.data["domain"] == LIGHT_DOMAIN + and event.data["service"] == SERVICE_TURN_ON + ] + assert ( + sum(ATTR_COLOR_TEMP_KELVIN in call for call in light_calls) + == expected_color_calls + ) + assert sum(ATTR_BRIGHTNESS in call for call in light_calls) == ( + expected_brightness == 128 + ) + assert ( + hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] + == expected_brightness + ) + assert [event.data[SWITCH_DOMAIN] for event in events] == [ + profiles[name].entity_id for name in event_profiles + ] + assert all(event.context == context for event in events) + assert all( + event.data[CONF_MANUAL_CONTROL] == LightControlAttributes.BRIGHTNESS + for event in events + ) + for switch in profiles.values(): + if not switch.is_on: + continue + attrs = hass.states.get(switch.entity_id).attributes + assert attrs["manual_control_brightness"] == ( + [ENTITY_LIGHT_1] if event_profiles else [] + ) + assert attrs["manual_control_color"] == [] + + async def test_manual_control_state_ignores_incomplete_entries(hass): """An entry awaiting platform setup must not break another profile's updates.""" switch, (light, *_) = await setup_lights_and_switch(hass) @@ -4969,3 +5118,74 @@ async def test_forced_split_apply_stays_off(hass, off_action, cleanup): assert ATTR_BRIGHTNESS in turn_on_events[0].data["service_data"] assert ATTR_COLOR_TEMP_KELVIN not in turn_on_events[0].data["service_data"] assert hass.states.get(ENTITY_LIGHT_3).state == STATE_OFF + + +@pytest.mark.parametrize("intercept", [False, True]) +async def test_shared_profiles_keep_independent_sun_schedules( + hass, + intercept, + reset_time_zone, +): + """A later color sunrise keeps running after manual brightness takeover.""" + await hass.config.async_set_time_zone("UTC") + await setup_lights(hass) + profiles = [] + for name, sunrise, sunset in (("brightness", 6, 18), ("color", 10, 22)): + _, switch = await setup_switch( + hass, + { + CONF_NAME: name, + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_INTERCEPT: intercept, + CONF_TAKE_OVER_CONTROL_MODE: TakeOverControlMode.PAUSE_CHANGED, + CONF_DETECT_NON_HA_CHANGES: False, + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_SUNRISE_TIME: datetime.time(sunrise), + CONF_SUNSET_TIME: datetime.time(sunset), + CONF_MIN_BRIGHTNESS: 10, + CONF_MAX_BRIGHTNESS: 90, + CONF_MIN_COLOR_TEMP: 2000, + CONF_MAX_COLOR_TEMP: 6000, + }, + ) + other_axis = ( + switch.adapt_color_switch + if name == "brightness" + else switch.adapt_brightness_switch + ) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: other_axis.entity_id}, + blocking=True, + ) + profiles.append(switch) + with patch( + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", + return_value=datetime.datetime.fromisoformat("2026-09-05T08:00:00+00:00"), + ): + for switch in profiles: + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + morning = hass.states.get(ENTITY_LIGHT_1) + assert morning.attributes[ATTR_BRIGHTNESS] > 26 + assert morning.attributes[ATTR_COLOR_TEMP_KELVIN] == 2000 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_BRIGHTNESS: 77}, + blocking=True, + ) + await hass.async_block_till_done() + with patch( + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", + return_value=datetime.datetime.fromisoformat("2026-09-05T12:00:00+00:00"), + ): + for switch in profiles: + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + noon = hass.states.get(ENTITY_LIGHT_1) + assert noon.attributes[ATTR_BRIGHTNESS] == 77 + assert noon.attributes[ATTR_COLOR_TEMP_KELVIN] > 2000 From ba2b7a3e442ac457dfa7844dc888ec14bee2b32d Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:09:30 +0200 Subject: [PATCH 1055/1077] docs: add jaynis as a contributor for code (#1571) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 161b5633..6a23f772 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1523,6 +1523,15 @@ "contributions": [ "bug" ] + }, + { + "login": "jaynis", + "name": "jaynis", + "avatar_url": "https://avatars.githubusercontent.com/u/1553675?v=4", + "profile": "https://github.com/jaynis", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 3afc9a0d..281fbe1c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-167-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-168-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -926,6 +926,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Wosten
Wosten

🐛 Zachary Priddy
Zachary Priddy

🤔 Andrew Blakeslee Moore
Andrew Blakeslee Moore

🐛 + jaynis
jaynis

💻 From 3231a1ac279d4800925ef734689e599459026dae Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 19:11:55 +0200 Subject: [PATCH 1056/1077] Add on-demand Home Assistant diagnostics (#1575) * Add privacy-safe config entry diagnostics * Clarify accumulated diagnostics values --- README.md | 10 + .../adaptive_lighting/diagnostics.py | 131 ++++++ docs/troubleshooting.md | 10 + tests/test_diagnostics.py | 372 ++++++++++++++++++ 4 files changed, 523 insertions(+) create mode 100644 custom_components/adaptive_lighting/diagnostics.py create mode 100644 tests/test_diagnostics.py diff --git a/README.md b/README.md index 281fbe1c..606341a6 100644 --- a/README.md +++ b/README.md @@ -583,6 +583,16 @@ logger: ``` After the issue occurs, create a new issue report with the log (`/config/home-assistant.log`). + +For support, use Home Assistant's **Download diagnostics** action on the +Adaptive Lighting config entry. The download is an on-demand snapshot of the +profile's current switches and currently tracked light targets. It does not +refresh group membership or predict targets a disabled profile would use after +being enabled. It does not create live sensors; existing switch attributes +remain the interface for automations. +The reported last adaptation values are the shared manager's latest retained +value for each attribute. They can come from different commands and do not +represent one sent command or the current desired state. diff --git a/custom_components/adaptive_lighting/diagnostics.py b/custom_components/adaptive_lighting/diagnostics.py new file mode 100644 index 00000000..80ae56d0 --- /dev/null +++ b/custom_components/adaptive_lighting/diagnostics.py @@ -0,0 +1,131 @@ +"""Diagnostics support for Adaptive Lighting.""" + +from typing import Any + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP_KELVIN, + ATTR_RGB_COLOR, + ATTR_TRANSITION, +) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from .adaptation_utils import LightControlAttributes +from .const import ( + ADAPT_BRIGHTNESS_SWITCH, + ADAPT_COLOR_SWITCH, + ATTR_ADAPTIVE_LIGHTING_MANAGER, + DOMAIN, + SLEEP_MODE_SWITCH, +) +from .switch import AdaptiveLightingManager, AdaptiveSwitch + +_REPORTABLE_LIGHT_STATES = { + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +} +_TARGET_ATTRIBUTES = ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP_KELVIN, + ATTR_RGB_COLOR, + ATTR_TRANSITION, +) + + +def _last_adaptation_values( + manager: AdaptiveLightingManager, + light: str, +) -> dict[str, Any] | None: + """Return latest retained value for each allowlisted adaptation attribute. + + Values may come from different commands because the manager merges partial + service data per attribute. + """ + service_data = manager.last_service_data.get(light) + if service_data is None: + return None + target = { + attribute: ( + list(service_data[attribute]) + if attribute == ATTR_RGB_COLOR + else service_data[attribute] + ) + for attribute in _TARGET_ATTRIBUTES + if attribute in service_data + } + return target or None + + +def _autoreset_seconds( + manager: AdaptiveLightingManager, + light: str, +) -> float | None: + """Return remaining time for a running global manual-control reset.""" + timer = manager.auto_reset_manual_control_timers.get(light) + if timer is None or not timer.is_running(): + return None + remaining = timer.remaining_time() + return round(remaining, 3) if remaining > 0 else None + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, + config_entry: ConfigEntry, +) -> dict[str, Any]: + """Return an allowlisted, on-demand snapshot for one config entry.""" + domain_data = hass.data.get(DOMAIN) + if not isinstance(domain_data, dict): + return {"loaded": False} + entry_data = domain_data.get(config_entry.entry_id) + manager = domain_data.get(ATTR_ADAPTIVE_LIGHTING_MANAGER) + if not isinstance(entry_data, dict) or not isinstance( + manager, + AdaptiveLightingManager, + ): + return {"loaded": False} + switch = entry_data.get(SWITCH_DOMAIN) + if not isinstance(switch, AdaptiveSwitch): + return {"loaded": False} + + lights: dict[str, Any] = {} + for index, light in enumerate(sorted(switch.lights), start=1): + state = hass.states.get(light) + state_value = "missing" + if state is not None: + state_value = ( + state.state + if state.state in _REPORTABLE_LIGHT_STATES + else STATE_UNKNOWN + ) + manual_control = manager.get_manual_control_attributes(light) + lights[f"light_{index}"] = { + "state": state_value, + "global_manager_manual_control": { + "brightness": bool( + manual_control & LightControlAttributes.BRIGHTNESS, + ), + "color": bool(manual_control & LightControlAttributes.COLOR), + }, + "global_manager_autoreset_seconds": _autoreset_seconds(manager, light), + "global_manager_last_adaptation_values": _last_adaptation_values( + manager, + light, + ), + } + + return { + "loaded": True, + "profile_switches": { + "profile": switch.is_on, + "adapt_brightness": entry_data[ADAPT_BRIGHTNESS_SWITCH].is_on, + "adapt_color": entry_data[ADAPT_COLOR_SWITCH].is_on, + "sleep_mode": entry_data[SLEEP_MODE_SWITCH].is_on, + }, + "manager_fact_scope": "global_shared_across_profiles", + "lights": lights, + } diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 170f5ddf..c5f59353 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -25,6 +25,16 @@ logger: After the issue occurs, create a new issue report with the log (`/config/home-assistant.log`). +For support, use Home Assistant's **Download diagnostics** action on the +Adaptive Lighting config entry. The download is an on-demand snapshot of the +profile's current switches and currently tracked light targets. It does not +refresh group membership or predict targets a disabled profile would use after +being enabled. It does not create live sensors; existing switch attributes +remain the interface for automations. +The reported last adaptation values are the shared manager's latest retained +value for each attribute. They can come from different commands and do not +represent one sent command or the current desired state. + ## Common Problems & Solutions diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py new file mode 100644 index 00000000..9c96b295 --- /dev/null +++ b/tests/test_diagnostics.py @@ -0,0 +1,372 @@ +"""Tests for Adaptive Lighting diagnostics.""" + +import json +from copy import deepcopy +from unittest.mock import patch + +import pytest +from homeassistant.components.adaptive_lighting.adaptation_utils import ( + AdaptationData, + LightControlAttributes, + _create_service_call_data_iterator, +) +from homeassistant.components.adaptive_lighting.const import ( + ATTR_ADAPTIVE_LIGHTING_MANAGER, + CONF_AUTORESET_CONTROL, + CONF_INTERCEPT, + CONF_MANUAL_CONTROL, + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, +) +from homeassistant.components.adaptive_lighting.diagnostics import ( + async_get_config_entry_diagnostics, +) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP_KELVIN, + ATTR_RGB_COLOR, + ATTR_TRANSITION, + SERVICE_TURN_ON, +) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SERVICE_DATA, + CONF_LIGHTS, + CONF_NAME, + EVENT_CALL_SERVICE, + EVENT_STATE_CHANGED, + STATE_OFF, + STATE_UNAVAILABLE, +) +from homeassistant.core import State + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + +from .test_switch import ( + ENTITY_LIGHT_1, + ENTITY_LIGHT_2, + ENTITY_LIGHT_3, + setup_lights, +) + + +@pytest.fixture +async def cleanup_diagnostics(hass): + """Cancel integration tasks created by diagnostics fixtures.""" + yield + manager = hass.data.get(DOMAIN, {}).get(ATTR_ADAPTIVE_LIGHTING_MANAGER) + if manager is None: + return + for timer in manager.auto_reset_manual_control_timers.values(): + timer.cancel() + for timer in manager.transition_timers.values(): + timer.cancel() + for task in manager.adaptation_tasks: + task.cancel() + + +async def _setup_entry(hass, name, lights, **data): + """Set up a real Adaptive Lighting config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_NAME: name, + CONF_LIGHTS: lights, + CONF_INTERCEPT: False, + **data, + }, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.LOADED + return entry, hass.data[DOMAIN][entry.entry_id][SWITCH_DOMAIN] + + +async def test_config_entry_diagnostics_reports_allowlisted_current_facts( + hass, + hass_client, + cleanup_diagnostics, +): + """Diagnostics report current selected-profile facts without identifiers.""" + await setup_lights(hass) + entry, switch = await _setup_entry( + hass, + "Private Upstairs Profile", + [ENTITY_LIGHT_1, ENTITY_LIGHT_2], + **{CONF_AUTORESET_CONTROL: 60}, + ) + other_entry, _ = await _setup_entry( + hass, + "Private Basement Profile", + [ENTITY_LIGHT_3], + ) + + await switch.adapt_color_switch.async_turn_off() + await switch.sleep_mode_switch.async_turn_on() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_MANUAL_CONTROL: "brightness", + }, + blocking=True, + ) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [ENTITY_LIGHT_2], + CONF_MANUAL_CONTROL: "color", + }, + blocking=True, + ) + hass.states.async_set( + ENTITY_LIGHT_2, + STATE_UNAVAILABLE, + {"friendly_name": "Private Bedside Lamp", "room": "Private Bedroom"}, + ) + await hass.async_block_till_done() + + manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER] + assert manager.get_manual_control_attributes(ENTITY_LIGHT_1) == ( + LightControlAttributes.BRIGHTNESS + ) + manager.last_service_data[ENTITY_LIGHT_1] = { + ATTR_ENTITY_ID: ENTITY_LIGHT_1, + ATTR_BRIGHTNESS: 123, + ATTR_COLOR_TEMP_KELVIN: 3456, + ATTR_RGB_COLOR: (12, 34, 56), + ATTR_TRANSITION: 4.5, + "context_id": "private-context-id", + "friendly_name": "Private Bedside Lamp", + } + manager.last_service_data.pop(ENTITY_LIGHT_2, None) + + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) + + assert result["loaded"] is True + assert result["profile_switches"] == { + "profile": True, + "adapt_brightness": True, + "adapt_color": False, + "sleep_mode": True, + } + assert result["manager_fact_scope"] == "global_shared_across_profiles" + assert list(result["lights"]) == ["light_1", "light_2"] + assert result["lights"]["light_1"]["state"] == "on" + assert result["lights"]["light_1"]["global_manager_manual_control"] == { + "brightness": True, + "color": False, + } + assert result["lights"]["light_1"][ + "global_manager_autoreset_seconds" + ] == pytest.approx(60, abs=2) + assert result["lights"]["light_1"]["global_manager_last_adaptation_values"] == { + ATTR_BRIGHTNESS: 123, + ATTR_COLOR_TEMP_KELVIN: 3456, + ATTR_RGB_COLOR: [12, 34, 56], + ATTR_TRANSITION: 4.5, + } + assert result["lights"]["light_2"] == { + "state": STATE_UNAVAILABLE, + "global_manager_manual_control": { + "brightness": False, + "color": True, + }, + "global_manager_autoreset_seconds": pytest.approx(60, abs=2), + "global_manager_last_adaptation_values": None, + } + + serialized = json.dumps(result, sort_keys=True) + for sensitive_value in ( + entry.entry_id, + other_entry.entry_id, + ENTITY_LIGHT_1, + ENTITY_LIGHT_2, + ENTITY_LIGHT_3, + "Private Upstairs Profile", + "Private Basement Profile", + "Private Bedside Lamp", + "Private Bedroom", + "private-context-id", + ): + assert sensitive_value not in serialized + + +async def test_diagnostics_labels_accumulated_partial_adaptation_values( + hass, + cleanup_diagnostics, +): + """Diagnostics do not describe merged per-attribute history as one command.""" + await setup_lights(hass) + entry, switch = await _setup_entry( + hass, + "Private Profile", + [ENTITY_LIGHT_1], + ) + commands = [ + { + ATTR_ENTITY_ID: ENTITY_LIGHT_1, + ATTR_BRIGHTNESS: 100, + ATTR_RGB_COLOR: (12, 34, 56), + ATTR_TRANSITION: 2, + }, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_BRIGHTNESS: 180}, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_COLOR_TEMP_KELVIN: 3500}, + ] + call_events = [] + remove_listener = hass.bus.async_listen(EVENT_CALL_SERVICE, call_events.append) + + await switch._execute_adaptation_calls( + AdaptationData( + entity_id=ENTITY_LIGHT_1, + context=switch.create_context("diagnostics_test"), + sleep_time=0, + service_call_datas=_create_service_call_data_iterator( + hass, + commands, + filter_by_state=False, + ), + force=True, + max_length=len(commands), + attributes=LightControlAttributes.ALL, + ), + ) + await hass.async_block_till_done() + remove_listener() + + actual_commands = [ + event.data[ATTR_SERVICE_DATA] + for event in call_events + if event.data["domain"] == LIGHT_DOMAIN + and event.data["service"] == SERVICE_TURN_ON + ] + assert actual_commands == commands + + result = await async_get_config_entry_diagnostics(hass, entry) + light = result["lights"]["light_1"] + assert "global_manager_last_sent_target" not in light + assert light["global_manager_last_adaptation_values"] == { + ATTR_BRIGHTNESS: 180, + ATTR_COLOR_TEMP_KELVIN: 3500, + ATTR_RGB_COLOR: [12, 34, 56], + ATTR_TRANSITION: 2, + } + + +async def test_diagnostics_preserves_restored_off_profile_tracked_group(hass): + """Diagnostics report tracked targets without refreshing late groups.""" + await setup_lights(hass) + group = "light.private_late_group" + members = [ENTITY_LIGHT_1, ENTITY_LIGHT_2] + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=State("switch.restored", STATE_OFF), + ): + entry, switch = await _setup_entry( + hass, + "Private Restored Profile", + [group], + ) + assert not switch.is_on + assert switch.lights == [group] + + hass.states.async_set( + group, + STATE_UNAVAILABLE, + {ATTR_ENTITY_ID: members, "friendly_name": "Private Late Group"}, + ) + await hass.async_block_till_done() + manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER] + manager_lights_before = set(manager.lights) + reset_times_before = dict(manager.auto_reset_manual_control_times) + + result = await async_get_config_entry_diagnostics(hass, entry) + + assert result["lights"] == { + "light_1": { + "state": STATE_UNAVAILABLE, + "global_manager_manual_control": { + "brightness": False, + "color": False, + }, + "global_manager_autoreset_seconds": None, + "global_manager_last_adaptation_values": None, + }, + } + assert switch.lights == [group] + assert manager.lights == manager_lights_before + assert manager.auto_reset_manual_control_times == reset_times_before + assert group not in json.dumps(result) + + +async def test_diagnostics_handles_missing_states_and_unload_without_side_effects( + hass, +): + """Diagnostics normalize states and never change live integration state.""" + await setup_lights(hass) + entry, switch = await _setup_entry( + hass, + "Private Profile", + [ENTITY_LIGHT_1, ENTITY_LIGHT_2, ENTITY_LIGHT_3, "light.private_missing"], + ) + hass.states.async_set(ENTITY_LIGHT_2, STATE_OFF) + hass.states.async_set(ENTITY_LIGHT_3, STATE_UNAVAILABLE) + await hass.async_block_till_done() + + manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER] + manual_control_before = dict(manager.manual_control) + last_service_data_before = deepcopy(manager.last_service_data) + timers_before = dict(manager.auto_reset_manual_control_timers) + switch_states_before = ( + switch.is_on, + switch.adapt_brightness_switch.is_on, + switch.adapt_color_switch.is_on, + switch.sleep_mode_switch.is_on, + ) + service_events = [] + state_events = [] + remove_service_listener = hass.bus.async_listen( + EVENT_CALL_SERVICE, + service_events.append, + ) + remove_state_listener = hass.bus.async_listen( + EVENT_STATE_CHANGED, + state_events.append, + ) + + result = await async_get_config_entry_diagnostics(hass, entry) + await hass.async_block_till_done() + remove_service_listener() + remove_state_listener() + + assert [light["state"] for light in result["lights"].values()] == [ + "on", + STATE_OFF, + STATE_UNAVAILABLE, + "missing", + ] + assert json.dumps(result) + assert not service_events + assert not state_events + assert manager.manual_control == manual_control_before + assert manager.last_service_data == last_service_data_before + assert manager.auto_reset_manual_control_timers == timers_before + assert ( + switch.is_on, + switch.adapt_brightness_switch.is_on, + switch.adapt_color_switch.is_on, + switch.sleep_mode_switch.is_on, + ) == switch_states_before + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert await async_get_config_entry_diagnostics(hass, entry) == {"loaded": False} From 46d07388ba977e28b16a0201ed953aba43bad4d7 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:12:41 +0200 Subject: [PATCH 1057/1077] docs: add alistairg as a contributor for code (#1572) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .all-contributorsrc | 9 +++++++++ README.md | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 6a23f772..b346d2fd 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1532,6 +1532,15 @@ "contributions": [ "code" ] + }, + { + "login": "alistairg", + "name": "Alistair Galbraith", + "avatar_url": "https://avatars.githubusercontent.com/u/272786?v=4", + "profile": "https://github.com/alistairg", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 606341a6..12cc151a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-168-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-169-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -938,6 +938,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Andrew Blakeslee Moore
Andrew Blakeslee Moore

🐛 jaynis
jaynis

💻 + + Alistair Galbraith
Alistair Galbraith

💻 + From d09b15a138abb024e4ea198d401e70e1860cef25 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:13:13 +0200 Subject: [PATCH 1058/1077] docs: add hesseleo as a contributor for code (#1573) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index b346d2fd..260b8410 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1541,6 +1541,15 @@ "contributions": [ "code" ] + }, + { + "login": "hesseleo", + "name": "Leonhard Hesse", + "avatar_url": "https://avatars.githubusercontent.com/u/44778508?v=4", + "profile": "https://github.com/hesseleo", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 12cc151a..e56098eb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-169-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-170-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -940,6 +940,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Alistair Galbraith
Alistair Galbraith

💻 + Leonhard Hesse
Leonhard Hesse

💻 From 51f2878b1668b5adf9a35d13acfdc317dbcfce01 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:13:44 +0200 Subject: [PATCH 1059/1077] docs: add timstallmann as a contributor for code (#1574) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 260b8410..4099e0bc 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1550,6 +1550,15 @@ "contributions": [ "code" ] + }, + { + "login": "timstallmann", + "name": "Tim Stallmann", + "avatar_url": "https://avatars.githubusercontent.com/u/6741938?v=4", + "profile": "http://www.tim-maps.com", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index e56098eb..13785a60 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-170-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-171-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -941,6 +941,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Alistair Galbraith
Alistair Galbraith

💻 Leonhard Hesse
Leonhard Hesse

💻 + Tim Stallmann
Tim Stallmann

💻 From e2a3aae41666e52f6024386bde51e0df288a2124 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:14:21 +0200 Subject: [PATCH 1060/1077] docs: add lehneres as a contributor for ideas (#1576) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 4099e0bc..6004155e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1559,6 +1559,15 @@ "contributions": [ "code" ] + }, + { + "login": "lehneres", + "name": "lehneres", + "avatar_url": "https://avatars.githubusercontent.com/u/7437288?v=4", + "profile": "https://github.com/lehneres", + "contributions": [ + "ideas" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 13785a60..4b90347a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-171-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-172-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -942,6 +942,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Alistair Galbraith
Alistair Galbraith

💻 Leonhard Hesse
Leonhard Hesse

💻 Tim Stallmann
Tim Stallmann

💻 + lehneres
lehneres

🤔 From f7b50b12d94be7c921e42f40fe03a67b7a264ecf Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 19:59:56 +0200 Subject: [PATCH 1061/1077] Add validated blueprints for common automation examples (#1577) * Add minimum brightness automation and blueprint * Ignore independent profile event order in test * Add tested blueprints for sleep, schedules, and daylight --- README.md | 67 ++++ blueprints/automation/daylight_limit.yaml | 112 ++++++ blueprints/automation/schedule_profile.yaml | 57 +++ blueprints/automation/sleep_mode.yaml | 41 +++ .../automation/turn_off_at_minimum.yaml | 87 +++++ docs/automation-examples.md | 67 ++++ tests/test_automation_examples.py | 346 +++++++++++++++++- tests/test_switch.py | 5 +- 8 files changed, 772 insertions(+), 10 deletions(-) create mode 100644 blueprints/automation/daylight_limit.yaml create mode 100644 blueprints/automation/schedule_profile.yaml create mode 100644 blueprints/automation/sleep_mode.yaml create mode 100644 blueprints/automation/turn_off_at_minimum.yaml diff --git a/README.md b/README.md index 4b90347a..af016c54 100644 --- a/README.md +++ b/README.md @@ -272,6 +272,17 @@ Replace every entity ID below with the IDs from your Home Assistant instance. Fr Blocks that begin with `- alias` are entries for `automations.yaml`. Blocks with a top-level `script:` or `adaptive_lighting:` key are complete `configuration.yaml` examples. If your configuration uses `script: !include scripts.yaml`, omit that outer key and place its contents in `scripts.yaml`. +Four examples also have blueprints with selectors, so you can configure them without editing YAML: + +| Blueprint | Purpose | +| --- | --- | +| [Sleep mode](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml) | Synchronize several profiles with one sleep-mode helper. | +| [Minimum brightness](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/turn_off_at_minimum.yaml) | Turn one light off when its target crosses down to the minimum. | +| [Schedule profile](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml) | Apply brightness and color temperature from Schedule helper blocks. | +| [Daylight limit](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml) | Lower maximum brightness in strong daylight. | + +Copy a blueprint's link into **Settings → Automations & scenes → Blueprints → Import Blueprint**, then create an automation from it. Read the matching example below for setup and behavior. Each blueprint is tested through Home Assistant alongside its YAML example. The built-in manual-control timeout needs no automation; the scripts below remain useful as actions in your own automations. + `change_switch_settings` updates a profile while its main switch is off, but lights are adapted only while that switch is on. It preserves manual-control flags, so manually controlled lights remain paused.
@@ -294,6 +305,8 @@ This is a top-level `configuration.yaml` example. The timer clears manual contro
Toggle multiple Adaptive Lighting switches to "sleep mode" using an input_boolean.sleep_mode. +Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml). Select an input boolean and the sleep-mode switches it should control. + ```yaml - alias: "Adaptive lighting: toggle 'sleep mode'" trigger: @@ -316,6 +329,56 @@ This is a top-level `configuration.yaml` example. The timer clears manual contro
+
+Turn a light off when its adaptive brightness target reaches the minimum. + +Prefer a form over editing YAML? Import the [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/turn_off_at_minimum.yaml) in Home Assistant under **Settings → Automations & scenes → Blueprints → Import Blueprint**. Select your profile, its matching adapt brightness switch, one light managed by that profile, and its minimum brightness percentage. Create one automation per light. If you change the profile's minimum later, update the automation too. The blueprint and YAML example below have the same behavior. + +The Adaptive Lighting switch already exposes its calculated `brightness_pct` target. Use its state changes to choose a power policy in an automation; no custom event is needed. This example assumes `min_brightness: 1`. Change `minimum_pct` to match your profile, and replace the switch and light entity IDs with your own. + +The comparison uses the same rounded 0–255 brightness as an adaptation command. Comparing floating-point percentages for exact equality can miss the minimum between updates. This detects the calculated target reaching its minimum command, not the bulb finishing a transition or reaching its physical dimming limit. + +```yaml +- alias: "Adaptive lighting: turn off at minimum brightness" + mode: single + triggers: + - trigger: state + entity_id: switch.adaptive_lighting_living_room + attribute: brightness_pct + conditions: + - condition: state + entity_id: + - switch.adaptive_lighting_living_room + - switch.adaptive_lighting_living_room_adapt_brightness + state: "on" + - condition: template + value_template: >- + {% set minimum_pct = 1 %} + {% set minimum = (minimum_pct * 255 / 100) | round(0) %} + {% set before = trigger.from_state.attributes.get('brightness_pct') + if trigger.from_state else none %} + {% set after = trigger.to_state.attributes.get('brightness_pct') + if trigger.to_state else none %} + {{ is_number(before) and is_number(after) + and (before | float * 255 / 100) | round(0) > minimum + and (after | float * 255 / 100) | round(0) <= minimum }} + - condition: state + entity_id: light.living_room + state: "on" + - condition: template + value_template: >- + {{ 'light.living_room' not in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control') or []) }} + actions: + - action: light.turn_off + target: + entity_id: light.living_room +``` + +This runs once when a valid target crosses down into the minimum range. It skips lights currently marked as manually controlled, does not repeatedly turn them off while the target remains low, and does not turn them back on later. Startup or re-enabling the profile while already at the minimum is not a new crossing. Sleep mode can also cause a crossing if its brightness is at or below the chosen minimum. Changing sleep mode clears manual control by default; set `reset_manual_control_on_sleep_mode_change: false` if you want to preserve it. For a bedtime-only policy, trigger directly on the sleep-mode switch changing to `on` instead. + +
+
Set sunrise and sunset from an alarm. @@ -342,6 +405,8 @@ script:
Use a Schedule helper as a step-based custom lighting profile. +Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml). Select the main profile switch and your Schedule helper. + Create a [Schedule helper](https://www.home-assistant.io/integrations/schedule/) named `Adaptive Lighting Profile`. Add time blocks with Additional data like this: ```yaml @@ -391,6 +456,8 @@ This creates step changes at block boundaries. It does not interpolate between s
Reduce daytime brightness when an illuminance sensor detects strong daylight. +Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml). Select the profile and sensor, then set the lux thresholds and brightness limits. The high lux threshold must exceed the low threshold; the blueprint does nothing if they are reversed or equal. + Keep a low configured `min_brightness` for late night and let an automation lower `max_brightness` while the room has ample daylight. Use a sensor that is not significantly affected by the controlled lights to avoid a feedback loop. ```yaml diff --git a/blueprints/automation/daylight_limit.yaml b/blueprints/automation/daylight_limit.yaml new file mode 100644 index 00000000..bc57d434 --- /dev/null +++ b/blueprints/automation/daylight_limit.yaml @@ -0,0 +1,112 @@ +blueprint: + name: "Adaptive Lighting: limit brightness in daylight" + description: >- + Lower a profile's maximum brightness in strong daylight and restore the + chosen normal maximum when daylight falls. Separate lux thresholds prevent + repeated changes near a single threshold. Use a sensor not significantly + affected by the controlled lights. Keep the daylight maximum at or above + the profile's minimum unless you want an inverted brightness curve. + Startup waits up to five minutes for a numeric sensor reading; a reading + between the thresholds leaves the configured maximum unchanged. + domain: automation + homeassistant: + min_version: "2025.9.0" + input: + adaptive_switch: + name: Adaptive Lighting profile + description: Select the main profile switch, not a sleep or adaptation switch. + selector: + entity: + filter: + domain: switch + integration: adaptive_lighting + illuminance_sensor: + name: Illuminance sensor + selector: + entity: + filter: + domain: sensor + device_class: illuminance + high_lux: + name: Strong daylight threshold + description: Must be greater than the low daylight threshold. + default: 300 + selector: + number: + min: 0 + max: 200000 + mode: box + unit_of_measurement: lx + low_lux: + name: Low daylight threshold + default: 200 + selector: + number: + min: 0 + max: 200000 + mode: box + unit_of_measurement: lx + daylight_maximum: + name: Maximum brightness in strong daylight + default: 30 + selector: + number: + min: 1 + max: 100 + mode: box + unit_of_measurement: "%" + normal_maximum: + name: Normal maximum brightness + default: 100 + selector: + number: + min: 1 + max: 100 + mode: box + unit_of_measurement: "%" + +mode: restart +variables: + illuminance_sensor: !input illuminance_sensor + high_lux: !input high_lux + low_lux: !input low_lux +triggers: + - trigger: numeric_state + entity_id: !input illuminance_sensor + above: !input high_lux + - trigger: numeric_state + entity_id: !input illuminance_sensor + below: !input low_lux + - trigger: homeassistant + event: start + id: startup +conditions: + - condition: template + value_template: "{{ high_lux > low_lux }}" +actions: + - if: + - condition: trigger + id: startup + then: + - wait_template: "{{ is_number(states(illuminance_sensor)) }}" + timeout: "00:05:00" + continue_on_timeout: false + - choose: + - conditions: + - condition: numeric_state + entity_id: !input illuminance_sensor + above: !input high_lux + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: !input adaptive_switch + max_brightness: !input daylight_maximum + - conditions: + - condition: numeric_state + entity_id: !input illuminance_sensor + below: !input low_lux + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: !input adaptive_switch + max_brightness: !input normal_maximum diff --git a/blueprints/automation/schedule_profile.yaml b/blueprints/automation/schedule_profile.yaml new file mode 100644 index 00000000..64ccf17a --- /dev/null +++ b/blueprints/automation/schedule_profile.yaml @@ -0,0 +1,57 @@ +blueprint: + name: "Adaptive Lighting: scheduled profile" + description: >- + Apply fixed brightness and color temperature from a Schedule helper's + brightness_pct and color_temp_kelvin attributes. Uses step changes, not + interpolation. Missing attributes fall back to 1% and 2000 K. Outside an + active block, restores ALL configured profile settings. Use this only if + other automations do not also change runtime settings on this profile. + Reapplies the active block on Home Assistant startup. Updates settings + while the profile is off without enabling it; preserves manual control. + domain: automation + homeassistant: + min_version: "2025.9.0" + input: + adaptive_switch: + name: Adaptive Lighting profile + description: Select the main profile switch, not a sleep or adaptation switch. + selector: + entity: + filter: + domain: switch + integration: adaptive_lighting + schedule_entity: + name: Schedule helper + description: Add brightness_pct (1–100) and color_temp_kelvin to each block's Additional data. + selector: + entity: + filter: + domain: schedule + +mode: restart +variables: + schedule_entity: !input schedule_entity +triggers: + - trigger: state + entity_id: !input schedule_entity + - trigger: homeassistant + event: start +actions: + - choose: + - conditions: + - condition: state + entity_id: !input schedule_entity + state: "on" + sequence: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: !input adaptive_switch + min_brightness: "{{ state_attr(schedule_entity, 'brightness_pct') | int(1) }}" + max_brightness: "{{ state_attr(schedule_entity, 'brightness_pct') | int(1) }}" + min_color_temp: "{{ state_attr(schedule_entity, 'color_temp_kelvin') | int(2000) }}" + max_color_temp: "{{ state_attr(schedule_entity, 'color_temp_kelvin') | int(2000) }}" + default: + - action: adaptive_lighting.change_switch_settings + data: + entity_id: !input adaptive_switch + use_defaults: configuration diff --git a/blueprints/automation/sleep_mode.yaml b/blueprints/automation/sleep_mode.yaml new file mode 100644 index 00000000..fe57079b --- /dev/null +++ b/blueprints/automation/sleep_mode.yaml @@ -0,0 +1,41 @@ +blueprint: + name: "Adaptive Lighting: synchronize sleep mode" + description: >- + Keep the selected Adaptive Lighting sleep-mode switches in sync with an + input boolean, including its restored state at Home Assistant startup. + Unknown or unavailable helper states are ignored. Select only sleep-mode + switches, not the main profile or adaptation switches. + domain: automation + homeassistant: + min_version: "2025.9.0" + input: + sleep_helper: + name: Sleep-mode helper + selector: + entity: + filter: + domain: input_boolean + sleep_switches: + name: Adaptive Lighting sleep-mode switches + selector: + entity: + multiple: true + filter: + domain: switch + integration: adaptive_lighting + +triggers: + - trigger: state + entity_id: !input sleep_helper + - trigger: homeassistant + event: start +variables: + sleep_helper: !input sleep_helper + sleep_mode: "{{ states(sleep_helper) }}" +conditions: + - condition: template + value_template: "{{ sleep_mode in ['on', 'off'] }}" +actions: + - action: "switch.turn_{{ sleep_mode }}" + target: + entity_id: !input sleep_switches diff --git a/blueprints/automation/turn_off_at_minimum.yaml b/blueprints/automation/turn_off_at_minimum.yaml new file mode 100644 index 00000000..6593e398 --- /dev/null +++ b/blueprints/automation/turn_off_at_minimum.yaml @@ -0,0 +1,87 @@ +blueprint: + name: "Adaptive Lighting: turn off at minimum brightness" + description: >- + Turn one light off when its Adaptive Lighting target crosses down into the + chosen minimum brightness range. Compares rounded 0–255 commands, not the + bulb's physical dimming limit or transition completion. Skips lights currently + marked as manually controlled. Changing sleep mode clears manual control by + default; set reset_manual_control_on_sleep_mode_change to false in your + profile to preserve it. Does not turn lights back on or repeatedly turn them off + while the target stays low. Startup at the minimum does not trigger it. + Sleep mode can trigger it if its target crosses the chosen minimum. + Select a light managed by the chosen profile and its matching brightness switch. + domain: automation + homeassistant: + min_version: "2025.9.0" + input: + adaptive_switch: + name: Adaptive Lighting profile + description: Select the main Adaptive Lighting switch, not a sleep or adaptation switch. + selector: + entity: + filter: + domain: switch + integration: adaptive_lighting + brightness_switch: + name: Adapt brightness switch + description: Select the adapt brightness switch belonging to the same profile. + selector: + entity: + filter: + domain: switch + integration: adaptive_lighting + light_entity: + name: Light + description: Select one light managed by this profile. Create an automation for each light. + selector: + entity: + filter: + domain: light + minimum_pct: + name: Minimum brightness + description: Match the profile's min_brightness setting. Update this if that setting changes. + default: 1 + selector: + number: + min: 1 + max: 100 + step: 1 + unit_of_measurement: "%" + mode: box + +mode: single +variables: + adaptive_switch: !input adaptive_switch + light_entity: !input light_entity + minimum_pct: !input minimum_pct +triggers: + - trigger: state + entity_id: !input adaptive_switch + attribute: brightness_pct +conditions: + - condition: state + entity_id: !input adaptive_switch + state: "on" + - condition: state + entity_id: !input brightness_switch + state: "on" + - condition: template + value_template: >- + {% set minimum = (minimum_pct * 255 / 100) | round(0) %} + {% set before = trigger.from_state.attributes.get('brightness_pct') + if trigger.from_state else none %} + {% set after = trigger.to_state.attributes.get('brightness_pct') + if trigger.to_state else none %} + {{ is_number(before) and is_number(after) + and (before | float * 255 / 100) | round(0) > minimum + and (after | float * 255 / 100) | round(0) <= minimum }} + - condition: state + entity_id: !input light_entity + state: "on" + - condition: template + value_template: >- + {{ light_entity not in (state_attr(adaptive_switch, 'manual_control') or []) }} +actions: + - action: light.turn_off + target: + entity_id: !input light_entity diff --git a/docs/automation-examples.md b/docs/automation-examples.md index 9a8c21f3..5686c5d7 100644 --- a/docs/automation-examples.md +++ b/docs/automation-examples.md @@ -16,6 +16,17 @@ Replace every entity ID below with the IDs from your Home Assistant instance. Fr Blocks that begin with `- alias` are entries for `automations.yaml`. Blocks with a top-level `script:` or `adaptive_lighting:` key are complete `configuration.yaml` examples. If your configuration uses `script: !include scripts.yaml`, omit that outer key and place its contents in `scripts.yaml`. +Four examples also have blueprints with selectors, so you can configure them without editing YAML: + +| Blueprint | Purpose | +| --- | --- | +| [Sleep mode](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml) | Synchronize several profiles with one sleep-mode helper. | +| [Minimum brightness](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/turn_off_at_minimum.yaml) | Turn one light off when its target crosses down to the minimum. | +| [Schedule profile](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml) | Apply brightness and color temperature from Schedule helper blocks. | +| [Daylight limit](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml) | Lower maximum brightness in strong daylight. | + +Copy a blueprint's link into **Settings → Automations & scenes → Blueprints → Import Blueprint**, then create an automation from it. Read the matching example below for setup and behavior. Each blueprint is tested through Home Assistant alongside its YAML example. The built-in manual-control timeout needs no automation; the scripts below remain useful as actions in your own automations. + `change_switch_settings` updates a profile while its main switch is off, but lights are adapted only while that switch is on. It preserves manual-control flags, so manually controlled lights remain paused.
@@ -38,6 +49,8 @@ This is a top-level `configuration.yaml` example. The timer clears manual contro
Toggle multiple Adaptive Lighting switches to "sleep mode" using an input_boolean.sleep_mode. +Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml). Select an input boolean and the sleep-mode switches it should control. + ```yaml - alias: "Adaptive lighting: toggle 'sleep mode'" trigger: @@ -60,6 +73,56 @@ This is a top-level `configuration.yaml` example. The timer clears manual contro
+
+Turn a light off when its adaptive brightness target reaches the minimum. + +Prefer a form over editing YAML? Import the [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/turn_off_at_minimum.yaml) in Home Assistant under **Settings → Automations & scenes → Blueprints → Import Blueprint**. Select your profile, its matching adapt brightness switch, one light managed by that profile, and its minimum brightness percentage. Create one automation per light. If you change the profile's minimum later, update the automation too. The blueprint and YAML example below have the same behavior. + +The Adaptive Lighting switch already exposes its calculated `brightness_pct` target. Use its state changes to choose a power policy in an automation; no custom event is needed. This example assumes `min_brightness: 1`. Change `minimum_pct` to match your profile, and replace the switch and light entity IDs with your own. + +The comparison uses the same rounded 0–255 brightness as an adaptation command. Comparing floating-point percentages for exact equality can miss the minimum between updates. This detects the calculated target reaching its minimum command, not the bulb finishing a transition or reaching its physical dimming limit. + +```yaml +- alias: "Adaptive lighting: turn off at minimum brightness" + mode: single + triggers: + - trigger: state + entity_id: switch.adaptive_lighting_living_room + attribute: brightness_pct + conditions: + - condition: state + entity_id: + - switch.adaptive_lighting_living_room + - switch.adaptive_lighting_living_room_adapt_brightness + state: "on" + - condition: template + value_template: >- + {% set minimum_pct = 1 %} + {% set minimum = (minimum_pct * 255 / 100) | round(0) %} + {% set before = trigger.from_state.attributes.get('brightness_pct') + if trigger.from_state else none %} + {% set after = trigger.to_state.attributes.get('brightness_pct') + if trigger.to_state else none %} + {{ is_number(before) and is_number(after) + and (before | float * 255 / 100) | round(0) > minimum + and (after | float * 255 / 100) | round(0) <= minimum }} + - condition: state + entity_id: light.living_room + state: "on" + - condition: template + value_template: >- + {{ 'light.living_room' not in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control') or []) }} + actions: + - action: light.turn_off + target: + entity_id: light.living_room +``` + +This runs once when a valid target crosses down into the minimum range. It skips lights currently marked as manually controlled, does not repeatedly turn them off while the target remains low, and does not turn them back on later. Startup or re-enabling the profile while already at the minimum is not a new crossing. Sleep mode can also cause a crossing if its brightness is at or below the chosen minimum. Changing sleep mode clears manual control by default; set `reset_manual_control_on_sleep_mode_change: false` if you want to preserve it. For a bedtime-only policy, trigger directly on the sleep-mode switch changing to `on` instead. + +
+
Set sunrise and sunset from an alarm. @@ -86,6 +149,8 @@ script:
Use a Schedule helper as a step-based custom lighting profile. +Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml). Select the main profile switch and your Schedule helper. + Create a [Schedule helper](https://www.home-assistant.io/integrations/schedule/) named `Adaptive Lighting Profile`. Add time blocks with Additional data like this: ```yaml @@ -135,6 +200,8 @@ This creates step changes at block boundaries. It does not interpolate between s
Reduce daytime brightness when an illuminance sensor detects strong daylight. +Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml). Select the profile and sensor, then set the lux thresholds and brightness limits. The high lux threshold must exceed the low threshold; the blueprint does nothing if they are reversed or equal. + Keep a low configured `min_brightness` for late night and let an automation lower `max_brightness` while the room has ample daylight. Use a sensor that is not significantly affected by the controlled lights to avoid a feedback loop. ```yaml diff --git a/tests/test_automation_examples.py b/tests/test_automation_examples.py index e5f3c2d5..d0dc8e3e 100644 --- a/tests/test_automation_examples.py +++ b/tests/test_automation_examples.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import re +import shutil from datetime import UTC, datetime, timedelta from pathlib import Path from typing import TYPE_CHECKING @@ -16,6 +17,9 @@ from homeassistant.components.adaptive_lighting.adaptation_utils import ( LightControlAttributes, ) from homeassistant.components.adaptive_lighting.const import ( + CONF_BRIGHTNESS_MODE, + CONF_BRIGHTNESS_MODE_TIME_DARK, + CONF_BRIGHTNESS_MODE_TIME_LIGHT, CONF_INITIAL_TRANSITION, CONF_LIGHTS, CONF_MAX_BRIGHTNESS, @@ -31,6 +35,7 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_TRANSITION, DOMAIN, ) +from homeassistant.components.blueprint.models import Blueprint from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP_KELVIN, @@ -52,6 +57,7 @@ from homeassistant.const import ( from homeassistant.core import CoreState, Event, HomeAssistant, State, callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from homeassistant.util import yaml as yaml_util from tests.common import async_fire_time_changed @@ -146,12 +152,247 @@ def _prepare_hass_startup(hass: HomeAssistant) -> None: hass.set_state(CoreState.not_running) +def _blueprint_config(hass, tmp_path, filename, inputs, alias): + """Install an actual published blueprint for Home Assistant to load.""" + relative_path = f"adaptive_lighting/{filename}" + hass.config.config_dir = str(tmp_path) + destination = tmp_path / "blueprints" / "automation" / relative_path + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(README.parent / "blueprints" / "automation" / filename, destination) + return { + "alias": alias, + "use_blueprint": {"path": relative_path, "input": inputs}, + } + + +@pytest.fixture(params=["yaml", "blueprint"]) +def published_automation(hass: HomeAssistant, tmp_path: Path, request): + """Use the published YAML or blueprint with the same behavioral assertions.""" + + def config(summary, filename, inputs): + yaml_config = _yaml_documents(summary)[-1] + if request.param == "yaml": + return yaml_config + return _blueprint_config( + hass, + tmp_path, + filename, + inputs, + yaml_config[0]["alias"], + ) + + return config + + +@pytest.mark.parametrize( + "path", + sorted((README.parent / "blueprints" / "automation").glob("*.yaml")), + ids=lambda path: path.stem, +) +def test_published_blueprint_schema(path: Path) -> None: + """Validate every published blueprint with Home Assistant's own schema.""" + blueprint = Blueprint( + yaml_util.load_yaml(str(path)), + expected_domain=automation.DOMAIN, + schema=automation.config.AUTOMATION_BLUEPRINT_SCHEMA, + ) + assert blueprint.validate() is None + + +@pytest.fixture(params=["yaml", "blueprint", "blueprint-custom-minimum"]) +def minimum_automation_config(hass: HomeAssistant, tmp_path: Path, request): + """Run the same behavior checks against both published formats.""" + if request.param == "yaml": + return _yaml_documents( + "Turn a light off when its adaptive brightness target reaches the minimum.", + )[0] + inputs = { + "adaptive_switch": "switch.adaptive_lighting_living_room", + "brightness_switch": "switch.adaptive_lighting_living_room_adapt_brightness", + "light_entity": "light.living_room", + } + if request.param == "blueprint-custom-minimum": + inputs["minimum_pct"] = 10 + return _blueprint_config( + hass, + tmp_path, + "turn_off_at_minimum.yaml", + inputs, + "Turn off at minimum", + ) + + +@pytest.mark.parametrize("manual_control", [False, True]) +@pytest.mark.parametrize("trigger_kind", ["interval", "sleep"]) +@patch( + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", + new=dt_util.utcnow, +) +async def test_minimum_brightness_power_automation( + hass: HomeAssistant, + freezer, + manual_control: bool, + trigger_kind: str, + minimum_automation_config, +) -> None: + """Catch exact-float comparisons, repeated power actions, or lost manual control.""" + minimum = ( + minimum_automation_config.get("use_blueprint", {}) + .get("input", {}) + .get("minimum_pct", 1) + if isinstance(minimum_automation_config, dict) + else 1 + ) + freezer.move_to(datetime(2026, 9, 6, 18, 58, tzinfo=dt_util.DEFAULT_TIME_ZONE)) + await _setup_template_lights(hass, ["Living Room"]) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.living_room", ATTR_BRIGHTNESS: 77}, + blocking=True, + ) + _, adaptive_switch = await setup_switch( + hass, + { + CONF_NAME: "Living Room", + CONF_LIGHTS: ["light.living_room"], + CONF_MIN_BRIGHTNESS: minimum, + CONF_MAX_BRIGHTNESS: 100, + CONF_BRIGHTNESS_MODE: "linear", + CONF_BRIGHTNESS_MODE_TIME_DARK: timedelta(hours=1), + CONF_BRIGHTNESS_MODE_TIME_LIGHT: timedelta(hours=1), + CONF_SUNRISE_TIME: "06:00:00", + CONF_SUNSET_TIME: "18:00:00", + CONF_TRANSITION: 0, + CONF_INITIAL_TRANSITION: 0, + }, + ) + if manual_control: + await hass.services.async_call( + DOMAIN, + "set_manual_control", + {ATTR_ENTITY_ID: adaptive_switch.entity_id, "manual_control": True}, + blocking=True, + ) + await _setup_automation(hass, minimum_automation_config) + off_calls = [] + + @callback + def record_off(event: Event) -> None: + if ( + event.data["domain"] == LIGHT_DOMAIN + and event.data["service"] == SERVICE_TURN_OFF + ): + off_calls.append(event.data["service_data"]) + + hass.bus.async_listen(EVENT_CALL_SERVICE, record_off) + assert hass.states.get("light.living_room").state == STATE_ON + assert adaptive_switch.extra_state_attributes["brightness_pct"] > minimum + 1 + + # The curve is above the minimum, but rounds to the same brightness command. + freezer.move_to(datetime(2026, 9, 6, 18, 59, 50, tzinfo=dt_util.DEFAULT_TIME_ZONE)) + if trigger_kind == "sleep": + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.adaptive_lighting_living_room_sleep_mode"}, + blocking=True, + ) + else: + await adaptive_switch._async_update_at_interval_action() + await hass.async_block_till_done() + if trigger_kind == "sleep": + assert adaptive_switch.extra_state_attributes["brightness_pct"] == 1 + else: + assert ( + minimum + < adaptive_switch.extra_state_attributes["brightness_pct"] + < minimum + 0.2 + ) + # The default sleep-mode policy clears manual control before publishing its target. + should_turn_off = not manual_control or trigger_kind == "sleep" + assert hass.states.get("light.living_room").state == ( + STATE_OFF if should_turn_off else STATE_ON + ) + assert len(off_calls) == int(should_turn_off) + + # Further target changes inside the minimum command range do not retrigger. + freezer.move_to(datetime(2026, 9, 6, 19, 1, tzinfo=dt_util.DEFAULT_TIME_ZONE)) + await adaptive_switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert adaptive_switch.extra_state_attributes["brightness_pct"] == ( + 1 if trigger_kind == "sleep" else minimum + ) + assert len(off_calls) == int(should_turn_off) + + if should_turn_off: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.living_room"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("light.living_room").state == STATE_ON + assert len(off_calls) == 1 + + +@pytest.mark.parametrize("previous", [None, "unknown", "unavailable"]) +async def test_minimum_brightness_ignores_missing_previous_target( + hass: HomeAssistant, + previous: str | None, + minimum_automation_config, +) -> None: + """A missing target must not become a numeric crossing during recovery.""" + await _setup_template_lights(hass, ["Living Room"]) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.living_room"}, + blocking=True, + ) + _, adaptive_switch = await setup_switch( + hass, + { + CONF_NAME: "Living Room", + CONF_LIGHTS: ["light.living_room"], + CONF_MIN_BRIGHTNESS: 1, + CONF_MAX_BRIGHTNESS: 1, + CONF_TRANSITION: 0, + CONF_INITIAL_TRANSITION: 0, + }, + ) + await _setup_automation(hass, minimum_automation_config) + attributes = dict(hass.states.get(adaptive_switch.entity_id).attributes) + assert attributes["brightness_pct"] == 1 + if previous is None: + hass.states.async_remove(adaptive_switch.entity_id) + else: + hass.states.async_set( + adaptive_switch.entity_id, + previous, + {**attributes, "brightness_pct": previous}, + ) + await hass.async_block_till_done() + hass.states.async_set(adaptive_switch.entity_id, STATE_ON, attributes) + await hass.async_block_till_done() + assert hass.states.get("light.living_room").state == STATE_ON + + async def test_schedule_profile_executes_blocks_and_restore( hass: HomeAssistant, + published_automation, ) -> None: """Catch ignored attribute changes, incomplete restore, or switch coupling.""" summary = "Use a Schedule helper as a step-based custom lighting profile." - automation_config = _yaml_documents(summary)[-1] + automation_config = published_automation( + summary, + "schedule_profile.yaml", + { + "adaptive_switch": "switch.adaptive_lighting_living_room", + "schedule_entity": "schedule.adaptive_lighting_profile", + }, + ) _, adaptive_switch = await setup_switch( hass, { @@ -220,11 +461,21 @@ async def test_schedule_profile_executes_blocks_and_restore( assert adaptive_switch._sun_light_settings.max_color_temp == 2750 -async def test_schedule_profile_reapplies_at_startup(hass: HomeAssistant) -> None: +async def test_schedule_profile_reapplies_at_startup( + hass: HomeAssistant, + published_automation, +) -> None: """Verify startup applies the already-active schedule block.""" _prepare_hass_startup(hass) summary = "Use a Schedule helper as a step-based custom lighting profile." - automation_config = _yaml_documents(summary)[-1] + automation_config = published_automation( + summary, + "schedule_profile.yaml", + { + "adaptive_switch": "switch.adaptive_lighting_living_room", + "schedule_entity": "schedule.adaptive_lighting_profile", + }, + ) _, adaptive_switch = await setup_switch(hass, {CONF_NAME: "Living Room"}) hass.states.async_set( "schedule.adaptive_lighting_profile", @@ -241,12 +492,22 @@ async def test_schedule_profile_reapplies_at_startup(hass: HomeAssistant) -> Non assert adaptive_switch._sun_light_settings.max_color_temp == 2500 -async def test_lux_profile_executes_hysteresis(hass: HomeAssistant) -> None: +async def test_lux_profile_executes_hysteresis( + hass: HomeAssistant, + published_automation, +) -> None: """Catch missing threshold actions or changes inside the dead band.""" summary = ( "Reduce daytime brightness when an illuminance sensor detects strong daylight." ) - automation_config = _yaml_documents(summary)[0] + automation_config = published_automation( + summary, + "daylight_limit.yaml", + { + "adaptive_switch": "switch.adaptive_lighting_living_room", + "illuminance_sensor": "sensor.living_room_illuminance", + }, + ) _, adaptive_switch = await setup_switch( hass, {CONF_NAME: "Living Room", CONF_MAX_BRIGHTNESS: 80}, @@ -273,13 +534,21 @@ async def test_lux_profile_executes_hysteresis(hass: HomeAssistant) -> None: async def test_lux_profile_executes_unknown_recovery_at_startup( hass: HomeAssistant, + published_automation, ) -> None: """Catch a startup hang or failure to recover from an unknown sensor.""" _prepare_hass_startup(hass) summary = ( "Reduce daytime brightness when an illuminance sensor detects strong daylight." ) - automation_config = _yaml_documents(summary)[0] + automation_config = published_automation( + summary, + "daylight_limit.yaml", + { + "adaptive_switch": "switch.adaptive_lighting_living_room", + "illuminance_sensor": "sensor.living_room_illuminance", + }, + ) _, adaptive_switch = await setup_switch( hass, {CONF_NAME: "Living Room", CONF_MAX_BRIGHTNESS: 80}, @@ -302,6 +571,44 @@ async def test_lux_profile_executes_unknown_recovery_at_startup( assert adaptive_switch._sun_light_settings.max_brightness == 30 +@pytest.mark.parametrize(("high_lux", "low_lux"), [(400, 250), (200, 300), (200, 200)]) +async def test_daylight_blueprint_custom_inputs( + hass: HomeAssistant, + tmp_path: Path, + high_lux: int, + low_lux: int, +) -> None: + """Use selected entities and limits; invalid threshold order must do nothing.""" + config = _blueprint_config( + hass, + tmp_path, + "daylight_limit.yaml", + { + "adaptive_switch": "switch.adaptive_lighting_office", + "illuminance_sensor": "sensor.office_illuminance", + "high_lux": high_lux, + "low_lux": low_lux, + "daylight_maximum": 20, + "normal_maximum": 70, + }, + "Custom daylight", + ) + _, adaptive_switch = await setup_switch( + hass, + {CONF_NAME: "Office", CONF_MAX_BRIGHTNESS: 80}, + ) + hass.states.async_set("sensor.office_illuminance", "300") + await _setup_automation(hass, config) + assert hass.states.get("automation.custom_daylight") is not None + valid_thresholds = high_lux > low_lux + for lux, expected in [(500, 20), (300, 20), (100, 70)]: + hass.states.async_set("sensor.office_illuminance", str(lux)) + await hass.async_block_till_done() + assert adaptive_switch._sun_light_settings.max_brightness == ( + expected if valid_thresholds else 80 + ) + + async def test_hue_script_applies_current_values_to_fresh_profile_targets( hass: HomeAssistant, ) -> None: @@ -574,13 +881,24 @@ async def test_autoreset_manual_control_uses_one_renewable_timer( async def test_sleep_toggle_uses_fresh_profile_entity_ids( hass: HomeAssistant, + published_automation, ) -> None: """Execute state triggers against fresh child entity IDs.""" summary = ( 'Toggle multiple Adaptive Lighting switches to "sleep mode" using an ' "input_boolean.sleep_mode." ) - automation_config = _yaml_documents(summary)[0] + automation_config = published_automation( + summary, + "sleep_mode.yaml", + { + "sleep_helper": "input_boolean.sleep_mode", + "sleep_switches": [ + "switch.adaptive_lighting_living_room_sleep_mode", + "switch.adaptive_lighting_bedroom_sleep_mode", + ], + }, + ) assert await async_setup_component( hass, "input_boolean", @@ -623,6 +941,7 @@ async def test_sleep_toggle_uses_fresh_profile_entity_ids( async def test_sleep_toggle_applies_restored_state_at_startup( hass: HomeAssistant, + published_automation, ) -> None: """Verify startup applies the input boolean's restored state.""" _prepare_hass_startup(hass) @@ -630,13 +949,24 @@ async def test_sleep_toggle_applies_restored_state_at_startup( 'Toggle multiple Adaptive Lighting switches to "sleep mode" using an ' "input_boolean.sleep_mode." ) - automation_config = _yaml_documents(summary)[0] + automation_config = published_automation( + summary, + "sleep_mode.yaml", + { + "sleep_helper": "input_boolean.sleep_mode", + "sleep_switches": [ + "switch.adaptive_lighting_living_room_sleep_mode", + "switch.adaptive_lighting_bedroom_sleep_mode", + ], + }, + ) assert await async_setup_component( hass, "input_boolean", {"input_boolean": {"sleep_mode": {}}}, ) await setup_switch(hass, {CONF_NAME: "Living Room"}) + await setup_switch(hass, {CONF_NAME: "Bedroom"}) await hass.services.async_call( "input_boolean", SERVICE_TURN_ON, diff --git a/tests/test_switch.py b/tests/test_switch.py index 733b1ef3..cfebc4f6 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1891,9 +1891,10 @@ async def test_shared_profiles_track_manual_brightness( hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == expected_brightness ) - assert [event.data[SWITCH_DOMAIN] for event in events] == [ + # Independent profiles may publish their events in either order. + assert sorted(event.data[SWITCH_DOMAIN] for event in events) == sorted( profiles[name].entity_id for name in event_profiles - ] + ) assert all(event.context == context for event in events) assert all( event.data[CONF_MANUAL_CONTROL] == LightControlAttributes.BRIGHTNESS From 11bd5cf2aa3bcd2ec60fe77396cda48da56e697b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 20:46:14 +0200 Subject: [PATCH 1062/1077] Pause brightness at minimum using existing manual-control resets (#1578) --- README.md | 106 ++++++- .../automation/manual_control_at_minimum.yaml | 123 ++++++++ docs/automation-examples.md | 106 ++++++- tests/test_automation_examples.py | 298 ++++++++++++++++++ 4 files changed, 617 insertions(+), 16 deletions(-) create mode 100644 blueprints/automation/manual_control_at_minimum.yaml diff --git a/README.md b/README.md index af016c54..1ee91993 100644 --- a/README.md +++ b/README.md @@ -272,16 +272,17 @@ Replace every entity ID below with the IDs from your Home Assistant instance. Fr Blocks that begin with `- alias` are entries for `automations.yaml`. Blocks with a top-level `script:` or `adaptive_lighting:` key are complete `configuration.yaml` examples. If your configuration uses `script: !include scripts.yaml`, omit that outer key and place its contents in `scripts.yaml`. -Four examples also have blueprints with selectors, so you can configure them without editing YAML: +Five examples also have blueprints with selectors, so you can configure them without editing YAML: -| Blueprint | Purpose | -| --- | --- | -| [Sleep mode](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml) | Synchronize several profiles with one sleep-mode helper. | -| [Minimum brightness](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/turn_off_at_minimum.yaml) | Turn one light off when its target crosses down to the minimum. | -| [Schedule profile](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml) | Apply brightness and color temperature from Schedule helper blocks. | -| [Daylight limit](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml) | Lower maximum brightness in strong daylight. | +| Blueprint | Purpose | Import | +| --- | --- | --- | +| [Sleep mode](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml) | Synchronize several profiles with one sleep-mode helper. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fsleep_mode.yaml) | +| [Minimum brightness](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/turn_off_at_minimum.yaml) | Turn one light off when its target crosses down to the minimum. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fturn_off_at_minimum.yaml) | +| [Pause at minimum](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/manual_control_at_minimum.yaml) | Pause brightness through manual control, using its existing reset behavior. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fmanual_control_at_minimum.yaml) | +| [Schedule profile](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml) | Apply brightness and color temperature from Schedule helper blocks. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fschedule_profile.yaml) | +| [Daylight limit](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml) | Lower maximum brightness in strong daylight. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fdaylight_limit.yaml) | -Copy a blueprint's link into **Settings → Automations & scenes → Blueprints → Import Blueprint**, then create an automation from it. Read the matching example below for setup and behavior. Each blueprint is tested through Home Assistant alongside its YAML example. The built-in manual-control timeout needs no automation; the scripts below remain useful as actions in your own automations. +Click a blueprint's import badge, confirm the import in Home Assistant, then create an automation and select your entities. You can also copy its source link into **Settings → Automations & scenes → Blueprints → Import Blueprint**. Read the matching example below for setup and behavior. Each blueprint is tested through Home Assistant alongside its YAML example. The built-in manual-control timeout needs no automation; the scripts below remain useful as actions in your own automations. `change_switch_settings` updates a profile while its main switch is off, but lights are adapted only while that switch is on. It preserves manual-control flags, so manually controlled lights remain paused. @@ -379,6 +380,95 @@ This runs once when a valid target crosses down into the minimum range. It skips
+
+Pause brightness at the minimum using manual control. + +Use the [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/manual_control_at_minimum.yaml) to mark an individual light's brightness as manually controlled when the calculated target reaches its minimum. The light stays on and the adaptation switches stay enabled. Set `take_over_control_mode: pause_changed` on the profile to keep adapting color; the default `pause_all` pauses both attributes. + +Select the profile, its adapt-brightness switch, and a light managed by it. Match the minimum percentage to the profile's `min_brightness`. This YAML example assumes `min_brightness: 1`; change `minimum_pct` and the entity IDs to match your setup. + +```yaml +- alias: "Adaptive lighting: pause brightness at minimum" + mode: single + variables: + minimum_pct: 1 + minimum: "{{ (minimum_pct * 255 / 100) | round(0) }}" + triggers: + - trigger: state + entity_id: switch.adaptive_lighting_living_room + attribute: brightness_pct + conditions: + - condition: state + entity_id: + - switch.adaptive_lighting_living_room + - switch.adaptive_lighting_living_room_adapt_brightness + state: "on" + - condition: template + value_template: >- + {% set before = trigger.from_state.attributes.get('brightness_pct') + if trigger.from_state else none %} + {% set after = trigger.to_state.attributes.get('brightness_pct') + if trigger.to_state else none %} + {{ is_number(before) and is_number(after) + and (before | float * 255 / 100) | round(0) > minimum + and (after | float * 255 / 100) | round(0) <= minimum }} + - condition: state + entity_id: light.living_room + state: "on" + - condition: template + value_template: >- + {{ 'light.living_room' not in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or []) }} + actions: + - variables: + light_session: "{{ states.light.living_room.last_changed.isoformat() }}" + profile_session: "{{ states.switch.adaptive_lighting_living_room.last_changed.isoformat() }}" + brightness_session: "{{ states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() }}" + - wait_template: >- + {% set target = state_attr('switch.adaptive_lighting_living_room', 'brightness_pct') %} + {{ not is_state('light.living_room', 'on') + or not is_state('switch.adaptive_lighting_living_room', 'on') + or not is_state('switch.adaptive_lighting_living_room_adapt_brightness', 'on') + or states.light.living_room.last_changed.isoformat() != light_session + or states.switch.adaptive_lighting_living_room.last_changed.isoformat() != profile_session + or states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() != brightness_session + or not is_number(target) or (target | float * 255 / 100) | round(0) > minimum + or 'light.living_room' in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or []) + or (state_attr('light.living_room', 'brightness') | float(256)) <= minimum }} + timeout: "00:05:00" + continue_on_timeout: false + - condition: template + value_template: >- + {% set target = state_attr('switch.adaptive_lighting_living_room', 'brightness_pct') %} + {{ is_state('light.living_room', 'on') + and is_state('switch.adaptive_lighting_living_room', 'on') + and is_state('switch.adaptive_lighting_living_room_adapt_brightness', 'on') + and states.light.living_room.last_changed.isoformat() == light_session + and states.switch.adaptive_lighting_living_room.last_changed.isoformat() == profile_session + and states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() == brightness_session + and is_number(target) and (target | float * 255 / 100) | round(0) <= minimum + and (state_attr('light.living_room', 'brightness') | float(256)) <= minimum + and 'light.living_room' not in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or []) }} + - action: adaptive_lighting.set_manual_control + data: + entity_id: switch.adaptive_lighting_living_room + lights: light.living_room + manual_control: >- + {{ true if 'light.living_room' in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control_color') or []) + else 'brightness' }} +``` + +The comparison uses the rounded 0–255 target, so it does not depend on sampling an exact floating-point minimum. It waits up to five minutes for the light to report that minimum before marking manual control, so the final dimming command can complete. Reported brightness does not prove physical fade completion. If the light, profile, or adapt-brightness switch is toggled, the target rises, brightness is marked manually controlled elsewhere, or brightness never reaches the minimum, the attempt is abandoned. Lights that cannot report the configured minimum will not be paused. Existing manual color flags are preserved, and lights whose brightness is already manually controlled are left alone. Manual-control state is shared for lights managed by multiple profiles, so their existing takeover policies still apply. + +The usual resets apply: turning the light off, the configured `autoreset_control_seconds` timeout, clearing manual control through its service, and existing profile/sleep-switch reset behavior. After a reset, normal adaptation can increase brightness again. This runs once per downward crossing; resetting while the target remains at its minimum does not immediately mark the light again. Startup at the minimum is not a crossing either. + +This pauses further dimming as well as brightening. To pause brightness immediately after a brightness change made through Home Assistant, use `take_over_control_mode: pause_changed` with `take_over_control: true`; that needs no additional automation. + +
+
Set sunrise and sunset from an alarm. diff --git a/blueprints/automation/manual_control_at_minimum.yaml b/blueprints/automation/manual_control_at_minimum.yaml new file mode 100644 index 00000000..95cb5f79 --- /dev/null +++ b/blueprints/automation/manual_control_at_minimum.yaml @@ -0,0 +1,123 @@ +blueprint: + name: "Adaptive Lighting: pause brightness at minimum" + description: >- + Mark one light's brightness as manually controlled when its calculated + brightness target crosses down to the configured minimum. Set the profile's + take_over_control_mode to pause_changed to keep adapting color; pause_all + pauses both. Existing manual color control is preserved. Uses the normal + manual-control resets, including turning the light off and any configured + autoreset_control_seconds timeout. Runs once per downward crossing, so a + reset while the target remains low does not immediately pause it again. + Waits up to five minutes for reported brightness to reach the rounded + 0–255 minimum before pausing. This does not prove physical fade completion. Does not + change power or adaptation switches. Select an individual light managed by + the profile. Shared profiles retain their usual manual-control behavior. + domain: automation + homeassistant: + min_version: "2025.9.0" + input: + adaptive_switch: + name: Adaptive Lighting profile + description: Select the main Adaptive Lighting switch, not a sleep or adaptation switch. + selector: + entity: + filter: + domain: switch + integration: adaptive_lighting + brightness_switch: + name: Adapt brightness switch + description: Select the adapt brightness switch belonging to the same profile. + selector: + entity: + filter: + domain: switch + integration: adaptive_lighting + light_entity: + name: Light + description: Select one light managed by this profile. Create an automation for each light. + selector: + entity: + filter: + domain: light + minimum_pct: + name: Minimum brightness + description: Match the profile's min_brightness setting. Update this if that setting changes. + default: 1 + selector: + number: + min: 1 + max: 100 + step: 1 + unit_of_measurement: "%" + mode: box + +mode: single +variables: + adaptive_switch: !input adaptive_switch + light_entity: !input light_entity + minimum_pct: !input minimum_pct + minimum: "{{ (minimum_pct * 255 / 100) | round(0) }}" + brightness_switch: !input brightness_switch +triggers: + - trigger: state + entity_id: !input adaptive_switch + attribute: brightness_pct +conditions: + - condition: state + entity_id: !input adaptive_switch + state: "on" + - condition: state + entity_id: !input brightness_switch + state: "on" + - condition: template + value_template: >- + {% set before = trigger.from_state.attributes.get('brightness_pct') + if trigger.from_state else none %} + {% set after = trigger.to_state.attributes.get('brightness_pct') + if trigger.to_state else none %} + {{ is_number(before) and is_number(after) + and (before | float * 255 / 100) | round(0) > minimum + and (after | float * 255 / 100) | round(0) <= minimum }} + - condition: state + entity_id: !input light_entity + state: "on" + - condition: template + value_template: >- + {{ light_entity not in (state_attr(adaptive_switch, 'manual_control_brightness') or []) }} +actions: + - variables: + light_session: "{{ states[light_entity].last_changed.isoformat() }}" + profile_session: "{{ states[adaptive_switch].last_changed.isoformat() }}" + brightness_session: "{{ states[brightness_switch].last_changed.isoformat() }}" + - wait_template: >- + {% set target = state_attr(adaptive_switch, 'brightness_pct') %} + {{ not is_state(light_entity, 'on') or not is_state(adaptive_switch, 'on') + or not is_state(brightness_switch, 'on') + or states[light_entity].last_changed.isoformat() != light_session + or states[adaptive_switch].last_changed.isoformat() != profile_session + or states[brightness_switch].last_changed.isoformat() != brightness_session + or not is_number(target) or (target | float * 255 / 100) | round(0) > minimum + or light_entity in (state_attr(adaptive_switch, 'manual_control_brightness') or []) + or (state_attr(light_entity, 'brightness') | float(256)) <= minimum }} + timeout: "00:05:00" + continue_on_timeout: false + - condition: template + value_template: >- + {% set target = state_attr(adaptive_switch, 'brightness_pct') %} + {{ is_state(light_entity, 'on') and is_state(adaptive_switch, 'on') + and is_state(brightness_switch, 'on') + and states[light_entity].last_changed.isoformat() == light_session + and states[adaptive_switch].last_changed.isoformat() == profile_session + and states[brightness_switch].last_changed.isoformat() == brightness_session + and is_number(target) and (target | float * 255 / 100) | round(0) <= minimum + and (state_attr(light_entity, 'brightness') | float(256)) <= minimum + and light_entity not in + (state_attr(adaptive_switch, 'manual_control_brightness') or []) }} + - action: adaptive_lighting.set_manual_control + data: + entity_id: !input adaptive_switch + lights: !input light_entity + manual_control: >- + {{ true if light_entity in + (state_attr(adaptive_switch, 'manual_control_color') or []) + else 'brightness' }} diff --git a/docs/automation-examples.md b/docs/automation-examples.md index 5686c5d7..ec5c2831 100644 --- a/docs/automation-examples.md +++ b/docs/automation-examples.md @@ -16,16 +16,17 @@ Replace every entity ID below with the IDs from your Home Assistant instance. Fr Blocks that begin with `- alias` are entries for `automations.yaml`. Blocks with a top-level `script:` or `adaptive_lighting:` key are complete `configuration.yaml` examples. If your configuration uses `script: !include scripts.yaml`, omit that outer key and place its contents in `scripts.yaml`. -Four examples also have blueprints with selectors, so you can configure them without editing YAML: +Five examples also have blueprints with selectors, so you can configure them without editing YAML: -| Blueprint | Purpose | -| --- | --- | -| [Sleep mode](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml) | Synchronize several profiles with one sleep-mode helper. | -| [Minimum brightness](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/turn_off_at_minimum.yaml) | Turn one light off when its target crosses down to the minimum. | -| [Schedule profile](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml) | Apply brightness and color temperature from Schedule helper blocks. | -| [Daylight limit](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml) | Lower maximum brightness in strong daylight. | +| Blueprint | Purpose | Import | +| --- | --- | --- | +| [Sleep mode](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml) | Synchronize several profiles with one sleep-mode helper. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fsleep_mode.yaml) | +| [Minimum brightness](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/turn_off_at_minimum.yaml) | Turn one light off when its target crosses down to the minimum. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fturn_off_at_minimum.yaml) | +| [Pause at minimum](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/manual_control_at_minimum.yaml) | Pause brightness through manual control, using its existing reset behavior. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fmanual_control_at_minimum.yaml) | +| [Schedule profile](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml) | Apply brightness and color temperature from Schedule helper blocks. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fschedule_profile.yaml) | +| [Daylight limit](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml) | Lower maximum brightness in strong daylight. | [![Import blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fdaylight_limit.yaml) | -Copy a blueprint's link into **Settings → Automations & scenes → Blueprints → Import Blueprint**, then create an automation from it. Read the matching example below for setup and behavior. Each blueprint is tested through Home Assistant alongside its YAML example. The built-in manual-control timeout needs no automation; the scripts below remain useful as actions in your own automations. +Click a blueprint's import badge, confirm the import in Home Assistant, then create an automation and select your entities. You can also copy its source link into **Settings → Automations & scenes → Blueprints → Import Blueprint**. Read the matching example below for setup and behavior. Each blueprint is tested through Home Assistant alongside its YAML example. The built-in manual-control timeout needs no automation; the scripts below remain useful as actions in your own automations. `change_switch_settings` updates a profile while its main switch is off, but lights are adapted only while that switch is on. It preserves manual-control flags, so manually controlled lights remain paused. @@ -123,6 +124,95 @@ This runs once when a valid target crosses down into the minimum range. It skips
+
+Pause brightness at the minimum using manual control. + +Use the [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/manual_control_at_minimum.yaml) to mark an individual light's brightness as manually controlled when the calculated target reaches its minimum. The light stays on and the adaptation switches stay enabled. Set `take_over_control_mode: pause_changed` on the profile to keep adapting color; the default `pause_all` pauses both attributes. + +Select the profile, its adapt-brightness switch, and a light managed by it. Match the minimum percentage to the profile's `min_brightness`. This YAML example assumes `min_brightness: 1`; change `minimum_pct` and the entity IDs to match your setup. + +```yaml +- alias: "Adaptive lighting: pause brightness at minimum" + mode: single + variables: + minimum_pct: 1 + minimum: "{{ (minimum_pct * 255 / 100) | round(0) }}" + triggers: + - trigger: state + entity_id: switch.adaptive_lighting_living_room + attribute: brightness_pct + conditions: + - condition: state + entity_id: + - switch.adaptive_lighting_living_room + - switch.adaptive_lighting_living_room_adapt_brightness + state: "on" + - condition: template + value_template: >- + {% set before = trigger.from_state.attributes.get('brightness_pct') + if trigger.from_state else none %} + {% set after = trigger.to_state.attributes.get('brightness_pct') + if trigger.to_state else none %} + {{ is_number(before) and is_number(after) + and (before | float * 255 / 100) | round(0) > minimum + and (after | float * 255 / 100) | round(0) <= minimum }} + - condition: state + entity_id: light.living_room + state: "on" + - condition: template + value_template: >- + {{ 'light.living_room' not in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or []) }} + actions: + - variables: + light_session: "{{ states.light.living_room.last_changed.isoformat() }}" + profile_session: "{{ states.switch.adaptive_lighting_living_room.last_changed.isoformat() }}" + brightness_session: "{{ states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() }}" + - wait_template: >- + {% set target = state_attr('switch.adaptive_lighting_living_room', 'brightness_pct') %} + {{ not is_state('light.living_room', 'on') + or not is_state('switch.adaptive_lighting_living_room', 'on') + or not is_state('switch.adaptive_lighting_living_room_adapt_brightness', 'on') + or states.light.living_room.last_changed.isoformat() != light_session + or states.switch.adaptive_lighting_living_room.last_changed.isoformat() != profile_session + or states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() != brightness_session + or not is_number(target) or (target | float * 255 / 100) | round(0) > minimum + or 'light.living_room' in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or []) + or (state_attr('light.living_room', 'brightness') | float(256)) <= minimum }} + timeout: "00:05:00" + continue_on_timeout: false + - condition: template + value_template: >- + {% set target = state_attr('switch.adaptive_lighting_living_room', 'brightness_pct') %} + {{ is_state('light.living_room', 'on') + and is_state('switch.adaptive_lighting_living_room', 'on') + and is_state('switch.adaptive_lighting_living_room_adapt_brightness', 'on') + and states.light.living_room.last_changed.isoformat() == light_session + and states.switch.adaptive_lighting_living_room.last_changed.isoformat() == profile_session + and states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() == brightness_session + and is_number(target) and (target | float * 255 / 100) | round(0) <= minimum + and (state_attr('light.living_room', 'brightness') | float(256)) <= minimum + and 'light.living_room' not in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or []) }} + - action: adaptive_lighting.set_manual_control + data: + entity_id: switch.adaptive_lighting_living_room + lights: light.living_room + manual_control: >- + {{ true if 'light.living_room' in + (state_attr('switch.adaptive_lighting_living_room', 'manual_control_color') or []) + else 'brightness' }} +``` + +The comparison uses the rounded 0–255 target, so it does not depend on sampling an exact floating-point minimum. It waits up to five minutes for the light to report that minimum before marking manual control, so the final dimming command can complete. Reported brightness does not prove physical fade completion. If the light, profile, or adapt-brightness switch is toggled, the target rises, brightness is marked manually controlled elsewhere, or brightness never reaches the minimum, the attempt is abandoned. Lights that cannot report the configured minimum will not be paused. Existing manual color flags are preserved, and lights whose brightness is already manually controlled are left alone. Manual-control state is shared for lights managed by multiple profiles, so their existing takeover policies still apply. + +The usual resets apply: turning the light off, the configured `autoreset_control_seconds` timeout, clearing manual control through its service, and existing profile/sleep-switch reset behavior. After a reset, normal adaptation can increase brightness again. This runs once per downward crossing; resetting while the target remains at its minimum does not immediately mark the light again. Startup at the minimum is not a crossing either. + +This pauses further dimming as well as brightening. To pause brightness immediately after a brightness change made through Home Assistant, use `take_over_control_mode: pause_changed` with `take_over_control: true`; that needs no additional automation. + +
+
Set sunrise and sunset from an alarm. diff --git a/tests/test_automation_examples.py b/tests/test_automation_examples.py index d0dc8e3e..b500f41e 100644 --- a/tests/test_automation_examples.py +++ b/tests/test_automation_examples.py @@ -17,6 +17,7 @@ from homeassistant.components.adaptive_lighting.adaptation_utils import ( LightControlAttributes, ) from homeassistant.components.adaptive_lighting.const import ( + CONF_AUTORESET_CONTROL, CONF_BRIGHTNESS_MODE, CONF_BRIGHTNESS_MODE_TIME_DARK, CONF_BRIGHTNESS_MODE_TIME_LIGHT, @@ -32,6 +33,7 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_SLEEP_RGB_OR_COLOR_TEMP, CONF_SUNRISE_TIME, CONF_SUNSET_TIME, + CONF_TAKE_OVER_CONTROL_MODE, CONF_TRANSITION, DOMAIN, ) @@ -379,6 +381,302 @@ async def test_minimum_brightness_ignores_missing_previous_target( assert hass.states.get("light.living_room").state == STATE_ON +@pytest.mark.parametrize("previous_manual", [None, "color", "brightness"]) +@patch( + "homeassistant.components.adaptive_lighting.color_and_brightness.utcnow", + new=dt_util.utcnow, +) +async def test_minimum_manual_control_lifecycle( + hass: HomeAssistant, + freezer, + published_automation, + previous_manual: str | None, +) -> None: + """Pause at the calculated floor, preserve other flags, and reset on off/on.""" + freezer.move_to(datetime(2026, 9, 6, 18, 58, tzinfo=dt_util.DEFAULT_TIME_ZONE)) + config = published_automation( + "Pause brightness at the minimum using manual control.", + "manual_control_at_minimum.yaml", + { + "adaptive_switch": "switch.adaptive_lighting_living_room", + "brightness_switch": "switch.adaptive_lighting_living_room_adapt_brightness", + "light_entity": "light.living_room", + }, + ) + await _setup_template_lights(hass, ["Living Room"]) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.living_room", ATTR_BRIGHTNESS: 77}, + blocking=True, + ) + _, profile = await setup_switch( + hass, + { + CONF_NAME: "Living Room", + CONF_LIGHTS: ["light.living_room"], + CONF_MIN_BRIGHTNESS: 1, + CONF_MAX_BRIGHTNESS: 100, + CONF_MIN_COLOR_TEMP: 3000, + CONF_MAX_COLOR_TEMP: 3000, + CONF_BRIGHTNESS_MODE: "linear", + CONF_BRIGHTNESS_MODE_TIME_DARK: timedelta(hours=1), + CONF_BRIGHTNESS_MODE_TIME_LIGHT: timedelta(hours=1), + CONF_SUNRISE_TIME: "06:00:00", + CONF_SUNSET_TIME: "18:00:00", + CONF_TRANSITION: 0, + CONF_INITIAL_TRANSITION: 0, + CONF_TAKE_OVER_CONTROL_MODE: "pause_changed", + }, + ) + if previous_manual: + await hass.services.async_call( + DOMAIN, + "set_manual_control", + {ATTR_ENTITY_ID: profile.entity_id, "manual_control": previous_manual}, + blocking=True, + ) + await _setup_automation(hass, config) + manual_calls = [] + + @callback + def record_manual_call(event: Event) -> None: + if ( + event.data["domain"] == DOMAIN + and event.data["service"] == "set_manual_control" + ): + manual_calls.append(event.data["service_data"]) + + hass.bus.async_listen(EVENT_CALL_SERVICE, record_manual_call) + freezer.move_to(datetime(2026, 9, 6, 18, 59, 50, tzinfo=dt_util.DEFAULT_TIME_ZONE)) + await profile._async_update_at_interval_action() + await hass.async_block_till_done() + flags = profile.manager.get_manual_control_attributes("light.living_room") + assert LightControlAttributes.BRIGHTNESS in flags + assert (LightControlAttributes.COLOR in flags) is (previous_manual == "color") + assert len(manual_calls) == (0 if previous_manual == "brightness" else 1) + paused_brightness = hass.states.get("light.living_room").attributes[ATTR_BRIGHTNESS] + assert profile.is_on + assert profile.adapt_brightness_switch.is_on + if previous_manual != "brightness": + assert paused_brightness == 3 + + freezer.move_to(datetime(2026, 9, 6, 19, 1, tzinfo=dt_util.DEFAULT_TIME_ZONE)) + await profile._async_update_at_interval_action() + await hass.async_block_till_done() + assert len(manual_calls) == (0 if previous_manual == "brightness" else 1) + await hass.services.async_call( + DOMAIN, + "change_switch_settings", + { + ATTR_ENTITY_ID: profile.entity_id, + CONF_MIN_BRIGHTNESS: 100, + CONF_MAX_BRIGHTNESS: 100, + CONF_MIN_COLOR_TEMP: 5000, + CONF_MAX_COLOR_TEMP: 5000, + }, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.living_room") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == paused_brightness + assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == pytest.approx( + 3000 if previous_manual == "color" else 5000, + abs=5, + ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.living_room"}, + blocking=True, + ) + await hass.async_block_till_done() + assert not profile.manager.get_manual_control_attributes("light.living_room") + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.living_room"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("light.living_room").attributes[ATTR_BRIGHTNESS] == 255 + + # Another crossing can pause again, but clearing it at the floor must stick. + await hass.services.async_call( + DOMAIN, + "change_switch_settings", + { + ATTR_ENTITY_ID: profile.entity_id, + CONF_MIN_BRIGHTNESS: 1, + CONF_MAX_BRIGHTNESS: 1, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert profile.manager.get_manual_control_attributes("light.living_room") + await hass.services.async_call( + DOMAIN, + "set_manual_control", + {ATTR_ENTITY_ID: profile.entity_id, "manual_control": False}, + blocking=True, + ) + await profile._async_update_at_interval_action() + await hass.async_block_till_done() + assert not profile.manager.get_manual_control_attributes("light.living_room") + + if previous_manual is None: + await hass.services.async_call( + DOMAIN, + "change_switch_settings", + { + ATTR_ENTITY_ID: profile.entity_id, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + CONF_AUTORESET_CONTROL: 1, + }, + blocking=True, + ) + await hass.services.async_call( + DOMAIN, + "change_switch_settings", + { + ATTR_ENTITY_ID: profile.entity_id, + CONF_MIN_BRIGHTNESS: 1, + CONF_MAX_BRIGHTNESS: 1, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert profile.manager.get_manual_control_attributes("light.living_room") + cleared, remove_listener = _state_waiter( + hass, + profile.entity_id, + lambda state: state.attributes.get("manual_control_brightness") == [], + ) + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass, dt_util.utcnow()) + await asyncio.wait_for(cleared, timeout=2) + remove_listener() + await hass.async_block_till_done() + assert not profile.manager.get_manual_control_attributes("light.living_room") + + +@pytest.mark.parametrize( + "abort_entity", + [ + "light.living_room", + "switch.adaptive_lighting_living_room", + "switch.adaptive_lighting_living_room_adapt_brightness", + ], +) +async def test_minimum_manual_control_aborts_when_disabled( + hass: HomeAssistant, + published_automation, + abort_entity: str, +) -> None: + """Abandon a pending wait immediately when the light or profile is disabled.""" + config = published_automation( + "Pause brightness at the minimum using manual control.", + "manual_control_at_minimum.yaml", + { + "adaptive_switch": "switch.adaptive_lighting_living_room", + "brightness_switch": "switch.adaptive_lighting_living_room_adapt_brightness", + "light_entity": "light.living_room", + }, + ) + await _setup_template_lights(hass, ["Living Room"]) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.living_room", ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + _, profile = await setup_switch( + hass, + { + CONF_NAME: "Living Room", + CONF_LIGHTS: ["light.living_room"], + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_TAKE_OVER_CONTROL_MODE: "pause_changed", + }, + ) + await _setup_automation(hass, config) + waiting, remove_listener = _state_waiter( + hass, + "automation.adaptive_lighting_pause_brightness_at_minimum", + lambda state: state.attributes.get("current") == 1, + ) + state = hass.states.get(profile.entity_id) + hass.states.async_set( + profile.entity_id, + STATE_ON, + {**state.attributes, "brightness_pct": 1}, + ) + await asyncio.wait_for(waiting, timeout=1) + remove_listener() + assert not profile.manager.get_manual_control_attributes("light.living_room") + stopped, remove_stopped = _state_waiter( + hass, + "automation.adaptive_lighting_pause_brightness_at_minimum", + lambda state: state.attributes.get("current") == 0, + ) + await hass.services.async_call( + abort_entity.split(".", maxsplit=1)[0], + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: abort_entity}, + blocking=True, + ) + try: + await asyncio.wait_for(stopped, timeout=1) + finally: + remove_stopped() + if stopped.cancelled(): + await hass.services.async_call( + automation.DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: "automation.adaptive_lighting_pause_brightness_at_minimum", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert not profile.manager.get_manual_control_attributes("light.living_room") + assert ( + hass.states.get( + "automation.adaptive_lighting_pause_brightness_at_minimum", + ).attributes["current"] + == 0 + ) + + await hass.services.async_call( + abort_entity.split(".", maxsplit=1)[0], + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: abort_entity}, + blocking=True, + ) + await profile._async_update_at_interval_action() + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + "change_switch_settings", + { + ATTR_ENTITY_ID: profile.entity_id, + CONF_MIN_BRIGHTNESS: 1, + CONF_MAX_BRIGHTNESS: 1, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert ( + profile.manager.get_manual_control_attributes("light.living_room") + == LightControlAttributes.BRIGHTNESS + ) + + async def test_schedule_profile_executes_blocks_and_restore( hass: HomeAssistant, published_automation, From 9e29a21197deeeba9a5eb96ad2da08e224fd7286 Mon Sep 17 00:00:00 2001 From: Alistair Galbraith Date: Sun, 6 Sep 2026 12:02:30 -0700 Subject: [PATCH 1063/1077] Add manual_control_on_external_turn_on option (#1490) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add `adapt_only_on_ha_turn_on` to skip adapting externally turned-on lights When a light turns on from `off` via a source outside Home Assistant — a physical wall switch or a hub/manufacturer scene (e.g. Lutron) — and `detect_non_ha_changes` is enabled, Adaptive Lighting adapts the light on the resulting `off` → `on` event, overriding the brightness/color the external source just set. Disabling `detect_non_ha_changes` avoids this but also stops detection of manual changes to already-on lights; the two behaviors were coupled to a single flag. Add `adapt_only_on_ha_turn_on` (default `false`, requires `take_over_control`). When enabled, an `off` → `on` transition with no matching HA `light.turn_on` context is marked `manual_control` and left untouched, independent of `detect_non_ha_changes`, decoupling the two behaviors. The off→on guard reduces to the previous expression when the option is `false`, so existing configurations are unaffected. Includes a parametrized regression test, docs, and regenerated strings/services/README via scripts/update-generated-content. Refs #435 Co-Authored-By: Claude Opus 4.8 * Shorten generated turn-on option description * Document shared turn-on policy limitations * Name external turn-on policy after manual-control behavior * Clarify settings needed to adapt unmatched turn-ons --------- Co-authored-by: Claude Opus 4.8 Co-authored-by: Bas Nijholt --- README.md | 3 + custom_components/adaptive_lighting/const.py | 15 ++ .../adaptive_lighting/services.yaml | 6 + .../adaptive_lighting/strings.json | 5 + custom_components/adaptive_lighting/switch.py | 30 +++- .../adaptive_lighting/translations/en.json | 5 + docs/advanced/manual-control.md | 22 +++ docs/configuration.md | 1 + docs/troubleshooting.md | 2 + tests/test_config_flow.py | 6 + tests/test_switch.py | 147 ++++++++++++++++++ 11 files changed, 235 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1ee91993..d15c5249 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ The YAML and frontend configuration methods support all of the options listed be | `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | | `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | | `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | +| `manual_control_on_external_turn_on` | Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | | `reset_manual_control_on_sleep_mode_change` | Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴 | `True` | `bool` | | `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | | `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | @@ -769,6 +770,8 @@ Addressing these issues will significantly improve your Home Assistant experienc In case lights are suddenly turning on by themselves, this is most likely due to the light incorrectly reporting an "on" state to Home Assistant, leading to an undesired Adaptive Lighting action. To prevent adapting in cases *where the state of the light is suddenly "on" and only adapt if there is an associated `light.turn_on` service call*, set `detect_non_ha_changes: false`. +To keep detecting manual changes to lights that are already on while leaving unmatched `off` to `on` state events unchanged, enable `manual_control_on_external_turn_on`. Matching uses the exact context of the most recently recorded `light.turn_on` call. Some integrations replace or omit that context, so Adaptive Lighting cannot distinguish every physical versus Home Assistant turn-on source. + #### :signal_strength: WiFi Networks Ensure your light bulbs have a strong WiFi connection. If the signal strength is less than -70dBm, the connection may be weak and prone to dropping messages. diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 58b37734..7acf2781 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -100,6 +100,16 @@ DOCS[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] = ( "Needs `take_over_control` enabled. 🕵️" ) +CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON = ( + "manual_control_on_external_turn_on", + False, +) +DOCS[CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON] = ( + "Treat turn-ons without a matching Home Assistant `light.turn_on` context as " + "manual control. Normal manual-control resets apply. Still allows " + "`detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️" +) + CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False DOCS[CONF_PREFER_RGB_COLOR] = ( "Whether to prefer RGB color adjustment over " @@ -416,6 +426,11 @@ VALIDATION_TUPLES: list[tuple[str, Any, Any]] = [ ), (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), (CONF_ADAPT_ONLY_ON_BARE_TURN_ON, DEFAULT_ADAPT_ONLY_ON_BARE_TURN_ON, bool), + ( + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, + DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, + bool, + ), ( CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, DEFAULT_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 2471e83a..23e8ceda 100644 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -238,6 +238,12 @@ change_switch_settings: example: false selector: boolean: null + manual_control_on_external_turn_on: + description: Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️ + required: false + example: false + selector: + boolean: null transition: description: Duration of transition when lights change, in seconds. 🕑 required: false diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index ff576a2b..6e07c69d 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -70,6 +70,7 @@ "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", + "manual_control_on_external_turn_on": "manual_control_on_external_turn_on: Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️", "reset_manual_control_on_sleep_mode_change": "reset_manual_control_on_sleep_mode_change: Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay", @@ -270,6 +271,10 @@ "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", "name": "detect_non_ha_changes" }, + "manual_control_on_external_turn_on": { + "description": "Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️", + "name": "manual_control_on_external_turn_on" + }, "transition": { "description": "Duration of transition when lights change, in seconds. 🕑", "name": "transition" diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 0a340c6c..ab9273df 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -104,6 +104,7 @@ from .const import ( CONF_INTERVAL, CONF_LIGHTS, CONF_MANUAL_CONTROL, + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, CONF_MAX_BRIGHTNESS, CONF_MAX_COLOR_TEMP, CONF_MAX_SUNRISE_TIME, @@ -955,12 +956,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._send_split_delay = data[CONF_SEND_SPLIT_DELAY] self._take_over_control = data[CONF_TAKE_OVER_CONTROL] if not data[CONF_TAKE_OVER_CONTROL] and ( - data[CONF_DETECT_NON_HA_CHANGES] or data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] + data[CONF_DETECT_NON_HA_CHANGES] + or data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] + or data[CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON] ): _LOGGER.warning( - "%s: Config mismatch: `detect_non_ha_changes` or `adapt_only_on_bare_turn_on` " - "set to `true` requires `take_over_control` to be enabled. Adjusting config " - "and continuing setup with `take_over_control: true`.", + "%s: Config mismatch: `detect_non_ha_changes`, `adapt_only_on_bare_turn_on`, " + "or `manual_control_on_external_turn_on` set to `true` requires `take_over_control` to be " + "enabled. Adjusting config and continuing setup with `take_over_control: true`.", self._name, ) self._take_over_control = True @@ -969,6 +972,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] self._adapt_only_on_bare_turn_on = data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] + self._manual_control_on_external_turn_on = data[ + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON + ] self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL] self._reset_manual_control_on_sleep_mode_change = data[ CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE @@ -1603,16 +1609,26 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) if ( self._take_over_control - and not self._detect_non_ha_changes + and ( + not self._detect_non_ha_changes + or self._manual_control_on_external_turn_on + ) and not from_turn_on ): # There is an edge case where 2 switches control the same light, e.g., # one for brightness and one for color. Now we will mark both switches # as manually controlled, which is not 100% correct. + # + # This 'off' → 'on' event does not exactly match the most recently tracked + # `light.turn_on` context for the entity. Hand control over when either: + # - `detect_non_ha_changes` is False (we can't reliably track manual changes + # to already-on lights anyway), or + # - `manual_control_on_external_turn_on` is True (the user explicitly wants external + # turn-ons left untouched, even while `detect_non_ha_changes` is enabled). _LOGGER.debug( "%s: Ignoring 'off' → 'on' event for '%s' with context.id='%s'" - " because 'light.turn_on' was not called by HA and" - " 'detect_non_ha_changes' is False", + " because it does not match a tracked 'light.turn_on' context and" + " ('detect_non_ha_changes' is False or 'manual_control_on_external_turn_on' is True)", self._name, entity_id, event.context.id, diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 688bb984..2f1a7085 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -71,6 +71,7 @@ "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️", + "manual_control_on_external_turn_on": "manual_control_on_external_turn_on: Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️", "reset_manual_control_on_sleep_mode_change": "reset_manual_control_on_sleep_mode_change: Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "send_split_delay": "send_split_delay", @@ -271,6 +272,10 @@ "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", "name": "detect_non_ha_changes" }, + "manual_control_on_external_turn_on": { + "description": "Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️", + "name": "manual_control_on_external_turn_on" + }, "transition": { "description": "Duration of transition when lights change, in seconds. 🕑", "name": "transition" diff --git a/docs/advanced/manual-control.md b/docs/advanced/manual-control.md index 4e84ca3f..dc099c57 100644 --- a/docs/advanced/manual-control.md +++ b/docs/advanced/manual-control.md @@ -112,6 +112,28 @@ adaptive_lighting: adapt_only_on_bare_turn_on: true ``` +### manual_control_on_external_turn_on + +When enabled, a turn-on without a state-change context matching the latest recorded Home Assistant `light.turn_on` is treated as manual control. This pauses brightness and color adaptation until manual control resets, rather than skipping just the first adjustment. The usual off/on, explicit reset, and configured timeout rules apply. A later unmatched turn-on marks the light manually controlled again. + +Manual-control flags are shared by profiles controlling the same light. Use the same turn-on policy on those profiles; mixed policies can allow an earlier profile to adapt before another marks the light manually controlled. + +Enable this if you want turn-ons from physical controls or native scenes to preserve their brightness and color. To adapt unmatched turn-ons, leave this disabled and enable `detect_non_ha_changes`. + +Its advantage over simply disabling `detect_non_ha_changes` is that the two behaviors are decoupled: you can keep `detect_non_ha_changes: true` to catch manual dimming of lights that are *already on*, while leaving unmatched turn-ons untouched. + +Adaptive Lighting cannot identify every physical versus Home Assistant source. Some integrations replace or omit the service context when they publish device state. In that case, even a Home Assistant turn-on does not match and this option treats it as external. + +```yaml +adaptive_lighting: + - name: "Respect physical switches and Lutron scenes" + lights: + - light.living_room + take_over_control: true + detect_non_ha_changes: true # still catch manual changes to already-on lights + manual_control_on_external_turn_on: true # leave unmatched off→on events unchanged +``` + ## Checking Manual Control Status You can see which lights are marked as manually controlled by checking the switch attributes: diff --git a/docs/configuration.md b/docs/configuration.md index 7edaac57..2a7402f0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -66,6 +66,7 @@ All configuration options are listed below with their default values. These opti | `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | | `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | | `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | +| `manual_control_on_external_turn_on` | Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | | `reset_manual_control_on_sleep_mode_change` | Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴 | `True` | `bool` | | `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | | `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index c5f59353..f39c62bc 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -59,6 +59,8 @@ Addressing these issues will significantly improve your Home Assistant experienc In case lights are suddenly turning on by themselves, this is most likely due to the light incorrectly reporting an "on" state to Home Assistant, leading to an undesired Adaptive Lighting action. To prevent adapting in cases *where the state of the light is suddenly "on" and only adapt if there is an associated `light.turn_on` service call*, set `detect_non_ha_changes: false`. +To keep detecting manual changes to lights that are already on while leaving unmatched `off` to `on` state events unchanged, enable `manual_control_on_external_turn_on`. Matching uses the exact context of the most recently recorded `light.turn_on` call. Some integrations replace or omit that context, so Adaptive Lighting cannot distinguish every physical versus Home Assistant turn-on source. + #### :signal_strength: WiFi Networks Ensure your light bulbs have a strong WiFi connection. If the signal strength is less than -70dBm, the connection may be weak and prone to dropping messages. diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 04cbed5b..241bd5ae 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -12,8 +12,10 @@ except ImportError: from homeassistant.components.adaptive_lighting.const import ( BASIC_OPTIONS, CONF_INITIAL_TRANSITION, + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, CONF_SUNRISE_TIME, CONF_SUNSET_TIME, + DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, DEFAULT_NAME, DOMAIN, NONE_STR, @@ -149,6 +151,10 @@ async def test_options_schema_has_each_setting_once(hass): advanced = _advanced_section(result) assert advanced.options == {"collapsed": True} + assert ( + _schema_defaults(advanced.schema)[CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON] + is DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON + ) assert {key.schema for key in schema if key.schema != "advanced"} == BASIC_OPTIONS assert {key.schema for key in advanced.schema.schema} == set( DEFAULT_DATA, diff --git a/tests/test_switch.py b/tests/test_switch.py index cfebc4f6..1765fb5e 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -39,6 +39,7 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_DETECT_NON_HA_CHANGES, CONF_INITIAL_TRANSITION, CONF_MANUAL_CONTROL, + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, CONF_MAX_BRIGHTNESS, CONF_MAX_COLOR_TEMP, CONF_MIN_BRIGHTNESS, @@ -4473,6 +4474,152 @@ async def test_automation_turn_on_from_off_not_marked_as_manual_control(hass): ) +@pytest.mark.parametrize("intercept", [True, False]) +async def test_manual_control_on_external_turn_on_allows_tracked_service_call( + hass, + intercept, +): + """Test a real HA turn-on remains eligible for initial adaptation.""" + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON: True, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_INTERCEPT: intercept, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1}, + blocking=True, + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_BRIGHTNESS: 200}, + blocking=True, + context=Context(id=f"ha_turn_on_{intercept}"), + ) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_LIGHT_1) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.NONE + ) + + +@pytest.mark.parametrize("intercept", [True, False]) +@pytest.mark.parametrize( + ( + "manual_control_on_external_turn_on", + "detect_non_ha_changes", + "expected_manual_control", + "expected_adaptation", + ), + [ + (True, True, LightControlAttributes.ALL, False), + (True, False, LightControlAttributes.ALL, False), + (False, True, LightControlAttributes.NONE, True), + (False, False, LightControlAttributes.ALL, False), + ], +) +async def test_manual_control_on_external_turn_on_external_state_change( + hass, + freezer, + intercept, + manual_control_on_external_turn_on, + detect_non_ha_changes, + expected_manual_control, + expected_adaptation, +): + """Test an unmatched off-to-on state event follows the opt-in policy.""" + switch, _ = await setup_lights_and_switch( + hass, + { + "manual_control_on_external_turn_on": manual_control_on_external_turn_on, + CONF_DETECT_NON_HA_CHANGES: detect_non_ha_changes, + CONF_INTERCEPT: intercept, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + external_attributes = dict(hass.states.get(ENTITY_LIGHT_1).attributes) + external_attributes[ATTR_BRIGHTNESS] = 200 + hass.states.async_set( + ENTITY_LIGHT_1, + STATE_OFF, + external_attributes, + context=Context(id=f"unmatched_turn_off_{intercept}"), + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_OFF + freezer.tick(6) + + hass.states.async_set( + ENTITY_LIGHT_1, + STATE_ON, + external_attributes, + context=Context(id=f"unmatched_turn_on_{intercept}"), + ) + await hass.async_block_till_done() + + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == expected_manual_control + ) + last_service_data = switch.manager.last_service_data.get(ENTITY_LIGHT_1) + if expected_adaptation: + assert last_service_data[ATTR_BRIGHTNESS] == 128 + else: + assert last_service_data is None + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 200 + + +@pytest.mark.parametrize("intercept", [True, False]) +async def test_manual_control_on_external_turn_on_keeps_non_ha_change_detection( + hass, + intercept, +): + """Test the option does not disable manual tracking for an on light.""" + switch, (light, *_) = await setup_lights_and_switch( + hass, + { + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON: True, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_INTERCEPT: intercept, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + transition=0, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 128 + + set_light_brightness(light, 200) + light.async_write_ha_state() + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + transition=0, + ) + await hass.async_block_till_done() + + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 200 + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.BRIGHTNESS + ) + + @pytest.mark.parametrize("intercept", [True, False]) async def test_adapt_only_on_bare_turn_on_respects_pause_changed_mode(hass, intercept): """Test that adapt_only_on_bare_turn_on respects take_over_control_mode=PAUSE_CHANGED. From aa84eda871b391363b992585f6f2bd32d8590f33 Mon Sep 17 00:00:00 2001 From: Leonhard Hesse <44778508+hesseleo@users.noreply.github.com> Date: Sun, 6 Sep 2026 21:13:35 +0200 Subject: [PATCH 1064/1077] feat: add expand_light_groups option (#1462) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add expand_light_groups option Some light group entities act as a proxy that must receive a single combined `light.turn_on` call to function correctly — virtual mixers like for instance, that blend a warm and a cold white channel into one entity. In such setups the individual member entities only expose `ColorMode.BRIGHTNESS`, so sending separate per-member commands bypasses the mixing logic. Setting `expand_light_groups: false` keeps the group entity in `self.lights` instead of expanding it to its members. Adaptation commands go to the group, and the interceptor no longer skips group entities for that switch. Default is `true` — no behaviour change for existing configurations. * tests: regression test for expand_light_groups=False _switches_with_lights was expanding the incoming entity_id globally, causing the switch to never be found when expand_light_groups=False * Resolve group targets consistently across adaptation paths * Discard delayed group events after target changes * Stabilize delayed group target regression test --------- Co-authored-by: Bas Nijholt --- README.md | 5 + custom_components/adaptive_lighting/const.py | 9 + .../adaptive_lighting/services.yaml | 6 + .../adaptive_lighting/strings.json | 7 +- custom_components/adaptive_lighting/switch.py | 102 +++- .../adaptive_lighting/translations/de.json | 6 +- .../adaptive_lighting/translations/en.json | 7 +- docs/advanced/manual-control.md | 4 + docs/configuration.md | 1 + tests/test_config_flow.py | 2 + tests/test_switch.py | 454 ++++++++++++++++-- 11 files changed, 547 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index d15c5249..3f924fb8 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,10 @@ This feature is available when `take_over_control` is enabled. Additionally, enabling `detect_non_ha_changes` allows Adaptive Lighting to detect all state changes, including those made outside of Home Assistant, by comparing the light's state to its previously used settings. The `adaptive_lighting.manual_control` event is fired when a light is marked as "manually controlled," allowing for integration with automations 🤖. +With `expand_light_groups: false`, manual control belongs to the group. A direct member change cannot pause adaptation for only that member; use group-level manual control or enable expansion for individual tracking. +Explicit member targets in Adaptive Lighting services stay individual targets and do not mark or command the whole group. +Changing expansion at runtime discards tracking and pending adaptation for targets no longer used by any profile. + The Adaptive Lighting switch exposes these read-only attributes for its lights: - `manual_control`: lights with any attribute marked as manually controlled. @@ -166,6 +170,7 @@ The YAML and frontend configuration methods support all of the options listed be | `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | | `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | | `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| `expand_light_groups` | Expand light groups to their members (`true`, default). Set `false` to send commands to the group and track manual control for the group. Explicit member targets in services stay individual targets. | `True` | `bool` | diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 7acf2781..afead25c 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -291,6 +291,14 @@ DOCS[CONF_MULTI_LIGHT_INTERCEPT] = ( "Requires `intercept` to be enabled." ) +CONF_EXPAND_LIGHT_GROUPS, DEFAULT_EXPAND_LIGHT_GROUPS = "expand_light_groups", True +DOCS[CONF_EXPAND_LIGHT_GROUPS] = ( + "Expand light groups to their members (`true`, default). Set `false` to send " + "commands to the group and track manual control for the group. Explicit member " + "targets in services stay individual targets." +) + + SLEEP_MODE_SWITCH = "sleep_mode_switch" ADAPT_COLOR_SWITCH = "adapt_color_switch" ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch" @@ -447,6 +455,7 @@ VALIDATION_TUPLES: list[tuple[str, Any, Any]] = [ (CONF_INTERCEPT, DEFAULT_INTERCEPT, bool), (CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT, bool), (CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES, bool), + (CONF_EXPAND_LIGHT_GROUPS, DEFAULT_EXPAND_LIGHT_GROUPS, bool), ] diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 23e8ceda..c95e31b5 100644 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -139,6 +139,12 @@ change_switch_settings: example: false selector: boolean: null + expand_light_groups: + description: Expand light groups to their members (`true`, default). Set `false` to send commands to the group and track manual control for the group. Explicit member targets in services stay individual targets. + required: false + example: true + selector: + boolean: null separate_turn_on_commands: description: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 required: false diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 6e07c69d..9e22ff57 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -78,7 +78,8 @@ "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.", "intercept": "intercept: Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness.", "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled.", - "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝" + "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝", + "expand_light_groups": "expand_light_groups: Expand light groups to their members (`true`, default). Set `false` to send commands to the group and track manual control for the group. Explicit member targets in services stay individual targets." }, "data_description": { "initial_transition": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", @@ -211,6 +212,10 @@ "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", "name": "prefer_rgb_color" }, + "expand_light_groups": { + "description": "Expand light groups to their members (`true`, default). Set `false` to send commands to the group and track manual control for the group. Explicit member targets in services stay individual targets.", + "name": "expand_light_groups" + }, "separate_turn_on_commands": { "description": "Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "name": "separate_turn_on_commands" diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index ab9273df..c85087a9 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -98,6 +98,7 @@ from .const import ( CONF_BRIGHTNESS_MODE_TIME_DARK, CONF_BRIGHTNESS_MODE_TIME_LIGHT, CONF_DETECT_NON_HA_CHANGES, + CONF_EXPAND_LIGHT_GROUPS, CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, CONF_INITIAL_TRANSITION, CONF_INTERCEPT, @@ -252,14 +253,11 @@ def _switches_with_lights( if not loaded_switches: return [] - all_check_lights = ( - _expand_light_groups(hass, lights) if expand_light_groups else set(lights) - ) switches: AdaptiveSwitches = [] for switch in loaded_switches: - switch._expand_light_groups(hass=hass) - # Check if any of the lights are in the switch's lights - if set(switch.lights) & set(all_check_lights): + switch._expand_light_groups() + check_lights = switch._resolve_lights(lights) if expand_light_groups else lights + if set(switch.lights) & set(check_lights): switches.append(switch) return switches @@ -422,7 +420,7 @@ async def handle_apply_service(hass: HomeAssistant, service_call: ServiceCall) - switches = _switches_from_service_call(hass, service_call) lights = data[CONF_LIGHTS] for switch in switches: - all_lights = switch.lights if not lights else _expand_light_groups(hass, lights) + all_lights = switch._resolve_lights(lights or None) switch.manager.lights.update(all_lights) for light in all_lights: if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light): @@ -457,7 +455,7 @@ async def handle_set_manual_control_service( switches = _switches_from_service_call(hass, service_call) lights = data[CONF_LIGHTS] for switch in switches: - all_lights = switch.lights if not lights else _expand_light_groups(hass, lights) + all_lights = switch._resolve_lights(lights or None) manual_attributes = manual_control_event_attribute_to_flags( data[CONF_MANUAL_CONTROL], ) @@ -626,17 +624,22 @@ def _expand_light_groups( hass: HomeAssistant, lights: list[str], ) -> list[str]: + """Resolve nested groups without changing another profile's tracked targets.""" all_lights: set[str] = set() - manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER] - for light in lights: + pending = list(lights) + visited: set[str] = set() + while pending: + light = pending.pop() + if light in visited: + continue + visited.add(light) state = hass.states.get(light) if state is None: _LOGGER.debug("State of %s is None", light) all_lights.add(light) elif _is_light_group(state): group = state.attributes["entity_id"] - manager.lights.discard(light) - all_lights.update(group) + pending.extend(group) _LOGGER.debug("Expanded %s to %s", light, group) else: all_lights.add(light) @@ -889,7 +892,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._name = data[CONF_NAME] self._interval: timedelta = data[CONF_INTERVAL] - self.lights: list[str] = data[CONF_LIGHTS] + self._configured_lights: list[str] = list(data[CONF_LIGHTS]) + self.lights: list[str] = [] # backup data for use in change_switch_settings "configuration" CONF_USE_DEFAULTS self._config_backup = deepcopy(data) @@ -990,6 +994,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._name, ) self._multi_light_intercept = False + self._expand_light_groups_flag = data[CONF_EXPAND_LIGHT_GROUPS] self._expand_light_groups() # updates manual control timers observer = get_astral_observer(self.hass) @@ -1073,15 +1078,29 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): """Remove the listeners upon removing the component.""" self._remove_listeners() - def _expand_light_groups(self, hass: HomeAssistant | None = None) -> None: - hass = hass or self.hass - all_lights = _expand_light_groups(hass, self.lights) + def _resolve_lights(self, lights: list[str] | None = None) -> list[str]: + """Apply this profile's group policy, preserving explicit member targets.""" + if lights is None: + lights = self._configured_lights + if self._expand_light_groups_flag: + return _expand_light_groups(self.hass, lights) + return sorted(set(lights)) + + def _expand_light_groups(self) -> None: + all_lights = self._resolve_lights() + removed = set(self.lights) - set(all_lights) + self.lights = all_lights + if removed: + # Other profiles may still own a retired member or group, even when off. + for entry in self.hass.data[DOMAIN].values(): + if isinstance(entry, dict) and (switch := entry.get(SWITCH_DOMAIN)): + removed.difference_update(switch._resolve_lights()) + self.manager.remove_lights(*removed) self.manager.lights.update(all_lights) self.manager.set_auto_reset_manual_control_times( all_lights, self._auto_reset_manual_control_time, ) - self.lights = list(all_lights) async def _setup_listeners(self, _: Event[NoEventData] | None = None) -> None: _LOGGER.debug("%s: Called '_setup_listeners'", self._name) @@ -1521,6 +1540,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return if lights is None: + self._expand_light_groups() lights = self.lights on_lights = [light for light in lights if is_on(self.hass, light)] @@ -1668,6 +1688,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self._adapt_delay > 0: await asyncio.sleep(self._adapt_delay) + # Runtime settings may retire this profile's target while the event waits. + if entity_id not in self.lights: + return + await self._update_attrs_and_maybe_adapt_lights( context=self.create_context("light_event", parent=event.context), lights=[entity_id], @@ -1983,8 +2007,12 @@ class AdaptiveLightingManager: if ( not switch.is_on or not switch._intercept - # Never adapt on light groups, because HA will make a separate light.turn_on - or ((e := self.hass.states.get(entity_id)) and _is_light_group(e)) + # Never adapt on light groups when expanding, because HA will make a separate light.turn_on + or ( + switch._expand_light_groups_flag + and (e := self.hass.states.get(entity_id)) + and _is_light_group(e) + ) # Prevent adaptation of TURN_ON calls when light is already on, # and of TOGGLE calls when toggling off. or self.hass.states.is_state(entity_id, STATE_ON) @@ -2381,7 +2409,11 @@ class AdaptiveLightingManager: delay, ) self.reset(light) - switches = _switches_with_lights(self.hass, [light]) + switches = _switches_with_lights( + self.hass, + [light], + expand_light_groups=False, + ) for switch in switches: if not switch.is_on: continue @@ -2572,6 +2604,30 @@ class AdaptiveLightingManager: if reset_manual_control: self._schedule_manual_control_state_update(*lights) + def remove_lights(self, *lights: str) -> None: + """Retire tracking and pending work for targets no profile owns anymore.""" + self.reset(*lights) + for light in lights: + self.lights.discard(light) + self.clear_proactively_adapting(light) + if timer := self.transition_timers.pop(light, None): + timer.cancel() + if task := self.sleep_tasks.pop(light, None): + task.cancel() + for records in ( + self.manual_control, + self.auto_reset_manual_control_times, + self.turn_on_event, + self.turn_off_event, + self.toggle_event, + self.on_to_off_event, + self.off_to_on_event, + self.turn_off_locks, + self.adaptation_tasks_brightness, + self.adaptation_tasks_color, + ): + records.pop(light, None) + def _get_entity_list(self, service_data: ServiceData) -> list[str]: if ATTR_ENTITY_ID in service_data: return cv.ensure_list_csv(service_data[ATTR_ENTITY_ID]) @@ -2806,7 +2862,11 @@ class AdaptiveLightingManager: ) return - switches = _switches_with_lights(self.hass, [entity_id]) + switches = _switches_with_lights( + self.hass, + [entity_id], + expand_light_groups=False, + ) for switch in switches: if switch.is_on: await switch._respond_to_off_to_on_event( diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json index a03c238f..4885e753 100644 --- a/custom_components/adaptive_lighting/translations/de.json +++ b/custom_components/adaptive_lighting/translations/de.json @@ -68,7 +68,8 @@ "skip_redundant_commands": "skip_redundant_commands: Überspringt das Senden von Anpassungsbefehlen, deren Zielzustand bereits mit dem bekannten Zustand der Leuchte übereinstimmt. Minimiert den Netzwerkverkehr und verbessert die Anpassungsreaktion in einigen Situationen. 📉 Deaktivieren, falls der physikalische Zustand der Lichter nicht mehr mit dem Zustand in HA übereinstimmt.", "intercept": "intercept: Abfangen und Anpassen von `light.turn_on`-Aufrufen, um eine sofortige Anpassung von Farbe und Helligkeit zu ermöglichen. 🏎️ Deaktivieren für Leuchten, die `light.turn_on` mit Farbe und Helligkeit nicht unterstützen.", "multi_light_intercept": "multi_light_intercept: Abfangen und Anpassen von `light.turn_on`-Aufrufen, die auf mehrere Lichter aufrufen. ➗⚠️ Dies kann dazu führen, dass ein einzelner `light.turn_on`-Aufruf in mehrere Aufrufe aufgeteilt wird, z.B. wenn Lichter in verschiedenen Schaltern sind. Erfordert, dass `intercept` aktiviert ist.", - "include_config_in_attributes": "include_config_in_attributes: Alle Optionen als Attribute auf dem Schalter im Home Assistant anzeigen, wenn auf `true` gesetzt. 📝" + "include_config_in_attributes": "include_config_in_attributes: Alle Optionen als Attribute auf dem Schalter im Home Assistant anzeigen, wenn auf `true` gesetzt. 📝", + "expand_light_groups": "expand_light_groups: Lichtgruppen auf einzelne Mitgliedsentitäten erweitern (`true`) oder die Gruppenentität direkt steuern (`false`)." }, "data_description": { "initial_transition": "Dauer des ersten Übergangs, wenn das Licht von `off` auf `on` schaltet, in Sekunden. ⏲️", @@ -88,7 +89,8 @@ "brightness_mode_time_light": "(Wird ignoriert, wenn `brightness_mode='default'`) Die Dauer in Sekunden, um die Helligkeit nach/vor Sonnenaufgang/Sonnenuntergang hoch/runter zu fahren. 📈📉.", "autoreset_control_seconds": "Setzt die manuelle Steuerung nach einer bestimmten Anzahl von Sekunden automatisch zurück. Zum Deaktivieren auf 0 setzen. ⏲️", "send_split_delay": "Verzögerung (ms) zwischen `separate_turn_on_commands` für Leuchten, die keine gleichzeitige Einstellung von Helligkeit und Farbe unterstützen. ⏲️", - "adapt_delay": "Wartezeit (Sekunden) zwischen dem Einschalten des Lichts und der Anwendung der adaptiven Beleuchtung. Könnte helfen, Flackern zu vermeiden. ⏲️" + "adapt_delay": "Wartezeit (Sekunden) zwischen dem Einschalten des Lichts und der Anwendung der adaptiven Beleuchtung. Könnte helfen, Flackern zu vermeiden. ⏲️", + "expand_light_groups": "Wenn auf `false` gesetzt, werden Anpassungsbefehle direkt an die Gruppenentität gesendet und nicht an die einzelnen Mitglieder." } } } diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 2f1a7085..cc589156 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -79,7 +79,8 @@ "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.", "intercept": "intercept: Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness.", "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled.", - "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝" + "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝", + "expand_light_groups": "expand_light_groups: Expand light groups to their members (`true`, default). Set `false` to send commands to the group and track manual control for the group. Explicit member targets in services stay individual targets." }, "data_description": { "initial_transition": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", @@ -212,6 +213,10 @@ "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", "name": "prefer_rgb_color" }, + "expand_light_groups": { + "description": "Expand light groups to their members (`true`, default). Set `false` to send commands to the group and track manual control for the group. Explicit member targets in services stay individual targets.", + "name": "expand_light_groups" + }, "separate_turn_on_commands": { "description": "Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", "name": "separate_turn_on_commands" diff --git a/docs/advanced/manual-control.md b/docs/advanced/manual-control.md index dc099c57..4b0bfe51 100644 --- a/docs/advanced/manual-control.md +++ b/docs/advanced/manual-control.md @@ -20,6 +20,10 @@ This feature is available when `take_over_control` is enabled. Additionally, enabling `detect_non_ha_changes` allows Adaptive Lighting to detect all state changes, including those made outside of Home Assistant, by comparing the light's state to its previously used settings. The `adaptive_lighting.manual_control` event is fired when a light is marked as "manually controlled," allowing for integration with automations 🤖. +With `expand_light_groups: false`, manual control belongs to the group. A direct member change cannot pause adaptation for only that member; use group-level manual control or enable expansion for individual tracking. +Explicit member targets in Adaptive Lighting services stay individual targets and do not mark or command the whole group. +Changing expansion at runtime discards tracking and pending adaptation for targets no longer used by any profile. + The Adaptive Lighting switch exposes these read-only attributes for its lights: - `manual_control`: lights with any attribute marked as manually controlled. diff --git a/docs/configuration.md b/docs/configuration.md index 2a7402f0..afd05721 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -75,6 +75,7 @@ All configuration options are listed below with their default values. These opti | `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | | `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | | `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| `expand_light_groups` | Expand light groups to their members (`true`, default). Set `false` to send commands to the group and track manual control for the group. Explicit member targets in services stay individual targets. | `True` | `bool` | diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 241bd5ae..addc460c 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -11,6 +11,7 @@ except ImportError: from voluptuous_serialize import convert as to_field_list from homeassistant.components.adaptive_lighting.const import ( BASIC_OPTIONS, + CONF_EXPAND_LIGHT_GROUPS, CONF_INITIAL_TRANSITION, CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, CONF_SUNRISE_TIME, @@ -106,6 +107,7 @@ async def test_options(hass): # Build input with advanced options nested in "advanced" section advanced_data = ADVANCED_DATA.copy() advanced_data[CONF_INITIAL_TRANSITION] = 23 + advanced_data[CONF_EXPAND_LIGHT_GROUPS] = False advanced_data[CONF_SUNRISE_TIME] = NONE_STR advanced_data[CONF_SUNSET_TIME] = NONE_STR basic_data = {**BASIC_DATA, "min_brightness": 12} diff --git a/tests/test_switch.py b/tests/test_switch.py index 1765fb5e..0b9129e3 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -37,6 +37,7 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_BRIGHTNESS_MODE_TIME_DARK, CONF_BRIGHTNESS_MODE_TIME_LIGHT, CONF_DETECT_NON_HA_CHANGES, + CONF_EXPAND_LIGHT_GROUPS, CONF_INITIAL_TRANSITION, CONF_MANUAL_CONTROL, CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, @@ -2301,7 +2302,8 @@ def test_attributes_have_changed(): async def test_state_change_handlers(hass): """Test AdaptiveLightingManager's EVENT_STATE_CHANGED listener. - ====================== + =============== + Sequence of events: 1. Transition from sleep mode to normal. 2. Create simulated transition events for that adapt. @@ -3824,6 +3826,407 @@ async def test_light_group( assert len(events) == 3 +def _track_adaptive_light_calls(hass, *, ours_only=True): + """Capture commands emitted by Adaptive Lighting at the HA service boundary.""" + calls = [] + + def track(event): + if ( + event.data["domain"] == LIGHT_DOMAIN + and event.data["service"] == SERVICE_TURN_ON + and (not ours_only or is_our_context(event.context)) + ): + calls.append(event.data["service_data"]) + + hass.bus.async_listen(EVENT_CALL_SERVICE, track) + return calls + + +async def _setup_group_switch(hass, **settings): + return await setup_switch( + hass, + { + CONF_LIGHTS: ["light.light_group"], + CONF_INITIAL_TRANSITION: 0, + CONF_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + **settings, + }, + ) + + +@pytest.mark.parametrize("intercept", [False, True]) +async def test_light_group_expand_disabled_off_to_on(hass, intercept, cleanup): + """Group adaptation reaches members through the group on both turn-on paths.""" + await setup_lights(hass, with_group=True) + _, switch = await _setup_group_switch( + hass, + expand_light_groups=False, + intercept=intercept, + ) + calls = _track_adaptive_light_calls(hass, ours_only=False) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.light_group"}, + blocking=True, + ) + await hass.async_block_till_done() + await asyncio.gather(*switch.manager.adaptation_tasks) + # HA emits the original service event before the interceptor changes its data. + # The group forwards the injected values to its real member entities. + expected_target = ( + ["light.light_4", "light.light_5"] if intercept else "light.light_group" + ) + assert any( + call[ATTR_ENTITY_ID] == expected_target and call.get(ATTR_BRIGHTNESS) == 128 + for call in calls + ), calls + for member in ["light.light_4", "light.light_5"]: + assert hass.states.get(member).attributes[ATTR_BRIGHTNESS] == 128 + + calls.clear() + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert any(call[ATTR_ENTITY_ID] == "light.light_group" for call in calls) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.light_group", ATTR_BRIGHTNESS: 77}, + blocking=True, + ) + await hass.async_block_till_done() + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert hass.states.get(switch.entity_id).attributes["manual_control"] == [ + "light.light_group", + ] + for member in ["light.light_4", "light.light_5"]: + assert hass.states.get(member).attributes[ATTR_BRIGHTNESS] == 77 + + +@pytest.mark.parametrize("expand", [False, True]) +@pytest.mark.parametrize("explicit_member", [False, True]) +async def test_group_apply_respects_target_policy( + hass, + expand, + explicit_member, + cleanup, +): + """Apply obeys profile expansion without widening an explicit member request.""" + await setup_lights(hass, with_group=True) + _, switch = await _setup_group_switch(hass, expand_light_groups=expand) + calls = _track_adaptive_light_calls(hass) + target = "light.light_4" if explicit_member else "light.light_group" + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: [target], + CONF_TURN_ON_LIGHTS: True, + }, + blocking=True, + ) + await hass.async_block_till_done() + expected = ( + ["light.light_4"] + if explicit_member + else (["light.light_4", "light.light_5"] if expand else ["light.light_group"]) + ) + # HA also emits forwarded member calls; AL's own commands use a scalar target. + assert ( + sorted( + { + call[ATTR_ENTITY_ID] + for call in calls + if isinstance(call[ATTR_ENTITY_ID], str) + }, + ) + == expected + ) + assert hass.states.get("light.light_4").attributes[ATTR_BRIGHTNESS] == 128 + assert hass.states.get("light.light_5").state == ( + STATE_OFF if explicit_member else STATE_ON + ) + + +@pytest.mark.parametrize("expand", [False, True]) +async def test_group_manual_control_service(hass, expand, cleanup): + """Group lookup and manual service use the same target policy, including reset.""" + await setup_lights(hass, with_group=True) + _, switch = await _setup_group_switch(hass, expand_light_groups=expand) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + {CONF_LIGHTS: ["light.light_group"], CONF_MANUAL_CONTROL: True}, + blocking=True, + ) + await hass.async_block_till_done() + expected = ["light.light_4", "light.light_5"] if expand else ["light.light_group"] + assert hass.states.get(switch.entity_id).attributes["manual_control"] == expected + for target in expected: + assert switch.manager.manual_control[target] == LightControlAttributes.ALL + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + {CONF_LIGHTS: ["light.light_group"], CONF_MANUAL_CONTROL: False}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(switch.entity_id).attributes["manual_control"] == [] + + +@pytest.mark.parametrize("shared_member", [False, True]) +async def test_group_runtime_expansion_restores_targets(hass, shared_member, cleanup): + """Changing expansion back and forth restores groups and retires member timers.""" + await setup_lights(hass, with_group=True) + _, switch = await _setup_group_switch(hass, autoreset_control_seconds=60) + if shared_member: + _, other = await _setup_group_switch( + hass, + name="member", + lights=["light.light_4"], + autoreset_control_seconds=60, + ) + await other.async_turn_off() + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + {ATTR_ENTITY_ID: switch.entity_id, CONF_TURN_ON_LIGHTS: True}, + blocking=True, + ) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_LIGHTS: ["light.light_group"], + CONF_MANUAL_CONTROL: True, + }, + blocking=True, + ) + manager = switch.manager + old_timers = dict(manager.auto_reset_manual_control_timers) + assert len(old_timers) == 2 + calls = _track_adaptive_light_calls(hass) + await hass.services.async_call( + DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + {ATTR_ENTITY_ID: switch.entity_id, CONF_EXPAND_LIGHT_GROUPS: False}, + blocking=True, + ) + await hass.async_block_till_done() + assert switch.lights == ["light.light_group"] + retained = {"light.light_4"} if shared_member else set() + assert manager.lights == {"light.light_group"} | retained + assert set(manager.auto_reset_manual_control_timers) == retained + for light, timer in old_timers.items(): + assert timer.is_running() == (light in retained) + assert ( + manager.manual_control.get("light.light_5", LightControlAttributes.NONE) + == LightControlAttributes.NONE + ) + assert any(call[ATTR_ENTITY_ID] == "light.light_group" for call in calls) + calls.clear() + await hass.services.async_call( + DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + {ATTR_ENTITY_ID: switch.entity_id, CONF_EXPAND_LIGHT_GROUPS: True}, + blocking=True, + ) + await hass.async_block_till_done() + assert switch.lights == ["light.light_4", "light.light_5"] + assert manager.lights == {"light.light_4", "light.light_5"} + expected = ( + ["light.light_5"] if shared_member else ["light.light_4", "light.light_5"] + ) + assert sorted({call[ATTR_ENTITY_ID] for call in calls}) == expected + if shared_member: + assert manager.manual_control["light.light_4"] == LightControlAttributes.ALL + + +@pytest.mark.parametrize("expand", [False, True]) +@pytest.mark.parametrize("shared_target", [False, True]) +async def test_group_runtime_change_retires_delayed_events( + hass, + expand, + shared_target, + cleanup, +): + """Delayed reactive handlers must not command targets this profile retired.""" + await setup_lights(hass, with_group=True) + _, switch = await _setup_group_switch( + hass, + expand_light_groups=expand, + adapt_delay=0.1234, + ) + retired_targets = ( + {"light.light_4", "light.light_5"} if expand else {"light.light_group"} + ) + retained_target = "light.light_4" if expand else "light.light_group" + if shared_target: + _, other = await _setup_group_switch( + hass, + name="retained", + lights=[retained_target], + expand_light_groups=False, + ) + await other.async_turn_off() + + entered, release = asyncio.Event(), asyncio.Event() + original_sleep = asyncio.sleep + delayed_count = 0 + + async def controlled_sleep(delay, *args, **kwargs): + nonlocal delayed_count + if delay == 0.1234: + delayed_count += 1 + if delayed_count == (2 if expand else 1): + entered.set() + await release.wait() + else: + await original_sleep(delay, *args, **kwargs) + + calls = _track_adaptive_light_calls(hass) + with patch.object(asyncio, "sleep", controlled_sleep): + try: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.light_group"}, + blocking=True, + ) + await asyncio.wait_for(entered.wait(), timeout=2) + await hass.services.async_call( + DOMAIN, + SERVICE_CHANGE_SWITCH_SETTINGS, + { + ATTR_ENTITY_ID: switch.entity_id, + CONF_EXPAND_LIGHT_GROUPS: not expand, + }, + blocking=True, + ) + expected = ( + ["light.light_group"] if expand else ["light.light_4", "light.light_5"] + ) + assert switch.lights == expected + assert switch.manager.lights == set(expected) | ( + {retained_target} if shared_target else set() + ) + calls.clear() + finally: + release.set() + await hass.async_block_till_done() + + retired_calls = [ + call + for call in calls + if isinstance(call[ATTR_ENTITY_ID], str) + and call[ATTR_ENTITY_ID] in retired_targets + ] + assert ( + not retired_calls + ), f"Retired reactive handlers issued commands: {retired_calls}" + for member in ["light.light_4", "light.light_5"]: + assert hass.states.get(member).attributes[ATTR_BRIGHTNESS] == 128 + + +@pytest.mark.parametrize("trigger", ["turn_on", "autoreset"]) +async def test_group_mixed_profiles_preserve_tracking(hass, trigger, cleanup): + """Expanding one profile must not remove another profile's group tracking.""" + await setup_lights(hass, with_group=True) + _, proxy = await _setup_group_switch( + hass, + expand_light_groups=False, + autoreset_control_seconds=60, + detect_non_ha_changes=True, + ) + _, expanded = await _setup_group_switch( + hass, + name="expanded", + min_brightness=70, + max_brightness=70, + detect_non_ha_changes=True, + ) + calls = _track_adaptive_light_calls(hass) + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + { + ATTR_ENTITY_ID: expanded.entity_id, + CONF_LIGHTS: ["light.light_group"], + CONF_TURN_ON_LIGHTS: True, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert "light.light_group" in proxy.manager.lights + calls.clear() + if trigger == "turn_on": + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.light_group"}, + blocking=True, + ) + await hass.async_block_till_done() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.light_group"}, + blocking=True, + ) + else: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MANUAL_CONTROL, + { + ATTR_ENTITY_ID: proxy.entity_id, + CONF_LIGHTS: ["light.light_group"], + CONF_MANUAL_CONTROL: True, + }, + blocking=True, + ) + timer = proxy.manager.auto_reset_manual_control_timers["light.light_group"] + timer.delay = 0 + timer.start() + await timer.task + await hass.async_block_till_done() + # Only the proxy profile may command the group; the expanded profile uses members. + assert { + call[ATTR_BRIGHTNESS] + for call in calls + if call[ATTR_ENTITY_ID] == "light.light_group" and ATTR_BRIGHTNESS in call + } == {128} + + +async def test_nested_group_apply_targets_leaves(hass, cleanup): + """Default expansion reaches nested leaves without sending commands to subgroups.""" + await setup_lights(hass, with_group=True) + hass.states.async_set( + "light.outer", + STATE_OFF, + {ATTR_ENTITY_ID: ["light.light_group", "light.light_3"]}, + ) + _, switch = await _setup_group_switch(hass, lights=["light.outer"]) + calls = _track_adaptive_light_calls(hass) + await hass.services.async_call( + DOMAIN, + SERVICE_APPLY, + {CONF_LIGHTS: ["light.outer"], CONF_TURN_ON_LIGHTS: True}, + blocking=True, + ) + await hass.async_block_till_done() + assert sorted({call[ATTR_ENTITY_ID] for call in calls}) == [ + "light.light_3", + "light.light_4", + "light.light_5", + ] + assert switch.lights == ["light.light_3", "light.light_4", "light.light_5"] + + def _state_changed_event(entity_id: str, ts: float, context: Context) -> Event: return Event( EVENT_STATE_CHANGED, @@ -3960,25 +4363,21 @@ async def test_just_turned_off_same_automation_context(hass, cleanup): async def test_just_turned_off_group_context_reuse_end_to_end(hass, cleanup): - """Drive the issue #1378 scenario through the real event bus listeners. - - Unlike `test_just_turned_off_group_context_reuse`, which calls - `just_turned_off` directly, this test fires the service and state-changed - events on the bus. Light groups are normally expanded out of - `manager.lights`, but they can remain tracked in real setups (e.g., when a - group is nested inside another configured group or is unavailable during - setup), which is the configuration under which issue #1378 was reported. - """ + """A tracked member turn-on explains a group's reused OFF context (#1378).""" await setup_lights(hass, with_group=True) - _, switch = await setup_switch(hass, {CONF_LIGHTS: ["light.light_group"]}) + _, switch = await _setup_group_switch( + hass, + lights=["light.light_group", "light.light_4"], + expand_light_groups=False, + detect_non_ha_changes=True, + ) await hass.async_block_till_done() manager = switch.manager group = "light.light_group" member = "light.light_4" assert member in manager.lights - # Simulate a setup in which the group entity itself remains tracked. - manager.lights.add(group) + assert group in manager.lights turn_off_context = Context() # The group was turned off... @@ -4008,25 +4407,18 @@ async def test_just_turned_off_group_context_reuse_end_to_end(hass, cleanup): assert member in manager.turn_on_event # ...which turned the group back on, but HA reused the old turn_off context. - with patch.object( - AdaptiveSwitch, - "_respond_to_off_to_on_event", - AsyncMock(), - ) as respond: - hass.bus.async_fire( - EVENT_STATE_CHANGED, - { - "entity_id": group, - "old_state": State(group, STATE_OFF), - "new_state": State(group, STATE_ON), - }, - context=turn_off_context, - ) - await hass.async_block_till_done() + calls = _track_adaptive_light_calls(hass) + state = hass.states.get(group) + hass.states.async_set(group, STATE_ON, state.attributes, context=turn_off_context) + await hass.async_block_till_done() - # Adaptation must not have been cancelled as a polling artifact. - respond.assert_called_once() - assert respond.call_args[0][0] == group + # The real group command must survive polling-artifact detection. + assert any( + call[ATTR_ENTITY_ID] == group and call.get(ATTR_BRIGHTNESS) == 128 + for call in calls + ) + for entity_id in ["light.light_4", "light.light_5"]: + assert hass.states.get(entity_id).attributes[ATTR_BRIGHTNESS] == 128 @pytest.mark.parametrize("brightness_mode", ["linear", "tanh"]) From c5d216ea6f07221281f77c05ebb0029c4f55298b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 21:50:32 +0200 Subject: [PATCH 1065/1077] fix: synchronize sleep mode and mixed-light split timing (#1579) * fix: synchronize sleep mode and mixed-light split timing * docs: refresh generated sleep automation example --- README.md | 1 + blueprints/automation/sleep_mode.yaml | 1 + custom_components/adaptive_lighting/switch.py | 4 ++ docs/automation-examples.md | 1 + tests/test_automation_examples.py | 51 +++++++++++++++++++ tests/test_switch.py | 28 ++++++++-- 6 files changed, 83 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3f924fb8..1ea941fd 100644 --- a/README.md +++ b/README.md @@ -316,6 +316,7 @@ Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/ ```yaml - alias: "Adaptive lighting: toggle 'sleep mode'" + mode: restart trigger: - platform: state entity_id: input_boolean.sleep_mode diff --git a/blueprints/automation/sleep_mode.yaml b/blueprints/automation/sleep_mode.yaml index fe57079b..62225aed 100644 --- a/blueprints/automation/sleep_mode.yaml +++ b/blueprints/automation/sleep_mode.yaml @@ -39,3 +39,4 @@ actions: - action: "switch.turn_{{ sleep_mode }}" target: entity_id: !input sleep_switches +mode: restart diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index c85087a9..d784f325 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2277,6 +2277,7 @@ class AdaptiveLightingManager: # state can change until the next call), so we just schedule it and let # it sort out by itself. already_applied = get_light_control_attributes(first_service_data) + shared_sleep_time = adaptation_data.sleep_time for index, entity_id in enumerate(entity_ids): self.set_proactively_adapting(call.context.id, entity_id) if index: @@ -2292,6 +2293,9 @@ class AdaptiveLightingManager: if adaptation_data is None or not adaptation_data.max_length: continue self.set_proactively_adapting(adaptation_data.context.id, entity_id) + # Every follow-up waits for the shared first command, even when a + # member's capabilities give it a different number of split phases. + adaptation_data.sleep_time = shared_sleep_time adaptation_data.initial_sleep = True # Don't await to avoid blocking the service call. diff --git a/docs/automation-examples.md b/docs/automation-examples.md index ec5c2831..87ca3aa1 100644 --- a/docs/automation-examples.md +++ b/docs/automation-examples.md @@ -54,6 +54,7 @@ Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/ ```yaml - alias: "Adaptive lighting: toggle 'sleep mode'" + mode: restart trigger: - platform: state entity_id: input_boolean.sleep_mode diff --git a/tests/test_automation_examples.py b/tests/test_automation_examples.py index b500f41e..28bd130a 100644 --- a/tests/test_automation_examples.py +++ b/tests/test_automation_examples.py @@ -1237,6 +1237,57 @@ async def test_sleep_toggle_uses_fresh_profile_entity_ids( assert state.state == STATE_OFF +async def test_sleep_toggle_tracks_rapid_helper_changes( + hass: HomeAssistant, + published_automation, +) -> None: + """Keep every sleep switch synchronized when the helper changes rapidly.""" + summary = ( + 'Toggle multiple Adaptive Lighting switches to "sleep mode" using an ' + "input_boolean.sleep_mode." + ) + sleep_switches = ( + "switch.adaptive_lighting_living_room_sleep_mode", + "switch.adaptive_lighting_bedroom_sleep_mode", + ) + automation_config = published_automation( + summary, + "sleep_mode.yaml", + { + "sleep_helper": "input_boolean.sleep_mode", + "sleep_switches": list(sleep_switches), + }, + ) + assert await async_setup_component( + hass, + "input_boolean", + {"input_boolean": {"sleep_mode": {}}}, + ) + await setup_switch(hass, {CONF_NAME: "Living Room"}) + await setup_switch(hass, {CONF_NAME: "Bedroom"}) + await _setup_automation(hass, automation_config) + + for _ in range(10): + await hass.services.async_call( + "input_boolean", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "input_boolean.sleep_mode"}, + blocking=True, + ) + await hass.services.async_call( + "input_boolean", + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "input_boolean.sleep_mode"}, + blocking=True, + ) + await hass.async_block_till_done() + + for entity_id in sleep_switches: + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + + async def test_sleep_toggle_applies_restored_state_at_startup( hass: HomeAssistant, published_automation, diff --git a/tests/test_switch.py b/tests/test_switch.py index 0b9129e3..d27aa5f6 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -48,6 +48,7 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_MULTI_LIGHT_INTERCEPT, CONF_PREFER_RGB_COLOR, CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, + CONF_SEND_SPLIT_DELAY, CONF_SEPARATE_TURN_ON_COMMANDS, CONF_SKIP_REDUNDANT_COMMANDS, CONF_SLEEP_RGB_OR_COLOR_TEMP, @@ -96,6 +97,7 @@ from homeassistant.components.light import ( ATTR_XY_COLOR, SERVICE_TURN_OFF, ColorMode, + LightEntityFeature, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -5421,13 +5423,18 @@ async def test_split_command_stays_off_after_turn_off(hass, physical_off): assert hass.states.get(ENTITY_LIGHT_3).state == STATE_OFF -@pytest.mark.parametrize("brightness_only_member", [0, 1]) +@pytest.mark.parametrize( + ("brightness_only_member", "initial_transition", "shared_transition"), + [(0, 0, 0), (1, 0, 0), (0, 0.4, 0.4), (1, 0.4, 0.2)], +) async def test_multi_light_split_with_brightness_only_member( hass, brightness_only_member, + initial_transition, + shared_transition, cleanup, ): - """A brightness-only member must not consume another member's color command.""" + """Each member gets its color command after the shared brightness transition.""" lights = await setup_lights(hass, with_group=True) members = ["light.light_4", "light.light_5"] light = lights[3 + brightness_only_member] @@ -5439,6 +5446,9 @@ async def test_multi_light_split_with_brightness_only_member( light._attr_supported_color_modes = {ColorMode.BRIGHTNESS} light._attr_color_mode = ColorMode.BRIGHTNESS light.async_write_ha_state() + for member in lights[3:5]: + member._attr_supported_features |= LightEntityFeature.TRANSITION + member.async_write_ha_state() _, switch = await setup_switch( hass, { @@ -5446,7 +5456,8 @@ async def test_multi_light_split_with_brightness_only_member( CONF_INTERCEPT: True, CONF_MULTI_LIGHT_INTERCEPT: True, CONF_SEPARATE_TURN_ON_COMMANDS: True, - CONF_INITIAL_TRANSITION: 0, + CONF_INITIAL_TRANSITION: initial_transition, + CONF_SEND_SPLIT_DELAY: 50, }, ) _mock_sun_light_settings( @@ -5457,6 +5468,14 @@ async def test_multi_light_split_with_brightness_only_member( "force_rgb_color": False, }, ) + call_times = [] + loop = asyncio.get_running_loop() + + async def record_call(event): + if event.data["domain"] == LIGHT_DOMAIN: + call_times.append(loop.time()) + + hass.bus.async_listen(EVENT_CALL_SERVICE, record_call) events = await _turn_on_and_track_event_contexts( hass, "mixed_split", @@ -5471,6 +5490,9 @@ async def test_multi_light_split_with_brightness_only_member( if ATTR_COLOR_TEMP_KELVIN in event.data["service_data"] ] assert color_targets == [members[1 - brightness_only_member]] + assert len(call_times) == 2 + # Leave room for event dispatch without accepting overlapping transitions. + assert call_times[1] - call_times[0] >= shared_transition + 0.05 - 0.02 for entity_id in members: state = hass.states.get(entity_id) assert state.state == STATE_ON From 7f7fec4e34d331fa5fd84b95514ddb1baf2a401e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 22:06:43 +0200 Subject: [PATCH 1066/1077] chore: release v1.32.0 (#1580) --- custom_components/adaptive_lighting/manifest.json | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 427b730a..db89265b 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.31.0" + "version": "1.32.0" } diff --git a/pyproject.toml b/pyproject.toml index bdaf9acf..29bee618 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "adaptive-lighting" -version = "1.30.1" +version = "1.32.0" description = "Automatically adjust brightness and color of lights based on the sun position" readme = "README.md" license = "Apache-2.0" diff --git a/uv.lock b/uv.lock index 57564ac4..56d3d2af 100644 --- a/uv.lock +++ b/uv.lock @@ -71,7 +71,7 @@ wheels = [ [[package]] name = "adaptive-lighting" -version = "1.30.1" +version = "1.32.0" source = { editable = "." } [package.dev-dependencies] From 77183ee3eb54e779fff104c99736ddc406e954d8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 22:38:25 +0200 Subject: [PATCH 1067/1077] docs: explain physical turn-ons that require reloading (#1581) * docs: explain physical turn-ons that require reloading * docs: list options that require takeover control --- README.md | 8 ++++++++ docs/troubleshooting.md | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/README.md b/README.md index 1ea941fd..6f710516 100644 --- a/README.md +++ b/README.md @@ -762,6 +762,14 @@ represent one sent command or the current desired state. ### :exclamation: Common Problems & Solutions +#### :bulb: Lights Only Adapt After Reloading + +If lights stop adapting after you turn them on with a physical switch or a Zigbee-bound remote, check the Adaptive Lighting switch's `manual_control` attribute. With `take_over_control: true` and `detect_non_ha_changes: false`, a turn-on without a matching Home Assistant `light.turn_on` call marks the light as manually controlled. Reloading clears that state, but the next physical turn-on can trigger it again. + +To adapt these turn-ons while still detecting later manual changes, enable `detect_non_ha_changes` and leave `manual_control_on_external_turn_on` disabled. This requires the light integration to report its state reliably. If you want Adaptive Lighting to keep adapting regardless of manual changes, disable `take_over_control` along with the options that require it: `detect_non_ha_changes`, `adapt_only_on_bare_turn_on`, and `manual_control_on_external_turn_on`. + +This explains the physical-switch case in [#1056](https://github.com/basnijholt/adaptive-lighting/issues/1056), but not every report in that thread. If the light is not listed in `manual_control`, include diagnostics and debug logs from the failed turn-on when reporting it. Lights returning from `unavailable` after a power cut are a separate case from an `off` to `on` state change. + #### :bulb: Lights Not Responding or Turning On by Themselves Adaptive Lighting sends more commands to lights than a typical human user would. If your light control network is unhealthy, you may experience: diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index f39c62bc..f0596563 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -45,6 +45,14 @@ represent one sent command or the current desired state. +#### :bulb: Lights Only Adapt After Reloading + +If lights stop adapting after you turn them on with a physical switch or a Zigbee-bound remote, check the Adaptive Lighting switch's `manual_control` attribute. With `take_over_control: true` and `detect_non_ha_changes: false`, a turn-on without a matching Home Assistant `light.turn_on` call marks the light as manually controlled. Reloading clears that state, but the next physical turn-on can trigger it again. + +To adapt these turn-ons while still detecting later manual changes, enable `detect_non_ha_changes` and leave `manual_control_on_external_turn_on` disabled. This requires the light integration to report its state reliably. If you want Adaptive Lighting to keep adapting regardless of manual changes, disable `take_over_control` along with the options that require it: `detect_non_ha_changes`, `adapt_only_on_bare_turn_on`, and `manual_control_on_external_turn_on`. + +This explains the physical-switch case in [#1056](https://github.com/basnijholt/adaptive-lighting/issues/1056), but not every report in that thread. If the light is not listed in `manual_control`, include diagnostics and debug logs from the failed turn-on when reporting it. Lights returning from `unavailable` after a power cut are a separate case from an `off` to `on` state change. + #### :bulb: Lights Not Responding or Turning On by Themselves Adaptive Lighting sends more commands to lights than a typical human user would. If your light control network is unhealthy, you may experience: From 2299161690922e98fe81c6c70c21a0aa1b36952a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 22:38:30 +0200 Subject: [PATCH 1068/1077] docs: clarify persistent sleep mode and daytime dimming (#1582) * docs: clarify persistent sleep mode state * docs: clarify when adaptation targets are available --- docs/advanced/sleep-mode.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/advanced/sleep-mode.md b/docs/advanced/sleep-mode.md index 40736340..9d90c854 100644 --- a/docs/advanced/sleep-mode.md +++ b/docs/advanced/sleep-mode.md @@ -22,6 +22,20 @@ target: entity_id: switch.adaptive_lighting_sleep_mode_living_room ``` +Sleep mode stays active until this switch is turned off. It does not turn off +automatically at sunrise, and Home Assistant restores its previous state after a +restart. Use an automation, such as the sleep-mode blueprint linked under +Automation Examples, when you want the switch to follow a schedule or helper. + +If lights unexpectedly use `sleep_brightness` or `sleep_color_temp` during the +day, first check that the sleep-mode switch is off. While the main Adaptive +Lighting switch is on, it reports the current calculated `brightness_pct` and +`color_temp_kelvin` targets, including the sleep settings while sleep mode is on. +You can compare these attributes with the physical light state. They are `None` +when the main switch is off. In debug logs, +`initial_sleep=True` describes an internal delay before sending a command; it does +not mean that sleep mode is active. + ## Configuration Options Sleep mode is configured through the main Adaptive Lighting configuration. See the [Configuration](../configuration.md) page for the full options table. The sleep-related options are: From 3a27c346b983a7c3491d73b8ad8eacc7dc38d391 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 22:38:35 +0200 Subject: [PATCH 1069/1077] test: preserve physical dimming across repeated turn-on calls (#1583) * test: cover repeated bare turn-on after physical dim * test: preserve reported color temperature baseline --- tests/test_switch.py | 82 ++++++++++++++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 21 deletions(-) diff --git a/tests/test_switch.py b/tests/test_switch.py index d27aa5f6..1198f49f 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -5099,7 +5099,27 @@ async def test_adapt_only_on_bare_turn_on_respects_pause_changed_mode(hass, inte ) -async def test_detect_non_ha_changes_with_separate_turn_on_commands(hass): +@pytest.mark.parametrize( + ("repeat_bare_turn_on", "mode", "intercept"), + [ + (False, TakeOverControlMode.PAUSE_ALL, False), + (False, TakeOverControlMode.PAUSE_CHANGED, False), + (True, TakeOverControlMode.PAUSE_ALL, True), + (True, TakeOverControlMode.PAUSE_CHANGED, True), + ], + ids=[ + "direct-pause-all-reactive", + "direct-pause-changed-reactive", + "bare-turn-on-pause-all-intercept", + "bare-turn-on-pause-changed-intercept", + ], +) +async def test_detect_non_ha_changes_with_separate_turn_on_commands( + hass, + repeat_bare_turn_on, + mode, + intercept, +): """Regression test for detect_non_ha_changes with separate_turn_on_commands. With separate_turn_on_commands=True, each adaptation cycle makes two sequential @@ -5107,6 +5127,9 @@ async def test_detect_non_ha_changes_with_separate_turn_on_commands(hass): last_service_data instead of merging, brightness is dropped — and _attributes_have_changed silently skips the brightness comparison, so a direct Zigbee brightness change is never detected as manual control. + + A repeated bare light.turn_on from an automation must not hide the physical + change before the periodic adaptation path runs. """ switch, (light, *_) = await setup_lights_and_switch( hass, @@ -5114,10 +5137,21 @@ async def test_detect_non_ha_changes_with_separate_turn_on_commands(hass): CONF_SEPARATE_TURN_ON_COMMANDS: True, CONF_DETECT_NON_HA_CHANGES: True, CONF_TAKE_OVER_CONTROL: True, + CONF_TAKE_OVER_CONTROL_MODE: mode, + CONF_INTERCEPT: intercept, }, ) - context = switch.create_context("test") + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 50, + ATTR_COLOR_TEMP_KELVIN: 3000, + "force_rgb_color": False, + }, + ) + + context = switch.create_context("interval") async def update(force: bool = False): await switch._update_attrs_and_maybe_adapt_lights( @@ -5129,17 +5163,10 @@ async def test_detect_non_ha_changes_with_separate_turn_on_commands(hass): await update(force=True) - last_sd = switch.manager.last_service_data.get(ENTITY_LIGHT_1) - assert last_sd is not None, "last_service_data not set after force adapt" - assert ( - ATTR_BRIGHTNESS in last_sd - ), f"brightness missing from last_service_data after split calls: {last_sd}" - assert ( - ATTR_COLOR_TEMP_KELVIN in last_sd or ATTR_RGB_COLOR in last_sd - ), f"color missing from last_service_data after split calls: {last_sd}" - al_brightness = light.brightness assert al_brightness is not None + al_color_temp = light.color_temp_kelvin + assert al_color_temp is not None switch.manager.manual_control[ENTITY_LIGHT_1] = LightControlAttributes.NONE manual_brightness = ( @@ -5147,6 +5174,24 @@ async def test_detect_non_ha_changes_with_separate_turn_on_commands(hass): ) set_light_brightness(light, manual_brightness) + if repeat_bare_turn_on: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + _mock_sun_light_settings( + switch, + { + ATTR_BRIGHTNESS_PCT: 50, + ATTR_COLOR_TEMP_KELVIN: 4000, + "force_rgb_color": False, + }, + ) + async def _flush_attr_state(hass, entity_id): """Mimic a ZHA attribute report: write current hardware state to HA.""" light.async_write_ha_state() @@ -5156,20 +5201,15 @@ async def test_detect_non_ha_changes_with_separate_turn_on_commands(hass): new=AsyncMock(side_effect=_flush_attr_state), ): await update(force=False) - - assert LightControlAttributes.BRIGHTNESS in switch.manager.manual_control.get( - ENTITY_LIGHT_1, - LightControlAttributes.NONE, - ), ( - f"manual_control={switch.manager.manual_control.get(ENTITY_LIGHT_1)}, " - f"last_service_data={switch.manager.last_service_data.get(ENTITY_LIGHT_1)}" - ) - await update(force=False) assert ( light.brightness == manual_brightness - ), f"AL overrode manual brightness {manual_brightness} with {al_brightness}" + ), f"AL overrode manual brightness {manual_brightness} with {light.brightness}" + expected_color_temp = ( + 4000 if mode == TakeOverControlMode.PAUSE_CHANGED else al_color_temp + ) + assert light.color_temp_kelvin == expected_color_temp async def test_fresh_install_entity_ids(hass): From a936519866fea7ab49b3b64e6b0f83aa5ed6a18b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 7 Sep 2026 07:41:28 +0200 Subject: [PATCH 1070/1077] fix: track mixed targets during light turn-off (#1584) --- .../adaptive_lighting/hass_utils.py | 33 ++- custom_components/adaptive_lighting/switch.py | 100 +++++---- tests/test_switch.py | 197 ++++++++++++++++++ 3 files changed, 276 insertions(+), 54 deletions(-) diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py index 550ae350..822e2e29 100644 --- a/custom_components/adaptive_lighting/hass_utils.py +++ b/custom_components/adaptive_lighting/hass_utils.py @@ -4,31 +4,30 @@ import logging from collections.abc import Awaitable, Callable from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers import device_registry, entity_registry +from homeassistant.helpers.target import async_extract_referenced_entity_ids from homeassistant.util.read_only_dict import ReadOnlyDict +try: + from homeassistant.helpers.target import TargetSelection +except ImportError: # Compatibility with older Home Assistant releases + from homeassistant.helpers.target import TargetSelectorData as TargetSelection + from .adaptation_utils import ServiceData _LOGGER = logging.getLogger(__name__) -def area_entities(hass: HomeAssistant, area_id: str): - """Get all entities linked to an area.""" - ent_reg = entity_registry.async_get(hass) - entity_ids = [ - entry.entity_id - for entry in entity_registry.async_entries_for_area(ent_reg, area_id) - ] - dev_reg = device_registry.async_get(hass) - entity_ids.extend( - [ - entity.entity_id - for device in device_registry.async_entries_for_area(dev_reg, area_id) - for entity in entity_registry.async_entries_for_device(ent_reg, device.id) - if entity.area_id is None - ], +def target_entities( + hass: HomeAssistant, + service_data: ServiceData, +) -> set[str]: + """Resolve all directly and indirectly targeted entities without groups.""" + selected = async_extract_referenced_entity_ids( + hass, + TargetSelection(service_data), + expand_group=False, ) - return entity_ids + return selected.referenced | selected.indirectly_referenced def setup_service_call_interceptor( diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index d784f325..a22c8cb4 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -11,7 +11,6 @@ from copy import deepcopy from datetime import timedelta from typing import TYPE_CHECKING, Any -import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util import ulid_transform from homeassistant.components.light import ( @@ -32,8 +31,11 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_AREA_ID, + ATTR_DEVICE_ID, ATTR_DOMAIN, ATTR_ENTITY_ID, + ATTR_FLOOR_ID, + ATTR_LABEL_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, @@ -149,7 +151,7 @@ from .const import ( change_switch_settings_schema, replace_none_str, ) -from .hass_utils import area_entities, setup_service_call_interceptor +from .hass_utils import setup_service_call_interceptor, target_entities from .helpers import ( clamp, color_difference_redmean, @@ -1970,13 +1972,6 @@ class AdaptiveLightingManager: self._context_cnt += 1 return context - def _is_excluded_from_area(self, entity_id: str) -> bool: - """Match Home Assistant's exclusions for indirect area targets.""" - entry = entity_registry.async_get(self.hass).async_get(entity_id) - return entry is not None and ( - entry.entity_category is not None or entry.hidden_by is not None - ) - def _separate_entity_ids( self, entity_ids: list[str], @@ -2160,8 +2155,14 @@ class AdaptiveLightingManager: entity_ids: list[str], ) -> dict[str, Any]: """Modify the service data to contain the entity IDs.""" - service_data.pop(ATTR_ENTITY_ID, None) - service_data.pop(ATTR_AREA_ID, None) + for target_key in ( + ATTR_ENTITY_ID, + ATTR_AREA_ID, + ATTR_DEVICE_ID, + ATTR_FLOOR_ID, + ATTR_LABEL_ID, + ): + service_data.pop(target_key, None) service_data[ATTR_ENTITY_ID] = entity_ids return service_data @@ -2633,31 +2634,11 @@ class AdaptiveLightingManager: records.pop(light, None) def _get_entity_list(self, service_data: ServiceData) -> list[str]: - if ATTR_ENTITY_ID in service_data: - return cv.ensure_list_csv(service_data[ATTR_ENTITY_ID]) - if ATTR_AREA_ID in service_data: - entity_ids: list[str] = [] - area_ids: list[str] = cv.ensure_list_csv(service_data[ATTR_AREA_ID]) - for area_id in area_ids: - area_entity_ids = area_entities(self.hass, area_id) - eids = [ - entity_id - for entity_id in area_entity_ids - if entity_id.startswith(LIGHT_DOMAIN) - and not self._is_excluded_from_area(entity_id) - ] - entity_ids.extend(eids) - _LOGGER.debug( - "Found entity_ids '%s' for area_id '%s'", - entity_ids, - area_id, - ) - return entity_ids - _LOGGER.debug( - "No entity_ids or area_ids found in service_data: %s", - service_data, + return sorted( + entity_id + for entity_id in target_entities(self.hass, service_data) + if entity_id.startswith(f"{LIGHT_DOMAIN}.") ) - return [] async def turn_on_off_event_listener(self, event: Event) -> None: """Track 'light.turn_off' and 'light.turn_on' service calls.""" @@ -3052,7 +3033,7 @@ class AdaptiveLightingManager: def _member_turn_on_explains_group_turn_on( self, entity_id: str, - on_to_off_event: Event[EventStateChangedData], + off_event: Event, off_to_on_event: Event[EventStateChangedData], ) -> bool: """Check if a light group's 'off' → 'on' is caused by a member's 'light.turn_on'. @@ -3072,7 +3053,7 @@ class AdaptiveLightingManager: member_turn_on = self.turn_on_event.get(member) if ( member_turn_on is not None - and on_to_off_event.time_fired + and off_event.time_fired < member_turn_on.time_fired <= off_to_on_event.time_fired ): @@ -3087,6 +3068,49 @@ class AdaptiveLightingManager: return True return False + def _off_to_on_event_is_during_turn_off( + self, + entity_id: str, + off_to_on_event: Event[EventStateChangedData], + ) -> bool: + """Check if a reported turn-on belongs to a recent turn-off window.""" + turn_off_event = self.turn_off_event.get(entity_id) + if ( + turn_off_event is None + or off_to_on_event.context.id != turn_off_event.context.id + ): + return False + + turn_on_event = self.turn_on_event.get(entity_id) + if ( + turn_on_event is not None + and turn_off_event.time_fired + < turn_on_event.time_fired + <= off_to_on_event.time_fired + ): + return False + if self._member_turn_on_explains_group_turn_on( + entity_id, + turn_off_event, + off_to_on_event, + ): + return False + + transition = turn_off_event.data[ATTR_SERVICE_DATA].get(ATTR_TRANSITION) + delay = max(transition or 0, TURNING_OFF_DELAY) + elapsed = (dt_util.utcnow() - turn_off_event.time_fired).total_seconds() + if not 0 <= elapsed <= delay: + return False + + _LOGGER.debug( + "just_turned_off: Fresh 'light.turn_off' for '%s' shares the" + " 'off' → 'on' context; ignoring the state during its %s second" + " transition window.", + entity_id, + delay, + ) + return True + async def just_turned_off( # noqa: PLR0911, PLR0912 self, entity_id: str, @@ -3105,6 +3129,8 @@ class AdaptiveLightingManager: """ off_to_on_event = self.off_to_on_event[entity_id] on_to_off_event = self.on_to_off_event.get(entity_id) + if self._off_to_on_event_is_during_turn_off(entity_id, off_to_on_event): + return True if on_to_off_event is None: _LOGGER.debug( diff --git a/tests/test_switch.py b/tests/test_switch.py index 1198f49f..5db7c9a5 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -108,6 +108,8 @@ from homeassistant.const import ( ATTR_AREA_ID, ATTR_DEVICE_ID, ATTR_ENTITY_ID, + ATTR_FLOOR_ID, + ATTR_LABEL_ID, ATTR_SUPPORTED_FEATURES, CONF_LIGHTS, CONF_NAME, @@ -4251,6 +4253,27 @@ def _turn_on_service_event(entity_ids: list[str], ts: float, context: Context) - ) +def _turn_off_service_event( + entity_ids: list[str], + ts: float, + context: Context, + transition: float, +) -> Event: + return Event( + EVENT_CALL_SERVICE, + { + "domain": LIGHT_DOMAIN, + "service": SERVICE_TURN_OFF, + "service_data": { + ATTR_ENTITY_ID: entity_ids, + ATTR_TRANSITION: transition, + }, + }, + time_fired_timestamp=ts, + context=context, + ) + + async def test_just_turned_off_group_context_reuse(hass, cleanup): """Group 'off' → 'on' with a reused 'turn_off' context must still adapt. @@ -4309,6 +4332,154 @@ async def test_just_turned_off_group_context_reuse(hass, cleanup): assert await manager.just_turned_off(group) +def _register_mixed_target_lights( + hass, + device_registry, + floor_registry, + label_registry, +): + """Assign the three test lights to mixed indirect HA targets.""" + floor = floor_registry.async_create("Upstairs") + area_registry = ar.async_get(hass) + upstairs_area = area_registry.async_create( + "Upstairs room", + floor_id=floor.floor_id, + ) + hall_area = area_registry.async_create("Hall") + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + device = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("test", "device-target")}, + ) + label = label_registry.async_create("Skipped light") + + registry = entity_registry.async_get(hass) + registry.async_update_entity(ENTITY_LIGHT_1, area_id=upstairs_area.id) + registry.async_update_entity(ENTITY_LIGHT_2, area_id=hall_area.id) + registry.async_update_entity( + ENTITY_LIGHT_3, + device_id=device.id, + labels={label.label_id}, + ) + return { + ATTR_FLOOR_ID: floor.floor_id, + ATTR_AREA_ID: hall_area.id, + ATTR_DEVICE_ID: device.id, + ATTR_LABEL_ID: label.label_id, + } + + +async def test_mixed_turn_off_targets_do_not_readapt_off_device_light( + hass, + device_registry, + floor_registry, + label_registry, + cleanup, +): + """A mixed-target turn-off must cover an already-off device light (#1069).""" + await setup_lights(hass) + targets = _register_mixed_target_lights( + hass, + device_registry, + floor_registry, + label_registry, + ) + targets.pop(ATTR_LABEL_ID) + + _, switch = await setup_switch( + hass, + { + CONF_LIGHTS: [ENTITY_LIGHT_1, ENTITY_LIGHT_2, ENTITY_LIGHT_3], + CONF_DETECT_NON_HA_CHANGES: True, + CONF_INTERCEPT: True, + CONF_INITIAL_TRANSITION: 0, + }, + ) + assert hass.states.is_state(ENTITY_LIGHT_3, STATE_OFF) + + turn_off_context = Context() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + { + **targets, + ATTR_TRANSITION: 10, + }, + blocking=True, + context=turn_off_context, + ) + await hass.async_block_till_done() + + calls = _track_adaptive_light_calls(hass) + off_state = hass.states.get(ENTITY_LIGHT_3) + assert off_state is not None + hass.states.async_set( + ENTITY_LIGHT_3, + STATE_ON, + off_state.attributes, + context=turn_off_context, + ) + await hass.async_block_till_done() + + assert not calls + + +async def test_intercept_replaces_all_mixed_target_selectors( + hass, + device_registry, + floor_registry, + label_registry, + cleanup, +): + """A narrowed intercepted call must not retain indirect target selectors.""" + lights = await setup_lights(hass) + targets = _register_mixed_target_lights( + hass, + device_registry, + floor_registry, + label_registry, + ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: [ENTITY_LIGHT_1, ENTITY_LIGHT_2, ENTITY_LIGHT_3], + }, + blocking=True, + ) + await setup_switch( + hass, + { + CONF_LIGHTS: [ENTITY_LIGHT_1, ENTITY_LIGHT_2], + CONF_INTERCEPT: True, + CONF_MULTI_LIGHT_INTERCEPT: True, + CONF_INITIAL_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + + with patch.object( + lights[2], + "async_turn_on", + wraps=lights[2].async_turn_on, + ) as skipped_turn_on: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {**targets, ATTR_BRIGHTNESS: 200}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 128 + assert hass.states.get(ENTITY_LIGHT_2).attributes[ATTR_BRIGHTNESS] == 128 + skipped_turn_on.assert_awaited_once() + assert skipped_turn_on.call_args.kwargs[ATTR_BRIGHTNESS] == 200 + + async def test_just_turned_off_same_automation_context(hass, cleanup): """'turn_off' and 'turn_on' from one automation share a context. @@ -4325,6 +4496,12 @@ async def test_just_turned_off_same_automation_context(hass, cleanup): now = dt_util.utcnow().timestamp() automation_context = Context() + manager.turn_off_event[ENTITY_LIGHT_1] = _turn_off_service_event( + [ENTITY_LIGHT_1], + now - 2, + automation_context, + transition=10, + ) manager.on_to_off_event[ENTITY_LIGHT_1] = _state_changed_event( ENTITY_LIGHT_1, now - 2, @@ -4363,6 +4540,26 @@ async def test_just_turned_off_same_automation_context(hass, cleanup): ) assert await manager.just_turned_off(ENTITY_LIGHT_1) + # A later physical turn-on has a fresh context and must not remain blocked by + # the old turn-off record after its transition window has elapsed. + manager.on_to_off_event[ENTITY_LIGHT_1] = _state_changed_event( + ENTITY_LIGHT_1, + now - 20, + automation_context, + ) + manager.turn_off_event[ENTITY_LIGHT_1] = _turn_off_service_event( + [ENTITY_LIGHT_1], + now - 20, + automation_context, + transition=10, + ) + manager.off_to_on_event[ENTITY_LIGHT_1] = _state_changed_event( + ENTITY_LIGHT_1, + now, + Context(), + ) + assert not await manager.just_turned_off(ENTITY_LIGHT_1) + async def test_just_turned_off_group_context_reuse_end_to_end(hass, cleanup): """A tracked member turn-on explains a group's reused OFF context (#1378).""" From 34356a6b56e08b2d82207d382a0e786b553753a4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 7 Sep 2026 08:01:35 +0200 Subject: [PATCH 1071/1077] ci: validate README TOC before merging (#1586) --- .github/workflows/toc.yaml | 12 ------------ .pre-commit-config.yaml | 6 ++++++ README.md | 1 + 3 files changed, 7 insertions(+), 12 deletions(-) delete mode 100644 .github/workflows/toc.yaml diff --git a/.github/workflows/toc.yaml b/.github/workflows/toc.yaml deleted file mode 100644 index cde1b6d4..00000000 --- a/.github/workflows/toc.yaml +++ /dev/null @@ -1,12 +0,0 @@ -on: - push: - branches: [main] -name: TOC Generator -jobs: - generateTOC: - name: TOC Generator - runs-on: ubuntu-latest - steps: - - uses: technote-space/toc-generator@v4.3.1 - with: - TOC_TITLE: "" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5cd6d52..53b84021 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,6 +7,12 @@ repos: - id: end-of-file-fixer - id: mixed-line-ending args: ["--fix=lf"] + - repo: https://github.com/thlorenz/doctoc + rev: v2.5.0 + hooks: + - id: doctoc + files: ^README[^/]*\.md$ + args: ["--notitle"] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.16.5 hooks: diff --git a/README.md b/README.md index 6f710516..0736b56a 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ The attributes are absent when the Adaptive Lighting switch is off. Use a fallba - [Additional Information](#additional-information) - [:sos: Troubleshooting](#sos-troubleshooting) - [:exclamation: Common Problems & Solutions](#exclamation-common-problems--solutions) + - [:bulb: Lights Only Adapt After Reloading](#bulb-lights-only-adapt-after-reloading) - [:bulb: Lights Not Responding or Turning On by Themselves](#bulb-lights-not-responding-or-turning-on-by-themselves) - [:signal_strength: WiFi Networks](#signal_strength-wifi-networks) - [:spider_web: Zigbee, Z-Wave, and Other Mesh Networks](#spider_web-zigbee-z-wave-and-other-mesh-networks) From 2f37b6ea4070e2b9b0637acc3ca2a2aab9252405 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 7 Sep 2026 09:03:03 +0200 Subject: [PATCH 1072/1077] Fix pending adaptations after light or profile removal (#1587) --- custom_components/adaptive_lighting/switch.py | 19 +- tests/test_switch.py | 245 ++++++++++++++++++ 2 files changed, 261 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index a22c8cb4..871dcf03 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -886,6 +886,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): assert hass is not None self.hass = hass self.manager = manager + self._removed = False self.sleep_mode_switch = sleep_mode_switch self.adapt_color_switch = adapt_color_switch self.adapt_brightness_switch = adapt_brightness_switch @@ -1078,6 +1079,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): async def async_will_remove_from_hass(self) -> None: """Remove the listeners upon removing the component.""" + self._removed = True self._remove_listeners() def _resolve_lights(self, lights: list[str] | None = None) -> list[str]: @@ -1433,6 +1435,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if not is_first_call or data.initial_sleep: await asyncio.sleep(data.sleep_time) + if self._removed: + return + # Instead of directly iterating the generator in the while-loop, we get # the next item here after the sleep to make sure it incorporates state # changes which happened during the sleep. @@ -1488,6 +1493,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): Wraps the sequence of service calls in a task that can be cancelled from elsewhere, e.g., to cancel an ongoing adaptation when a light is turned off. """ + if self._removed: + return + # Prevent overlap of multiple adaptation sequences self.manager.cancel_ongoing_adaptation_calls(data.entity_id) _LOGGER.debug( @@ -1691,7 +1699,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): await asyncio.sleep(self._adapt_delay) # Runtime settings may retire this profile's target while the event waits. - if entity_id not in self.lights: + if self._removed or entity_id not in self.lights: return await self._update_attrs_and_maybe_adapt_lights( @@ -1705,7 +1713,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self, event: Event[EventStateChangedData], ) -> None: - if not _is_state_event(event, (STATE_ON, STATE_OFF)): + new_state = event.data.get("new_state") + if new_state is None or new_state.state not in (STATE_ON, STATE_OFF): _LOGGER.debug("%s: Ignoring sleep event %s", self._name, event) return _LOGGER.debug( @@ -2730,7 +2739,7 @@ class AdaptiveLightingManager: elif state.state == STATE_OFF: # is turning on await on(eid, event) - async def state_changed_event_listener( + async def state_changed_event_listener( # noqa: PLR0912 self, event: Event[EventStateChangedData], ) -> None: @@ -2808,6 +2817,10 @@ class AdaptiveLightingManager: new_on.context.id, ) + if old_on and not new_on: + # Availability loss invalidates pending commands, not manual state. + self.cancel_ongoing_adaptation_calls(entity_id) + if old_on and new_off: # Tracks 'on' → 'off' state changes self.on_to_off_event[entity_id] = event diff --git a/tests/test_switch.py b/tests/test_switch.py index 5db7c9a5..6efe68d5 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -30,6 +30,7 @@ from homeassistant.components.adaptive_lighting.const import ( ATTR_ADAPT_BRIGHTNESS, ATTR_ADAPT_COLOR, ATTR_ADAPTIVE_LIGHTING_MANAGER, + CONF_ADAPT_DELAY, CONF_ADAPT_ONLY_ON_BARE_TURN_ON, CONF_ADAPT_UNTIL_SLEEP, CONF_AUTORESET_CONTROL, @@ -46,6 +47,7 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_MIN_BRIGHTNESS, CONF_MIN_COLOR_TEMP, CONF_MULTI_LIGHT_INTERCEPT, + CONF_ONLY_ONCE, CONF_PREFER_RGB_COLOR, CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, CONF_SEND_SPLIT_DELAY, @@ -119,6 +121,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, EntityCategory, ) from homeassistant.core import Context, CoreState, Event, HomeAssistant, State @@ -5988,3 +5991,245 @@ async def test_shared_profiles_keep_independent_sun_schedules( noon = hass.states.get(ENTITY_LIGHT_1) assert noon.attributes[ATTR_BRIGHTNESS] == 77 assert noon.attributes[ATTR_COLOR_TEMP_KELVIN] > 2000 + + +@pytest.mark.parametrize("via_unavailable", [False, True]) +async def test_split_adaptation_cancelled_after_physical_off( + hass, + monkeypatch, + via_unavailable, +): + """Pending split commands must not resurrect a physically switched-off light.""" + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_DETECT_NON_HA_CHANGES: True, + CONF_ONLY_ONCE: True, + CONF_SEPARATE_TURN_ON_COMMANDS: True, + CONF_SEND_SPLIT_DELAY: 1234, + CONF_INITIAL_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + state = hass.states.get(ENTITY_LIGHT_1) + hass.states.async_set(ENTITY_LIGHT_1, STATE_OFF, state.attributes) + await hass.async_block_till_done() + # Isolate the split-command lifetime from the separate turn-off debounce. + monkeypatch.setattr( + switch.manager, + "just_turned_off", + AsyncMock(return_value=False), + ) + entered, release = asyncio.Event(), asyncio.Event() + original_sleep = asyncio.sleep + + async def controlled_sleep(delay, *args, **kwargs): + if delay == 1.234: + entered.set() + await release.wait() + else: + await original_sleep(delay, *args, **kwargs) + + monkeypatch.setattr(asyncio, "sleep", controlled_sleep) + calls = _track_adaptive_light_calls(hass) + hass.states.async_set(ENTITY_LIGHT_1, STATE_ON, state.attributes) + await asyncio.wait_for(entered.wait(), 2) + assert len(calls) == 1 + if via_unavailable: + hass.states.async_set(ENTITY_LIGHT_1, STATE_UNAVAILABLE, state.attributes) + await original_sleep(0) + hass.states.async_set(ENTITY_LIGHT_1, STATE_OFF, state.attributes) + await original_sleep(0) + release.set() + await hass.async_block_till_done() + assert len(calls) == 1, f"Physical OFF resurrected by split command: {calls}" + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_OFF + + +@pytest.mark.parametrize("remaining_profile", [False, True]) +async def test_profile_unloaded_during_adapt_delay( + hass, + monkeypatch, + remaining_profile, +): + """A removed profile must not send commands after its adaptation delay.""" + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_DETECT_NON_HA_CHANGES: True, + CONF_ONLY_ONCE: True, + CONF_ADAPT_DELAY: 0.1234, + }, + ) + if remaining_profile: + _, other = await setup_switch( + hass, + { + CONF_NAME: "remaining", + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_ONLY_ONCE: True, + CONF_INITIAL_TRANSITION: 0, + }, + ) + await other.async_turn_off() + state = hass.states.get(ENTITY_LIGHT_1) + hass.states.async_set(ENTITY_LIGHT_1, STATE_OFF, state.attributes) + await hass.async_block_till_done() + monkeypatch.setattr( + switch.manager, + "just_turned_off", + AsyncMock(return_value=False), + ) + entered, release = asyncio.Event(), asyncio.Event() + original_sleep = asyncio.sleep + + async def controlled_sleep(delay, *args, **kwargs): + if delay == 0.1234: + entered.set() + await release.wait() + else: + await original_sleep(delay, *args, **kwargs) + + monkeypatch.setattr(asyncio, "sleep", controlled_sleep) + calls = _track_adaptive_light_calls(hass) + hass.states.async_set(ENTITY_LIGHT_1, STATE_ON, state.attributes) + await asyncio.wait_for(entered.wait(), 2) + entry = hass.config_entries.async_entries(DOMAIN)[0] + await hass.config_entries.async_unload(entry.entry_id) + calls.clear() + release.set() + await hass.async_block_till_done() + assert calls == [] + if remaining_profile: + await other.async_turn_on() + await other._update_attrs_and_maybe_adapt_lights( + context=other.create_context("test"), + lights=[ENTITY_LIGHT_1], + force=True, + ) + await hass.async_block_till_done() + assert calls + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + + +async def test_profile_unloaded_during_split_delay(hass, monkeypatch): + """Removed profiles must not send remaining split commands.""" + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_DETECT_NON_HA_CHANGES: True, + CONF_ONLY_ONCE: True, + CONF_SEPARATE_TURN_ON_COMMANDS: True, + CONF_SEND_SPLIT_DELAY: 1234, + CONF_INITIAL_TRANSITION: 0, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + state = hass.states.get(ENTITY_LIGHT_1) + hass.states.async_set(ENTITY_LIGHT_1, STATE_OFF, state.attributes) + await hass.async_block_till_done() + # Isolate the split-command lifetime from the separate turn-off debounce. + monkeypatch.setattr( + switch.manager, + "just_turned_off", + AsyncMock(return_value=False), + ) + entered, release = asyncio.Event(), asyncio.Event() + original_sleep = asyncio.sleep + + async def controlled_sleep(delay, *args, **kwargs): + if delay == 1.234: + entered.set() + await release.wait() + else: + await original_sleep(delay, *args, **kwargs) + + monkeypatch.setattr(asyncio, "sleep", controlled_sleep) + calls = _track_adaptive_light_calls(hass) + hass.states.async_set(ENTITY_LIGHT_1, STATE_ON, state.attributes) + await asyncio.wait_for(entered.wait(), 2) + assert len(calls) == 1 + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert await hass.config_entries.async_unload(entry.entry_id) + release.set() + await hass.async_block_till_done() + assert len(calls) == 1 + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + + +@pytest.mark.parametrize("unload_before_split", [False, True]) +async def test_unloaded_polling_profile_preserves_other_split_adaptation( + hass, + monkeypatch, + unload_before_split, +): + """A removed profile resuming a poll must not cancel another profile's work.""" + switch, _ = await setup_lights_and_switch(hass, {CONF_ONLY_ONCE: True}) + _, other = await setup_switch( + hass, + { + CONF_NAME: "remaining", + CONF_LIGHTS: [ENTITY_LIGHT_1], + CONF_ONLY_ONCE: True, + CONF_SEPARATE_TURN_ON_COMMANDS: True, + CONF_SEND_SPLIT_DELAY: 1234, + CONF_INITIAL_TRANSITION: 0, + }, + ) + poll_entered, poll_release = asyncio.Event(), asyncio.Event() + split_entered, split_release = asyncio.Event(), asyncio.Event() + original_update = switch.manager.update_manually_controlled_from_untracked_change + original_sleep = asyncio.sleep + + async def delayed_update(profile, *args, **kwargs): + if profile is switch: + poll_entered.set() + await poll_release.wait() + await original_update(profile, *args, **kwargs) + + async def controlled_sleep(delay, *args, **kwargs): + if delay == 1.234: + split_entered.set() + await split_release.wait() + else: + await original_sleep(delay, *args, **kwargs) + + monkeypatch.setattr( + switch.manager, + "update_manually_controlled_from_untracked_change", + delayed_update, + ) + monkeypatch.setattr(asyncio, "sleep", controlled_sleep) + calls = _track_adaptive_light_calls(hass) + polling = hass.async_create_task( + switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + lights=[ENTITY_LIGHT_1], + force=True, + ), + ) + await asyncio.wait_for(poll_entered.wait(), 2) + entry = hass.config_entries.async_entries(DOMAIN)[0] + if unload_before_split: + assert await hass.config_entries.async_unload(entry.entry_id) + adapting = hass.async_create_task( + other._adapt_light( + ENTITY_LIGHT_1, + other.create_context("test"), + 0, + force=True, + ), + ) + await asyncio.wait_for(split_entered.wait(), 2) + assert len(calls) == 1 + if not unload_before_split: + assert await hass.config_entries.async_unload(entry.entry_id) + poll_release.set() + await polling + split_release.set() + await adapting + await hass.async_block_till_done() + assert len(calls) == 2 + assert ATTR_COLOR_TEMP_KELVIN in calls[-1] From 7c445af63bf2684f1b21d1fcb8094fa1f59088aa Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:54:05 +0200 Subject: [PATCH 1073/1077] docs: add ahmadtawakol as a contributor for code, bug, and maintenance (#1595) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 11 +++++++++++ README.md | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 6004155e..fbff14b8 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1568,6 +1568,17 @@ "contributions": [ "ideas" ] + }, + { + "login": "ahmadtawakol", + "name": "Ahmad Tawakol", + "avatar_url": "https://avatars.githubusercontent.com/u/2355493?v=4", + "profile": "https://github.com/ahmadtawakol", + "contributions": [ + "code", + "bug", + "maintenance" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 0736b56a..89685620 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/hacs/integration) ![Version](https://img.shields.io/github/v/release/basnijholt/adaptive-lighting?style=for-the-badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-172-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-173-orange.svg?style=flat-square)](#contributors-) # 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙 @@ -1118,6 +1118,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark Leonhard Hesse
Leonhard Hesse

💻 Tim Stallmann
Tim Stallmann

💻 lehneres
lehneres

🤔 + Ahmad Tawakol
Ahmad Tawakol

💻 🐛 🚧 From 51ea83dba3bc1de7a3a36736de6e8ff34ccc5284 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:02:08 +0200 Subject: [PATCH 1074/1077] [pre-commit.ci] pre-commit autoupdate (#1592) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.16.5 → v0.16.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.16.5...v0.16.6) * test: avoid mired rounding boundary --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt --- .pre-commit-config.yaml | 2 +- tests/test_switch.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 53b84021..80893d1c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: files: ^README[^/]*\.md$ args: ["--notitle"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.16.5 + rev: v0.16.6 hooks: - id: ruff args: ["--fix"] diff --git a/tests/test_switch.py b/tests/test_switch.py index 6efe68d5..65fb5c32 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1537,8 +1537,10 @@ async def test_apply_updates_non_ha_change_baseline( ) direction = 1 if manual_value < adaptive_value else -1 + # Legacy template lights round via mireds; 70 K keeps one reported step + # below 100 K and two steps above it across the configured range. small_change = ( - 15 if manual_attribute == LightControlAttributes.BRIGHTNESS else 60 + 15 if manual_attribute == LightControlAttributes.BRIGHTNESS else 70 ) freezer.tick(90) set_physical_state(manual_value + direction * small_change) From 3e78ac7e212cc215b01ceea29abbd8bfe63f6c62 Mon Sep 17 00:00:00 2001 From: Ahmad Tawakol <2355493+ahmadtawakol@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:02:45 -0300 Subject: [PATCH 1075/1077] Make scripts/setup-symlinks idempotent (#1590) `ln -fs` dereferences an existing symlink to a directory and creates the new link *inside* it, so running the script a second time left two stray symlinks in the working tree instead of replacing the existing ones: tests/tests -> ../../../tests/ custom_components/adaptive_lighting/adaptive_lighting -> ../../../custom_components/adaptive_lighting Neither path is gitignored, so `git add -A` commits them. Add `-n` so an existing symlink is treated as a file and replaced. Co-authored-by: Claude Opus 5 --- scripts/setup-symlinks | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/setup-symlinks b/scripts/setup-symlinks index 91026b3a..af95b831 100755 --- a/scripts/setup-symlinks +++ b/scripts/setup-symlinks @@ -2,12 +2,17 @@ set -ex cd "$(dirname "$0")/.." +# '-n' keeps a re-run idempotent: without it 'ln -fs' follows an existing +# symlink and creates the new link *inside* the target directory, leaving a +# stray 'tests/tests' and 'custom_components/adaptive_lighting/adaptive_lighting' +# in the working tree. + # Link custom components cd core/homeassistant/components/ -ln -fs ../../../custom_components/adaptive_lighting adaptive_lighting +ln -fsn ../../../custom_components/adaptive_lighting adaptive_lighting cd - # Link tests cd core/tests/components/ -ln -fs ../../../tests/ adaptive_lighting +ln -fsn ../../../tests/ adaptive_lighting cd - From da749bcf6153d0537bc66fb18f16728f6acdf3b5 Mon Sep 17 00:00:00 2001 From: Ahmad Tawakol <2355493+ahmadtawakol@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:02:51 -0300 Subject: [PATCH 1076/1077] Add a .dockerignore (#1591) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests/README.md has developers clone Home Assistant core into ./core, but Docker does not read .gitignore, so `COPY . /app/` shipped that ~300MB checkout into the build context and into the image on every build. It also changed what the build did. With /app/core already present as a real directory, `ln -s /core /app/core` linked *inside* it — leaving a stray /app/core/core -> /core — and scripts/setup-dependencies then installed from the copied host checkout rather than the image's own pinned clone. Excluding core/ (plus local virtualenvs, VCS state and caches) takes the build context from 412MB to 4.6MB and the image from 2.34GB to 2.1GB, and makes a build with a local ./core behave like a clean one: /app/core is the intended symlink to /core. This does remove an accident. An image built while a local ./core existed happened to run without `-v $(pwd):/app`, because the copied checkout carried relative symlinks that still resolved inside /app. A clean-checkout build never had that property — there the symlinks setup-symlinks writes into /core dangle — and tests/README.md requires the mount either way. 479 passed, unchanged. Co-authored-by: Claude Opus 5 --- .dockerignore | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..767b6084 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +# The Home Assistant core checkout. tests/README.md has you clone it to ./core, +# but the Dockerfile clones its own copy to /core and links /app/core to it. +# Without this entry `COPY . /app/` ships ~300MB into every build and leaves +# /app/core as a real directory, so `ln -s /core /app/core` links *inside* it +# rather than creating the intended symlink. +core/ + +# Local virtualenvs +.venv/ +venv/ +env/ +ENV/ + +# Not used by the build +.git/ +.vscode/ +.idea/ + +# Caches and test output +__pycache__/ +*.py[cod] +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +htmlcov/ +.coverage +.coverage.* +coverage.xml From 7d0f4b610acb5088125aff266ba3d9af10164b2f Mon Sep 17 00:00:00 2001 From: Ahmad Tawakol <2355493+ahmadtawakol@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:03:07 -0300 Subject: [PATCH 1077/1077] Fix TypeError when 'light.turn_off' is called with a string transition (#1589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix TypeError when 'light.turn_off' is called with a string transition `EVENT_CALL_SERVICE` carries the *raw* service data, not the data `light.turn_off`'s schema produced for the service handler, so its `vol.Coerce(float)` never reaches `AdaptiveLightingManager`. A caller passing `transition: "2"` — a template rendering to a string, or any JSON payload where the value was quoted — therefore stores a `str` in `turn_off_event`. Both places that derive a delay from it compare it against an int: delay = max(transition or 0, TURNING_OFF_DELAY) # during turn-off delay = max(transition, TURNING_OFF_DELAY) # just_turned_off which raises `TypeError: '>' not supported between instances of 'int' and 'str'`. Because `just_turned_off` runs inside the state-change listener task, the exception is swallowed: it surfaces only as "Error doing job: Task exception was never retrieved (task: None)", while the light quietly stops being adapted after that turn-off. Read the transition through a helper that coerces to float. Schema validation runs before the event fires, so whatever reaches the helper is coercible. Co-Authored-By: Claude Opus 5 * Normalize turn-off transitions with the light service validator --------- Co-authored-by: Claude Opus 5 Co-authored-by: Bas Nijholt --- custom_components/adaptive_lighting/switch.py | 17 ++- tests/test_switch.py | 119 +++++++++++++++++- 2 files changed, 129 insertions(+), 7 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 871dcf03..db1c2d0b 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -20,6 +20,7 @@ from homeassistant.components.light import ( ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_XY_COLOR, + VALID_TRANSITION, ColorMode, LightEntityFeature, is_on, @@ -622,6 +623,18 @@ def _is_state_event( ) +def _turn_off_transition(turn_off_event: Event) -> float | None: + """Normalize the raw event transition using the light service's validator. + + Service-call events retain raw data after validation, so repeat the + service's coercion and clamping before calculating transition windows. + """ + transition = turn_off_event.data[ATTR_SERVICE_DATA].get(ATTR_TRANSITION) + if transition is None: + return None + return VALID_TRANSITION(transition) + + def _expand_light_groups( hass: HomeAssistant, lights: list[str], @@ -3109,7 +3122,7 @@ class AdaptiveLightingManager: ): return False - transition = turn_off_event.data[ATTR_SERVICE_DATA].get(ATTR_TRANSITION) + transition = _turn_off_transition(turn_off_event) delay = max(transition or 0, TURNING_OFF_DELAY) elapsed = (dt_util.utcnow() - turn_off_event.time_fired).total_seconds() if not 0 <= elapsed <= delay: @@ -3193,7 +3206,7 @@ class AdaptiveLightingManager: turn_off_event = self.turn_off_event.get(entity_id) if turn_off_event is not None: - transition = turn_off_event.data[ATTR_SERVICE_DATA].get(ATTR_TRANSITION) + transition = _turn_off_transition(turn_off_event) else: transition = None diff --git a/tests/test_switch.py b/tests/test_switch.py index 65fb5c32..a3f5b689 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -83,6 +83,7 @@ from homeassistant.components.adaptive_lighting.switch import ( SimpleSwitch, _attributes_have_changed, _expand_light_groups, + _turn_off_transition, color_difference_redmean, create_context, is_our_context, @@ -112,6 +113,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FLOOR_ID, ATTR_LABEL_ID, + ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, CONF_LIGHTS, CONF_NAME, @@ -4262,17 +4264,17 @@ def _turn_off_service_event( entity_ids: list[str], ts: float, context: Context, - transition: float, + transition: float | str | None, ) -> Event: + service_data = {ATTR_ENTITY_ID: entity_ids} + if transition is not None: + service_data[ATTR_TRANSITION] = transition return Event( EVENT_CALL_SERVICE, { "domain": LIGHT_DOMAIN, "service": SERVICE_TURN_OFF, - "service_data": { - ATTR_ENTITY_ID: entity_ids, - ATTR_TRANSITION: transition, - }, + "service_data": service_data, }, time_fired_timestamp=ts, context=context, @@ -4566,6 +4568,113 @@ async def test_just_turned_off_same_automation_context(hass, cleanup): assert not await manager.just_turned_off(ENTITY_LIGHT_1) +@pytest.mark.parametrize( + ("transition", "window"), + [(10, 10), (10.0, 10), ("10", 10), ("10000", 6553), ("inf", 6553), (None, 5)], +) +async def test_just_turned_off_normalized_transition(hass, cleanup, transition, window): + """Both turn-off guards use coerced and clamped transition windows.""" + await setup_lights(hass) + _, switch = await setup_switch(hass, {CONF_LIGHTS: [ENTITY_LIGHT_1]}) + await hass.async_block_till_done() + manager = switch.manager + + now = dt_util.utcnow().timestamp() + context = Context() + other_context = Context() + + # Setting up the switch turns the light on, and that 'turn_on' would be read + # as the legitimate explanation for the 'off' → 'on' state changes below. + manager.turn_on_event.pop(ENTITY_LIGHT_1, None) + + def set_events(turn_off_ts: float, off_to_on_context: Context) -> None: + manager.turn_off_event[ENTITY_LIGHT_1] = _turn_off_service_event( + [ENTITY_LIGHT_1], + turn_off_ts, + context, + transition=transition, + ) + manager.on_to_off_event[ENTITY_LIGHT_1] = _state_changed_event( + ENTITY_LIGHT_1, + turn_off_ts, + other_context, + ) + manager.off_to_on_event[ENTITY_LIGHT_1] = _state_changed_event( + ENTITY_LIGHT_1, + now, + off_to_on_context, + ) + + # A matching context is ignored within the normalized transition window. + set_events(now - window + 1, context) + assert await manager.just_turned_off(ENTITY_LIGHT_1) + + # Past that window the same shape must stop matching. + set_events(now - window - 1, context) + assert not await manager.just_turned_off(ENTITY_LIGHT_1) + + # `just_turned_off`'s own `max(transition, TURNING_OFF_DELAY)`: reached when + # the 'off' → 'on' state change carries a fresh context, so the check above + # returns early and the delay is computed from the 'on' → 'off' change. + manager.turn_off_event[ENTITY_LIGHT_1] = _turn_off_service_event( + [ENTITY_LIGHT_1], + now - window - 1, + context, + transition=transition, + ) + manager.on_to_off_event[ENTITY_LIGHT_1] = _state_changed_event( + ENTITY_LIGHT_1, + now - window - 1, + context, + ) + manager.off_to_on_event[ENTITY_LIGHT_1] = _state_changed_event( + ENTITY_LIGHT_1, + now, + Context(), + ) + assert not await manager.just_turned_off(ENTITY_LIGHT_1) + + +@pytest.mark.parametrize( + ("transition", "expected"), + [("2", 2.0), ("10000", 6553), ("inf", 6553), ("-2", 0), (None, None)], +) +async def test_turn_off_event_keeps_raw_transition(hass, cleanup, transition, expected): + """Normalize raw event data to the same transition used by the light service.""" + await setup_lights(hass) + _, switch = await setup_switch(hass, {CONF_LIGHTS: [ENTITY_LIGHT_1]}) + await hass.async_block_till_done() + manager = switch.manager + + service_data = {ATTR_ENTITY_ID: ENTITY_LIGHT_1} + if transition is not None: + service_data[ATTR_TRANSITION] = transition + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + service_data, + blocking=True, + ) + await hass.async_block_till_done() + + event = manager.turn_off_event[ENTITY_LIGHT_1] + assert event.data[ATTR_SERVICE_DATA].get(ATTR_TRANSITION) == transition + assert _turn_off_transition(event) == expected + + # A 'transition' that cannot be coerced is rejected by the schema, so it + # never reaches the listener. + manager.turn_off_event.pop(ENTITY_LIGHT_1) + with pytest.raises(voluptuous.error.MultipleInvalid): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_TRANSITION: "not-a-number"}, + blocking=True, + ) + await hass.async_block_till_done() + assert ENTITY_LIGHT_1 not in manager.turn_off_event + + async def test_just_turned_off_group_context_reuse_end_to_end(hass, cleanup): """A tracked member turn-on explains a group's reused OFF context (#1378).""" await setup_lights(hass, with_group=True)