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-actions[bot]@users.noreply.github.com>
This commit is contained in:
Bas Nijholt 2023-07-26 12:52:03 -07:00 committed by GitHub
commit e9297d562f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 447 additions and 51 deletions

View file

@ -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)

View file

@ -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: |

View file

@ -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)

View file

@ -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"
}

View file

@ -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"
}
}
}
}
}

View file

@ -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."""

View file

@ -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"
}
}
}
}
}