From 17ac3951240a65e75e79a548b85d50d7319d9f9b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 30 Oct 2020 07:04:41 +0100 Subject: [PATCH 001/100] 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 002/100] 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 003/100] 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 004/100] 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 005/100] 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 006/100] 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 007/100] 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 008/100] 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 009/100] 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 010/100] 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 011/100] 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 012/100] 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 013/100] 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 014/100] 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 015/100] 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 016/100] 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 017/100] 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 018/100] 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 019/100] 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 020/100] 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 021/100] 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 022/100] 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 023/100] 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 024/100] 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 025/100] 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 026/100] 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 027/100] 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 028/100] 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 029/100] 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 030/100] 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 031/100] 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 032/100] 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 033/100] 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 034/100] 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 035/100] 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 036/100] 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 037/100] 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 038/100] 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 039/100] 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 040/100] 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 041/100] 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 042/100] 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 043/100] 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 044/100] 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 045/100] 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 046/100] 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 047/100] 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 048/100] 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 049/100] 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 050/100] 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 051/100] 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 052/100] 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 053/100] 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 054/100] 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 055/100] 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 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 056/100] 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 057/100] 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 058/100] 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 059/100] 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 060/100] 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 061/100] 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 062/100] 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 063/100] 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 064/100] 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 065/100] 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 066/100] 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 067/100] 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 068/100] 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 069/100] 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 070/100] 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 071/100] 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 072/100] 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 073/100] 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 074/100] 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 075/100] 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 076/100] 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 077/100] 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 078/100] 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 079/100] 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 080/100] 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 081/100] 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 082/100] 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 083/100] 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 084/100] 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 085/100] 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 086/100] 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 087/100] 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 088/100] 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 089/100] 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 090/100] 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 091/100] 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 092/100] 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 093/100] 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 094/100] 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 095/100] 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 096/100] 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 097/100] 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 098/100] 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 099/100] 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 100/100] 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"