From b6790b14eba0902fa170a94d9431dd111c9a2399 Mon Sep 17 00:00:00 2001 From: Benjamin Auquite Date: Tue, 11 Apr 2023 19:07:48 -0500 Subject: [PATCH] Squashed commit of the following: commit 8dd1b5660e4a296663a1762d3642f85229fc9912 Merge: 3b3f56f a888afd Author: Benjamin Auquite Date: Mon Apr 10 12:51:11 2023 -0500 merge origin commit 3b3f56f2d245ce815a3e62ccccff41b221168960 Author: Benjamin Auquite Date: Mon Apr 10 12:39:26 2023 -0500 Update test_switch.py commit 258964d97bace1cc306c2690b4b9cf5f945eae7b Author: Benjamin Auquite Date: Mon Apr 10 12:07:18 2023 -0500 pass flake8 move stuff out of `async_setup_entry` commit 5b6ad02e1d7862c09f2c4a08f28209d8e7799f6d Merge: 251babc fe7bdd3 Author: Benjamin Auquite Date: Sat Apr 8 06:01:44 2023 -0500 Merge branch 'main' into new-service-calls commit 251babce9d7d911cf2f31f5eb967c98ee85b126e Author: Benjamin Auquite Date: Sat Apr 8 05:35:16 2023 -0500 fixes and now passes tests commit d9cd74bef3023bfc6349320e0fd97689da708f7d Author: Benjamin Auquite Date: Sat Apr 8 05:15:17 2023 -0500 initial commit, passes most tests. 'function too complex' --- custom_components/adaptive_lighting/const.py | 28 ++- custom_components/adaptive_lighting/switch.py | 222 +++++++++++++----- tests/test_switch.py | 80 ++++++- 3 files changed, 267 insertions(+), 63 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 6d36ae2e..babea0a5 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -1,7 +1,12 @@ """Constants for the Adaptive Lighting integration.""" from homeassistant.components.light import VALID_TRANSITION -from homeassistant.const import CONF_ENTITY_ID +from homeassistant.const import ( + CONF_ENTITY_ID, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv import voluptuous as vol @@ -201,6 +206,19 @@ DOCS[CONF_USE_DEFAULTS] = ( 'documented defaults), or "configuration" (reverts to switch config defaults). ⚙️' ) +CONF_WHICH_SWITCH, DEFAULT_WHICH_SWITCH = "switch_type", "main" +DOCS[CONF_WHICH_SWITCH] = ( + "Which switch to target in this service call. Options: " + '"main" (default, targets the main switch), "sleep", "brightness", "color"' +) +DOCS[ + SERVICE_TURN_ON +] = "Turn on an Adaptive Lighting main/sleep/brightness/color switch" +DOCS[ + SERVICE_TURN_OFF +] = "Turn off an Adaptive Lighting main/sleep/brightness/color switch" +DOCS[SERVICE_TOGGLE] = "Toggle an Adaptive Lighting main/sleep/brightness/color switch" + TURNING_OFF_DELAY = 5 DOCS_MANUAL_CONTROL = { @@ -341,6 +359,14 @@ def apply_service_schema(initial_transition: int = 1): ) +SERVICE_TOGGLE_SCHEMA = vol.Schema( + { + vol.Optional(CONF_ENTITY_ID): cv.entity_ids, + vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, + vol.Optional(CONF_WHICH_SWITCH): cv.string, + } +) + SET_MANUAL_CONTROL_SCHEMA = vol.Schema( { vol.Optional(CONF_ENTITY_ID): cv.entity_ids, diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 3eabf7ff..276b6198 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -61,6 +61,7 @@ from homeassistant.const import ( EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_STARTED, EVENT_STATE_CHANGED, + SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, @@ -74,6 +75,7 @@ from homeassistant.core import ( HomeAssistant, ServiceCall, State, + async_get_hass, callback, ) from homeassistant.helpers import entity_platform, entity_registry @@ -134,6 +136,7 @@ from .const import ( CONF_TRANSITION, CONF_TURN_ON_LIGHTS, CONF_USE_DEFAULTS, + CONF_WHICH_SWITCH, CONST_COLOR, DOMAIN, EXTRA_VALIDATION, @@ -144,6 +147,7 @@ from .const import ( SERVICE_APPLY, SERVICE_CHANGE_SWITCH_SETTINGS, SERVICE_SET_MANUAL_CONTROL, + SERVICE_TOGGLE_SCHEMA, SET_MANUAL_CONTROL_SCHEMA, SLEEP_MODE_SWITCH, SUN_EVENT_MIDNIGHT, @@ -428,6 +432,139 @@ async def handle_change_switch_settings( ) +@callback +async def handle_turn_on(service_call: ServiceCall): + """Toggles the specified switch.""" + hass = async_get_hass() + data = service_call.data + _LOGGER.debug( + "Called 'adaptive_lighting.turn_on' service with '%s'", + data, + ) + switches = _get_switches_from_service_call(hass, service_call) + if data[CONF_WHICH_SWITCH] == "sleep": + switches = [s.sleep_mode_switch for s in switches] + elif data[CONF_WHICH_SWITCH] == "brightness": + switches = [s.adapt_brightness_switch for s in switches] + elif data[CONF_WHICH_SWITCH] == "color": + switches = [s.adapt_color_switch for s in switches] + + _LOGGER.debug("Turning on switches [%s]", switches) + for switch in switches: + await switch.async_turn_on() + + +@callback +async def handle_turn_off(service_call: ServiceCall): + """Toggles the specified switch.""" + hass = async_get_hass() + data = service_call.data + _LOGGER.debug( + "Called 'adaptive_lighting.turn_off' service with '%s'", + data, + ) + switches = _get_switches_from_service_call(hass, service_call) + if data[CONF_WHICH_SWITCH] == "sleep": + switches = [s.sleep_mode_switch for s in switches] + elif data[CONF_WHICH_SWITCH] == "brightness": + switches = [s.adapt_brightness_switch for s in switches] + elif data[CONF_WHICH_SWITCH] == "color": + switches = [s.adapt_color_switch for s in switches] + _LOGGER.debug("Turning off switches [%s]", switches) + for switch in switches: + await switch.async_turn_off() + + +@callback +async def handle_toggle(service_call: ServiceCall): + """Toggles the specified switch.""" + hass = async_get_hass() + data = service_call.data + _LOGGER.debug( + "Called 'adaptive_lighting.toggle' service with '%s'", + data, + ) + switches = _get_switches_from_service_call(hass, service_call) + if data[CONF_WHICH_SWITCH] == "sleep": + switches = [s.sleep_mode_switch for s in switches] + elif data[CONF_WHICH_SWITCH] == "brightness": + switches = [s.adapt_brightness_switch for s in switches] + elif data[CONF_WHICH_SWITCH] == "color": + switches = [s.adapt_color_switch for s in switches] + _LOGGER.debug("Toggling switches [%s]", switches) + for switch in switches: + if switch.is_on: + await switch.async_turn_off() + else: + await switch.async_turn_on() + + +@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: + 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'.""" + 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: + switch.turn_on_off_listener.mark_as_manual_control(light) + _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 def _fire_manual_control_event( switch: AdaptiveSwitch, light: str, context: Context, is_async=True @@ -489,67 +626,6 @@ 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, @@ -560,6 +636,30 @@ async def async_setup_entry( ), # pylint: disable=protected-access ) + # Register `turn_on` service + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_TURN_ON, + service_func=handle_turn_on, + schema=SERVICE_TOGGLE_SCHEMA, + ) + + # Register `turn_off` service + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_TURN_OFF, + service_func=handle_turn_off, + schema=SERVICE_TOGGLE_SCHEMA, + ) + + # Register `toggle` service + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_TOGGLE, + service_func=handle_toggle, + schema=SERVICE_TOGGLE_SCHEMA, + ) + # Register `set_manual_control` service hass.services.async_register( domain=DOMAIN, diff --git a/tests/test_switch.py b/tests/test_switch.py index 476e83cc..c569f16d 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -25,6 +25,7 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_TRANSITION, CONF_TURN_ON_LIGHTS, CONF_USE_DEFAULTS, + CONF_WHICH_SWITCH, DEFAULT_MAX_BRIGHTNESS, DEFAULT_NAME, DEFAULT_SLEEP_BRIGHTNESS, @@ -52,7 +53,6 @@ from homeassistant.components.light import ( ATTR_XY_COLOR, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.light import SERVICE_TURN_OFF from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN import homeassistant.config as config_util from homeassistant.config_entries import ConfigEntryState @@ -64,6 +64,8 @@ from homeassistant.const import ( CONF_NAME, CONF_PLATFORM, EVENT_STATE_CHANGED, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON, @@ -1252,6 +1254,82 @@ async def test_area(hass): assert light.entity_id not in switch.turn_on_off_listener.last_service_data +@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) +async def test_switch_turn_on_off_toggle(hass): + """Test adaptive_lighting.change_switch_settings service.""" + switch, (_, _, light) = await setup_lights_and_switch(hass) + entity_id = switch.entity_id + assert entity_id not in switch._lights + + async def turn_on(which: str, **kwargs): + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: entity_id, + CONF_WHICH_SWITCH: which, + **kwargs, + }, + blocking=True, + ) + await hass.async_block_till_done() + + async def turn_off(which: str, **kwargs): + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: entity_id, + CONF_WHICH_SWITCH: which, + **kwargs, + }, + blocking=True, + ) + await hass.async_block_till_done() + + async def toggle(which: str, **kwargs): + await hass.services.async_call( + DOMAIN, + SERVICE_TOGGLE, + { + ATTR_ENTITY_ID: entity_id, + CONF_WHICH_SWITCH: which, + **kwargs, + }, + blocking=True, + ) + await hass.async_block_till_done() + + # Test sleep + await turn_on("sleep") + assert switch.sleep_mode_switch.is_on + await turn_off("sleep") + assert not switch.sleep_mode_switch.is_on + await toggle("sleep") + assert switch.sleep_mode_switch.is_on + # Test brightness + await turn_on("brightness") + assert switch.adapt_brightness_switch.is_on + await turn_off("brightness") + assert not switch.adapt_brightness_switch.is_on + await toggle("brightness") + assert switch.adapt_brightness_switch.is_on + # Test color + await turn_on("color") + assert switch.adapt_color_switch.is_on + await turn_off("color") + assert not switch.adapt_color_switch.is_on + await toggle("color") + assert switch.adapt_color_switch.is_on + # Test main + await turn_on("main") + assert switch.is_on + await turn_off("main") + assert not switch.is_on + await toggle("main") + assert switch.is_on + + @pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) async def test_change_switch_settings_service(hass): """Test adaptive_lighting.change_switch_settings service."""