mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-11 22:34:04 +02:00
detect significant changes
This commit is contained in:
parent
7f6deaa7da
commit
60cc02af26
1 changed files with 123 additions and 29 deletions
|
|
@ -14,6 +14,7 @@ import astral
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_PCT,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_RGB_COLOR,
|
||||
|
|
@ -126,6 +127,7 @@ async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall):
|
|||
data[CONF_ADAPT_BRIGHTNESS],
|
||||
data[CONF_ADAPT_COLOR_TEMP],
|
||||
data[CONF_ADAPT_RGB_COLOR],
|
||||
force=True,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -211,6 +213,11 @@ def _supported_features(hass, light: str):
|
|||
return {key for key, value in _SUPPORT_OPTS.items() if supported_features & value}
|
||||
|
||||
|
||||
def abs_rel_diff(a, b):
|
||||
"""Absolute relative difference in %."""
|
||||
return abs((a - b) / b) * 100
|
||||
|
||||
|
||||
class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
||||
"""Representation of a Adaptive Lighting switch."""
|
||||
|
||||
|
|
@ -238,7 +245,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
self._only_once = data[CONF_ONLY_ONCE]
|
||||
self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR]
|
||||
self._take_over_control = data[CONF_TAKE_OVER_CONTROL]
|
||||
self._transition = data[CONF_TRANSITION]
|
||||
self._transition = min(
|
||||
data[CONF_TRANSITION], self._interval.total_seconds() // 2
|
||||
)
|
||||
|
||||
self._sun_light_settings = SunLightSettings(
|
||||
name=self._name,
|
||||
|
|
@ -270,7 +279,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
self.__context = Context() # self._context will be overwritten
|
||||
|
||||
# Set in self._update_attrs_and_maybe_adapt_lights
|
||||
self._light_settings = {}
|
||||
self._settings: Dict[str, Any] = {}
|
||||
|
||||
# Set and unset tracker in async_turn_on and async_turn_off
|
||||
self.remove_listeners = []
|
||||
|
|
@ -364,12 +373,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
def device_state_attributes(self) -> Dict[str, Any]:
|
||||
"""Return the attributes of the switch."""
|
||||
if not self.is_on:
|
||||
return {key: None for key in self._light_settings}
|
||||
return self._light_settings
|
||||
return {key: None for key in self._settings}
|
||||
return self._settings
|
||||
|
||||
def _reset_take_over_control(self):
|
||||
for light in self._lights:
|
||||
self.turn_on_off_listener.manually_controlled[light] = False
|
||||
self.turn_on_off_listener.reset(*self._lights)
|
||||
|
||||
async def async_turn_on( # pylint: disable=arguments-differ
|
||||
self, adapt_lights: bool = True
|
||||
|
|
@ -406,6 +414,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
adapt_brightness: Optional[bool] = None,
|
||||
adapt_color_temp: Optional[bool] = None,
|
||||
adapt_rgb_color: Optional[bool] = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
lock = self._locks.get(light)
|
||||
if lock is not None and lock.locked():
|
||||
|
|
@ -428,7 +437,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
service_data[ATTR_TRANSITION] = transition
|
||||
|
||||
if "brightness" in features and adapt_brightness:
|
||||
service_data[ATTR_BRIGHTNESS_PCT] = self._light_settings["brightness_pct"]
|
||||
service_data[ATTR_BRIGHTNESS_PCT] = self._settings["brightness_pct"]
|
||||
|
||||
if (
|
||||
"color_temp" in features
|
||||
|
|
@ -437,18 +446,29 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
):
|
||||
attributes = self.hass.states.get(light).attributes
|
||||
min_mireds, max_mireds = attributes["min_mireds"], attributes["max_mireds"]
|
||||
color_temp_mired = self._light_settings["color_temp_mired"]
|
||||
color_temp_mired = self._settings["color_temp_mired"]
|
||||
color_temp_mired = max(min(color_temp_mired, max_mireds), min_mireds)
|
||||
service_data[ATTR_COLOR_TEMP] = color_temp_mired
|
||||
elif "color" in features and adapt_rgb_color:
|
||||
service_data[ATTR_RGB_COLOR] = self._light_settings["rgb_color"]
|
||||
service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"]
|
||||
|
||||
if (
|
||||
self._take_over_control
|
||||
and not force
|
||||
and self.turn_on_off_listener.significant_change(
|
||||
light,
|
||||
self._adapt_brightness,
|
||||
self._adapt_color_temp,
|
||||
self._adapt_rgb_color,
|
||||
)
|
||||
):
|
||||
return
|
||||
self.turn_on_off_listener.last_service_data[light] = service_data
|
||||
_LOGGER.debug(
|
||||
"%s: Scheduling 'light.turn_on' with the following 'service_data': %s",
|
||||
self._name,
|
||||
service_data,
|
||||
)
|
||||
|
||||
await self.hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
|
|
@ -464,7 +484,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
):
|
||||
_LOGGER.debug("%s: '_update_attrs_and_maybe_adapt_lights' called", self._name)
|
||||
assert self.is_on
|
||||
self._light_settings = self._sun_light_settings.get_settings(
|
||||
self._settings = self._sun_light_settings.get_settings(
|
||||
self.sleep_mode_switch.is_on
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
|
@ -487,19 +507,21 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
for light in lights:
|
||||
if not is_on(self.hass, light):
|
||||
continue
|
||||
if self._take_over_control:
|
||||
if self.turn_on_off_listener.is_manually_controlled(
|
||||
if (
|
||||
self._take_over_control
|
||||
and self.turn_on_off_listener.is_manually_controlled(
|
||||
light,
|
||||
force,
|
||||
adaptive_lighting_context=self.__context,
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s: '%s' is being manually controlled, stop adapting.",
|
||||
self._name,
|
||||
light,
|
||||
)
|
||||
continue
|
||||
await self._adapt_light(light, transition)
|
||||
)
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s: '%s' is being manually controlled, stop adapting.",
|
||||
self._name,
|
||||
light,
|
||||
)
|
||||
continue
|
||||
await self._adapt_light(light, transition, force=force)
|
||||
|
||||
async def _sleep_state_event(self, event: Event):
|
||||
if not match_state_event(event, ("on", "off")):
|
||||
|
|
@ -553,7 +575,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
):
|
||||
# Tracks 'off' → 'on' state changes
|
||||
self._on_to_off_event[entity_id] = event
|
||||
self.turn_on_off_listener.manually_controlled[entity_id] = False
|
||||
self.turn_on_off_listener.reset(entity_id)
|
||||
|
||||
|
||||
class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity):
|
||||
|
|
@ -762,23 +784,28 @@ class TurnOnOffListener:
|
|||
self.sleep_tasks: Dict[str, asyncio.Task] = {}
|
||||
# Tracks which lights are manually controlled
|
||||
self.manually_controlled: Dict[str, bool] = {}
|
||||
# Track which settings were applied to a light
|
||||
self.last_service_data: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
self.remove_listener = self.hass.bus.async_listen(
|
||||
EVENT_CALL_SERVICE, self.turn_on_off_event_listener
|
||||
)
|
||||
|
||||
def reset(self, *lights):
|
||||
"""Reset the 'manually_controlled' status of the lights."""
|
||||
for light in lights:
|
||||
self.manually_controlled[light] = False
|
||||
self.last_service_data.pop(light, None)
|
||||
|
||||
async def turn_on_off_event_listener(self, event: Event):
|
||||
"""Track 'light.turn_off' and 'light.turn_on' service calls."""
|
||||
domain = event.data.get(ATTR_DOMAIN)
|
||||
if domain != LIGHT_DOMAIN:
|
||||
return
|
||||
|
||||
service = event.data.get(ATTR_SERVICE)
|
||||
service_data = event.data.get(ATTR_SERVICE_DATA, {})
|
||||
|
||||
entity_ids = service_data.get(ATTR_ENTITY_ID)
|
||||
if isinstance(entity_ids, str):
|
||||
entity_ids = [entity_ids]
|
||||
service = event.data[ATTR_SERVICE]
|
||||
service_data = event.data[ATTR_SERVICE_DATA]
|
||||
entity_ids = cv.ensure_list(service_data[ATTR_ENTITY_ID])
|
||||
|
||||
if not any(eid in self.lights for eid in entity_ids):
|
||||
return
|
||||
|
|
@ -792,7 +819,7 @@ class TurnOnOffListener:
|
|||
)
|
||||
for eid in entity_ids:
|
||||
self.turn_off_event[eid] = event
|
||||
self.manually_controlled[eid] = False
|
||||
self.reset(eid)
|
||||
|
||||
elif service == SERVICE_TURN_ON:
|
||||
_LOGGER.debug("Detected an 'light.turn_on('%s')' event", entity_ids)
|
||||
|
|
@ -832,6 +859,73 @@ class TurnOnOffListener:
|
|||
)
|
||||
return manually_controlled
|
||||
|
||||
def significant_change(
|
||||
self, light, adapt_brightness, adapt_color_temp, adapt_rgb_color, threshold=5
|
||||
):
|
||||
"""Has the light made a significant change since last update.
|
||||
|
||||
This method will detect changes that were made to the light without
|
||||
calling 'light.turn_on', so outside of Home Assistant. If a change is
|
||||
detected, we mark the light as 'manually_controlled' until the light
|
||||
or switch is turned 'off' and 'on' again.
|
||||
"""
|
||||
if light not in self.last_service_data:
|
||||
return False
|
||||
changed = False
|
||||
service_data = self.last_service_data[light]
|
||||
attributes = self.hass.states.get(light).attributes
|
||||
if (
|
||||
adapt_brightness
|
||||
and ATTR_BRIGHTNESS_PCT in service_data
|
||||
and ATTR_BRIGHTNESS in attributes
|
||||
):
|
||||
applied_brightness = round(255 * service_data[ATTR_BRIGHTNESS_PCT] / 100)
|
||||
current_brightness = attributes["brightness"]
|
||||
if abs_rel_diff(current_brightness, applied_brightness) > threshold:
|
||||
_LOGGER.debug("Brightness of '%s' significantly changed", light)
|
||||
changed = True
|
||||
|
||||
if (
|
||||
adapt_color_temp
|
||||
and ATTR_COLOR_TEMP in service_data
|
||||
and ATTR_COLOR_TEMP in attributes
|
||||
):
|
||||
applied_color_temp = service_data[ATTR_COLOR_TEMP]
|
||||
current_color_temp = attributes[ATTR_COLOR_TEMP]
|
||||
if abs_rel_diff(current_color_temp, applied_color_temp) > threshold:
|
||||
_LOGGER.debug(
|
||||
"Color temperature of '%s' significantly changed",
|
||||
light,
|
||||
)
|
||||
changed = True
|
||||
|
||||
if (
|
||||
adapt_rgb_color
|
||||
and ATTR_RGB_COLOR in service_data
|
||||
and ATTR_RGB_COLOR in attributes
|
||||
):
|
||||
applied_rgb_color = service_data[ATTR_RGB_COLOR]
|
||||
current_rgb_color = attributes[ATTR_RGB_COLOR]
|
||||
for col_applied, col_current in zip(applied_rgb_color, current_rgb_color):
|
||||
if abs_rel_diff(col_applied, col_current) > threshold:
|
||||
_LOGGER.debug(
|
||||
"color RGB of '%s' significantly changed",
|
||||
light,
|
||||
)
|
||||
changed = True
|
||||
|
||||
if (ATTR_RGB_COLOR in service_data and ATTR_RGB_COLOR not in attributes) or (
|
||||
ATTR_COLOR_TEMP in service_data and ATTR_COLOR_TEMP not in attributes
|
||||
):
|
||||
# Light switched from RGB mode to color_temp or visa versa
|
||||
_LOGGER.debug(
|
||||
"'%s' switched from RGB mode to color_temp or visa versa",
|
||||
light,
|
||||
)
|
||||
changed = True
|
||||
self.manually_controlled[light] = changed
|
||||
return changed
|
||||
|
||||
async def maybe_cancel_adjusting(
|
||||
self, entity_id: str, off_to_on_event: Event, on_to_off_event: Optional[Event]
|
||||
) -> bool:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue