From 7d77fa92c2f49c81c130fe318eff475a98787513 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 14 Jul 2020 19:47:59 +0200 Subject: [PATCH] Add mode info attributes to script and automation (#37815) Co-authored-by: J. Nick Koston --- homeassistant/components/alexa/entities.py | 3 +-- .../components/automation/__init__.py | 13 ++++++++- .../components/homekit/type_switches.py | 4 --- homeassistant/components/script/__init__.py | 16 ++++++++--- homeassistant/helpers/script.py | 27 ++++++++++++++----- tests/components/alexa/test_smart_home.py | 21 +-------------- .../components/homekit/test_type_switches.py | 26 +++++------------- tests/helpers/test_script.py | 23 ---------------- 8 files changed, 53 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 972df08bd1e..9b89f4f15d7 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -590,9 +590,8 @@ class ScriptCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" - can_cancel = bool(self.entity.attributes.get("can_cancel")) return [ - AlexaSceneController(self.entity, supports_deactivation=can_cancel), + AlexaSceneController(self.entity, supports_deactivation=True), Alexa(self.hass), ] diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 7a6955869d7..4d2cbcc94d2 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -31,6 +31,9 @@ from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.script import ( + ATTR_CUR, + ATTR_MAX, + ATTR_MODE, CONF_MAX, SCRIPT_MODE_PARALLEL, Script, @@ -276,7 +279,15 @@ class AutomationEntity(ToggleEntity, RestoreEntity): @property def state_attributes(self): """Return the entity state attributes.""" - return {ATTR_LAST_TRIGGERED: self._last_triggered} + attrs = { + ATTR_LAST_TRIGGERED: self._last_triggered, + ATTR_MODE: self.action_script.script_mode, + } + if self.action_script.supports_max: + attrs[ATTR_MAX] = self.action_script.max_runs + if self.is_on: + attrs[ATTR_CUR] = self.action_script.runs + return attrs @property def is_on(self) -> bool: diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 635b0e1d036..beaccd1f3dc 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -9,7 +9,6 @@ from pyhap.const import ( CATEGORY_SWITCH, ) -from homeassistant.components.script import ATTR_CAN_CANCEL from homeassistant.components.switch import DOMAIN from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, @@ -111,11 +110,8 @@ class Switch(HomeAccessory): def is_activate(self, state): """Check if entity is activate only.""" - can_cancel = state.attributes.get(ATTR_CAN_CANCEL) if self._domain == "scene": return True - if self._domain == "script" and not can_cancel: - return True return False def reset_switch(self, *args): diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 50c5b1740cf..95696981cca 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -24,6 +24,9 @@ from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.script import ( + ATTR_CUR, + ATTR_MAX, + ATTR_MODE, CONF_MAX, SCRIPT_MODE_SINGLE, Script, @@ -35,7 +38,7 @@ from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) DOMAIN = "script" -ATTR_CAN_CANCEL = "can_cancel" + ATTR_LAST_ACTION = "last_action" ATTR_LAST_TRIGGERED = "last_triggered" ATTR_VARIABLES = "variables" @@ -272,9 +275,14 @@ class ScriptEntity(ToggleEntity): @property def state_attributes(self): """Return the state attributes.""" - attrs = {ATTR_LAST_TRIGGERED: self.script.last_triggered} - if self.script.can_cancel: - attrs[ATTR_CAN_CANCEL] = self.script.can_cancel + attrs = { + ATTR_LAST_TRIGGERED: self.script.last_triggered, + ATTR_MODE: self.script.script_mode, + } + if self.script.supports_max: + attrs[ATTR_MAX] = self.script.max_runs + if self.is_on: + attrs[ATTR_CUR] = self.script.runs if self.script.last_action: attrs[ATTR_LAST_ACTION] = self.script.last_action return attrs diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index a14106053fe..e890766cf2c 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -69,6 +69,10 @@ DEFAULT_SCRIPT_MODE = SCRIPT_MODE_SINGLE CONF_MAX = "max" DEFAULT_MAX = 10 +ATTR_CUR = "current" +ATTR_MAX = "max" +ATTR_MODE = "mode" + _LOG_EXCEPTION = logging.ERROR + 1 _TIMEOUT_MSG = "Timeout reached, abort script." @@ -561,7 +565,7 @@ class Script: template.attach(hass, self.sequence) self.name = name self.change_listener = change_listener - self._script_mode = script_mode + self.script_mode = script_mode if logger: self._logger = logger else: @@ -573,10 +577,9 @@ class Script: self.last_action = None self.last_triggered: Optional[datetime] = None - self.can_cancel = True self._runs: List[_ScriptRun] = [] - self._max_runs = max_runs + self.max_runs = max_runs if script_mode == SCRIPT_MODE_QUEUED: self._queue_lck = asyncio.Lock() self._config_cache: Dict[Set[Tuple], Callable[..., bool]] = {} @@ -601,6 +604,16 @@ class Script: """Return true if script is on.""" return len(self._runs) > 0 + @property + def runs(self) -> int: + """Return the number of current runs.""" + return len(self._runs) + + @property + def supports_max(self) -> bool: + """Return true if the current mode support max.""" + return self.script_mode in (SCRIPT_MODE_PARALLEL, SCRIPT_MODE_QUEUED) + @property def referenced_devices(self): """Return a set of referenced devices.""" @@ -668,17 +681,17 @@ class Script: ) -> None: """Run script.""" if self.is_running: - if self._script_mode == SCRIPT_MODE_SINGLE: + if self.script_mode == SCRIPT_MODE_SINGLE: self._log("Already running", level=logging.WARNING) return - if self._script_mode == SCRIPT_MODE_RESTART: + if self.script_mode == SCRIPT_MODE_RESTART: self._log("Restarting") await self.async_stop(update_state=False) - elif len(self._runs) == self._max_runs: + elif len(self._runs) == self.max_runs: self._log("Maximum number of runs exceeded", level=logging.WARNING) return - if self._script_mode != SCRIPT_MODE_QUEUED: + if self.script_mode != SCRIPT_MODE_QUEUED: cls = _ScriptRun else: cls = _QueuedScriptRun diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index f2777ab00f8..1ac01c3d3ff 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -306,25 +306,6 @@ async def test_script(hass): assert appliance["displayCategories"][0] == "ACTIVITY_TRIGGER" assert appliance["friendlyName"] == "Test script" - capabilities = assert_endpoint_capabilities( - appliance, "Alexa.SceneController", "Alexa" - ) - scene_capability = get_capability(capabilities, "Alexa.SceneController") - assert not scene_capability["supportsDeactivation"] - - await assert_scene_controller_works("script#test", "script.turn_on", None, hass) - - -async def test_cancelable_script(hass): - """Test cancalable script discovery.""" - device = ( - "script.test_2", - "off", - {"friendly_name": "Test script 2", "can_cancel": True}, - ) - appliance = await discovery_test(device, hass) - - assert appliance["endpointId"] == "script#test_2" capabilities = assert_endpoint_capabilities( appliance, "Alexa.SceneController", "Alexa" ) @@ -332,7 +313,7 @@ async def test_cancelable_script(hass): assert scene_capability["supportsDeactivation"] await assert_scene_controller_works( - "script#test_2", "script.turn_on", "script.turn_off", hass + "script#test", "script.turn_on", "script.turn_off", hass ) diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 483f08e4db5..4f36adc99e1 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -16,7 +16,6 @@ from homeassistant.components.homekit.type_switches import ( Switch, Valve, ) -from homeassistant.components.script import ATTR_CAN_CANCEL from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, SERVICE_RETURN_TO_BASE, @@ -80,7 +79,7 @@ async def test_outlet_set_state(hass, hk_driver, events): ("automation.test", {}), ("input_boolean.test", {}), ("remote.test", {}), - ("script.test", {ATTR_CAN_CANCEL: True}), + ("script.test", {}), ("switch.test", {}), ], ) @@ -240,19 +239,12 @@ async def test_vacuum_set_state(hass, hk_driver, events): assert events[-1].data[ATTR_VALUE] is None -@pytest.mark.parametrize( - "entity_id, attrs", - [ - ("scene.test", {}), - ("script.test", {}), - ("script.test", {ATTR_CAN_CANCEL: False}), - ], -) -async def test_reset_switch(hass, hk_driver, entity_id, attrs, events): +async def test_reset_switch(hass, hk_driver, events): """Test if switch accessory is reset correctly.""" - domain = split_entity_id(entity_id)[0] + domain = "scene" + entity_id = "scene.test" - hass.states.async_set(entity_id, None, attrs) + hass.states.async_set(entity_id, None) await hass.async_block_till_done() acc = Switch(hass, hk_driver, "Switch", entity_id, 2, None) await acc.run_handler() @@ -295,12 +287,8 @@ async def test_reset_switch_reload(hass, hk_driver, events): await acc.run_handler() await hass.async_block_till_done() - assert acc.activate_only is True - - hass.states.async_set(entity_id, None, {ATTR_CAN_CANCEL: True}) - await hass.async_block_till_done() assert acc.activate_only is False - hass.states.async_set(entity_id, None, {ATTR_CAN_CANCEL: False}) + hass.states.async_set(entity_id, None) await hass.async_block_till_done() - assert acc.activate_only is True + assert acc.char_on.value is False diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 2e196b1f4ed..875ad17a2ee 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -93,15 +93,12 @@ async def test_firing_event_basic(hass): sequence = cv.SCRIPT_SCHEMA({"event": event, "event_data": {"hello": "world"}}) script_obj = script.Script(hass, sequence) - assert script_obj.can_cancel - await script_obj.async_run(context=context) await hass.async_block_till_done() assert len(events) == 1 assert events[0].context is context assert events[0].data.get("hello") == "world" - assert script_obj.can_cancel async def test_firing_event_template(hass): @@ -125,8 +122,6 @@ async def test_firing_event_template(hass): ) script_obj = script.Script(hass, sequence) - assert script_obj.can_cancel - await script_obj.async_run({"is_world": "yes"}, context=context) await hass.async_block_till_done() @@ -146,8 +141,6 @@ async def test_calling_service_basic(hass): sequence = cv.SCRIPT_SCHEMA({"service": "test.script", "data": {"hello": "world"}}) script_obj = script.Script(hass, sequence) - assert script_obj.can_cancel - await script_obj.async_run(context=context) await hass.async_block_till_done() @@ -182,8 +175,6 @@ async def test_calling_service_template(hass): ) script_obj = script.Script(hass, sequence) - assert script_obj.can_cancel - await script_obj.async_run({"is_world": "yes"}, context=context) await hass.async_block_till_done() @@ -267,8 +258,6 @@ async def test_activating_scene(hass): sequence = cv.SCRIPT_SCHEMA({"scene": "scene.hello"}) script_obj = script.Script(hass, sequence) - assert script_obj.can_cancel - await script_obj.async_run(context=context) await hass.async_block_till_done() @@ -328,8 +317,6 @@ async def test_delay_basic(hass, mock_timeout): script_obj = script.Script(hass, sequence) delay_started_flag = async_watch_for_action(script_obj, delay_alias) - assert script_obj.can_cancel - try: hass.async_create_task(script_obj.async_run()) await asyncio.wait_for(delay_started_flag.wait(), 1) @@ -394,8 +381,6 @@ async def test_delay_template_ok(hass, mock_timeout): script_obj = script.Script(hass, sequence) delay_started_flag = async_watch_for_action(script_obj, "delay") - assert script_obj.can_cancel - try: hass.async_create_task(script_obj.async_run()) await asyncio.wait_for(delay_started_flag.wait(), 1) @@ -444,8 +429,6 @@ async def test_delay_template_complex_ok(hass, mock_timeout): script_obj = script.Script(hass, sequence) delay_started_flag = async_watch_for_action(script_obj, "delay") - assert script_obj.can_cancel - try: hass.async_create_task(script_obj.async_run()) await asyncio.wait_for(delay_started_flag.wait(), 1) @@ -530,8 +513,6 @@ async def test_wait_template_basic(hass): script_obj = script.Script(hass, sequence) wait_started_flag = async_watch_for_action(script_obj, wait_alias) - assert script_obj.can_cancel - try: hass.states.async_set("switch.test", "on") hass.async_create_task(script_obj.async_run()) @@ -687,8 +668,6 @@ async def test_wait_template_variables(hass): script_obj = script.Script(hass, sequence) wait_started_flag = async_watch_for_action(script_obj, "wait") - assert script_obj.can_cancel - try: hass.states.async_set("switch.test", "on") hass.async_create_task(script_obj.async_run({"data": "switch.test"})) @@ -721,8 +700,6 @@ async def test_condition_basic(hass): ) script_obj = script.Script(hass, sequence) - assert script_obj.can_cancel - hass.states.async_set("test.entity", "hello") await script_obj.async_run() await hass.async_block_till_done()