diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 3fc9c5d4..d496106b 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -274,6 +274,10 @@ 'documented defaults), or "configuration" (reverts to switch config defaults). βš™οΈ' ) +CONF_HUE_KEYWORD, DEFAULT_HUE_KEYWORD = "hue_keyword", "None" +DOCS[CONF_HUE_KEYWORD] = ( + "Scenes witch contain this keyword will be updated on the HUE bridge" +) TURNING_OFF_DELAY = 5 DOCS_MANUAL_CONTROL = { @@ -367,6 +371,7 @@ def int_between(min_int, max_int): (CONF_INTERCEPT, DEFAULT_INTERCEPT, bool), (CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT, bool), (CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES, bool), + (CONF_HUE_KEYWORD, NONE_STR, str), ] @@ -435,6 +440,7 @@ def apply_service_schema(initial_transition: int = 1): vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean, vol.Optional(CONF_PREFER_RGB_COLOR, default=False): cv.boolean, vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean, + vol.Optional(CONF_HUE_KEYWORD): cv.string, }, ) diff --git a/custom_components/adaptive_lighting/hue_utils.py b/custom_components/adaptive_lighting/hue_utils.py new file mode 100644 index 00000000..91276cd5 --- /dev/null +++ b/custom_components/adaptive_lighting/hue_utils.py @@ -0,0 +1,17 @@ +"""Help fixing a serialization issue.""" + +from dataclasses import dataclass + + +@dataclass +class ResourceIdentifier: + """Represent a ResourceIdentifier object as used by the Hue api. + + clip-api.schema.json#/definitions/ResourceIdentifierGet + clip-api.schema.json#/definitions/ResourceIdentifierPost + clip-api.schema.json#/definitions/ResourceIdentifierPut + clip-api.schema.json#/definitions/ResourceIdentifierDelete + """ + + rid: str # UUID + rtype: str diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 29955d0a..4e309f32 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -14,6 +14,8 @@ import homeassistant.util.dt as dt_util import ulid_transform import voluptuous as vol +from aiohue import HueBridgeV2 +from aiohue.v2.models.scene import ScenePut from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -74,6 +76,8 @@ from homeassistant.helpers import entity_platform, entity_registry from homeassistant.helpers.entity_component import async_update_entity +from .hue_utils import ResourceIdentifier + if [MAJOR_VERSION, MINOR_VERSION] < [2023, 9]: from homeassistant.helpers.entity import DeviceInfo else: @@ -89,6 +93,7 @@ from homeassistant.loader import bind_hass from homeassistant.util import slugify from homeassistant.util.color import ( + color_temperature_kelvin_to_mired, color_temperature_to_rgb, color_xy_to_RGB, ) @@ -115,6 +120,7 @@ CONF_BRIGHTNESS_MODE_TIME_DARK, CONF_BRIGHTNESS_MODE_TIME_LIGHT, CONF_DETECT_NON_HA_CHANGES, + CONF_HUE_KEYWORD, CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, CONF_INITIAL_TRANSITION, CONF_INTERCEPT, @@ -200,6 +206,9 @@ # Keep a short domain version for the context instances (which can only be 36 chars) _DOMAIN_SHORT = "al" +# The interval of time between each scene updates +HUE_SCENE_UPDATE_DELAY = 300 # 5 minutes + def create_context( name: str, @@ -817,6 +826,7 @@ def __init__( self._name = data[CONF_NAME] self._interval: timedelta = data[CONF_INTERVAL] self.lights: list[str] = data[CONF_LIGHTS] + self.hue_keyword = data[CONF_HUE_KEYWORD] # backup data for use in change_switch_settings "configuration" CONF_USE_DEFAULTS self._config_backup = deepcopy(data) @@ -1392,6 +1402,8 @@ async def _update_attrs_and_maybe_adapt_lights( # noqa: PLR0912 ) self.async_write_ha_state() + await self.update_hue_run() + if not force and self._only_once: return @@ -1568,6 +1580,68 @@ async def _sleep_mode_switch_state_event_action(self, event: Event) -> None: force=True, ) + async def update_hue_run(self): + """Function updating HUE scene.""" + if self.hue_keyword is None: + return + + timer = self.manager.hue_scene_update_timers.get(self.hue_keyword) + if timer is not None and timer.is_running(): + _LOGGER.debug( + "%s: Not updating %s because there is throttle, remaining_time:%s", + self._name, + self.hue_keyword, + timer.remaining_time(), + ) + return + + self.manager.start_hue_scene_update_throttle(self.hue_keyword) + + _LOGGER.debug( + "%s: Will updates scenes containing %s", + self._name, + self.hue_keyword, + ) + config_entries = self.hass.config_entries.async_entries(domain="hue") + config_entry = config_entries[0] + + color_temp = color_temperature_kelvin_to_mired( + self._settings["color_temp_kelvin"], + ) + + brightness = round(self._settings["brightness_pct"], 2) + + async with HueBridgeV2( + config_entry.data["host"], + config_entry.data["api_key"], + ) as bridge: + for scene in bridge.scenes: + if self.hue_keyword in scene.metadata.name: + _LOGGER.debug( + "%s: Updating %s with values bri:%s, color_temp:%s", + self._name, + scene.metadata.name, + brightness, + color_temp, + ) + actions = [] + for action in scene.actions: + action.target = ResourceIdentifier( + rid=action.target.rid, + rtype="light", + ) + light = bridge.lights.get(action.target.rid) + color_temp = clamp( + color_temp, + light.color_temperature.mirek_schema.mirek_minimum, + light.color_temperature.mirek_schema.mirek_maximum, + ) + action.action.color_temperature.mirek = color_temp + action.action.dimming.brightness = brightness + actions.append(action) + update_obj = ScenePut(actions=actions) + await bridge.scenes.scene.update(scene.id, update_obj) + class SimpleSwitch(SwitchEntity, RestoreEntity): """Representation of a Adaptive Lighting switch.""" @@ -1684,6 +1758,9 @@ def __init__(self, hass: HomeAssistant) -> None: # Track light transitions self.transition_timers: dict[str, _AsyncSingleShotTimer] = {} + # Handle HUE delay to avoid overloading the bridge + self.hue_scene_update_timers: dict[str, _AsyncSingleShotTimer] = {} + # Track _execute_cancellable_adaptation_calls tasks self.adaptation_tasks = set() @@ -2141,6 +2218,28 @@ async def reset(): self._handle_timer(light, self.transition_timers, last_transition, reset) + def start_hue_scene_update_throttle(self, scene: str) -> None: + """Set a timer between each scene update call.""" + _LOGGER.debug( + "Start throttle timer of %s seconds for scene %s", + HUE_SCENE_UPDATE_DELAY, + scene, + ) + + async def reset(): + # Called when the timer expires, doesn't need to do anything + _LOGGER.debug( + "Throttle finished for scene %s", + scene, + ) + + self._handle_timer( + scene, + self.hue_scene_update_timers, + HUE_SCENE_UPDATE_DELAY, + reset, + ) + def set_auto_reset_manual_control_times(self, lights: list[str], time: float): """Set the time after which the lights are automatically reset.""" if time == 0: diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 9991d232..e23662fb 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -57,7 +57,8 @@ "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. πŸ“‰Disable if physical light states get out of sync with HA's recorded state.", "intercept": "intercept: Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness.", "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. βž—βš οΈ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled.", - "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. πŸ“" + "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. πŸ“", + "hue_keyword": "hue_keyword" }, "data_description": { "interval": "Frequency to adapt the lights, in seconds. πŸ”„", @@ -81,7 +82,8 @@ "brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. πŸ“ˆπŸ“‰.", "autoreset_control_seconds": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", "send_split_delay": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️", - "adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️" + "adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️", + "hue_keyword": "Set the HUE scene keyword to update" } } }, @@ -262,6 +264,10 @@ "autoreset_control_seconds": { "description": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️", "name": "autoreset_control_seconds" + }, + "hue_keyword": { + "description": "Set the HUE scene keyword to update", + "name": "hue_keyword" } } } diff --git a/scripts/setup-dependencies b/scripts/setup-dependencies index 9409ac0c..06d92b34 100755 --- a/scripts/setup-dependencies +++ b/scripts/setup-dependencies @@ -22,4 +22,5 @@ uv pip install -r core/requirements_test.txt uv pip install -e core/ uv pip install ulid-transform # this is in Adaptive-lighting's manifest.json +uv pip install aiohue uv pip install $(python test_dependencies.py)