Compare commits

...

7 commits

Author SHA1 Message Date
Bas Nijholt
df4f623639 test: add backward compatibility test for change_switch_settings
Add a regression test to verify that change_switch_settings works with
both calling conventions:
- entity_id in data (new domain service style)
- entity_id in target (old entity service style)

Both conventions work because Home Assistant's core automatically merges
the target parameter into service_data before calling the handler.

This test serves as documentation and ensures future changes don't break
either calling convention for existing automations.
2026-01-19 23:49:37 -08:00
Adam DeMuri
0020cdd241 Automated update of generated docs 2026-01-19 23:43:29 -07:00
Adam DeMuri
154e2211c5 Fix lint errors 2026-01-19 23:43:28 -07:00
pre-commit-ci[bot]
114688613e [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-01-19 23:40:35 -07:00
Adam DeMuri
282d7631c8 Clean up and add tests 2026-01-19 23:40:35 -07:00
pre-commit-ci[bot]
ab50cc9e3f [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-01-19 23:40:35 -07:00
Adam DeMuri
3952b4767a Register service actions in async_setup for Bronze tier compliance
- Move 'apply' and 'set_manual_control' service registration from async_setup_entry to async_setup.
- Move service handlers to module-level functions in switch.py.
- Update apply_service_schema to support dynamic defaults for transition duration.
- Clean up related unused imports and fix Python 3.10 syntax compatibility.
2026-01-19 23:40:35 -07:00
7 changed files with 327 additions and 181 deletions

View file

@ -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 |

View file

@ -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(

View file

@ -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]

View file

@ -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)

View file

@ -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:

View file

@ -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 |

View file

@ -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}"
)