mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-15 08:14:04 +02:00
Compare commits
7 commits
main
...
bronze-act
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df4f623639 | ||
|
|
0020cdd241 | ||
|
|
154e2211c5 | ||
|
|
114688613e | ||
|
|
282d7631c8 | ||
|
|
ab50cc9e3f | ||
|
|
3952b4767a |
7 changed files with 327 additions and 181 deletions
|
|
@ -199,7 +199,7 @@ adaptive_lighting:
|
|||
|:-------------------------|:--------------------------------------------------------------------------------------|:-----------|:---------------------|
|
||||
| `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ✅ | list of `entity_id`s |
|
||||
| `lights` | A light (or list of lights) to apply the settings to. 💡 | ❌ | list of `entity_id`s |
|
||||
| `transition` | Duration of transition when lights change, in seconds. 🕑 | ❌ | `float` 0-6553 |
|
||||
| `transition` | Duration of transition when lights change, in seconds. 🕑 | ✅ | `float` 0-6553 |
|
||||
| `adapt_brightness` | Whether to adapt the brightness of the light. 🌞 | ❌ | bool |
|
||||
| `adapt_color` | Whether to adapt the color on supporting lights. 🌈 | ❌ | bool |
|
||||
| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | ❌ | bool |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Adaptive Lighting integration in Home-Assistant."""
|
||||
|
||||
import logging
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
|
@ -14,7 +15,18 @@ from .const import (
|
|||
ATTR_ADAPTIVE_LIGHTING_MANAGER,
|
||||
CONF_NAME,
|
||||
DOMAIN,
|
||||
SERVICE_APPLY,
|
||||
SERVICE_CHANGE_SWITCH_SETTINGS,
|
||||
SERVICE_SET_MANUAL_CONTROL,
|
||||
SET_MANUAL_CONTROL_SCHEMA,
|
||||
UNDO_UPDATE_LISTENER,
|
||||
apply_service_schema,
|
||||
change_switch_settings_schema,
|
||||
)
|
||||
from .switch import (
|
||||
handle_apply_service,
|
||||
handle_change_switch_settings,
|
||||
handle_set_manual_control_service,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
|
@ -47,6 +59,27 @@ async def reload_configuration_yaml(event: Event) -> None:
|
|||
|
||||
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
|
||||
"""Import integration from config."""
|
||||
hass.services.async_register(
|
||||
domain=DOMAIN,
|
||||
service=SERVICE_APPLY,
|
||||
service_func=partial(handle_apply_service, hass),
|
||||
schema=apply_service_schema(),
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
domain=DOMAIN,
|
||||
service=SERVICE_SET_MANUAL_CONTROL,
|
||||
service_func=partial(handle_set_manual_control_service, hass),
|
||||
schema=SET_MANUAL_CONTROL_SCHEMA,
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
domain=DOMAIN,
|
||||
service=SERVICE_CHANGE_SWITCH_SETTINGS,
|
||||
service_func=partial(handle_change_switch_settings, hass),
|
||||
schema=change_switch_settings_schema(),
|
||||
)
|
||||
|
||||
if DOMAIN in config:
|
||||
for entry in config[DOMAIN]:
|
||||
hass.async_create_task(
|
||||
|
|
|
|||
|
|
@ -459,16 +459,13 @@ _DOMAIN_SCHEMA = vol.Schema(
|
|||
)
|
||||
|
||||
|
||||
def apply_service_schema(initial_transition: int = 1) -> vol.Schema:
|
||||
def apply_service_schema() -> vol.Schema:
|
||||
"""Return the schema for the apply service."""
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ENTITY_ID): cv.entity_ids, # type: ignore[arg-type]
|
||||
vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, # type: ignore[arg-type]
|
||||
vol.Optional(
|
||||
CONF_TRANSITION,
|
||||
default=initial_transition,
|
||||
): VALID_TRANSITION,
|
||||
vol.Optional(CONF_TRANSITION): VALID_TRANSITION,
|
||||
vol.Optional(ATTR_ADAPT_BRIGHTNESS, default=True): cv.boolean,
|
||||
vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean,
|
||||
vol.Optional(CONF_PREFER_RGB_COLOR, default=False): cv.boolean,
|
||||
|
|
@ -477,6 +474,20 @@ def apply_service_schema(initial_transition: int = 1) -> vol.Schema:
|
|||
)
|
||||
|
||||
|
||||
def change_switch_settings_schema() -> vol.Schema:
|
||||
"""Return the schema for the change_switch_settings service."""
|
||||
args: dict[vol.Marker, Any] = {
|
||||
vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(CONF_USE_DEFAULTS, default="current"): cv.string,
|
||||
}
|
||||
# Modifying these after init isn't possible
|
||||
skip = (CONF_INTERVAL, CONF_NAME, CONF_LIGHTS)
|
||||
for k, _, valid in VALIDATION_TUPLES:
|
||||
if k not in skip:
|
||||
args[vol.Optional(k)] = valid
|
||||
return vol.Schema(args)
|
||||
|
||||
|
||||
SET_MANUAL_CONTROL_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ENTITY_ID): cv.entity_ids, # type: ignore[arg-type]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Utility functions for HA core."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
|
|
@ -80,9 +81,8 @@ def setup_service_call_interceptor(
|
|||
"Error for call '%s' in service_func_proxy",
|
||||
call.data,
|
||||
)
|
||||
# Call original service handler with processed data
|
||||
import asyncio
|
||||
|
||||
# Call original service handler with processed data
|
||||
target = existing_service.job.target
|
||||
if asyncio.iscoroutinefunction(target):
|
||||
await target(call)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from typing import TYPE_CHECKING, Any
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
import ulid_transform
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
|
|
@ -55,9 +54,9 @@ from homeassistant.core import (
|
|||
HomeAssistant,
|
||||
ServiceCall,
|
||||
State,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import entity_platform, entity_registry
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
from homeassistant.helpers.event import (
|
||||
|
|
@ -137,15 +136,10 @@ from .const import (
|
|||
ICON_COLOR_TEMP,
|
||||
ICON_MAIN,
|
||||
ICON_SLEEP,
|
||||
SERVICE_APPLY,
|
||||
SERVICE_CHANGE_SWITCH_SETTINGS,
|
||||
SERVICE_SET_MANUAL_CONTROL,
|
||||
SET_MANUAL_CONTROL_SCHEMA,
|
||||
SLEEP_MODE_SWITCH,
|
||||
TURNING_OFF_DELAY,
|
||||
VALIDATION_TUPLES,
|
||||
TakeOverControlMode,
|
||||
apply_service_schema,
|
||||
replace_none_str,
|
||||
)
|
||||
from .hass_utils import area_entities, setup_service_call_interceptor
|
||||
|
|
@ -162,9 +156,7 @@ if TYPE_CHECKING:
|
|||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import NoEventData, VolDictType
|
||||
|
||||
|
||||
from homeassistant.helpers.typing import NoEventData
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
|
@ -284,7 +276,7 @@ def _switches_from_service_call(
|
|||
service_call: ServiceCall,
|
||||
) -> AdaptiveSwitches:
|
||||
data = service_call.data
|
||||
lights = data[CONF_LIGHTS]
|
||||
lights = data.get(CONF_LIGHTS)
|
||||
switch_entity_ids: list[str] | None = data.get("entity_id")
|
||||
|
||||
if not lights and not switch_entity_ids:
|
||||
|
|
@ -295,7 +287,7 @@ def _switches_from_service_call(
|
|||
" use case. Currently, you must pass either an adaptive-lighting switch or"
|
||||
" the lights to an `adaptive_lighting` service call."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
raise ServiceValidationError(msg)
|
||||
|
||||
if switch_entity_ids is not None:
|
||||
if len(switch_entity_ids) > 1 and lights:
|
||||
|
|
@ -303,13 +295,18 @@ def _switches_from_service_call(
|
|||
"adaptive-lighting: Cannot pass multiple switches with lights argument."
|
||||
f" Invalid service data received: {service_call.data}"
|
||||
)
|
||||
raise ValueError(msg)
|
||||
raise ServiceValidationError(msg)
|
||||
switches: AdaptiveSwitches = []
|
||||
ent_reg = entity_registry.async_get(hass)
|
||||
for entity_id in switch_entity_ids:
|
||||
ent_entry = ent_reg.async_get(entity_id)
|
||||
assert ent_entry is not None
|
||||
if ent_entry is None:
|
||||
msg = f"adaptive-lighting: Entity '{entity_id}' not found in registry."
|
||||
raise ServiceValidationError(msg)
|
||||
config_id = ent_entry.config_entry_id
|
||||
if config_id not in hass.data[DOMAIN]:
|
||||
msg = f"adaptive-lighting: Entity '{entity_id}' does not belong to this integration or is not loaded."
|
||||
raise ServiceValidationError(msg)
|
||||
switches.append(hass.data[DOMAIN][config_id][SWITCH_DOMAIN])
|
||||
return switches
|
||||
|
||||
|
|
@ -321,47 +318,128 @@ def _switches_from_service_call(
|
|||
"adaptive-lighting: Incorrect data provided in service call."
|
||||
f" Entities not found in the integration. Service data: {service_call.data}"
|
||||
)
|
||||
raise ValueError(msg)
|
||||
raise ServiceValidationError(msg)
|
||||
|
||||
|
||||
async def handle_change_switch_settings(
|
||||
switch: AdaptiveSwitch,
|
||||
hass: HomeAssistant,
|
||||
service_call: ServiceCall,
|
||||
) -> None:
|
||||
"""Allows HASS to change config values via a service call."""
|
||||
data = service_call.data
|
||||
which = data.get(CONF_USE_DEFAULTS, "current")
|
||||
if which == "current": # use whatever we're already using.
|
||||
defaults = switch._current_settings # pylint: disable=protected-access
|
||||
elif which == "factory": # use actual defaults listed in the documentation
|
||||
defaults = None
|
||||
elif which == "configuration":
|
||||
# use whatever's in the config flow or configuration.yaml
|
||||
defaults = switch._config_backup
|
||||
else:
|
||||
defaults = None
|
||||
switches = _switches_from_service_call(hass, service_call)
|
||||
for switch in switches:
|
||||
which = data.get(CONF_USE_DEFAULTS, "current")
|
||||
if which == "current": # use whatever we're already using.
|
||||
defaults = switch._current_settings # pylint: disable=protected-access
|
||||
elif which == "factory": # use actual defaults listed in the documentation
|
||||
defaults = None
|
||||
elif which == "configuration":
|
||||
# use whatever's in the config flow or configuration.yaml
|
||||
defaults = switch._config_backup
|
||||
else:
|
||||
defaults = None
|
||||
|
||||
# deep copy the defaults so we don't modify the original dicts
|
||||
switch._set_changeable_settings(data=data, defaults=deepcopy(defaults))
|
||||
if switch.is_on:
|
||||
switch._update_time_interval_listener()
|
||||
# deep copy the defaults so we don't modify the original dicts
|
||||
switch._set_changeable_settings(data=data, defaults=deepcopy(defaults))
|
||||
if switch.is_on:
|
||||
switch._update_time_interval_listener()
|
||||
|
||||
_LOGGER.debug(
|
||||
"Called 'adaptive_lighting.change_switch_settings' service with '%s'",
|
||||
data,
|
||||
)
|
||||
|
||||
switch.manager.reset(*switch.lights, reset_manual_control=False)
|
||||
if switch.is_on:
|
||||
await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access
|
||||
context=switch.create_context("service", parent=service_call.context),
|
||||
lights=switch.lights,
|
||||
transition=switch.initial_transition,
|
||||
force=True,
|
||||
_LOGGER.debug(
|
||||
"Called 'adaptive_lighting.change_switch_settings' service with '%s'",
|
||||
data,
|
||||
)
|
||||
|
||||
switch.manager.reset(*switch.lights, reset_manual_control=False)
|
||||
if switch.is_on:
|
||||
await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access
|
||||
context=switch.create_context("service", parent=service_call.context),
|
||||
lights=switch.lights,
|
||||
transition=switch.initial_transition,
|
||||
force=True,
|
||||
)
|
||||
|
||||
async def async_setup_entry( # noqa: PLR0915
|
||||
|
||||
async def handle_apply_service(hass: HomeAssistant, service_call: ServiceCall) -> None:
|
||||
"""Handle the entity service apply."""
|
||||
data = service_call.data
|
||||
_LOGGER.debug(
|
||||
"Called 'adaptive_lighting.apply' service with '%s'",
|
||||
data,
|
||||
)
|
||||
switches = _switches_from_service_call(hass, service_call)
|
||||
lights = data[CONF_LIGHTS]
|
||||
for switch in switches:
|
||||
all_lights = switch.lights if not lights else _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):
|
||||
context = switch.create_context(
|
||||
"service",
|
||||
parent=service_call.context,
|
||||
)
|
||||
# Handle optional transition
|
||||
transition = data.get(CONF_TRANSITION)
|
||||
if transition is None:
|
||||
transition = switch.initial_transition
|
||||
|
||||
await switch._adapt_light( # pylint: disable=protected-access
|
||||
light,
|
||||
context=context,
|
||||
transition=transition,
|
||||
adapt_brightness=data[ATTR_ADAPT_BRIGHTNESS],
|
||||
adapt_color=data[ATTR_ADAPT_COLOR],
|
||||
prefer_rgb_color=data[CONF_PREFER_RGB_COLOR],
|
||||
force=True,
|
||||
)
|
||||
|
||||
|
||||
async def handle_set_manual_control_service(
|
||||
hass: HomeAssistant,
|
||||
service_call: ServiceCall,
|
||||
) -> None:
|
||||
"""Set or unset lights as 'manually controlled'."""
|
||||
data = service_call.data
|
||||
_LOGGER.debug(
|
||||
"Called 'adaptive_lighting.set_manual_control' service with '%s'",
|
||||
data,
|
||||
)
|
||||
switches = _switches_from_service_call(hass, service_call)
|
||||
lights = data[CONF_LIGHTS]
|
||||
for switch in switches:
|
||||
all_lights = switch.lights if not lights else _expand_light_groups(hass, lights)
|
||||
|
||||
manual_attributes = manual_control_event_attribute_to_flags(
|
||||
service_call.data[CONF_MANUAL_CONTROL],
|
||||
)
|
||||
|
||||
if manual_attributes:
|
||||
for light in all_lights:
|
||||
switch.manager.set_manual_control_attributes(
|
||||
light,
|
||||
manual_attributes,
|
||||
)
|
||||
switch.fire_manual_control_event(
|
||||
light,
|
||||
service_call.context,
|
||||
)
|
||||
else:
|
||||
switch.manager.reset(*all_lights)
|
||||
if switch.is_on:
|
||||
context = switch.create_context(
|
||||
"service",
|
||||
parent=service_call.context,
|
||||
)
|
||||
# pylint: disable=protected-access
|
||||
await switch._update_attrs_and_maybe_adapt_lights(
|
||||
context=context,
|
||||
lights=all_lights,
|
||||
transition=switch.initial_transition,
|
||||
force=True,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
|
|
@ -431,113 +509,6 @@ async def async_setup_entry( # noqa: PLR0915
|
|||
update_before_add=True,
|
||||
)
|
||||
|
||||
@callback
|
||||
async def handle_apply(service_call: ServiceCall) -> None:
|
||||
"""Handle the entity service apply."""
|
||||
data = service_call.data
|
||||
_LOGGER.debug(
|
||||
"Called 'adaptive_lighting.apply' service with '%s'",
|
||||
data,
|
||||
)
|
||||
switches = _switches_from_service_call(hass, service_call)
|
||||
lights = data[CONF_LIGHTS]
|
||||
for switch in switches:
|
||||
if not lights:
|
||||
all_lights = switch.lights
|
||||
else:
|
||||
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):
|
||||
context = switch.create_context(
|
||||
"service",
|
||||
parent=service_call.context,
|
||||
)
|
||||
await switch._adapt_light( # pylint: disable=protected-access
|
||||
light,
|
||||
context=context,
|
||||
transition=data[CONF_TRANSITION],
|
||||
adapt_brightness=data[ATTR_ADAPT_BRIGHTNESS],
|
||||
adapt_color=data[ATTR_ADAPT_COLOR],
|
||||
prefer_rgb_color=data[CONF_PREFER_RGB_COLOR],
|
||||
force=True,
|
||||
)
|
||||
|
||||
@callback
|
||||
async def handle_set_manual_control(service_call: ServiceCall) -> None:
|
||||
"""Set or unset lights as 'manually controlled'."""
|
||||
data = service_call.data
|
||||
_LOGGER.debug(
|
||||
"Called 'adaptive_lighting.set_manual_control' service with '%s'",
|
||||
data,
|
||||
)
|
||||
switches = _switches_from_service_call(hass, service_call)
|
||||
lights = data[CONF_LIGHTS]
|
||||
for switch in switches:
|
||||
if not lights:
|
||||
all_lights = switch.lights
|
||||
else:
|
||||
all_lights = _expand_light_groups(hass, lights)
|
||||
|
||||
manual_attributes = manual_control_event_attribute_to_flags(
|
||||
service_call.data[CONF_MANUAL_CONTROL],
|
||||
)
|
||||
|
||||
if manual_attributes:
|
||||
for light in all_lights:
|
||||
switch.manager.set_manual_control_attributes(
|
||||
light,
|
||||
manual_attributes,
|
||||
)
|
||||
switch.fire_manual_control_event(
|
||||
light,
|
||||
service_call.context,
|
||||
)
|
||||
else:
|
||||
switch.manager.reset(*all_lights)
|
||||
if switch.is_on:
|
||||
context = switch.create_context(
|
||||
"service",
|
||||
parent=service_call.context,
|
||||
)
|
||||
# pylint: disable=protected-access
|
||||
await switch._update_attrs_and_maybe_adapt_lights(
|
||||
context=context,
|
||||
lights=all_lights,
|
||||
transition=switch.initial_transition,
|
||||
force=True,
|
||||
)
|
||||
|
||||
# Register `apply` service
|
||||
hass.services.async_register(
|
||||
domain=DOMAIN,
|
||||
service=SERVICE_APPLY,
|
||||
service_func=handle_apply,
|
||||
schema=apply_service_schema(switch.initial_transition),
|
||||
)
|
||||
|
||||
# Register `set_manual_control` service
|
||||
hass.services.async_register(
|
||||
domain=DOMAIN,
|
||||
service=SERVICE_SET_MANUAL_CONTROL,
|
||||
service_func=handle_set_manual_control,
|
||||
schema=SET_MANUAL_CONTROL_SCHEMA,
|
||||
)
|
||||
|
||||
args: VolDictType = {vol.Optional(CONF_USE_DEFAULTS, default="current"): cv.string}
|
||||
# Modifying these after init isn't possible
|
||||
skip = (CONF_INTERVAL, CONF_NAME, CONF_LIGHTS)
|
||||
for k, _, valid in VALIDATION_TUPLES:
|
||||
if k not in skip:
|
||||
args[vol.Optional(k)] = valid
|
||||
platform = entity_platform.current_platform.get()
|
||||
assert platform is not None
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_CHANGE_SWITCH_SETTINGS,
|
||||
args,
|
||||
handle_change_switch_settings,
|
||||
)
|
||||
|
||||
|
||||
def validate(
|
||||
config_entry: ConfigEntry | None,
|
||||
|
|
@ -1681,8 +1652,8 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
|
|||
self._state = False
|
||||
|
||||
|
||||
type AdaptiveSwitches = list[AdaptiveSwitch]
|
||||
type AdaptiveSwitchMap = dict[AdaptiveSwitch, list[str]]
|
||||
AdaptiveSwitches = list[AdaptiveSwitch]
|
||||
AdaptiveSwitchMap = dict[AdaptiveSwitch, list[str]]
|
||||
|
||||
|
||||
class AdaptiveLightingManager:
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ Applies the current Adaptive Lighting settings to lights on demand. Useful for f
|
|||
|:-------------------------|:--------------------------------------------------------------------------------------|:-----------|:---------------------|
|
||||
| `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ✅ | list of `entity_id`s |
|
||||
| `lights` | A light (or list of lights) to apply the settings to. 💡 | ❌ | list of `entity_id`s |
|
||||
| `transition` | Duration of transition when lights change, in seconds. 🕑 | ❌ | `float` 0-6553 |
|
||||
| `transition` | Duration of transition when lights change, in seconds. 🕑 | ✅ | `float` 0-6553 |
|
||||
| `adapt_brightness` | Whether to adapt the brightness of the light. 🌞 | ❌ | bool |
|
||||
| `adapt_color` | Whether to adapt the color on supporting lights. 🌈 | ❌ | bool |
|
||||
| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | ❌ | bool |
|
||||
|
|
|
|||
|
|
@ -87,6 +87,9 @@ from homeassistant.components.light import (
|
|||
)
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.helpers.normalized_name_base_registry import (
|
||||
NormalizedNameBaseRegistryItems,
|
||||
)
|
||||
|
||||
try:
|
||||
# HA >= 2025.8
|
||||
|
|
@ -113,6 +116,8 @@ from homeassistant.const import (
|
|||
)
|
||||
from homeassistant.const import __version__ as ha_version
|
||||
from homeassistant.core import Context, Event, HomeAssistant, State
|
||||
from homeassistant.core_config import async_process_ha_core_config
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import area_registry as ar
|
||||
from homeassistant.helpers import entity_registry
|
||||
from homeassistant.helpers.entity_platform import async_get_platforms
|
||||
|
|
@ -375,19 +380,6 @@ async def test_adaptive_lighting_switches(hass):
|
|||
assert len(data.keys()) == 5
|
||||
|
||||
|
||||
def async_process_ha_core_config(hass, config):
|
||||
"""Set up the Home Assistant configuration."""
|
||||
try:
|
||||
# ha >= "2023.11.0"
|
||||
from homeassistant.core_config import async_process_ha_core_config
|
||||
|
||||
return async_process_ha_core_config(hass, config)
|
||||
except ModuleNotFoundError:
|
||||
import homeassistant.config as config_util
|
||||
|
||||
return config_util.async_process_ha_core_config(hass, config)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("lat", "long", "timezone"), LAT_LONG_TZS)
|
||||
async def test_adaptive_lighting_time_zones_with_default_settings(
|
||||
hass,
|
||||
|
|
@ -890,7 +882,7 @@ async def test_auto_reset_manual_control(hass):
|
|||
|
||||
async def test_adaptation_attribute_selection(hass):
|
||||
"""Test the 'manual control' tracking."""
|
||||
switch, (light, *_) = await setup_lights_and_switch(hass)
|
||||
switch, (_, *_) = await setup_lights_and_switch(hass)
|
||||
|
||||
# Assert default settings
|
||||
assert switch._take_over_control
|
||||
|
|
@ -1480,7 +1472,7 @@ async def test_offset_too_large(hass):
|
|||
which makes the adaptive lighting algorithm fail with a ValueError.
|
||||
"""
|
||||
_, switch = await setup_switch(hass, {CONF_SUNRISE_OFFSET: 3600 * 12})
|
||||
with pytest.raises(ValueError, match="sun events.*not in the expected order"):
|
||||
with pytest.raises(ValueError, match=r"sun events.*not in the expected order"):
|
||||
await switch._update_attrs_and_maybe_adapt_lights(
|
||||
context=switch.create_context("test"),
|
||||
)
|
||||
|
|
@ -1584,10 +1576,6 @@ def mock_area_registry(
|
|||
# https://github.com/home-assistant/core/pull/114777
|
||||
registry.areas = ar.AreaRegistryItems()
|
||||
elif dt == datetime.date(2024, 4, 1):
|
||||
from homeassistant.helpers.normalized_name_base_registry import (
|
||||
NormalizedNameBaseRegistryItems,
|
||||
)
|
||||
|
||||
registry.areas = NormalizedNameBaseRegistryItems()
|
||||
else:
|
||||
registry.areas = OrderedDict()
|
||||
|
|
@ -2005,7 +1993,7 @@ async def test_proactive_multiple_lights_all_at_once(hass):
|
|||
|
||||
async def test_proactive_multiple_lights_turn_on_non_managed_light(hass):
|
||||
"""Create switch and demo lights."""
|
||||
lights, switch1, switch2 = await setup_proactive_multiple_lights_two_switches(hass)
|
||||
lights, _, _ = await setup_proactive_multiple_lights_two_switches(hass)
|
||||
turn_ons = await _turn_on_and_track_event_contexts(hass, "test1", lights)
|
||||
assert len(turn_ons) == 3, turn_ons
|
||||
await hass.async_block_till_done()
|
||||
|
|
@ -2027,7 +2015,7 @@ async def test_proactive_multiple_lights_turn_on_non_managed_light(hass):
|
|||
|
||||
async def test_proactive_multiple_lights_turn_on_managed_lights_only(hass):
|
||||
"""Create switch and demo lights."""
|
||||
lights, switch1, switch2 = await setup_proactive_multiple_lights_two_switches(hass)
|
||||
lights, _, _ = await setup_proactive_multiple_lights_two_switches(hass)
|
||||
_LOGGER.debug("Start test_proactive_multiple_lights_all_at_once")
|
||||
# Setup demo lights and turn on
|
||||
events = await _turn_on_and_track_event_contexts(
|
||||
|
|
@ -2154,7 +2142,7 @@ async def test_adapt_until_sleep_and_rgb_colors(hass):
|
|||
hass,
|
||||
{"latitude": lat, "longitude": long, "time_zone": timezone, "country": "US"},
|
||||
)
|
||||
switch, lights = await setup_lights_and_switch(
|
||||
switch, _ = await setup_lights_and_switch(
|
||||
hass,
|
||||
{
|
||||
CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour),
|
||||
|
|
@ -2716,7 +2704,7 @@ async def test_skipped_lights_context_not_from_arbitrary_switch(hass):
|
|||
See: https://github.com/basnijholt/adaptive-lighting/pull/1348
|
||||
"""
|
||||
# Setup two switches with different lights
|
||||
lights, switch1, switch2 = await setup_proactive_multiple_lights_two_switches(hass)
|
||||
lights, _, _ = await setup_proactive_multiple_lights_two_switches(hass)
|
||||
|
||||
# Turn on all three lights at once:
|
||||
# - ENTITY_LIGHT_1 is in switch1
|
||||
|
|
@ -2914,3 +2902,146 @@ async def test_adapt_only_on_bare_turn_on_respects_pause_changed_mode(hass, inte
|
|||
f"With take_over_control_mode=PAUSE_CHANGED and only brightness marked "
|
||||
f"as manually controlled, color_temp should still be adapted."
|
||||
)
|
||||
|
||||
|
||||
async def test_service_validation_error_invalid_entity(hass):
|
||||
"""Test that ServiceValidationError is raised for invalid entities."""
|
||||
await setup_lights_and_switch(hass)
|
||||
|
||||
# Test change_switch_settings with non-existent entity
|
||||
with pytest.raises(ServiceValidationError, match="not found in registry"):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_CHANGE_SWITCH_SETTINGS,
|
||||
{ATTR_ENTITY_ID: "switch.non_existent"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_service_validation_error_missing_input(hass):
|
||||
"""Test that ServiceValidationError is raised for missing input."""
|
||||
await setup_lights_and_switch(hass)
|
||||
|
||||
# Test change_switch_settings with no entity and no lights
|
||||
with pytest.raises(
|
||||
ServiceValidationError,
|
||||
match="Neither a switch nor a light was provided",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_CHANGE_SWITCH_SETTINGS,
|
||||
{},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_change_switch_settings_multiple_entities(hass):
|
||||
"""Test change_switch_settings with multiple entities."""
|
||||
# Setup two switches
|
||||
await setup_lights(hass)
|
||||
_, switch1 = await setup_switch(
|
||||
hass,
|
||||
{CONF_LIGHTS: [ENTITY_LIGHT_1], CONF_NAME: "switch1"},
|
||||
)
|
||||
_, switch2 = await setup_switch(
|
||||
hass,
|
||||
{CONF_LIGHTS: [ENTITY_LIGHT_2], CONF_NAME: "switch2"},
|
||||
)
|
||||
|
||||
assert switch1._sun_light_settings.min_color_temp != 3000
|
||||
assert switch2._sun_light_settings.min_color_temp != 3000
|
||||
|
||||
# Call service for both switches
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_CHANGE_SWITCH_SETTINGS,
|
||||
{
|
||||
ATTR_ENTITY_ID: [
|
||||
"switch.adaptive_lighting_switch1",
|
||||
"switch.adaptive_lighting_switch2",
|
||||
],
|
||||
"min_color_temp": 3000,
|
||||
"use_defaults": "configuration", # Required to set new value
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert switch1._sun_light_settings.min_color_temp == 3000
|
||||
assert switch2._sun_light_settings.min_color_temp == 3000
|
||||
|
||||
|
||||
async def test_apply_service_validation(hass):
|
||||
"""Test validation for apply service."""
|
||||
await setup_lights_and_switch(hass)
|
||||
|
||||
with pytest.raises(ServiceValidationError, match="not found in registry"):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_APPLY,
|
||||
{ATTR_ENTITY_ID: "switch.non_existent"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_set_manual_control_validation(hass):
|
||||
"""Test validation for set_manual_control service."""
|
||||
await setup_lights_and_switch(hass)
|
||||
|
||||
with pytest.raises(ServiceValidationError, match="not found in registry"):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_MANUAL_CONTROL,
|
||||
{ATTR_ENTITY_ID: "switch.non_existent"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"use_target",
|
||||
[False, True],
|
||||
ids=["entity_id_in_data", "entity_id_in_target"],
|
||||
)
|
||||
async def test_change_switch_settings_backward_compatibility(hass, use_target):
|
||||
"""Test change_switch_settings works with both calling conventions.
|
||||
|
||||
This is a regression test to ensure backward compatibility when
|
||||
change_switch_settings was converted from an entity service to a domain service.
|
||||
|
||||
Previously (entity service): entity_id was passed via `target` parameter
|
||||
Now (domain service): entity_id is passed via `data` parameter
|
||||
|
||||
Both conventions should work to avoid breaking existing automations.
|
||||
"""
|
||||
switch, _ = await setup_lights_and_switch(hass)
|
||||
|
||||
# Verify initial state
|
||||
original_min_color_temp = switch._sun_light_settings.min_color_temp
|
||||
new_min_color_temp = 3000
|
||||
assert original_min_color_temp != new_min_color_temp
|
||||
|
||||
if use_target:
|
||||
# Old convention: entity_id in target (entity service style)
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_CHANGE_SWITCH_SETTINGS,
|
||||
{"min_color_temp": new_min_color_temp},
|
||||
target={"entity_id": ENTITY_SWITCH},
|
||||
blocking=True,
|
||||
)
|
||||
else:
|
||||
# New convention: entity_id in data (domain service style)
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_CHANGE_SWITCH_SETTINGS,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_SWITCH,
|
||||
"min_color_temp": new_min_color_temp,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Both conventions should result in the setting being changed
|
||||
assert switch._sun_light_settings.min_color_temp == new_min_color_temp, (
|
||||
f"change_switch_settings failed with {'target' if use_target else 'data'} convention. "
|
||||
f"Expected min_color_temp={new_min_color_temp}, got {switch._sun_light_settings.min_color_temp}"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue