From e9297d562f11c208b568c425b478974a9c48aa29 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 26 Jul 2023 12:52:03 -0700 Subject: [PATCH] Cleanup after add_to_platform_abort is called, try-except intercept, update strings.json, fixes #658 (#659) * Cleanup after add_to_platform_abort is called, fixes #658 * Add note * try-catch because of priv * Catch all exceptions * Bump to 1.17.3 in manifest.json * assert that hass is not None * Rename _light_event_action -> _light_state_event_action * update .github/update-strings.py * Update README.md, strings.json, and services.yaml * update name * Fixes * Update README.md, strings.json, and services.yaml * Add name * Do not add name --------- Co-authored-by: github-actions[bot] --- .github/update-strings.py | 24 ++- .github/workflows/update-readme.yml | 8 +- .../adaptive_lighting/hass_utils.py | 20 +- .../adaptive_lighting/manifest.json | 2 +- .../adaptive_lighting/strings.json | 176 ++++++++++++++++++ custom_components/adaptive_lighting/switch.py | 92 +++++---- .../adaptive_lighting/translations/en.json | 176 ++++++++++++++++++ 7 files changed, 447 insertions(+), 51 deletions(-) diff --git a/.github/update-strings.py b/.github/update-strings.py index 8f8f9ef4..44dc6bd6 100644 --- a/.github/update-strings.py +++ b/.github/update-strings.py @@ -3,6 +3,8 @@ import json import sys from pathlib import Path +import yaml + sys.path.append(str(Path(__file__).parent.parent)) from custom_components.adaptive_lighting import const # noqa: E402 @@ -13,20 +15,40 @@ en_fname = folder / "translations" / "en.json" with strings_fname.open() as f: strings = json.load(f) +# Set "options" data = {k: f"{k}: {const.DOCS[k]}" for k, _, _ in const.VALIDATION_TUPLES} strings["options"]["step"]["init"]["data"] = data +# Set "services" +services_filename = Path("custom_components") / "adaptive_lighting" / "services.yaml" +with open(services_filename) as f: # noqa: PTH123 + services = yaml.safe_load(f) +services_json = {} +for service_name, dct in services.items(): + services_json[service_name] = { + "name": service_name, + "description": dct["description"], + "fields": {}, + } + for field_name, field in dct["fields"].items(): + services_json[service_name]["fields"][field_name] = { + "description": field["description"], + "name": field_name, + } +strings["services"] = services_json + +# Write changes to strings.json with strings_fname.open("w") as f: json.dump(strings, f, indent=2, ensure_ascii=False) f.write("\n") - # Sync changes from strings.json to en.json with en_fname.open() as f: en = json.load(f) en["config"]["step"]["user"] = strings["config"]["step"]["user"] en["options"]["step"]["init"]["data"] = data +en["services"] = services_json with en_fname.open("w") as f: json.dump(en, f, indent=2, ensure_ascii=False) diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index a31d556e..8fca3d77 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -1,4 +1,4 @@ -name: Update README.md +name: Update README.md, strings.json, and services.yaml on: push: @@ -34,12 +34,12 @@ jobs: - name: Run markdown-code-runner run: markdown-code-runner --debug README.md - - name: Run update strings.json - run: python .github/update-strings.py - - name: Run update services.yaml run: python .github/update-services.py + - name: Run update strings.json + run: python .github/update-strings.py + - name: Commit updated README.md, strings.json, and services.yaml id: commit run: | diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py index 3f08b3fc..cb9b8ab4 100644 --- a/custom_components/adaptive_lighting/hass_utils.py +++ b/custom_components/adaptive_lighting/hass_utils.py @@ -1,4 +1,5 @@ """Utility functions for HA core.""" +import logging from collections.abc import Awaitable, Callable from homeassistant.core import HomeAssistant, ServiceCall @@ -6,6 +7,8 @@ from homeassistant.util.read_only_dict import ReadOnlyDict from .adaptation_utils import ServiceData +_LOGGER = logging.getLogger(__name__) + def setup_service_call_interceptor( hass: HomeAssistant, @@ -39,15 +42,18 @@ def setup_service_call_interceptor( existing_service = registered_services[domain][service] async def service_func_proxy(call: ServiceCall) -> None: - # Convert read-only data to writeable dictionary for modification by interceptor - data = dict(call.data) + try: + # Convert read-only data to writeable dictionary for modification by interceptor + data = dict(call.data) - # Call interceptor - await intercept_func(call, data) - - # Convert data back to read-only - call.data = ReadOnlyDict(data) + # Call interceptor + await intercept_func(call, data) + # Convert data back to read-only + call.data = ReadOnlyDict(data) + except Exception as e: # noqa: BLE001 + # Blindly catch all exceptions to avoid breaking light.turn_on + _LOGGER.error("Error in service_func_proxy: %s", e) # Call original service handler with processed data await existing_service.job.target(call) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 1e232dab..a5293b7a 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.17.2" + "version": "1.17.3" } diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 7ad84741..d142c7b7 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -56,5 +56,181 @@ "option_error": "Invalid option", "entity_missing": "One or more selected light entities are missing from Home Assistant" } + }, + "services": { + "apply": { + "name": "apply", + "description": "Applies the current Adaptive Lighting settings to lights.", + "fields": { + "entity_id": { + "description": "The `entity_id` of the switch with the settings to apply. 📝", + "name": "entity_id" + }, + "lights": { + "description": "A light (or list of lights) to apply the settings to. 💡", + "name": "lights" + }, + "transition": { + "description": "Duration of transition when lights change, in seconds. 🕑", + "name": "transition" + }, + "adapt_brightness": { + "description": "Whether to adapt the brightness of the light. 🌞", + "name": "adapt_brightness" + }, + "adapt_color": { + "description": "Whether to adapt the color on supporting lights. 🌈", + "name": "adapt_color" + }, + "prefer_rgb_color": { + "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "name": "prefer_rgb_color" + }, + "turn_on_lights": { + "description": "Whether to turn on lights that are currently off. 🔆", + "name": "turn_on_lights" + } + } + }, + "set_manual_control": { + "name": "set_manual_control", + "description": "Mark whether a light is 'manually controlled'.", + "fields": { + "entity_id": { + "description": "The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝", + "name": "entity_id" + }, + "lights": { + "description": "entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡", + "name": "lights" + }, + "manual_control": { + "description": "Whether to add (\"true\") or remove (\"false\") the light from the \"manual_control\" list. 🔒", + "name": "manual_control" + } + } + }, + "change_switch_settings": { + "name": "change_switch_settings", + "description": "Change any settings you'd like in the switch. All options here are the same as in the config flow.", + "fields": { + "entity_id": { + "description": "Entity ID of the switch. 📝", + "name": "entity_id" + }, + "use_defaults": { + "description": "Sets the default values not specified in this service call. Options: \"current\" (default, retains current values), \"factory\" (resets to documented defaults), or \"configuration\" (reverts to switch config defaults). ⚙️", + "name": "use_defaults" + }, + "include_config_in_attributes": { + "description": "Show all options as attributes on the switch in Home Assistant when set to `true`. 📝", + "name": "include_config_in_attributes" + }, + "turn_on_lights": { + "description": "Whether to turn on lights that are currently off. 🔆", + "name": "turn_on_lights" + }, + "initial_transition": { + "description": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "name": "initial_transition" + }, + "sleep_transition": { + "description": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "name": "sleep_transition" + }, + "max_brightness": { + "description": "Maximum brightness percentage. 💡", + "name": "max_brightness" + }, + "max_color_temp": { + "description": "Coldest color temperature in Kelvin. ❄️", + "name": "max_color_temp" + }, + "min_brightness": { + "description": "Minimum brightness percentage. 💡", + "name": "min_brightness" + }, + "min_color_temp": { + "description": "Warmest color temperature in Kelvin. 🔥", + "name": "min_color_temp" + }, + "only_once": { + "description": "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", + "name": "only_once" + }, + "prefer_rgb_color": { + "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "name": "prefer_rgb_color" + }, + "separate_turn_on_commands": { + "description": "Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", + "name": "separate_turn_on_commands" + }, + "send_split_delay": { + "description": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", + "name": "send_split_delay" + }, + "sleep_brightness": { + "description": "Brightness percentage of lights in sleep mode. 😴", + "name": "sleep_brightness" + }, + "sleep_rgb_or_color_temp": { + "description": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", + "name": "sleep_rgb_or_color_temp" + }, + "sleep_rgb_color": { + "description": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "name": "sleep_rgb_color" + }, + "sleep_color_temp": { + "description": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", + "name": "sleep_color_temp" + }, + "sunrise_offset": { + "description": "Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "name": "sunrise_offset" + }, + "sunrise_time": { + "description": "Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "name": "sunrise_time" + }, + "sunset_offset": { + "description": "Adjust sunset time with a positive or negative offset in seconds. ⏰", + "name": "sunset_offset" + }, + "sunset_time": { + "description": "Set a fixed time (HH:MM:SS) for sunset. 🌇", + "name": "sunset_time" + }, + "max_sunrise_time": { + "description": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅", + "name": "max_sunrise_time" + }, + "min_sunset_time": { + "description": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇", + "name": "min_sunset_time" + }, + "take_over_control": { + "description": "Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", + "name": "take_over_control" + }, + "detect_non_ha_changes": { + "description": "Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️", + "name": "detect_non_ha_changes" + }, + "transition": { + "description": "Duration of transition when lights change, in seconds. 🕑", + "name": "transition" + }, + "adapt_delay": { + "description": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", + "name": "adapt_delay" + }, + "autoreset_control_seconds": { + "description": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "name": "autoreset_control_seconds" + } + } + } } } diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 6e337a30..6dae39f2 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -437,15 +437,13 @@ async def async_setup_entry( # noqa: PLR0915 async_add_entities: AddEntitiesCallback, ): """Set up the AdaptiveLighting switch.""" + assert hass is not None data = hass.data[DOMAIN] assert config_entry.entry_id in data - - if ATTR_ADAPTIVE_LIGHTING_MANAGER not in data: - data[ATTR_ADAPTIVE_LIGHTING_MANAGER] = AdaptiveLightingManager( - hass, - config_entry, - ) - manager: AdaptiveLightingManager = data[ATTR_ADAPTIVE_LIGHTING_MANAGER] + manager = data.setdefault( + ATTR_ADAPTIVE_LIGHTING_MANAGER, + AdaptiveLightingManager(hass, config_entry), + ) sleep_mode_switch = SimpleSwitch( which="Sleep Mode", initial_state=False, @@ -503,7 +501,7 @@ async def async_setup_entry( # noqa: PLR0915 if not lights: all_lights = switch.lights else: - all_lights = _expand_light_groups(switch.hass, lights) + all_lights = _expand_light_groups(hass, lights) switch.manager.lights.update(all_lights) for light in all_lights: if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light): @@ -533,7 +531,7 @@ async def async_setup_entry( # noqa: PLR0915 if not lights: all_lights = switch.lights else: - all_lights = _expand_light_groups(switch.hass, lights) + all_lights = _expand_light_groups(hass, lights) if service_call.data[CONF_MANUAL_CONTROL]: for light in all_lights: _fire_manual_control_event(switch, light, service_call.context) @@ -585,7 +583,7 @@ def validate( config_entry: ConfigEntry, service_data: dict[str, Any] | None = None, defaults: dict[str, Any] | None = None, -): +) -> dict[str, Any]: """Get the options and data from the config_entry and add defaults.""" if defaults is None: data = {key: default for key, default, _ in VALIDATION_TUPLES} @@ -812,6 +810,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) -> None: """Initialize the Adaptive Lighting switch.""" # Set attributes that can't be modified during runtime + assert hass is not None self.hass = hass self.manager = manager self.sleep_mode_switch = sleep_mode_switch @@ -847,7 +846,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): # Set and unset tracker in async_turn_on and async_turn_off self.remove_listeners = [] self.remove_interval: Callable[[], None] = lambda: None - _LOGGER.debug( "%s: Setting up with '%s'," " config_entry.data: '%s'," @@ -1005,7 +1003,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): remove_state = async_track_state_change_event( self.hass, entity_ids=self.lights, - action=self._light_event_action, + action=self._light_state_event_action, ) self.remove_listeners.append(remove_state) @@ -1035,8 +1033,27 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): interval=adaptation_interval, ) + def _call_on_remove_callbacks(self) -> None: + """Call callbacks registered by async_on_remove.""" + # This is called when the integration is removed from HA + # and in `Entity.add_to_platform_abort`. + # For some unknown reason (to me) `async_will_remove_from_hass` + # is not called in `add_to_platform_abort`. + # See https://github.com/basnijholt/adaptive-lighting/issues/658 + self._remove_listeners() + try: + # HACK: this is a private method in `Entity` which can change + super()._call_on_remove_callbacks() + except AttributeError as err: + _LOGGER.error( + "%s: Caught AttributeError in `_call_on_remove_callbacks`: %s", + self._name, + err, + ) + def _remove_interval_listener(self) -> None: self.remove_interval() + self.remove_interval = lambda: None def _remove_listeners(self) -> None: self._remove_interval_listener() @@ -1152,7 +1169,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) # Build service data. - service_data = {ATTR_ENTITY_ID: light} + service_data: dict[str, Any] = {ATTR_ENTITY_ID: light} features = _supported_features(self.hass, light) # Check transition == 0 to fix #378 @@ -1289,8 +1306,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): to cancel an ongoing adaptation when a light is turned off. """ # Prevent overlap of multiple adaptation sequences - listener = self.manager - listener.cancel_ongoing_adaptation_calls(data.entity_id, which=data.which) + self.manager.cancel_ongoing_adaptation_calls(data.entity_id, which=data.which) _LOGGER.debug( "%s: execute_cancellable_adaptation_calls with data: %s", self._name, @@ -1300,9 +1316,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): try: task = asyncio.ensure_future(self._execute_adaptation_calls(data)) if data.which in ("both", "brightness"): - listener.adaptation_tasks_brightness[data.entity_id] = task + self.manager.adaptation_tasks_brightness[data.entity_id] = task if data.which in ("both", "color"): - listener.adaptation_tasks_color[data.entity_id] = task + self.manager.adaptation_tasks_color[data.entity_id] = task await task except asyncio.CancelledError: _LOGGER.debug( @@ -1427,7 +1443,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context=self.create_context("sleep", parent=event.context), ) - async def _light_event_action(self, event: Event) -> None: + async def _light_state_event_action(self, event: Event) -> None: old_state = event.data.get("old_state") new_state = event.data.get("new_state") entity_id = event.data.get("entity_id") @@ -1801,9 +1817,10 @@ class AdaptiveLightingManager: def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize the AdaptiveLightingManager that is shared among all switches.""" + assert hass is not None self.hass = hass data = validate(config_entry) - self.lights = set() + self.lights: set[str] = set() # Tracks 'light.turn_off' service calls self.turn_off_event: dict[str, Event] = {} @@ -1935,6 +1952,15 @@ class AdaptiveLightingManager: return entity_id = entity_ids[0] + + # Prevent adaptation of TURN_ON calls when light is already on, + # and of TOGGLE calls when toggling off. + if self.hass.states.is_state(entity_id, STATE_ON): + return + + if self.manual_control.get(entity_id, False): + return + try: adaptive_switch = _switch_with_lights(self.hass, [entity_id]) except NoSwitchFoundError: @@ -1952,14 +1978,6 @@ class AdaptiveLightingManager: if entity_id not in adaptive_switch.lights: return - if self.manual_control.get(entity_id, False): - return - - # Prevent adaptation of TURN_ON calls when light is already on, - # and of TOGGLE calls when toggling off. - if self.hass.states.is_state(entity_id, STATE_ON): - return - _LOGGER.debug( "Intercepted TURN_ON call with data %s (%s)", data, @@ -2143,11 +2161,10 @@ class AdaptiveLightingManager: self.cancel_ongoing_adaptation_calls(light) def _get_entity_list(self, service_data: ServiceData) -> list[str]: - entity_ids = [] - if ATTR_ENTITY_ID in service_data: - entity_ids = cv.ensure_list_csv(service_data[ATTR_ENTITY_ID]) - elif ATTR_AREA_ID in service_data: + return cv.ensure_list_csv(service_data[ATTR_ENTITY_ID]) + if ATTR_AREA_ID in service_data: + entity_ids = [] area_ids = cv.ensure_list_csv(service_data[ATTR_AREA_ID]) for area_id in area_ids: area_entity_ids = area_entities(self.hass, area_id) @@ -2162,13 +2179,12 @@ class AdaptiveLightingManager: entity_ids, area_id, ) - else: - _LOGGER.debug( - "No entity_ids or area_ids found in service_data: %s", - service_data, - ) - - return entity_ids + return entity_ids + _LOGGER.debug( + "No entity_ids or area_ids found in service_data: %s", + service_data, + ) + return [] async def turn_on_off_event_listener(self, event: Event) -> None: """Track 'light.turn_off' and 'light.turn_on' service calls.""" diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index e6d1f9a9..199c333c 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -57,5 +57,181 @@ "option_error": "Invalid option", "entity_missing": "One or more selected light entities are missing from Home Assistant" } + }, + "services": { + "apply": { + "name": "apply", + "description": "Applies the current Adaptive Lighting settings to lights.", + "fields": { + "entity_id": { + "description": "The `entity_id` of the switch with the settings to apply. 📝", + "name": "entity_id" + }, + "lights": { + "description": "A light (or list of lights) to apply the settings to. 💡", + "name": "lights" + }, + "transition": { + "description": "Duration of transition when lights change, in seconds. 🕑", + "name": "transition" + }, + "adapt_brightness": { + "description": "Whether to adapt the brightness of the light. 🌞", + "name": "adapt_brightness" + }, + "adapt_color": { + "description": "Whether to adapt the color on supporting lights. 🌈", + "name": "adapt_color" + }, + "prefer_rgb_color": { + "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "name": "prefer_rgb_color" + }, + "turn_on_lights": { + "description": "Whether to turn on lights that are currently off. 🔆", + "name": "turn_on_lights" + } + } + }, + "set_manual_control": { + "name": "set_manual_control", + "description": "Mark whether a light is 'manually controlled'.", + "fields": { + "entity_id": { + "description": "The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝", + "name": "entity_id" + }, + "lights": { + "description": "entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡", + "name": "lights" + }, + "manual_control": { + "description": "Whether to add (\"true\") or remove (\"false\") the light from the \"manual_control\" list. 🔒", + "name": "manual_control" + } + } + }, + "change_switch_settings": { + "name": "change_switch_settings", + "description": "Change any settings you'd like in the switch. All options here are the same as in the config flow.", + "fields": { + "entity_id": { + "description": "Entity ID of the switch. 📝", + "name": "entity_id" + }, + "use_defaults": { + "description": "Sets the default values not specified in this service call. Options: \"current\" (default, retains current values), \"factory\" (resets to documented defaults), or \"configuration\" (reverts to switch config defaults). ⚙️", + "name": "use_defaults" + }, + "include_config_in_attributes": { + "description": "Show all options as attributes on the switch in Home Assistant when set to `true`. 📝", + "name": "include_config_in_attributes" + }, + "turn_on_lights": { + "description": "Whether to turn on lights that are currently off. 🔆", + "name": "turn_on_lights" + }, + "initial_transition": { + "description": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️", + "name": "initial_transition" + }, + "sleep_transition": { + "description": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴", + "name": "sleep_transition" + }, + "max_brightness": { + "description": "Maximum brightness percentage. 💡", + "name": "max_brightness" + }, + "max_color_temp": { + "description": "Coldest color temperature in Kelvin. ❄️", + "name": "max_color_temp" + }, + "min_brightness": { + "description": "Minimum brightness percentage. 💡", + "name": "min_brightness" + }, + "min_color_temp": { + "description": "Warmest color temperature in Kelvin. 🔥", + "name": "min_color_temp" + }, + "only_once": { + "description": "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄", + "name": "only_once" + }, + "prefer_rgb_color": { + "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", + "name": "prefer_rgb_color" + }, + "separate_turn_on_commands": { + "description": "Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀", + "name": "separate_turn_on_commands" + }, + "send_split_delay": { + "description": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", + "name": "send_split_delay" + }, + "sleep_brightness": { + "description": "Brightness percentage of lights in sleep mode. 😴", + "name": "sleep_brightness" + }, + "sleep_rgb_or_color_temp": { + "description": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙", + "name": "sleep_rgb_or_color_temp" + }, + "sleep_rgb_color": { + "description": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈", + "name": "sleep_rgb_color" + }, + "sleep_color_temp": { + "description": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴", + "name": "sleep_color_temp" + }, + "sunrise_offset": { + "description": "Adjust sunrise time with a positive or negative offset in seconds. ⏰", + "name": "sunrise_offset" + }, + "sunrise_time": { + "description": "Set a fixed time (HH:MM:SS) for sunrise. 🌅", + "name": "sunrise_time" + }, + "sunset_offset": { + "description": "Adjust sunset time with a positive or negative offset in seconds. ⏰", + "name": "sunset_offset" + }, + "sunset_time": { + "description": "Set a fixed time (HH:MM:SS) for sunset. 🌇", + "name": "sunset_time" + }, + "max_sunrise_time": { + "description": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅", + "name": "max_sunrise_time" + }, + "min_sunset_time": { + "description": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇", + "name": "min_sunset_time" + }, + "take_over_control": { + "description": "Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒", + "name": "take_over_control" + }, + "detect_non_ha_changes": { + "description": "Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️", + "name": "detect_non_ha_changes" + }, + "transition": { + "description": "Duration of transition when lights change, in seconds. 🕑", + "name": "transition" + }, + "adapt_delay": { + "description": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", + "name": "adapt_delay" + }, + "autoreset_control_seconds": { + "description": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", + "name": "autoreset_control_seconds" + } + } + } } }