mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-11 22:34:04 +02:00
keep contexts around that identify where they come from
This commit is contained in:
parent
ba44113277
commit
31176f8897
1 changed files with 259 additions and 99 deletions
|
|
@ -7,6 +7,7 @@ from copy import deepcopy
|
|||
from dataclasses import dataclass
|
||||
import datetime
|
||||
from datetime import timedelta
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
|
|
@ -20,9 +21,17 @@ from homeassistant.components.homeassistant import (
|
|||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_PCT,
|
||||
ATTR_BRIGHTNESS_STEP,
|
||||
ATTR_BRIGHTNESS_STEP_PCT,
|
||||
ATTR_COLOR_NAME,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_EFFECT,
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_KELVIN,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_TRANSITION,
|
||||
ATTR_WHITE_VALUE,
|
||||
ATTR_XY_COLOR,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR,
|
||||
|
|
@ -120,6 +129,48 @@ BRIGHTNESS_CHANGE = 25 # ≈10% of total range
|
|||
COLOR_TEMP_CHANGE = 20 # ≈5% of total range
|
||||
RGB_CHANGE = 30 # ≈12% of total range per component
|
||||
|
||||
# Keep a short domain version for the context instances (which can only be 36 chars)
|
||||
_DOMAIN_SHORT = "adapt_lgt"
|
||||
|
||||
_DISABLE_ON = {
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_PCT,
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_STEP,
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_STEP_PCT,
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_NAME,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_XY_COLOR,
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_KELVIN,
|
||||
ATTR_WHITE_VALUE,
|
||||
ATTR_EFFECT,
|
||||
}
|
||||
|
||||
|
||||
def _short_hash(string: str, length: int = 4) -> str:
|
||||
"""Creates a hash of 'string' with length 'length'."""
|
||||
return hashlib.sha1(string.encode("UTF-8")).hexdigest()[:length]
|
||||
|
||||
|
||||
def create_context(name: str, which: str, index: int) -> 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}")
|
||||
|
||||
|
||||
def is_our_context(context: Optional[Context]) -> bool:
|
||||
"""Check whether this integration created 'context'."""
|
||||
if context is None:
|
||||
return False
|
||||
return context.id.startswith(_DOMAIN_SHORT)
|
||||
|
||||
|
||||
async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall):
|
||||
"""Handle the entity service apply."""
|
||||
|
|
@ -207,6 +258,7 @@ def match_state_event(event: Event, from_or_to_state: 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:
|
||||
state = hass.states.get(light)
|
||||
if state is None:
|
||||
|
|
@ -214,6 +266,7 @@ def _expand_light_groups(hass: HomeAssistant, lights: List[str]) -> List[str]:
|
|||
all_lights.add(light)
|
||||
elif "entity_id" in state.attributes: # it's a light group
|
||||
group = state.attributes["entity_id"]
|
||||
turn_on_off_listener.lights.discard(light)
|
||||
all_lights.update(group)
|
||||
_LOGGER.debug("Expanded %s to %s", light, group)
|
||||
else:
|
||||
|
|
@ -227,6 +280,86 @@ def _supported_features(hass: HomeAssistant, light: str):
|
|||
return {key for key, value in _SUPPORT_OPTS.items() if supported_features & value}
|
||||
|
||||
|
||||
def _attributes_have_changed(
|
||||
light,
|
||||
old_attributes,
|
||||
new_attributes,
|
||||
adapt_brightness,
|
||||
adapt_color_temp,
|
||||
adapt_rgb_color,
|
||||
context,
|
||||
):
|
||||
if (
|
||||
adapt_brightness
|
||||
and ATTR_BRIGHTNESS in old_attributes
|
||||
and ATTR_BRIGHTNESS in new_attributes
|
||||
):
|
||||
last_brightness = old_attributes[ATTR_BRIGHTNESS]
|
||||
current_brightness = new_attributes[ATTR_BRIGHTNESS]
|
||||
if abs(current_brightness - last_brightness) > BRIGHTNESS_CHANGE:
|
||||
_LOGGER.debug(
|
||||
"Brightness of '%s' significantly changed from %s to %s with"
|
||||
" context.id='%s'",
|
||||
light,
|
||||
last_brightness,
|
||||
current_brightness,
|
||||
context.id,
|
||||
)
|
||||
return True
|
||||
|
||||
if (
|
||||
adapt_color_temp
|
||||
and ATTR_COLOR_TEMP in old_attributes
|
||||
and ATTR_COLOR_TEMP in new_attributes
|
||||
):
|
||||
last_color_temp = old_attributes[ATTR_COLOR_TEMP]
|
||||
current_color_temp = new_attributes[ATTR_COLOR_TEMP]
|
||||
if abs(current_color_temp - last_color_temp) > COLOR_TEMP_CHANGE:
|
||||
_LOGGER.debug(
|
||||
"Color temperature of '%s' significantly changed from %s to %s with"
|
||||
" context.id='%s'",
|
||||
light,
|
||||
last_color_temp,
|
||||
current_color_temp,
|
||||
context.id,
|
||||
)
|
||||
return True
|
||||
|
||||
if (
|
||||
adapt_rgb_color
|
||||
and ATTR_RGB_COLOR in old_attributes
|
||||
and ATTR_RGB_COLOR in new_attributes
|
||||
):
|
||||
last_rgb_color = old_attributes[ATTR_RGB_COLOR]
|
||||
current_rgb_color = new_attributes[ATTR_RGB_COLOR]
|
||||
for last_col, current_col in zip(last_rgb_color, current_rgb_color):
|
||||
if abs(last_col - current_col) > RGB_CHANGE:
|
||||
_LOGGER.debug(
|
||||
"color RGB of '%s' significantly changed from %s to %s with"
|
||||
" context.id='%s'",
|
||||
light,
|
||||
last_rgb_color,
|
||||
current_rgb_color,
|
||||
context.id,
|
||||
)
|
||||
return True
|
||||
|
||||
switched_color_temp = (
|
||||
ATTR_RGB_COLOR in old_attributes and ATTR_RGB_COLOR not in new_attributes
|
||||
)
|
||||
switched_to_rgb_color = (
|
||||
ATTR_COLOR_TEMP in old_attributes and ATTR_COLOR_TEMP not in new_attributes
|
||||
)
|
||||
if switched_color_temp or switched_to_rgb_color:
|
||||
# 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,
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
||||
"""Representation of a Adaptive Lighting switch."""
|
||||
|
||||
|
|
@ -285,9 +418,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
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] = {}
|
||||
# To identify that this integration made a change
|
||||
self.__context = Context() # self._context would be overwritten
|
||||
self.turn_on_off_listener.contexts.add(self.__context)
|
||||
# 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] = {}
|
||||
|
|
@ -297,14 +429,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
_LOGGER.debug(
|
||||
"%s: Setting up with '%s',"
|
||||
" config_entry.data: '%s',"
|
||||
" config_entry.options: '%s', converted to '%s',"
|
||||
" with context '%s'.",
|
||||
" config_entry.options: '%s', converted to '%s'.",
|
||||
self._name,
|
||||
self._lights,
|
||||
config_entry.data,
|
||||
config_entry.options,
|
||||
data,
|
||||
self.__context,
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
@ -394,6 +524,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
]
|
||||
return dict(self._settings, manually_controlled=manually_controlled)
|
||||
|
||||
def create_context(self, which: str = "default") -> 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'
|
||||
# 'adapt_lgt_XXXX_interval_999999999999'
|
||||
# 'adapt_lgt_XXXX_adapt_lights_99999999'
|
||||
# 'adapt_lgt_XXXX_sleep_999999999999999'
|
||||
# 'adapt_lgt_XXXX_light_event_999999999'
|
||||
# So 100 million calls before we run into the 36 chars limit.
|
||||
context = create_context(self._name, which, self._context_cnt)
|
||||
self._context_cnt += 1
|
||||
return context
|
||||
|
||||
async def async_turn_on( # pylint: disable=arguments-differ
|
||||
self, adapt_lights: bool = True
|
||||
) -> None:
|
||||
|
|
@ -408,7 +551,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
await self._setup_listeners()
|
||||
if adapt_lights:
|
||||
await self._update_attrs_and_maybe_adapt_lights(
|
||||
transition=self._initial_transition, force=True
|
||||
transition=self._initial_transition,
|
||||
force=True,
|
||||
context=self.create_context("turn_on"),
|
||||
)
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
|
|
@ -420,7 +565,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
self.turn_on_off_listener.reset(*self._lights)
|
||||
|
||||
async def _async_update_at_interval(self, now=None) -> None:
|
||||
await self._update_attrs_and_maybe_adapt_lights(force=False)
|
||||
await self._update_attrs_and_maybe_adapt_lights(
|
||||
force=False, context=self.create_context("interval")
|
||||
)
|
||||
|
||||
async def _adapt_light(
|
||||
self,
|
||||
|
|
@ -430,12 +577,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
adapt_color_temp: Optional[bool] = None,
|
||||
adapt_rgb_color: Optional[bool] = None,
|
||||
force: bool = False,
|
||||
context: Optional[Context] = None,
|
||||
) -> None:
|
||||
lock = self._locks.get(light)
|
||||
if lock is not None and lock.locked():
|
||||
_LOGGER.debug("%s: '%s' is locked", self._name, light)
|
||||
return
|
||||
|
||||
service_data = {ATTR_ENTITY_ID: light}
|
||||
features = _supported_features(self.hass, light)
|
||||
|
||||
|
|
@ -466,7 +613,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
service_data[ATTR_COLOR_TEMP] = color_temp_mired
|
||||
elif "color" in features and adapt_rgb_color:
|
||||
service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"]
|
||||
|
||||
context = context or self.create_context("adapt_lights")
|
||||
if (
|
||||
self._take_over_control
|
||||
and self._detect_non_ha_changes
|
||||
|
|
@ -476,20 +623,22 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
self._adapt_brightness,
|
||||
self._adapt_color_temp,
|
||||
self._adapt_rgb_color,
|
||||
self.__context,
|
||||
context,
|
||||
)
|
||||
):
|
||||
return
|
||||
_LOGGER.debug(
|
||||
"%s: Scheduling 'light.turn_on' with the following 'service_data': %s",
|
||||
"%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,
|
||||
service_data,
|
||||
context=self.__context,
|
||||
context=context,
|
||||
)
|
||||
|
||||
async def _update_attrs_and_maybe_adapt_lights(
|
||||
|
|
@ -497,7 +646,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
lights: Optional[List[str]] = None,
|
||||
transition: Optional[int] = None,
|
||||
force: bool = False,
|
||||
):
|
||||
context: Optional[Context] = None,
|
||||
) -> None:
|
||||
_LOGGER.debug("%s: '_update_attrs_and_maybe_adapt_lights' called", self._name)
|
||||
assert self.is_on
|
||||
self._settings = self._sun_light_settings.get_settings(
|
||||
|
|
@ -508,28 +658,31 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
lights = self._lights
|
||||
if (self._only_once and not force) or not lights:
|
||||
return
|
||||
await self._adapt_lights(lights, transition, force)
|
||||
await self._adapt_lights(lights, transition, force, context)
|
||||
|
||||
async def _adapt_lights(
|
||||
self, lights: List[str], transition: Optional[int], force: bool
|
||||
):
|
||||
self,
|
||||
lights: List[str],
|
||||
transition: Optional[int],
|
||||
force: bool,
|
||||
context: Optional[Context],
|
||||
) -> None:
|
||||
_LOGGER.debug(
|
||||
"%s: '_adapt_lights(%s, %s, %s)' called",
|
||||
"%s: '_adapt_lights(%s, %s, force=%s, context.id=%s)' called",
|
||||
self.name,
|
||||
lights,
|
||||
transition,
|
||||
force,
|
||||
context.id,
|
||||
)
|
||||
for light in lights:
|
||||
if not is_on(self.hass, light):
|
||||
continue
|
||||
if (
|
||||
self._take_over_control
|
||||
and False # XXX: REMOVE THIS
|
||||
and self.turn_on_off_listener.is_manually_controlled(
|
||||
light,
|
||||
force,
|
||||
adaptive_lighting_context=self.__context,
|
||||
)
|
||||
):
|
||||
_LOGGER.debug(
|
||||
|
|
@ -538,18 +691,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
light,
|
||||
)
|
||||
continue
|
||||
await self._adapt_light(light, transition, force=force)
|
||||
await self._adapt_light(light, transition, force=force, context=context)
|
||||
|
||||
async def _sleep_state_event(self, event: Event):
|
||||
async def _sleep_state_event(self, event: Event) -> None:
|
||||
if not match_state_event(event, (STATE_ON, STATE_OFF)):
|
||||
return
|
||||
_LOGGER.debug("%s: _sleep_state_event, event: '%s'", self._name, event)
|
||||
self.turn_on_off_listener.reset(*self._lights)
|
||||
await self._update_attrs_and_maybe_adapt_lights(
|
||||
transition=self._initial_transition, force=True
|
||||
transition=self._initial_transition,
|
||||
force=True,
|
||||
context=self.create_context("sleep"),
|
||||
)
|
||||
|
||||
async def _light_event(self, event: Event):
|
||||
async def _light_event(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")
|
||||
|
|
@ -560,7 +715,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
and new_state.state == STATE_ON
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s: Detected an 'off' → 'on' event for '%s'", self._name, entity_id
|
||||
"%s: Detected an 'off' → 'on' event for '%s' with context.id='%s'",
|
||||
self._name,
|
||||
entity_id,
|
||||
event.context.id,
|
||||
)
|
||||
self.turn_on_off_listener.reset(entity_id)
|
||||
# Tracks 'off' → 'on' state changes
|
||||
|
|
@ -584,6 +742,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
lights=[entity_id],
|
||||
transition=self._initial_transition,
|
||||
force=True,
|
||||
context=self.create_context("light_event"),
|
||||
)
|
||||
elif (
|
||||
old_state is not None
|
||||
|
|
@ -630,6 +789,7 @@ class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity):
|
|||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity about to be added to hass."""
|
||||
last_state = await self.async_get_last_state()
|
||||
# XXX: state isn't correctly restored!
|
||||
if last_state is None or STATE_OFF: # newly added to HA
|
||||
await self.async_turn_off()
|
||||
else:
|
||||
|
|
@ -793,7 +953,6 @@ class TurnOnOffListener:
|
|||
"""Initialize the TurnOnOffListener that is shared among all switches."""
|
||||
self.hass = hass
|
||||
self.lights = set()
|
||||
self.contexts = set() # contexts of different AdaptiveSwitch instances
|
||||
|
||||
# Tracks 'light.turn_off' service calls
|
||||
self.turn_off_event: Dict[str, Event] = {}
|
||||
|
|
@ -804,7 +963,7 @@ class TurnOnOffListener:
|
|||
# Tracks which lights are manually controlled
|
||||
self.manually_controlled: Dict[str, bool] = {}
|
||||
# Track 'state_changed' events of self.lights resulting from this integration
|
||||
self.last_state_change: Dict[str, State] = {}
|
||||
self.last_state_change: Dict[str, List[State]] = {}
|
||||
|
||||
self.remove_listener = self.hass.bus.async_listen(
|
||||
EVENT_CALL_SERVICE, self.turn_on_off_event_listener
|
||||
|
|
@ -844,7 +1003,11 @@ class TurnOnOffListener:
|
|||
self.reset(eid)
|
||||
|
||||
elif service == SERVICE_TURN_ON:
|
||||
_LOGGER.debug("Detected an 'light.turn_on('%s')' event", entity_ids)
|
||||
_LOGGER.debug(
|
||||
"Detected an 'light.turn_on('%s')' event with context.id='%s'",
|
||||
entity_ids,
|
||||
event.context.id,
|
||||
)
|
||||
for eid in entity_ids:
|
||||
task = self.sleep_tasks.get(eid)
|
||||
if task is not None:
|
||||
|
|
@ -854,28 +1017,54 @@ class TurnOnOffListener:
|
|||
async def state_changed_event_listener(self, event: Event):
|
||||
"""Track 'state_changed' events."""
|
||||
entity_id = event.data.get(ATTR_ENTITY_ID, "")
|
||||
if entity_id not in self.lights and entity_id.split(".")[0] != LIGHT_DOMAIN:
|
||||
if entity_id not in self.lights or entity_id.split(".")[0] != LIGHT_DOMAIN:
|
||||
return
|
||||
|
||||
new_state = event.data.get("new_state")
|
||||
if (
|
||||
new_state is not None
|
||||
and new_state.state == STATE_ON
|
||||
and new_state.context in self.contexts
|
||||
and is_our_context(new_state.context)
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Detected a '%s' 'state_changed' event: '%s'",
|
||||
"Detected a '%s' 'state_changed' event: '%s' with context.id='%s'",
|
||||
entity_id,
|
||||
new_state.attributes,
|
||||
new_state.context.id,
|
||||
)
|
||||
self.last_state_change[entity_id] = new_state
|
||||
# It is possible to have multiple state change events with the same context.
|
||||
# This can happen because a `turn_on.light(brightness_pct=100, transition=30)`
|
||||
# event leads to an instant state change of
|
||||
# `new_state=dict(brightness=100, ...)`. However, after polling the light
|
||||
# could still only be `new_state=dict(brightness=50, ...)`.
|
||||
# We save both events because the first event change might indicate at what
|
||||
# settings the light will be later *or* the second event might indicate a
|
||||
# final state. The latter case happens for example when a light was
|
||||
# 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)
|
||||
if (
|
||||
old_state is not None
|
||||
and old_state[0].context.id == new_state.context.id
|
||||
):
|
||||
# If there is already a state change event from this event (with this
|
||||
# context) then append it to the already existing list.
|
||||
_LOGGER.debug(
|
||||
"State change event of '%s' is already in 'self.last_state_change' (%s)"
|
||||
" adding this state also",
|
||||
entity_id,
|
||||
new_state.context.id,
|
||||
)
|
||||
self.last_state_change[entity_id].append(new_state)
|
||||
else:
|
||||
self.last_state_change[entity_id] = [new_state]
|
||||
|
||||
def is_manually_controlled(
|
||||
self,
|
||||
light: str,
|
||||
force: bool,
|
||||
adaptive_lighting_context: Context,
|
||||
):
|
||||
) -> bool:
|
||||
"""Check if the light has been 'on' and is now manually being adjusted."""
|
||||
manually_controlled = self.manually_controlled.setdefault(light, False)
|
||||
if manually_controlled:
|
||||
|
|
@ -885,7 +1074,7 @@ class TurnOnOffListener:
|
|||
turn_on_event = self.turn_on_event.get(light)
|
||||
if (
|
||||
turn_on_event is not None
|
||||
and adaptive_lighting_context.id != turn_on_event.context.id
|
||||
and not is_our_context(turn_on_event.context)
|
||||
and not force
|
||||
):
|
||||
# Light was already on and 'light.turn_on' was not called by
|
||||
|
|
@ -898,16 +1087,28 @@ class TurnOnOffListener:
|
|||
" then on again.",
|
||||
light,
|
||||
)
|
||||
# if (
|
||||
# turn_on_event is not None
|
||||
# and not is_our_context(turn_on_event.context)
|
||||
# and any(_DISABLE_ON.intersection(turn_on_event.data[ATTR_SERVICE_DATA]))
|
||||
# ):
|
||||
# # XXX: add comment
|
||||
# _LOGGER.debug(
|
||||
# "'light.turn_on' was called on '%s' with settings '%s' so we stop adapting",
|
||||
# light,
|
||||
# turn_on_event.data[ATTR_SERVICE_DATA],
|
||||
# )
|
||||
# manually_controlled = self.manually_controlled[light] = True
|
||||
return manually_controlled
|
||||
|
||||
async def significant_change(
|
||||
self,
|
||||
light,
|
||||
adapt_brightness,
|
||||
adapt_color_temp,
|
||||
adapt_rgb_color,
|
||||
context,
|
||||
):
|
||||
light: str,
|
||||
adapt_brightness: bool,
|
||||
adapt_color_temp: bool,
|
||||
adapt_rgb_color: bool,
|
||||
context: Context,
|
||||
) -> bool:
|
||||
"""Has the light made a significant change since last update.
|
||||
|
||||
This method will detect changes that were made to the light without
|
||||
|
|
@ -917,8 +1118,7 @@ class TurnOnOffListener:
|
|||
"""
|
||||
if light not in self.last_state_change:
|
||||
return False
|
||||
changed = False
|
||||
old_attributes = self.last_state_change[light].attributes
|
||||
old_states: List[State] = self.last_state_change[light]
|
||||
await self.hass.services.async_call(
|
||||
HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
|
|
@ -926,66 +1126,26 @@ class TurnOnOffListener:
|
|||
blocking=True,
|
||||
context=context,
|
||||
)
|
||||
attributes = self.hass.states.get(light).attributes
|
||||
if (
|
||||
adapt_brightness
|
||||
and ATTR_BRIGHTNESS in old_attributes
|
||||
and ATTR_BRIGHTNESS in attributes
|
||||
):
|
||||
last_brightness = old_attributes[ATTR_BRIGHTNESS]
|
||||
current_brightness = attributes[ATTR_BRIGHTNESS]
|
||||
if abs(current_brightness - last_brightness) > BRIGHTNESS_CHANGE:
|
||||
_LOGGER.debug(
|
||||
"Brightness of '%s' significantly changed from %s to %s",
|
||||
light,
|
||||
last_brightness,
|
||||
current_brightness,
|
||||
)
|
||||
changed = True
|
||||
|
||||
if (
|
||||
adapt_color_temp
|
||||
and ATTR_COLOR_TEMP in old_attributes
|
||||
and ATTR_COLOR_TEMP in attributes
|
||||
):
|
||||
last_color_temp = old_attributes[ATTR_COLOR_TEMP]
|
||||
current_color_temp = attributes[ATTR_COLOR_TEMP]
|
||||
if abs(current_color_temp - last_color_temp) > COLOR_TEMP_CHANGE:
|
||||
_LOGGER.debug(
|
||||
"Color temperature of '%s' significantly changed from %s to %s",
|
||||
light,
|
||||
last_color_temp,
|
||||
current_color_temp,
|
||||
)
|
||||
changed = True
|
||||
|
||||
if (
|
||||
adapt_rgb_color
|
||||
and ATTR_RGB_COLOR in old_attributes
|
||||
and ATTR_RGB_COLOR in attributes
|
||||
):
|
||||
last_rgb_color = old_attributes[ATTR_RGB_COLOR]
|
||||
current_rgb_color = attributes[ATTR_RGB_COLOR]
|
||||
for last_col, current_col in zip(last_rgb_color, current_rgb_color):
|
||||
if abs(last_col - current_col) > RGB_CHANGE:
|
||||
_LOGGER.debug(
|
||||
"color RGB of '%s' significantly changed from %s to %s",
|
||||
light,
|
||||
last_rgb_color,
|
||||
current_rgb_color,
|
||||
)
|
||||
changed = True
|
||||
break
|
||||
|
||||
if (ATTR_RGB_COLOR in old_attributes and ATTR_RGB_COLOR not in attributes) or (
|
||||
ATTR_COLOR_TEMP in old_attributes 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",
|
||||
new_state = self.hass.states.get(light)
|
||||
for index, old_state in enumerate(old_states):
|
||||
changed = _attributes_have_changed(
|
||||
light,
|
||||
old_state.attributes,
|
||||
new_state.attributes,
|
||||
adapt_brightness,
|
||||
adapt_color_temp,
|
||||
adapt_rgb_color,
|
||||
context,
|
||||
)
|
||||
changed = True
|
||||
if not changed:
|
||||
_LOGGER.debug(
|
||||
"States of '%s' didn't change wrt change event nr. %s (context.id=%s)",
|
||||
light,
|
||||
index,
|
||||
context.id,
|
||||
)
|
||||
break
|
||||
|
||||
self.manually_controlled[light] = changed
|
||||
return changed
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue