mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-10-28 06:07:13 +01:00
Merge 1770ec29c7 into b2ca188df3
This commit is contained in:
commit
2d54bde22c
32 changed files with 271 additions and 139 deletions
|
|
@ -8,7 +8,7 @@ import homeassistant.helpers.config_validation as cv
|
|||
import voluptuous as vol
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_SOURCE, Platform
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import service
|
||||
|
||||
from .const import (
|
||||
|
|
@ -49,15 +49,6 @@ CONFIG_SCHEMA = vol.Schema(
|
|||
)
|
||||
|
||||
|
||||
async def reload_configuration_yaml(event: Event) -> None:
|
||||
"""Reload configuration.yaml."""
|
||||
hass: HomeAssistant | None = event.data.get("hass")
|
||||
if hass is not None:
|
||||
await hass.services.async_call("homeassistant", "check_config", {})
|
||||
else:
|
||||
_LOGGER.error("HomeAssistant instance not found in event data.")
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
|
||||
"""Import integration from config."""
|
||||
hass.services.async_register(
|
||||
|
|
@ -104,10 +95,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||
"""Set up the component."""
|
||||
data = hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
# This will reload any changes the user made to any YAML configurations.
|
||||
# Called during 'quick reload' or hass.reload_config_entry
|
||||
hass.bus.async_listen("hass.config.entry_updated", reload_configuration_yaml)
|
||||
|
||||
undo_listener = config_entry.add_update_listener(async_update_options)
|
||||
data[config_entry.entry_id] = {UNDO_UPDATE_LISTENER: undo_listener}
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
|
@ -126,12 +113,16 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||
config_entry,
|
||||
"switch",
|
||||
)
|
||||
data = hass.data[DOMAIN]
|
||||
data[config_entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||
if unload_ok:
|
||||
data.pop(config_entry.entry_id)
|
||||
if not unload_ok:
|
||||
return False
|
||||
|
||||
if len(data) == 1 and ATTR_ADAPTIVE_LIGHTING_MANAGER in data:
|
||||
data = hass.data[DOMAIN]
|
||||
data.pop(config_entry.entry_id)[UNDO_UPDATE_LISTENER]()
|
||||
|
||||
# Keys that are not config entries; "__yaml__" must outlive the entries so
|
||||
# a later reload can tell imported YAML switches from deleted ones.
|
||||
non_entry_keys = {ATTR_ADAPTIVE_LIGHTING_MANAGER, "__yaml__"}
|
||||
if ATTR_ADAPTIVE_LIGHTING_MANAGER in data and not set(data) - non_entry_keys:
|
||||
# no more config_entries
|
||||
manager = data.pop(ATTR_ADAPTIVE_LIGHTING_MANAGER)
|
||||
manager.disable()
|
||||
|
|
|
|||
|
|
@ -326,6 +326,22 @@ class SunLightSettings:
|
|||
delta_brightness = self.max_brightness - self.min_brightness
|
||||
return (delta_brightness * (1 + sun_position)) + self.min_brightness
|
||||
|
||||
def _brightness_ramp_seconds(self) -> float:
|
||||
return (
|
||||
self.brightness_mode_time_dark.total_seconds()
|
||||
+ self.brightness_mode_time_light.total_seconds()
|
||||
)
|
||||
|
||||
def _brightness_pct_step(self, dt: datetime.datetime) -> float:
|
||||
event, ts_event = self.sun.closest_event(dt)
|
||||
after_event = dt.timestamp() >= ts_event
|
||||
if event == SunEvent.SUNRISE:
|
||||
return self.max_brightness if after_event else self.min_brightness
|
||||
if event == SunEvent.SUNSET:
|
||||
return self.min_brightness if after_event else self.max_brightness
|
||||
msg = "Unsupported sun event"
|
||||
raise ValueError(msg)
|
||||
|
||||
def _brightness_pct_tanh(self, dt: datetime.datetime) -> float:
|
||||
event, ts_event = self.sun.closest_event(dt)
|
||||
dark = self.brightness_mode_time_dark.total_seconds()
|
||||
|
|
@ -387,6 +403,10 @@ class SunLightSettings:
|
|||
if is_sleep:
|
||||
return self.sleep_brightness
|
||||
assert self.brightness_mode in ("default", "linear", "tanh")
|
||||
if self.brightness_mode != "default" and self._brightness_ramp_seconds() <= 0:
|
||||
# A zero-length (or negative) ramp leaves nothing to interpolate
|
||||
# over, so switch instantly at sunrise/sunset instead.
|
||||
return self._brightness_pct_step(dt)
|
||||
if self.brightness_mode == "default":
|
||||
return self._brightness_pct_default(dt)
|
||||
if self.brightness_mode == "linear":
|
||||
|
|
@ -404,8 +424,8 @@ class SunLightSettings:
|
|||
if sun_position == 0 or not self.adapt_until_sleep:
|
||||
return self.min_color_temp
|
||||
if self.adapt_until_sleep and sun_position < 0:
|
||||
delta = abs(self.min_color_temp - self.sleep_color_temp)
|
||||
ct = (delta * abs(1 + sun_position)) + self.sleep_color_temp
|
||||
delta = self.min_color_temp - self.sleep_color_temp
|
||||
ct = (delta * (1 + sun_position)) + self.sleep_color_temp
|
||||
return 5 * round(ct / 5) # round to nearest 5
|
||||
msg = "Should not happen"
|
||||
raise ValueError(msg)
|
||||
|
|
@ -581,9 +601,13 @@ def lerp_color_hsv(
|
|||
hsv1 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb1])
|
||||
hsv2 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb2])
|
||||
|
||||
# Linear interpolation in HSV space
|
||||
# White/grey has no hue, so fade along the other color's hue instead
|
||||
hue1 = hsv2[0] if hsv1[1] == 0 else hsv1[0]
|
||||
hue2 = hsv1[0] if hsv2[1] == 0 else hsv2[0]
|
||||
# Linear interpolation in HSV space, taking the short way around the hue circle
|
||||
hue_delta = (hue2 - hue1 + 0.5) % 1.0 - 0.5
|
||||
hsv = (
|
||||
hsv1[0] + t * (hsv2[0] - hsv1[0]),
|
||||
(hue1 + t * hue_delta) % 1.0,
|
||||
hsv1[1] + t * (hsv2[1] - hsv1[1]),
|
||||
hsv1[2] + t * (hsv2[2] - hsv1[2]),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -85,12 +85,6 @@ change_switch_settings:
|
|||
required: false
|
||||
selector:
|
||||
boolean: null
|
||||
turn_on_lights:
|
||||
description: Whether to turn on lights that are currently off. 🔆
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean: null
|
||||
initial_transition:
|
||||
description: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️
|
||||
example: 1
|
||||
|
|
|
|||
|
|
@ -176,10 +176,6 @@
|
|||
"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"
|
||||
|
|
|
|||
|
|
@ -1118,6 +1118,24 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
all_lights,
|
||||
self._auto_reset_manual_control_time,
|
||||
)
|
||||
if not self._auto_reset_manual_control_time:
|
||||
# Auto-reset is off here: drop a delay left from an earlier setting,
|
||||
# unless another profile for the same light still uses one.
|
||||
still_reset: set[str] = set()
|
||||
for entry in self.hass.data[DOMAIN].values():
|
||||
if (
|
||||
isinstance(entry, dict)
|
||||
and (switch := entry.get(SWITCH_DOMAIN))
|
||||
and switch is not self
|
||||
and switch._auto_reset_manual_control_time
|
||||
):
|
||||
still_reset.update(switch._resolve_lights())
|
||||
timers = self.manager.auto_reset_manual_control_timers
|
||||
for light in set(all_lights) - still_reset:
|
||||
self.manager.auto_reset_manual_control_times.pop(light, None)
|
||||
# A timer started under the old delay must not reset the light.
|
||||
if timer := timers.pop(light, None):
|
||||
timer.cancel()
|
||||
|
||||
async def _setup_listeners(self, _: Event[NoEventData] | None = None) -> None:
|
||||
_LOGGER.debug("%s: Called '_setup_listeners'", self._name)
|
||||
|
|
@ -1464,7 +1482,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
not data.force
|
||||
and not is_on(self.hass, data.entity_id)
|
||||
# if proactively adapting, we are sure that it came from a `light.turn_on`
|
||||
and not self.manager.is_proactively_adapting(data.context.id)
|
||||
and not self.manager.is_proactively_adapting(
|
||||
data.context.id,
|
||||
data.entity_id,
|
||||
)
|
||||
):
|
||||
# Do a last-minute check if the entity is still on.
|
||||
_LOGGER.debug(
|
||||
|
|
@ -1645,7 +1666,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
entity_id: str,
|
||||
event: Event[EventStateChangedData],
|
||||
) -> None:
|
||||
assert not self.manager.is_proactively_adapting(event.context.id)
|
||||
assert not self.manager.is_proactively_adapting(event.context.id, entity_id)
|
||||
from_turn_on = self.manager._off_to_on_state_event_is_from_turn_on(
|
||||
entity_id,
|
||||
event,
|
||||
|
|
@ -1711,8 +1732,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
if self._adapt_delay > 0:
|
||||
await asyncio.sleep(self._adapt_delay)
|
||||
|
||||
# Runtime settings may retire this profile's target while the event waits.
|
||||
if self._removed or entity_id not in self.lights:
|
||||
# Runtime settings may retire this profile's target, or the switch may be
|
||||
# turned off, while the event waits.
|
||||
if self._removed or not self.is_on or entity_id not in self.lights:
|
||||
return
|
||||
|
||||
await self._update_attrs_and_maybe_adapt_lights(
|
||||
|
|
@ -1913,7 +1935,7 @@ class AdaptiveLightingManager:
|
|||
),
|
||||
]
|
||||
|
||||
self._proactively_adapting_contexts: dict[str, str] = {}
|
||||
self._proactively_adapting_contexts: dict[str, set[str]] = {}
|
||||
self._context_cnt: int = 0
|
||||
|
||||
try:
|
||||
|
|
@ -1956,18 +1978,25 @@ class AdaptiveLightingManager:
|
|||
"""Declare the adaptation with context_id as proactively adapting,
|
||||
and associate it to an entity_id.
|
||||
""" # noqa: D205
|
||||
self._proactively_adapting_contexts[context_id] = entity_id
|
||||
self._proactively_adapting_contexts.setdefault(context_id, set()).add(
|
||||
entity_id,
|
||||
)
|
||||
|
||||
def is_proactively_adapting(self, context_id: str) -> bool:
|
||||
"""Determine whether an adaptation with the given context_id is proactive."""
|
||||
is_proactively_adapting_context = (
|
||||
context_id in self._proactively_adapting_contexts
|
||||
def is_proactively_adapting(self, context_id: str, entity_id: str) -> bool:
|
||||
"""Determine whether we are proactively adapting entity_id under context_id.
|
||||
|
||||
Scoped per entity: one automation run shares a context across calls, and
|
||||
a proactive turn-on of one light must not hide manual changes to another.
|
||||
"""
|
||||
is_proactively_adapting_context = entity_id in (
|
||||
self._proactively_adapting_contexts.get(context_id, ())
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"is_proactively_adapting_context='%s', context_id='%s'",
|
||||
"is_proactively_adapting_context='%s', context_id='%s', entity_id='%s'",
|
||||
is_proactively_adapting_context,
|
||||
context_id,
|
||||
entity_id,
|
||||
)
|
||||
|
||||
return is_proactively_adapting_context
|
||||
|
|
@ -1979,10 +2008,13 @@ class AdaptiveLightingManager:
|
|||
"""
|
||||
# First get the keys to avoid modifying the dict while iterating it
|
||||
keys = [
|
||||
k for k, v in self._proactively_adapting_contexts.items() if v == entity_id
|
||||
k for k, v in self._proactively_adapting_contexts.items() if entity_id in v
|
||||
]
|
||||
for key in keys:
|
||||
self._proactively_adapting_contexts.pop(key)
|
||||
entity_ids = self._proactively_adapting_contexts[key]
|
||||
entity_ids.discard(entity_id)
|
||||
if not entity_ids:
|
||||
self._proactively_adapting_contexts.pop(key)
|
||||
|
||||
def create_context(
|
||||
self,
|
||||
|
|
@ -2711,7 +2743,7 @@ class AdaptiveLightingManager:
|
|||
timer is not None
|
||||
and timer.is_running()
|
||||
and not is_our_context(event.context)
|
||||
and not self.is_proactively_adapting(event.context.id)
|
||||
and not self.is_proactively_adapting(event.context.id, eid)
|
||||
and event.time_fired > timer.start_time # type: ignore[operator]
|
||||
):
|
||||
# Only external turn-ons extend manual control, not our adaptations.
|
||||
|
|
@ -2852,7 +2884,7 @@ class AdaptiveLightingManager:
|
|||
event.context.id,
|
||||
)
|
||||
|
||||
if self.is_proactively_adapting(event.context.id):
|
||||
if self.is_proactively_adapting(event.context.id, entity_id):
|
||||
_LOGGER.debug(
|
||||
"Skipping responding to 'off' → 'on' event for '%s' with context.id='%s' because"
|
||||
" we are already proactively adapting",
|
||||
|
|
@ -2899,7 +2931,7 @@ class AdaptiveLightingManager:
|
|||
|
||||
if (
|
||||
turn_on_event is None
|
||||
or self.is_proactively_adapting(turn_on_event.context.id)
|
||||
or self.is_proactively_adapting(turn_on_event.context.id, light)
|
||||
or is_our_context(turn_on_event.context)
|
||||
or force
|
||||
):
|
||||
|
|
|
|||
|
|
@ -174,10 +174,6 @@
|
|||
"description": "Показва всички опции като атрибути на ключа в Home Assistant, когато е зададено на \"true\". 📝",
|
||||
"name": "include_config_in_attributes"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Дали да се включат лампите, които в момента са изключени. 🔆",
|
||||
"name": "turn_on_lights"
|
||||
},
|
||||
"initial_transition": {
|
||||
"description": "Продължителност на първия преход, когато лампите преминават от 'изключено' на 'включено' в секунди. ⏲️",
|
||||
"name": "initial_transition"
|
||||
|
|
|
|||
|
|
@ -98,9 +98,6 @@
|
|||
"entity_id": {
|
||||
"description": "ID de la entitat de l'interruptor. 📝"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Si s'encenen les llums que estan apagades en aquest moment. 🔆"
|
||||
},
|
||||
"initial_transition": {
|
||||
"description": "Durada de la primera transició quan els llums canvien `off` a `on` en segons. ⏲️"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -123,9 +123,6 @@
|
|||
"sleep_rgb_or_color_temp": {
|
||||
"description": "V režimu spánku se použije buď `\"rgb_color\"`, nebo `\"color_temp\"`. 🌙"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Zda se mají zapnout světla, která jsou aktuálně vypnutá. 🔆"
|
||||
},
|
||||
"initial_transition": {
|
||||
"description": "Doba trvání prvního přechodu, kdy se světla změní z `vypnuto` na `zapnuto`, v sekundách. ⏲️"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -119,9 +119,6 @@
|
|||
"entity_id": {
|
||||
"description": "Entity ID af kontakten. 📝"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Om lys der i øjeblikket er slukket, skal tændes. 🔆"
|
||||
},
|
||||
"sleep_transition": {
|
||||
"description": "Varigheden af overgangen, når \"sovetilstand\" skiftes, i sekunder. 😴"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -196,9 +196,6 @@
|
|||
"entity_id": {
|
||||
"description": "Entity ID des Schalters. 📝"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Ob Lichter eingeschaltet werden sollen, die derzeit ausgeschaltet sind. 🔆"
|
||||
},
|
||||
"initial_transition": {
|
||||
"description": "Dauer des ersten Übergangs, wenn das Licht von `off` auf `on` schaltet, in Sekunden. ⏲️"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -177,10 +177,6 @@
|
|||
"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"
|
||||
|
|
|
|||
|
|
@ -137,9 +137,6 @@
|
|||
"prefer_rgb_color": {
|
||||
"description": "Preferir ajustes de color RGB a temperatura de color cuando sea posible. 🌈"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Encender (o no) luces que estén apagadas. 🔆"
|
||||
},
|
||||
"max_brightness": {
|
||||
"description": "Porcentaje máximo de brillo. 💡"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -50,9 +50,6 @@
|
|||
"entity_id": {
|
||||
"description": "Kytkimen entiteettitunnus. 📝"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Sytytetäänkö valot, jotka ovat tällä hetkellä pois päältä. 🔆"
|
||||
},
|
||||
"include_config_in_attributes": {
|
||||
"description": "Näytä kaikki vaihtoehdot attribuutteina kotiavustajan kytkimellä, kun sen arvo on \"true\". 📝"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -162,9 +162,6 @@
|
|||
"sleep_rgb_or_color_temp": {
|
||||
"description": "Utilisez soit \"rgb_color\" soit \"color_temp\" en mode nuit. 🌙"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Indique s'il faut allumer les lumières qui sont actuellement éteintes. 🔆"
|
||||
},
|
||||
"include_config_in_attributes": {
|
||||
"description": "Afficher toutes les options en tant qu'attributs sur l'interrupteur dans Home Assistant lorsqu'il est défini sur \"true\". 📝"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -98,9 +98,6 @@
|
|||
"sleep_rgb_or_color_temp": {
|
||||
"description": "Az `\"rgb_color\" vagy a `\"color_temp\" használata alvó üzemmódban. 🌙"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "A jelenleg kikapcsolt fények bekapcsolása. 🔆"
|
||||
},
|
||||
"initial_transition": {
|
||||
"description": "Az első transition időtartama, amikor a lights \"kikapcsolt\" állapotból \"bekapcsolt\" állapotba váltanak, másodpercben. ⏲️"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -26,9 +26,6 @@
|
|||
"sleep_rgb_or_color_temp": {
|
||||
"description": "Gunakan `\"rgb_color\"` atau `\"color_temp\"` dalam mode tidur. 🌙"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Kalau ingin menyalakan lampu yang sedang mati. 🔆"
|
||||
},
|
||||
"initial_transition": {
|
||||
"description": "Durasi transisi pertama saat lampu berubah dari `mati` ke `hidup` dalam hitungan detik. ⏲️"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -117,9 +117,6 @@
|
|||
"sleep_rgb_or_color_temp": {
|
||||
"description": "Usa uno tra `\"rgb_color\"` or`\"color_temp\"` in modalità notturna. 🌙"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Seleziona per accedere le luci, qualora fossero spente. 🔆"
|
||||
},
|
||||
"initial_transition": {
|
||||
"description": "Durata della prima transizione quando le luci passano dallo stato `off` a `on`, espressa in secondi. ⏲️"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -167,10 +167,6 @@
|
|||
"description": "`true`로 설정하면 Home Assistant에서 스위치의 모든 옵션을 속성으로 표시합니다. 📝",
|
||||
"name": "include_config_in_attributes"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "현재 꺼져 있는 조명을 켤지 여부. 🔆",
|
||||
"name": "turn_on_lights"
|
||||
},
|
||||
"initial_transition": {
|
||||
"description": "조명이 `off`에서 `on`으로 바뀔 때 첫 번째 전환의 지속 시간, 초 단위. ⏲️",
|
||||
"name": "initial_transition"
|
||||
|
|
|
|||
|
|
@ -169,9 +169,6 @@
|
|||
"adapt_delay": {
|
||||
"description": "Ventetid (sekunder) mellom at lyset skrues på og Adaptive Lightning sender endringer. Kan hjelpe til for å unngå blinking."
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Skru på lys som fortiden er skrudd av."
|
||||
},
|
||||
"take_over_control": {
|
||||
"description": "Skrur av Adaptive Lightning hvis en annen kilde kaller `light.turn_on` mens lysene er på og blir styrt. Merk at dette kaller `homeassistant.update_entity` hvert eneste`interval`!"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,9 +154,6 @@
|
|||
"sleep_rgb_or_color_temp": {
|
||||
"description": "Gebruik één van beide `\"rgb_color\"` of `\"color_temp\"` in slaapstand. 🌙"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Of de lampen moeten worden aangezet die momenteel uit zijn.🔆"
|
||||
},
|
||||
"sunrise_time": {
|
||||
"description": "Stel een vaste tijd (HH:MM:SS) in voor zonsopkomst. 🌅"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -145,9 +145,6 @@
|
|||
"max_brightness": {
|
||||
"description": "Maksymalna jasność (w procentach). 💡"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Czy włączyć światła, które są aktualnie wyłączone? 🔆"
|
||||
},
|
||||
"initial_transition": {
|
||||
"description": "Długość pierwszego przejścia, gdy światło zostanie przełączone z `off` na `on` (w sekundach). ⏲️"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -109,9 +109,6 @@
|
|||
"sleep_brightness": {
|
||||
"description": "Porcentagem do brilho das luzes no modo dormir. 😴"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Se deve ligar as luzes que estão atualmente desligadas. 🔆"
|
||||
},
|
||||
"initial_transition": {
|
||||
"description": "Duração da primeira transição, em segundos, quando as luzes alternarem de 'desligado' para 'ligado'. ⏲️"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@
|
|||
"sunset_offset": {
|
||||
"description": "Ajustar a hora do pôr do sol com um offset positivo ou negativo em segundos. ⏰"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Para ligar luzes que estão neste momento desligadas. 🔆"
|
||||
},
|
||||
"entity_id": {
|
||||
"description": "ID Entidade do interruptor. 📝"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -170,9 +170,6 @@
|
|||
"sleep_rgb_or_color_temp": {
|
||||
"description": "Используйте либо «rgb_color», либо «color_temp» в режиме сна. 🌙"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Включать ли свет, который в данный момент выключен. 🔆"
|
||||
},
|
||||
"initial_transition": {
|
||||
"description": "Длительность первого плавного перехода, когда освещение переключается с «выключено» на «включено» в секундах. ⏲️"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -106,9 +106,6 @@
|
|||
"sleep_rgb_or_color_temp": {
|
||||
"description": "V režime spánku použiť `\"rgb_color\"` alebo `\"color_temp\"`. 🌙"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Či sa majú zapnúť svetlá, ktoré sú momentálne vypnuté. 🔆"
|
||||
},
|
||||
"initial_transition": {
|
||||
"description": "Trvanie prvého prechodu, keď sú svetlá zapnuté z `off` na `on` (v sekundách). ⏲️"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -141,9 +141,6 @@
|
|||
"sleep_rgb_or_color_temp": {
|
||||
"description": "Använd antingen`\"rgb_color\"` eller `\"color_temp\"` i sovläge. 🌙"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Om att tända lampor som är för närvarande släckta. 🔆"
|
||||
},
|
||||
"initial_transition": {
|
||||
"description": "Den första övergångens varaktighet när lampan slås från ”av” till ”på” i sekunder. ⏲️"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -31,9 +31,6 @@
|
|||
"include_config_in_attributes": {
|
||||
"description": "`உண்மை` என அமைக்கப்பட்டிருக்கும் போது வீட்டு உதவியாளரின் சுவிட்சில் உள்ள அனைத்து விருப்பங்களையும் பண்புகளாகக் காட்டுங்கள். ."
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "தற்போது முடக்கப்பட்ட விளக்குகளை இயக்க வேண்டுமா. ."
|
||||
},
|
||||
"initial_transition": {
|
||||
"description": "விளக்குகள் `ஆஃப்` முதல்` ஆன் `வரை நொடிகளில் மாறும் போது முதல் மாற்றத்தின் காலம். ."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -146,9 +146,6 @@
|
|||
"prefer_rgb_color": {
|
||||
"description": "Mümkün olduğunda ışık renk sıcaklığı yerine RGB renk ayarını tercih edip etmeyeceğini belirler. 🌈"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Şu anda kapalı olan ışıkların açılıp açılmayacağını belirler. 🔆"
|
||||
},
|
||||
"adapt_delay": {
|
||||
"description": "Işık açıldıktan sonra Adaptive Lighting’in değişiklikleri uygulamasına kadar bekleme süresi (saniye cinsinden). Titremeyi önlemeye yardımcı olabilir. ⏲️"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -206,9 +206,6 @@
|
|||
"prefer_rgb_color": {
|
||||
"description": "Чи надавати перевагу налаштуванню кольору RGB над температурою кольору світла, коли це можливо. 🌈"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "Чи вмикати світло, яке наразі вимкнене. 🔆"
|
||||
},
|
||||
"take_over_control_mode": {
|
||||
"description": "Режим призупинення адаптації, коли інші джерела змінюють яскравість та/або колір світла. `pause_all` завжди призупиняє адаптацію як яскравості, так і кольору. `pause_changed` призупиняє адаптацію лише змінених атрибутів та продовжує адаптацію незмінних атрибутів, наприклад, продовжує адаптацію кольору, коли змінювалася лише яскравість."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,6 @@
|
|||
"sleep_rgb_or_color_temp": {
|
||||
"description": "نیند کے موڈ میں \"rgb_color\" یا \"color_temp\" کا استعمال کریں۔ 🌙"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "کیا ان لائٹس کو آن کرنا ہے جو فی الحال بند ہیں۔ 🔆"
|
||||
},
|
||||
"initial_transition": {
|
||||
"description": "پہلی منتقلی کا دورانیہ جب لائٹس سیکنڈوں میں 'بند' سے 'آن' میں تبدیل ہوجاتی ہیں۔ ⏲️"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -167,10 +167,6 @@
|
|||
"description": "在Home Assistant中将所有选项显示为开关的属性时,设置为`true`。📝",
|
||||
"name": "include_config_in_attributes"
|
||||
},
|
||||
"turn_on_lights": {
|
||||
"description": "是否打开当前关闭的灯光。🔆",
|
||||
"name": "turn_on_lights"
|
||||
},
|
||||
"initial_transition": {
|
||||
"description": "灯光从“关闭”到“开启”时的第一个过渡持续时间,以秒为单位。⏲️",
|
||||
"name": "initial_transition"
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ from homeassistant.components.adaptive_lighting.const import (
|
|||
CONF_SEND_SPLIT_DELAY,
|
||||
CONF_SEPARATE_TURN_ON_COMMANDS,
|
||||
CONF_SKIP_REDUNDANT_COMMANDS,
|
||||
CONF_SLEEP_COLOR_TEMP,
|
||||
CONF_SLEEP_RGB_OR_COLOR_TEMP,
|
||||
CONF_SLEEP_TRANSITION,
|
||||
CONF_SUNRISE_OFFSET,
|
||||
|
|
@ -3252,7 +3253,7 @@ async def test_proactive_adaptation_toggle(hass):
|
|||
context=Context(id="test1"),
|
||||
)
|
||||
|
||||
assert switch.manager.is_proactively_adapting("test1")
|
||||
assert switch.manager.is_proactively_adapting("test1", ENTITY_LIGHT_3)
|
||||
|
||||
# Toggle OFF
|
||||
await hass.services.async_call(
|
||||
|
|
@ -3263,7 +3264,7 @@ async def test_proactive_adaptation_toggle(hass):
|
|||
context=Context(id="test2"),
|
||||
)
|
||||
|
||||
assert not switch.manager.is_proactively_adapting("test2")
|
||||
assert not switch.manager.is_proactively_adapting("test2", ENTITY_LIGHT_3)
|
||||
|
||||
|
||||
async def test_proactive_adaptation_transition_override(hass):
|
||||
|
|
@ -3372,8 +3373,8 @@ async def test_proactive_multiple_lights_all_at_once(hass):
|
|||
assert events[2].data["service_data"][ATTR_ENTITY_ID] == [ENTITY_LIGHT_3]
|
||||
assert ":skpp:" in events[2].context.id
|
||||
|
||||
assert switch1.manager.is_proactively_adapting("test1")
|
||||
assert switch2.manager.is_proactively_adapting("test1")
|
||||
assert switch1.manager.is_proactively_adapting("test1", ENTITY_LIGHT_1)
|
||||
assert switch2.manager.is_proactively_adapting("test1", ENTITY_LIGHT_1)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
|
@ -3457,8 +3458,8 @@ async def test_proactive_multiple_lights_one_switch_and_one_skipped(hass):
|
|||
assert events[1].data["service_data"][ATTR_ENTITY_ID] == [ENTITY_LIGHT_3]
|
||||
assert ":skpp:" in events[1].context.id
|
||||
|
||||
assert switch1.manager.is_proactively_adapting("test1")
|
||||
assert switch2.manager.is_proactively_adapting("test1")
|
||||
assert switch1.manager.is_proactively_adapting("test1", ENTITY_LIGHT_1)
|
||||
assert switch2.manager.is_proactively_adapting("test1", ENTITY_LIGHT_1)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
|
@ -3619,7 +3620,12 @@ def test_lerp_color_hsv():
|
|||
assert lerp_color_hsv((255, 0, 0), (0, 255, 0), 0) == (255, 0, 0)
|
||||
assert lerp_color_hsv((255, 0, 0), (0, 255, 0), 1) == (0, 255, 0)
|
||||
assert lerp_color_hsv((255, 0, 0), (0, 255, 0), 0.5) == (255, 255, 0)
|
||||
assert lerp_color_hsv((0, 0, 255), (255, 255, 255), 0.5) == (128, 255, 128)
|
||||
# Fading to white keeps the hue of the colored end instead of passing through green
|
||||
assert lerp_color_hsv((0, 0, 255), (255, 255, 255), 0.5) == (128, 128, 255)
|
||||
# Hue takes the short way around the color wheel: orange -> pink, not via cyan
|
||||
mid = lerp_color_hsv((255, 140, 40), (255, 0, 128), 0.5)
|
||||
assert mid[0] == 255
|
||||
assert mid[1] < 140
|
||||
|
||||
# Tests that the interpolation is consistent
|
||||
for t in [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]:
|
||||
|
|
@ -3792,8 +3798,12 @@ async def test_light_group(
|
|||
assert ":lght:" in events[2].context.id
|
||||
assert ":lght:" in events[3].context.id
|
||||
assert len(events) == 4
|
||||
assert not switch.manager.is_proactively_adapting(events[0].context.id)
|
||||
assert not switch.manager.is_proactively_adapting(events[1].context.id)
|
||||
for event in events[:2]:
|
||||
for light in ("light.light_4", "light.light_5"):
|
||||
assert not switch.manager.is_proactively_adapting(
|
||||
event.context.id,
|
||||
light,
|
||||
)
|
||||
|
||||
# Turn off all lights, and then turn on all lights
|
||||
await hass.services.async_call(
|
||||
|
|
@ -6344,3 +6354,170 @@ async def test_unloaded_polling_profile_preserves_other_split_adaptation(
|
|||
await hass.async_block_till_done()
|
||||
assert len(calls) == 2
|
||||
assert ATTR_COLOR_TEMP_KELVIN in calls[-1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("brightness_mode", ["linear", "tanh"])
|
||||
async def test_zero_length_brightness_ramp_switches_instantly(hass, brightness_mode):
|
||||
"""A ramp of zero seconds must not divide by zero; brightness steps instead."""
|
||||
_, switch = await setup_switch(
|
||||
hass,
|
||||
{
|
||||
CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour),
|
||||
CONF_SUNSET_TIME: datetime.time(SUNSET.hour),
|
||||
CONF_BRIGHTNESS_MODE: brightness_mode,
|
||||
CONF_BRIGHTNESS_MODE_TIME_DARK: datetime.timedelta(0),
|
||||
CONF_BRIGHTNESS_MODE_TIME_LIGHT: datetime.timedelta(0),
|
||||
},
|
||||
)
|
||||
settings = switch._sun_light_settings
|
||||
second = datetime.timedelta(seconds=1)
|
||||
sunset = SUNSET.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC)
|
||||
sunrise = SUNRISE.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC)
|
||||
assert settings.brightness_pct(sunset - second, False) == settings.max_brightness
|
||||
assert settings.brightness_pct(sunset + second, False) == settings.min_brightness
|
||||
assert settings.brightness_pct(sunrise - second, False) == settings.min_brightness
|
||||
assert settings.brightness_pct(sunrise + second, False) == settings.max_brightness
|
||||
|
||||
|
||||
async def test_adapt_until_sleep_with_sleep_color_temp_above_min(hass):
|
||||
"""Color temp must move smoothly from min to a cooler sleep color after sunset."""
|
||||
_, switch = await setup_switch(
|
||||
hass,
|
||||
{
|
||||
CONF_MIN_COLOR_TEMP: 2000,
|
||||
CONF_SLEEP_COLOR_TEMP: 3000,
|
||||
CONF_ADAPT_UNTIL_SLEEP: True,
|
||||
},
|
||||
)
|
||||
settings = switch._sun_light_settings
|
||||
assert settings.color_temp_kelvin(-0.0001) == 2000
|
||||
assert settings.color_temp_kelvin(-0.5) == 2500
|
||||
assert settings.color_temp_kelvin(-1) == 3000
|
||||
|
||||
|
||||
async def test_autoreset_control_seconds_zero_disables_existing_reset(hass):
|
||||
"""Changing autoreset_control_seconds to 0 must drop the old per-light delay."""
|
||||
switch, _ = await setup_lights_and_switch(hass, {CONF_AUTORESET_CONTROL: 3600})
|
||||
manager = switch.manager
|
||||
assert manager.auto_reset_manual_control_times[ENTITY_LIGHT_1] == 3600
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_CHANGE_SWITCH_SETTINGS,
|
||||
{ATTR_ENTITY_ID: switch.entity_id, CONF_AUTORESET_CONTROL: 0},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert ENTITY_LIGHT_1 not in manager.auto_reset_manual_control_times
|
||||
manager.set_manual_control_attributes(ENTITY_LIGHT_1)
|
||||
assert ENTITY_LIGHT_1 not in manager.auto_reset_manual_control_timers
|
||||
|
||||
|
||||
async def test_autoreset_zero_cancels_running_reset_timer(hass):
|
||||
"""Disabling auto-reset must also stop a timer that is already running."""
|
||||
switch, _ = await setup_lights_and_switch(hass, {CONF_AUTORESET_CONTROL: 0.2})
|
||||
manager = switch.manager
|
||||
manager.set_manual_control_attributes(ENTITY_LIGHT_1)
|
||||
timer = manager.auto_reset_manual_control_timers[ENTITY_LIGHT_1]
|
||||
assert timer.is_running()
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_CHANGE_SWITCH_SETTINGS,
|
||||
{ATTR_ENTITY_ID: switch.entity_id, CONF_AUTORESET_CONTROL: 0},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not timer.is_running()
|
||||
assert ENTITY_LIGHT_1 not in manager.auto_reset_manual_control_timers
|
||||
await asyncio.sleep(0.4) # longer than the old delay
|
||||
await hass.async_block_till_done()
|
||||
# The light stays manually controlled instead of being reset and re-adapted.
|
||||
assert manager.get_manual_control_attributes(ENTITY_LIGHT_1)
|
||||
assert switch.extra_state_attributes["autoreset_time_remaining"] == {}
|
||||
|
||||
|
||||
async def test_autoreset_zero_keeps_other_profiles_reset(hass):
|
||||
"""A profile without auto-reset must not clear another profile's delay."""
|
||||
switch, _ = await setup_lights_and_switch(hass, {CONF_AUTORESET_CONTROL: 3600})
|
||||
await setup_switch(
|
||||
hass,
|
||||
{CONF_NAME: "other", CONF_LIGHTS: [ENTITY_LIGHT_1]},
|
||||
)
|
||||
assert switch.manager.auto_reset_manual_control_times[ENTITY_LIGHT_1] == 3600
|
||||
|
||||
|
||||
async def test_proactive_adapting_is_scoped_per_light(hass):
|
||||
"""A proactive turn-on of one light must not hide manual changes to another."""
|
||||
switch, _ = await setup_lights_and_switch(hass)
|
||||
manager = switch.manager
|
||||
manager.set_proactively_adapting("ctx", ENTITY_LIGHT_1)
|
||||
assert manager.is_proactively_adapting("ctx", ENTITY_LIGHT_1)
|
||||
assert not manager.is_proactively_adapting("ctx", ENTITY_LIGHT_2)
|
||||
|
||||
# Several lights may share one context, e.g. one intercepted multi-light call
|
||||
manager.set_proactively_adapting("ctx", ENTITY_LIGHT_2)
|
||||
assert manager.is_proactively_adapting("ctx", ENTITY_LIGHT_1)
|
||||
assert manager.is_proactively_adapting("ctx", ENTITY_LIGHT_2)
|
||||
|
||||
manager.clear_proactively_adapting(ENTITY_LIGHT_1)
|
||||
assert not manager.is_proactively_adapting("ctx", ENTITY_LIGHT_1)
|
||||
assert manager.is_proactively_adapting("ctx", ENTITY_LIGHT_2)
|
||||
manager.clear_proactively_adapting(ENTITY_LIGHT_2)
|
||||
assert "ctx" not in manager._proactively_adapting_contexts
|
||||
|
||||
|
||||
async def test_switch_turned_off_during_adapt_delay(hass, monkeypatch, caplog):
|
||||
"""Turning the switch off while adapt_delay waits must skip quietly."""
|
||||
switch, _ = await setup_lights_and_switch(
|
||||
hass,
|
||||
{CONF_DETECT_NON_HA_CHANGES: True, CONF_ADAPT_DELAY: 0.1234},
|
||||
)
|
||||
state = hass.states.get(ENTITY_LIGHT_1)
|
||||
hass.states.async_set(ENTITY_LIGHT_1, STATE_OFF, state.attributes)
|
||||
await hass.async_block_till_done()
|
||||
monkeypatch.setattr(
|
||||
switch.manager,
|
||||
"just_turned_off",
|
||||
AsyncMock(return_value=False),
|
||||
)
|
||||
entered, release = asyncio.Event(), asyncio.Event()
|
||||
original_sleep = asyncio.sleep
|
||||
|
||||
async def controlled_sleep(delay, *args, **kwargs):
|
||||
if delay == 0.1234:
|
||||
entered.set()
|
||||
await release.wait()
|
||||
else:
|
||||
await original_sleep(delay, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(asyncio, "sleep", controlled_sleep)
|
||||
calls = _track_adaptive_light_calls(hass)
|
||||
hass.states.async_set(ENTITY_LIGHT_1, STATE_ON, state.attributes)
|
||||
await asyncio.wait_for(entered.wait(), 2)
|
||||
# Don't wait for pending work here: the delayed handler is still paused
|
||||
await switch.async_turn_off()
|
||||
calls.clear()
|
||||
caplog.clear()
|
||||
release.set()
|
||||
await hass.async_block_till_done()
|
||||
assert calls == []
|
||||
assert not [r for r in caplog.records if r.levelno >= logging.ERROR]
|
||||
|
||||
|
||||
async def test_last_unload_disables_manager_after_yaml_import(hass):
|
||||
"""The manager must be disabled on the last unload even if YAML was used.
|
||||
|
||||
The YAML marker must survive so a later reload keeps the YAML switches.
|
||||
"""
|
||||
entry, switch = await setup_switch(hass, {})
|
||||
data = hass.data[DOMAIN]
|
||||
data.setdefault("__yaml__", set()).add("some_yaml_switch")
|
||||
manager = data[ATTR_ADAPTIVE_LIGHTING_MANAGER]
|
||||
with patch.object(manager, "disable", wraps=manager.disable) as disable:
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
disable.assert_called_once()
|
||||
assert ATTR_ADAPTIVE_LIGHTING_MANAGER not in hass.data[DOMAIN]
|
||||
assert hass.data[DOMAIN]["__yaml__"] == {"some_yaml_switch"}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue