mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-10-29 06:37:13 +01:00
🐛 get_commands: leave out what lights already have with skip_redundant_commands
As when Adaptive Lighting adapts a light with `skip_redundant_commands`, leave out the attributes the light's current state already has, and calls with nothing left. The filter is shared with Adaptive Lighting's own calls (`remove_redundant_service_data`), whose behaviour doesn't change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d26fadc5bb
commit
0302f509bb
5 changed files with 131 additions and 15 deletions
|
|
@ -256,7 +256,7 @@ Provide a switch in `entity_id`, a list of `lights`, or both.
|
|||
<!-- OUTPUT:END -->
|
||||
|
||||
<!-- SECTION:get-commands:START -->
|
||||
Call it with a `response_variable`. For each light, the response lists the calls in order, each with its `service_data` and the `delay` in seconds before it (only nonzero between split calls, see `separate_turn_on_commands`). Only the lights and light groups in a switch's `lights` can be passed; without `entity_id`, each uses the switch it's in (of several, the one that's on). A light group is replaced by its members if the switch expands light groups. Options you leave out follow the switch, with its `initial_transition`, and in sleep mode the calls are for the sleep settings, whatever the time. Manual control and whether the lights are on don't change the calls. In YAML files, quote the time (`"22:00:00"`), since `22:00:00` is read as a number.
|
||||
Call it with a `response_variable`. For each light, the response lists the calls in order, each with its `service_data` and the `delay` in seconds before it (only nonzero between split calls, see `separate_turn_on_commands`). Only the lights and light groups in a switch's `lights` can be passed; without `entity_id`, each uses the switch it's in (of several, the one that's on). A light group is replaced by its members if the switch expands light groups. Options you leave out follow the switch, with its `initial_transition`, and in sleep mode the calls are for the sleep settings, whatever the time. With `skip_redundant_commands`, what a light already has is left out, as when Adaptive Lighting adapts it. Manual control doesn't change the calls. In YAML files, quote the time (`"22:00:00"`), since `22:00:00` is read as a number.
|
||||
|
||||
Making the calls is a manual change for Adaptive Lighting: with `take_over_control`, it stops adapting those lights until manual control is reset. A light that's off is adapted to the current settings as it turns on (unless `take_over_control` and `adapt_only_on_bare_turn_on` are enabled), so this example only changes lights that are on:
|
||||
|
||||
|
|
|
|||
|
|
@ -176,6 +176,19 @@ def _has_relevant_service_data_attributes(service_data: ServiceData) -> bool:
|
|||
return any(attr not in common_attrs for attr in service_data)
|
||||
|
||||
|
||||
def remove_redundant_service_data(
|
||||
service_data: ServiceData,
|
||||
state: State | None,
|
||||
) -> ServiceData | None:
|
||||
"""Remove what the light's `state` already has from the data of a service call.
|
||||
|
||||
Returns `None` if nothing is left that justifies the service call.
|
||||
"""
|
||||
if state is not None:
|
||||
service_data = _remove_redundant_attributes(service_data, state=state)
|
||||
return service_data if _has_relevant_service_data_attributes(service_data) else None
|
||||
|
||||
|
||||
async def _create_service_call_data_iterator(
|
||||
hass: HomeAssistant,
|
||||
service_datas: list[ServiceData],
|
||||
|
|
@ -192,18 +205,12 @@ async def _create_service_call_data_iterator(
|
|||
"""
|
||||
for service_data in service_datas:
|
||||
if filter_by_state and (entity_id := service_data.get(ATTR_ENTITY_ID)):
|
||||
current_entity_state = hass.states.get(entity_id)
|
||||
|
||||
# Filter data to remove attributes that equal the current state
|
||||
if current_entity_state is not None:
|
||||
service_data = _remove_redundant_attributes( # noqa: PLW2901
|
||||
service_data,
|
||||
state=current_entity_state,
|
||||
)
|
||||
|
||||
# Emit service data if it still contains relevant attributes (else try next)
|
||||
if _has_relevant_service_data_attributes(service_data):
|
||||
yield service_data
|
||||
if filtered := remove_redundant_service_data(
|
||||
service_data,
|
||||
hass.states.get(entity_id),
|
||||
):
|
||||
yield filtered
|
||||
else:
|
||||
yield service_data
|
||||
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ from .adaptation_utils import (
|
|||
manual_control_event_attribute_to_flags,
|
||||
plan_service_calls,
|
||||
prepare_adaptation_data,
|
||||
remove_redundant_service_data,
|
||||
)
|
||||
from .color_and_brightness import SunLightSettings
|
||||
from .const import (
|
||||
|
|
@ -1501,10 +1502,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
|
||||
For each light (all of the switch's if `lights` is `None`), the calls in
|
||||
order, each with its service data and the time to wait before it, adapting
|
||||
now or at the `time` of day today (lights without a state are left out).
|
||||
now or at the `time` of day today (lights without a state are left out). With
|
||||
`skip_redundant_commands`, what a light already has is left out.
|
||||
Omitted options default to the switch's settings (unlike for `apply`), and
|
||||
`transition` to its `initial_transition` (like for `apply`). Manual control
|
||||
and the light's current state don't change the calls, and nothing is sent.
|
||||
doesn't change the calls, and nothing is sent.
|
||||
"""
|
||||
if transition is None:
|
||||
transition = self.initial_transition
|
||||
|
|
@ -1550,6 +1552,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
self._send_split_delay / 1000.0,
|
||||
split=self._separate_turn_on_commands,
|
||||
)
|
||||
if self._skip_redundant_commands:
|
||||
# Leave out what the light already has, as when adapting it
|
||||
state = self.hass.states.get(light)
|
||||
service_datas = [
|
||||
filtered
|
||||
for data in service_datas
|
||||
if (filtered := remove_redundant_service_data(data, state))
|
||||
]
|
||||
commands[light] = [
|
||||
{
|
||||
"service_data": {
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ Provide a switch in `entity_id`, a list of `lights`, or both.
|
|||
<!-- CODE:END -->
|
||||
<!-- OUTPUT:START -->
|
||||
<!-- ⚠️ This content is auto-generated by `markdown-code-runner`. -->
|
||||
Call it with a `response_variable`. For each light, the response lists the calls in order, each with its `service_data` and the `delay` in seconds before it (only nonzero between split calls, see `separate_turn_on_commands`). Only the lights and light groups in a switch's `lights` can be passed; without `entity_id`, each uses the switch it's in (of several, the one that's on). A light group is replaced by its members if the switch expands light groups. Options you leave out follow the switch, with its `initial_transition`, and in sleep mode the calls are for the sleep settings, whatever the time. Manual control and whether the lights are on don't change the calls. In YAML files, quote the time (`"22:00:00"`), since `22:00:00` is read as a number.
|
||||
Call it with a `response_variable`. For each light, the response lists the calls in order, each with its `service_data` and the `delay` in seconds before it (only nonzero between split calls, see `separate_turn_on_commands`). Only the lights and light groups in a switch's `lights` can be passed; without `entity_id`, each uses the switch it's in (of several, the one that's on). A light group is replaced by its members if the switch expands light groups. Options you leave out follow the switch, with its `initial_transition`, and in sleep mode the calls are for the sleep settings, whatever the time. With `skip_redundant_commands`, what a light already has is left out, as when Adaptive Lighting adapts it. Manual control doesn't change the calls. In YAML files, quote the time (`"22:00:00"`), since `22:00:00` is read as a number.
|
||||
|
||||
Making the calls is a manual change for Adaptive Lighting: with `take_over_control`, it stops adapting those lights until manual control is reset. A light that's off is adapted to the current settings as it turns on (unless `take_over_control` and `adapt_only_on_bare_turn_on` are enabled), so this example only changes lights that are on:
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from homeassistant.components.adaptive_lighting.const import (
|
|||
CONF_PREFER_RGB_COLOR,
|
||||
CONF_SEND_SPLIT_DELAY,
|
||||
CONF_SEPARATE_TURN_ON_COMMANDS,
|
||||
CONF_SKIP_REDUNDANT_COMMANDS,
|
||||
CONF_SLEEP_RGB_OR_COLOR_TEMP,
|
||||
CONF_TRANSITION,
|
||||
DEFAULT_SLEEP_BRIGHTNESS,
|
||||
|
|
@ -738,3 +739,101 @@ async def test_get_commands_response_is_json(hass):
|
|||
switch, _ = await setup_lights_and_switch(hass, {CONF_PREFER_RGB_COLOR: True})
|
||||
commands = await _get_commands(hass, switch)
|
||||
assert json.loads(json.dumps(commands)) == commands
|
||||
|
||||
|
||||
@pytest.mark.parametrize("skip_redundant", [True, False])
|
||||
async def test_get_commands_skip_redundant(hass, skip_redundant):
|
||||
"""With `skip_redundant_commands`, what a light already has is left out."""
|
||||
switch, _ = await setup_lights_and_switch(
|
||||
hass,
|
||||
{CONF_SKIP_REDUNDANT_COMMANDS: skip_redundant},
|
||||
)
|
||||
with patch(UTCNOW, return_value=_noon()):
|
||||
full = await _get_commands(hass, switch, lights=[ENTITY_LIGHT_1])
|
||||
(call,) = full[ENTITY_LIGHT_1]
|
||||
target = call["service_data"]
|
||||
# The light has the brightness already, but another color temperature
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_LIGHT_1,
|
||||
ATTR_BRIGHTNESS: target[ATTR_BRIGHTNESS],
|
||||
ATTR_COLOR_TEMP_KELVIN: target[ATTR_COLOR_TEMP_KELVIN] - 1000,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
partial = await _get_commands(hass, switch, lights=[ENTITY_LIGHT_1])
|
||||
# And then everything
|
||||
await hass.services.async_call("light", SERVICE_TURN_ON, target, blocking=True)
|
||||
nothing = await _get_commands(hass, switch, lights=[ENTITY_LIGHT_1])
|
||||
if not skip_redundant:
|
||||
assert partial == nothing == full
|
||||
return
|
||||
(call,) = partial[ENTITY_LIGHT_1]
|
||||
assert set(call["service_data"]) == {ATTR_ENTITY_ID, ATTR_COLOR_TEMP_KELVIN}
|
||||
assert nothing == {ENTITY_LIGHT_1: []}
|
||||
|
||||
|
||||
async def test_get_commands_skip_redundant_split(hass):
|
||||
"""A split call with nothing left is dropped, and the rest isn't delayed."""
|
||||
switch, _ = await setup_lights_and_switch(
|
||||
hass,
|
||||
{
|
||||
CONF_SKIP_REDUNDANT_COMMANDS: True,
|
||||
CONF_SEPARATE_TURN_ON_COMMANDS: True,
|
||||
CONF_SEND_SPLIT_DELAY: 100,
|
||||
},
|
||||
)
|
||||
with patch(UTCNOW, return_value=_noon()):
|
||||
full = await _get_commands(hass, switch, lights=[ENTITY_LIGHT_1])
|
||||
brightness_call, color_call = full[ENTITY_LIGHT_1]
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
**brightness_call["service_data"],
|
||||
ATTR_COLOR_TEMP_KELVIN: (
|
||||
color_call["service_data"][ATTR_COLOR_TEMP_KELVIN] - 1000
|
||||
),
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
commands = await _get_commands(hass, switch, lights=[ENTITY_LIGHT_1])
|
||||
assert commands == {
|
||||
ENTITY_LIGHT_1: [{"service_data": color_call["service_data"], "delay": 0}],
|
||||
}
|
||||
|
||||
|
||||
async def test_get_commands_skip_redundant_match_what_apply_sends(hass):
|
||||
"""With `skip_redundant_commands`, the calls are still what `apply` sends."""
|
||||
switch, _ = await setup_lights_and_switch(
|
||||
hass,
|
||||
{CONF_SKIP_REDUNDANT_COMMANDS: True},
|
||||
)
|
||||
with patch(UTCNOW, return_value=_noon()):
|
||||
(call,) = (await _get_commands(hass, switch, lights=[ENTITY_LIGHT_1]))[
|
||||
ENTITY_LIGHT_1
|
||||
]
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_LIGHT_1,
|
||||
ATTR_BRIGHTNESS: call["service_data"][ATTR_BRIGHTNESS],
|
||||
ATTR_COLOR_TEMP_KELVIN: (
|
||||
call["service_data"][ATTR_COLOR_TEMP_KELVIN] - 1000
|
||||
),
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
commands = await _get_commands(hass, switch, lights=[ENTITY_LIGHT_1])
|
||||
calls = _track_adaptive_light_calls(hass)
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_APPLY,
|
||||
{ATTR_ENTITY_ID: switch.entity_id, CONF_LIGHTS: [ENTITY_LIGHT_1]},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert [c["service_data"] for c in commands[ENTITY_LIGHT_1]] == calls
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue