Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow more flexibility in service calls #561

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 29 additions & 21 deletions custom_components/adaptive_lighting/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,9 @@
SERVICE_CHANGE_SWITCH_SETTINGS = "change_switch_settings"
CONF_USE_DEFAULTS = "use_defaults"
DOCS[CONF_USE_DEFAULTS] = (
"Sets the default values not specified in this service call. Options: "
"Where to autofill config options that are not passed to this service. Options: "
'"current" (default, retains current values), "factory" (resets to '
'documented defaults), or "configuration" (reverts to switch config defaults). ⚙️'
'documented defaults), or "configuration" (reverts to original user config). ⚙️'
)

TURNING_OFF_DELAY = 5
Expand Down Expand Up @@ -323,28 +323,36 @@ def replace_none_str(value, replace_with=None):
)


def apply_service_schema(initial_transition: int = 1):
"""Return the schema for the apply service."""
return vol.Schema(
{
vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids,
vol.Optional(
CONF_TRANSITION,
default=initial_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,
vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean,
}
)


SET_MANUAL_CONTROL_SCHEMA = vol.Schema(
SCHEMA_APPLY = vol.Schema(
{
vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids,
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,
vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean,
}
)


SCHEMA_SET_MANUAL_CONTROL = vol.Schema(
{
vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids,
vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean,
}
)

SCHEMA_CHANGE_SWITCH_SETTINGS = vol.Schema(
{
vol.Optional(CONF_USE_DEFAULTS): cv.string,
vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
vol.Required(CONF_LIGHTS, default=[]): [],
**{
vol.Optional(k): valid
for k, _, valid in VALIDATION_TUPLES
if k not in [CONF_INTERVAL, CONF_NAME, CONF_LIGHTS]
},
}
)
235 changes: 121 additions & 114 deletions custom_components/adaptive_lighting/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@
HomeAssistant,
ServiceCall,
State,
async_get_hass,
callback,
)
from homeassistant.helpers import entity_platform, entity_registry
from homeassistant.helpers import entity_registry
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import (
async_track_state_change_event,
Expand All @@ -94,7 +95,6 @@
)
import homeassistant.util.dt as dt_util
import ulid_transform
import voluptuous as vol

from .const import (
ADAPT_BRIGHTNESS_SWITCH,
Expand Down Expand Up @@ -141,16 +141,17 @@
ICON_COLOR_TEMP,
ICON_MAIN,
ICON_SLEEP,
SCHEMA_APPLY,
SCHEMA_CHANGE_SWITCH_SETTINGS,
SCHEMA_SET_MANUAL_CONTROL,
SERVICE_APPLY,
SERVICE_CHANGE_SWITCH_SETTINGS,
SERVICE_SET_MANUAL_CONTROL,
SET_MANUAL_CONTROL_SCHEMA,
SLEEP_MODE_SWITCH,
SUN_EVENT_MIDNIGHT,
SUN_EVENT_NOON,
TURNING_OFF_DELAY,
VALIDATION_TUPLES,
apply_service_schema,
replace_none_str,
)

Expand Down Expand Up @@ -303,9 +304,9 @@ def _split_service_data(service_data, adapt_brightness, adapt_color):


def _get_switches_with_lights(
hass: HomeAssistant, lights: list[str]
hass: HomeAssistant, lights: list[str] | None = None
) -> list[AdaptiveSwitch]:
"""Get all switches that control at least one of the lights passed."""
"""Get all switches. If lights is defined, return only switches found with these lights."""
config_entries = hass.config_entries.async_entries(DOMAIN)
data = hass.data[DOMAIN]
switches = []
Expand All @@ -314,10 +315,13 @@ def _get_switches_with_lights(
if entry is None: # entry might be disabled and therefore missing
continue
switch = data[config.entry_id]["instance"]
all_check_lights = _expand_light_groups(hass, lights)
switch._expand_light_groups()
# Check if any of the lights are in the switch's lights
if set(switch._lights) & set(all_check_lights):
if lights:
all_check_lights = _expand_light_groups(hass, lights)
switch._expand_light_groups()
# Check if any of the lights are in the switch's lights
if set(switch._lights) & set(all_check_lights):
switches.append(switch)
else:
switches.append(switch)
return switches

Expand Down Expand Up @@ -359,13 +363,7 @@ def _get_switches_from_service_call(
switch_entity_ids: list[str] | None = data.get("entity_id")

if not lights and not switch_entity_ids:
raise ValueError(
"adaptive-lighting: Neither a switch nor a light was provided in the service call."
" If you intend to adapt all lights on all switches, please inform the developers at"
" https://github.com/basnijholt/adaptive-lighting about your use case."
" Currently, you must pass either an adaptive-lighting switch or the lights to an"
" `adaptive_lighting` service call."
)
return _get_switches_with_lights(hass)

if switch_entity_ids is not None:
if len(switch_entity_ids) > 1 and lights:
Expand All @@ -391,39 +389,117 @@ def _get_switches_from_service_call(
)


async def handle_change_switch_settings(
switch: AdaptiveSwitch, service_call: ServiceCall
) -> None:
"""Allows HASS to change config values via a service call."""
@callback
async def handle_apply(service_call: ServiceCall):
"""Handle the entity service apply."""
hass = async_get_hass()
data = service_call.data
_LOGGER.debug(
"Called 'adaptive_lighting.apply' service with '%s'",
data,
)
switches = _get_switches_from_service_call(hass, service_call)
lights = data[CONF_LIGHTS]
for switch in switches:
if not lights:
all_lights = switch._lights # pylint: disable=protected-access
else:
all_lights = _expand_light_groups(switch.hass, lights)
switch.turn_on_off_listener.lights.update(all_lights)
for light in all_lights:
transition = data.get(CONF_TRANSITION)
if not data[CONF_TURN_ON_LIGHTS]:
if not is_on(hass, light):
continue
if not transition:
transition = switch._transition # pylint: disable=protected-access
elif not transition:
transition = (
switch._initial_transition
) # pylint: disable=protected-access
await switch._adapt_light( # pylint: disable=protected-access
light,
transition,
data[ATTR_ADAPT_BRIGHTNESS],
data[ATTR_ADAPT_COLOR],
data[CONF_PREFER_RGB_COLOR],
force=True,
context=switch.create_context("service", parent=service_call.context),
)

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 = {key: default for key, default, _ in VALIDATION_TUPLES}
elif which == "configuration":
# use whatever's in the config flow or configuration.yaml
defaults = switch._config_backup # pylint: disable=protected-access
else:
defaults = None

switch._set_changeable_settings(
data=data,
defaults=defaults,
@callback
async def handle_set_manual_control(service_call: ServiceCall):
"""Set or unset lights as 'manually controlled'."""
hass = async_get_hass()
data = service_call.data
_LOGGER.debug(
"Called 'adaptive_lighting.set_manual_control' service with '%s'",
data,
)
switches = _get_switches_from_service_call(hass, service_call)
lights = data[CONF_LIGHTS]
for switch in switches:
if not lights:
all_lights = switch._lights # pylint: disable=protected-access
else:
all_lights = _expand_light_groups(switch.hass, lights)
if service_call.data[CONF_MANUAL_CONTROL]:
for light in all_lights:
_fire_manual_control_event(switch, light, service_call.context)
else:
switch.turn_on_off_listener.reset(*all_lights)
if switch.is_on:
# pylint: disable=protected-access
await switch._update_attrs_and_maybe_adapt_lights(
all_lights,
transition=switch._initial_transition,
force=True,
context=switch.create_context(
"service", parent=service_call.context
),
)


@callback
async def handle_change_switch_settings(service_call: ServiceCall) -> None:
"""Allows HASS to change config values via a service call."""
hass = async_get_hass()
data = service_call.data
_LOGGER.debug(
"Called 'adaptive_lighting.change_switch_settings' service with '%s'",
data,
)

all_lights = switch._lights # pylint: disable=protected-access
switch.turn_on_off_listener.reset(*all_lights, reset_manual_control=False)
if switch.is_on:
switches = _get_switches_from_service_call(hass, service_call)
for switch in switches:
# which denotes where to autofill blank config options.
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 = {key: default for key, default, _ in VALIDATION_TUPLES}
elif which == "configuration":
# use whatever's in the config flow or configuration.yaml
defaults = switch._config_backup # pylint: disable=protected-access
else:
defaults = None

switch._set_changeable_settings(
data=data,
defaults=defaults,
)

all_lights = switch._lights # pylint: disable=protected-access
switch.turn_on_off_listener.reset(*all_lights, reset_manual_control=False)

if not switch.is_on:
continue
await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access
all_lights,
transition=switch._initial_transition,
transition=switch._transition,
force=True,
context=switch.create_context("service", parent=service_call.context),
)
Expand Down Expand Up @@ -490,96 +566,27 @@ async def async_setup_entry(
update_before_add=True,
)

@callback
async def handle_apply(service_call: ServiceCall):
"""Handle the entity service apply."""
data = service_call.data
_LOGGER.debug(
"Called 'adaptive_lighting.apply' service with '%s'",
data,
)
switches = _get_switches_from_service_call(hass, service_call)
lights = data[CONF_LIGHTS]
for switch in switches:
if not lights:
all_lights = switch._lights # pylint: disable=protected-access
else:
all_lights = _expand_light_groups(switch.hass, lights)
switch.turn_on_off_listener.lights.update(all_lights)
for light in all_lights:
if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light):
await switch._adapt_light( # pylint: disable=protected-access
light,
data[CONF_TRANSITION],
data[ATTR_ADAPT_BRIGHTNESS],
data[ATTR_ADAPT_COLOR],
data[CONF_PREFER_RGB_COLOR],
force=True,
context=switch.create_context(
"service", parent=service_call.context
),
)

@callback
async def handle_set_manual_control(service_call: ServiceCall):
"""Set or unset lights as 'manually controlled'."""
data = service_call.data
_LOGGER.debug(
"Called 'adaptive_lighting.set_manual_control' service with '%s'",
data,
)
switches = _get_switches_from_service_call(hass, service_call)
lights = data[CONF_LIGHTS]
for switch in switches:
if not lights:
all_lights = switch._lights # pylint: disable=protected-access
else:
all_lights = _expand_light_groups(switch.hass, lights)
if service_call.data[CONF_MANUAL_CONTROL]:
for light in all_lights:
_fire_manual_control_event(switch, light, service_call.context)
else:
switch.turn_on_off_listener.reset(*all_lights)
if switch.is_on:
# pylint: disable=protected-access
await switch._update_attrs_and_maybe_adapt_lights(
all_lights,
transition=switch._initial_transition,
force=True,
context=switch.create_context(
"service", parent=service_call.context
),
)

# Register `apply` service
hass.services.async_register(
domain=DOMAIN,
service=SERVICE_APPLY,
service_func=handle_apply,
schema=apply_service_schema(
switch._initial_transition
), # pylint: disable=protected-access
schema=SCHEMA_APPLY,
)

# 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,
schema=SCHEMA_SET_MANUAL_CONTROL,
)

args = {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()
platform.async_register_entity_service(
SERVICE_CHANGE_SWITCH_SETTINGS,
args,
handle_change_switch_settings,
hass.services.async_register(
domain=DOMAIN,
service=SERVICE_CHANGE_SWITCH_SETTINGS,
service_func=handle_change_switch_settings,
schema=SCHEMA_CHANGE_SWITCH_SETTINGS,
)


Expand Down
Loading