diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 6d36ae2e..300bae35 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -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 @@ -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] + }, + } +) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index f933f068..d423700c 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -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, @@ -94,7 +95,6 @@ ) import homeassistant.util.dt as dt_util import ulid_transform -import voluptuous as vol from .const import ( ADAPT_BRIGHTNESS_SWITCH, @@ -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, ) @@ -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 = [] @@ -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 @@ -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: @@ -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), ) @@ -490,75 +566,12 @@ 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 @@ -566,20 +579,14 @@ async def handle_set_manual_control(service_call: ServiceCall): 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, ) diff --git a/tests/test_switch.py b/tests/test_switch.py index 884b796b..fa2fac59 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1338,6 +1338,9 @@ async def test_area(hass): async def test_change_switch_settings_service(hass): """Test adaptive_lighting.change_switch_settings service.""" switch, (_, _, light) = await setup_lights_and_switch(hass) + switch2, (_, light2, _) = await setup_lights_and_switch( + hass, {CONF_NAME: "second_switch"} + ) entity_id = light.entity_id assert entity_id not in switch._lights @@ -1346,7 +1349,6 @@ async def change_switch_settings(**kwargs): DOMAIN, SERVICE_CHANGE_SWITCH_SETTINGS, { - ATTR_ENTITY_ID: ENTITY_SWITCH, **kwargs, }, blocking=True, @@ -1355,12 +1357,16 @@ async def change_switch_settings(**kwargs): # Test changing sunrise offset assert switch._sun_light_settings.sunrise_offset.total_seconds() == 0 - await change_switch_settings(**{CONF_SUNRISE_OFFSET: 10}) + await change_switch_settings( + **{ATTR_ENTITY_ID: ENTITY_SWITCH, CONF_SUNRISE_OFFSET: 10} + ) assert switch._sun_light_settings.sunrise_offset.total_seconds() == 10 # Test changing max brightness assert switch._sun_light_settings.max_brightness == 100 - await change_switch_settings(**{CONF_MAX_BRIGHTNESS: 50}) + await change_switch_settings( + **{ATTR_ENTITY_ID: ENTITY_SWITCH, CONF_MAX_BRIGHTNESS: 50} + ) assert switch._sun_light_settings.max_brightness == 50 # Test changing to illegal max brightness @@ -1368,20 +1374,33 @@ async def change_switch_settings(**kwargs): voluptuous.error.MultipleInvalid, match="value must be at most 100 for dictionary", ): - await change_switch_settings(**{CONF_MAX_BRIGHTNESS: 5000}) + await change_switch_settings( + **{ATTR_ENTITY_ID: ENTITY_SWITCH, CONF_MAX_BRIGHTNESS: 5000} + ) # Change CONF_MIN_COLOR_TEMP, the factory default is 2000, but setup_lights_and_switch # sets it to 2500 assert switch._sun_light_settings.min_color_temp == 2500 # testing with "factory" should change it to 2000 - await change_switch_settings(**{CONF_USE_DEFAULTS: "factory"}) + await change_switch_settings( + **{ATTR_ENTITY_ID: ENTITY_SWITCH, CONF_USE_DEFAULTS: "factory"} + ) assert switch._sun_light_settings.min_color_temp == 2000 # testing with "current" should not change things - await change_switch_settings(**{CONF_USE_DEFAULTS: "current"}) + await change_switch_settings( + **{ATTR_ENTITY_ID: ENTITY_SWITCH, CONF_USE_DEFAULTS: "current"} + ) assert switch._sun_light_settings.min_color_temp == 2000 # testing with "configuration" should revert back to 2500 - await change_switch_settings(**{CONF_USE_DEFAULTS: "configuration"}) + await change_switch_settings( + **{ATTR_ENTITY_ID: ENTITY_SWITCH, CONF_USE_DEFAULTS: "configuration"} + ) assert switch._sun_light_settings.min_color_temp == 2500 + + # testing with no switches or lights defined. + assert switch2._sun_light_settings.max_brightness == 100 + await change_switch_settings(**{CONF_MAX_BRIGHTNESS: 50}) + assert switch2._sun_light_settings.max_brightness == 50