From e9b746e8744eb564650a4a6c98c7eb875110ce3a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 3 Jan 2022 10:41:30 +0100 Subject: [PATCH] Add turned on or off device trigger to toggle entity (#61089) * Add turned on or off device trigger to toggle entity * Renamed changed_states trigger to toggled * Adjust tests * Fix homekit triggers test * Add tests * Adjust tests after rebase Co-authored-by: J. Nick Koston --- .../components/device_automation/const.py | 1 + .../components/device_automation/entity.py | 108 ++++++++++ .../device_automation/toggle_entity.py | 18 +- .../components/fan/device_trigger.py | 5 +- homeassistant/components/fan/strings.json | 1 + .../components/humidifier/device_trigger.py | 12 +- .../components/humidifier/strings.json | 1 + .../components/light/device_trigger.py | 5 +- homeassistant/components/light/strings.json | 1 + .../components/remote/device_trigger.py | 5 +- homeassistant/components/remote/strings.json | 1 + .../components/switch/device_trigger.py | 5 +- homeassistant/components/switch/strings.json | 1 + .../components/device_automation/test_init.py | 15 +- .../device_automation/test_toggle_entity.py | 199 ++++++++++++++++++ tests/components/fan/test_device_trigger.py | 40 +++- .../components/homekit/test_type_triggers.py | 12 +- .../humidifier/test_device_trigger.py | 49 ++++- tests/components/light/test_device_trigger.py | 49 ++++- .../components/remote/test_device_trigger.py | 49 ++++- .../components/switch/test_device_trigger.py | 49 ++++- tests/components/wemo/test_device_trigger.py | 7 + 22 files changed, 574 insertions(+), 59 deletions(-) create mode 100644 homeassistant/components/device_automation/entity.py create mode 100644 tests/components/device_automation/test_toggle_entity.py diff --git a/homeassistant/components/device_automation/const.py b/homeassistant/components/device_automation/const.py index 40bfc4ca0a1..bede5635f4c 100644 --- a/homeassistant/components/device_automation/const.py +++ b/homeassistant/components/device_automation/const.py @@ -1,4 +1,5 @@ """Constants for device automations.""" +CONF_CHANGED_STATES = "toggled" CONF_IS_OFF = "is_off" CONF_IS_ON = "is_on" CONF_TOGGLE = "toggle" diff --git a/homeassistant/components/device_automation/entity.py b/homeassistant/components/device_automation/entity.py new file mode 100644 index 00000000000..5a9d5a0a324 --- /dev/null +++ b/homeassistant/components/device_automation/entity.py @@ -0,0 +1,108 @@ +"""Device automation helpers for entity.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) +from homeassistant.components.device_automation.const import CONF_CHANGED_STATES +from homeassistant.components.homeassistant.triggers import state as state_trigger +from homeassistant.const import CONF_ENTITY_ID, CONF_FOR, CONF_PLATFORM, CONF_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers.typing import ConfigType + +from . import DEVICE_TRIGGER_BASE_SCHEMA + +# mypy: allow-untyped-calls, allow-untyped-defs + +ENTITY_TRIGGERS = [ + { + # Trigger when entity is turned on or off + CONF_PLATFORM: "device", + CONF_TYPE: CONF_CHANGED_STATES, + }, +] + +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In([CONF_CHANGED_STATES]), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } +) + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: + """Listen for state changes based on configuration.""" + to_state = None + state_config = { + CONF_PLATFORM: "state", + state_trigger.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state_trigger.CONF_TO: to_state, + } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] + + state_config = await state_trigger.async_validate_trigger_config(hass, state_config) + return await state_trigger.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) + + +async def _async_get_automations( + hass: HomeAssistant, + device_id: str, + automation_templates: list[dict[str, str]], + domain: str, +) -> list[dict[str, str]]: + """List device automations.""" + automations: list[dict[str, str]] = [] + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == domain + ] + + for entry in entries: + automations.extend( + { + **template, + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": domain, + } + for template in automation_templates + ) + + return automations + + +async def async_get_triggers( + hass: HomeAssistant, device_id: str, domain: str +) -> list[dict[str, Any]]: + """List device triggers.""" + return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain) + + +async def async_get_trigger_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: + """List trigger capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 8128eca9dbc..c09d297a1cf 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -23,7 +23,7 @@ from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from . import DEVICE_TRIGGER_BASE_SCHEMA +from . import DEVICE_TRIGGER_BASE_SCHEMA, entity from .const import ( CONF_IS_OFF, CONF_IS_ON, @@ -94,7 +94,7 @@ CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( } ) -TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( +_TOGGLE_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_TURNED_OFF, CONF_TURNED_ON]), @@ -102,6 +102,8 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( } ) +TRIGGER_SCHEMA = vol.Any(entity.TRIGGER_SCHEMA, _TOGGLE_TRIGGER_SCHEMA) + async def async_call_action_from_config( hass: HomeAssistant, @@ -155,6 +157,9 @@ async def async_attach_trigger( automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" + if config[CONF_TYPE] not in [CONF_TURNED_ON, CONF_TURNED_OFF]: + return await entity.async_attach_trigger(hass, config, action, automation_info) + if config[CONF_TYPE] == CONF_TURNED_ON: to_state = "on" else: @@ -221,7 +226,11 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str, domain: str ) -> list[dict[str, Any]]: """List device triggers.""" - return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain) + triggers = await entity.async_get_triggers(hass, device_id, domain) + triggers.extend( + await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain) + ) + return triggers async def async_get_condition_capabilities( @@ -239,6 +248,9 @@ async def async_get_trigger_capabilities( hass: HomeAssistant, config: ConfigType ) -> dict[str, vol.Schema]: """List trigger capabilities.""" + if config[CONF_TYPE] not in [CONF_TURNED_ON, CONF_TURNED_OFF]: + return await entity.async_get_trigger_capabilities(hass, config) + return { "extra_fields": vol.Schema( {vol.Optional(CONF_FOR): cv.positive_time_period_dict} diff --git a/homeassistant/components/fan/device_trigger.py b/homeassistant/components/fan/device_trigger.py index 503aaaac52a..1d81ef18f4d 100644 --- a/homeassistant/components/fan/device_trigger.py +++ b/homeassistant/components/fan/device_trigger.py @@ -16,8 +16,9 @@ from homeassistant.helpers.typing import ConfigType from . import DOMAIN -TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} +TRIGGER_SCHEMA = vol.All( + toggle_entity.TRIGGER_SCHEMA, + vol.Schema({vol.Required(CONF_DOMAIN): DOMAIN}, extra=vol.ALLOW_EXTRA), ) diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index 7ec4eebea7e..fb15354a8ef 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -6,6 +6,7 @@ "is_off": "{entity_name} is off" }, "trigger_type": { + "toggled": "{entity_name} turned on or off", "turned_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off" }, diff --git a/homeassistant/components/humidifier/device_trigger.py b/homeassistant/components/humidifier/device_trigger.py index 1c7a09305e1..e4b0440e869 100644 --- a/homeassistant/components/humidifier/device_trigger.py +++ b/homeassistant/components/humidifier/device_trigger.py @@ -35,7 +35,7 @@ from . import DOMAIN # mypy: disallow-any-generics -TARGET_TRIGGER_SCHEMA = vol.All( +HUMIDIFIER_TRIGGER_SCHEMA = vol.All( DEVICE_TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, @@ -48,12 +48,14 @@ TARGET_TRIGGER_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), ) -TOGGLE_TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} +TRIGGER_SCHEMA = vol.All( + vol.Any( + HUMIDIFIER_TRIGGER_SCHEMA, + toggle_entity.TRIGGER_SCHEMA, + ), + vol.Schema({vol.Required(CONF_DOMAIN): DOMAIN}, extra=vol.ALLOW_EXTRA), ) -TRIGGER_SCHEMA = vol.Any(TARGET_TRIGGER_SCHEMA, TOGGLE_TRIGGER_SCHEMA) - async def async_get_triggers( hass: HomeAssistant, device_id: str diff --git a/homeassistant/components/humidifier/strings.json b/homeassistant/components/humidifier/strings.json index 5a8864b496b..809eeb9fcfb 100644 --- a/homeassistant/components/humidifier/strings.json +++ b/homeassistant/components/humidifier/strings.json @@ -3,6 +3,7 @@ "device_automation": { "trigger_type": { "target_humidity_changed": "{entity_name} target humidity changed", + "toggled": "{entity_name} turned on or off", "turned_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off" }, diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py index 6714ee4cf9c..5b4083f41a6 100644 --- a/homeassistant/components/light/device_trigger.py +++ b/homeassistant/components/light/device_trigger.py @@ -16,8 +16,9 @@ from homeassistant.helpers.typing import ConfigType from . import DOMAIN -TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} +TRIGGER_SCHEMA = vol.All( + toggle_entity.TRIGGER_SCHEMA, + vol.Schema({vol.Required(CONF_DOMAIN): DOMAIN}, extra=vol.ALLOW_EXTRA), ) diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index ec0309958e7..9a63b53e8d7 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -14,6 +14,7 @@ "is_off": "{entity_name} is off" }, "trigger_type": { + "toggled": "{entity_name} turned on or off", "turned_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off" } diff --git a/homeassistant/components/remote/device_trigger.py b/homeassistant/components/remote/device_trigger.py index cf3a7427745..c358d86b176 100644 --- a/homeassistant/components/remote/device_trigger.py +++ b/homeassistant/components/remote/device_trigger.py @@ -16,8 +16,9 @@ from homeassistant.helpers.typing import ConfigType from . import DOMAIN -TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} +TRIGGER_SCHEMA = vol.All( + toggle_entity.TRIGGER_SCHEMA, + vol.Schema({vol.Required(CONF_DOMAIN): DOMAIN}, extra=vol.ALLOW_EXTRA), ) diff --git a/homeassistant/components/remote/strings.json b/homeassistant/components/remote/strings.json index bb7df2e89ef..91cf5ac0946 100644 --- a/homeassistant/components/remote/strings.json +++ b/homeassistant/components/remote/strings.json @@ -11,6 +11,7 @@ "is_off": "{entity_name} is off" }, "trigger_type": { + "toggled": "{entity_name} turned on or off", "turned_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off" } diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py index 6e4cf2f810e..533ee8bd54d 100644 --- a/homeassistant/components/switch/device_trigger.py +++ b/homeassistant/components/switch/device_trigger.py @@ -16,8 +16,9 @@ from homeassistant.helpers.typing import ConfigType from . import DOMAIN -TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} +TRIGGER_SCHEMA = vol.All( + toggle_entity.TRIGGER_SCHEMA, + vol.Schema({vol.Required(CONF_DOMAIN): DOMAIN}, extra=vol.ALLOW_EXTRA), ) diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json index 45fa08ab1d6..1e18b874972 100644 --- a/homeassistant/components/switch/strings.json +++ b/homeassistant/components/switch/strings.json @@ -11,6 +11,7 @@ "is_off": "{entity_name} is off" }, "trigger_type": { + "toggled": "{entity_name} turned on or off", "turned_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off" } diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index fe656663ca8..4aca4016b8b 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -140,6 +140,13 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r ) entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) expected_triggers = [ + { + "platform": "device", + "domain": "light", + "type": "toggled", + "device_id": device_entry.id, + "entity_id": "light.test_5678", + }, { "platform": "device", "domain": "light", @@ -395,14 +402,14 @@ async def test_async_get_device_automations_single_device_trigger( hass, device_automation.DeviceAutomationType.TRIGGER, [device_entry.id] ) assert device_entry.id in result - assert len(result[device_entry.id]) == 2 + assert len(result[device_entry.id]) == 3 # Test deprecated str automation_type works, to be removed in 2022.4 result = await device_automation.async_get_device_automations( hass, "trigger", [device_entry.id] ) assert device_entry.id in result - assert len(result[device_entry.id]) == 2 + assert len(result[device_entry.id]) == 3 # toggled, turned_on, turned_off async def test_async_get_device_automations_all_devices_trigger( @@ -421,7 +428,7 @@ async def test_async_get_device_automations_all_devices_trigger( hass, device_automation.DeviceAutomationType.TRIGGER ) assert device_entry.id in result - assert len(result[device_entry.id]) == 2 + assert len(result[device_entry.id]) == 3 # toggled, turned_on, turned_off async def test_async_get_device_automations_all_devices_condition( @@ -520,7 +527,7 @@ async def test_websocket_get_trigger_capabilities( triggers = msg["result"] id = 2 - assert len(triggers) == 2 + assert len(triggers) == 3 # toggled, turned_on, turned_off for trigger in triggers: await client.send_json( { diff --git a/tests/components/device_automation/test_toggle_entity.py b/tests/components/device_automation/test_toggle_entity.py new file mode 100644 index 00000000000..0414dc97944 --- /dev/null +++ b/tests/components/device_automation/test_toggle_entity.py @@ -0,0 +1,199 @@ +"""The test for device automation toggle entity helpers.""" +from datetime import timedelta + +import pytest + +import homeassistant.components.automation as automation +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed, async_mock_service +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_if_fires_on_state_change(hass, calls, enable_custom_integrations): + """Test for turn_on and turn_off triggers firing. + + This is a sanity test for the toggle entity device automation helper, this is + tested by each integration too. + """ + platform = getattr(hass.components, "test.switch") + + platform.init() + assert await async_setup_component( + hass, "switch", {"switch": {CONF_PLATFORM: "test"}} + ) + await hass.async_block_till_done() + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": "switch", + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_on", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_on {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": "switch", + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_off", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": "switch", + "device_id": "", + "entity_id": ent1.entity_id, + "type": "toggled", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_on_or_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 2 + assert {calls[0].data["some"], calls[1].data["some"]} == { + f"turn_off device - {ent1.entity_id} - on - off - None", + f"turn_on_or_off device - {ent1.entity_id} - on - off - None", + } + + hass.states.async_set(ent1.entity_id, STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 4 + assert {calls[2].data["some"], calls[3].data["some"]} == { + f"turn_on device - {ent1.entity_id} - off - on - None", + f"turn_on_or_off device - {ent1.entity_id} - off - on - None", + } + + +@pytest.mark.parametrize("trigger", ["turned_off", "toggled"]) +async def test_if_fires_on_state_change_with_for( + hass, calls, enable_custom_integrations, trigger +): + """Test for triggers firing with delay.""" + platform = getattr(hass.components, "test.switch") + + platform.init() + assert await async_setup_component( + hass, "switch", {"switch": {CONF_PLATFORM: "test"}} + ) + await hass.async_block_till_done() + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": "switch", + "device_id": "", + "entity_id": ent1.entity_id, + "type": trigger, + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + await hass.async_block_till_done() + assert calls[0].data["some"] == "turn_off device - {} - on - off - 0:00:05".format( + ent1.entity_id + ) diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py index 0d9edaf6fab..7b4bd8186c3 100644 --- a/tests/components/fan/test_device_trigger.py +++ b/tests/components/fan/test_device_trigger.py @@ -66,6 +66,13 @@ async def test_get_triggers(hass, device_reg, entity_reg): "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, + { + "platform": "device", + "domain": DOMAIN, + "type": "toggled", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id @@ -144,6 +151,25 @@ async def test_if_fires_on_state_change(hass, calls): }, }, }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "fan.entity", + "type": "toggled", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "turn_on_or_off - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, ] }, ) @@ -151,14 +177,20 @@ async def test_if_fires_on_state_change(hass, calls): # Fake that the entity is turning on. hass.states.async_set("fan.entity", STATE_ON) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "turn_on - device - fan.entity - off - on - None" + assert len(calls) == 2 + assert {calls[0].data["some"], calls[1].data["some"]} == { + "turn_on - device - fan.entity - off - on - None", + "turn_on_or_off - device - fan.entity - off - on - None", + } # Fake that the entity is turning off. hass.states.async_set("fan.entity", STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "turn_off - device - fan.entity - on - off - None" + assert len(calls) == 4 + assert {calls[2].data["some"], calls[3].data["some"]} == { + "turn_off - device - fan.entity - on - off - None", + "turn_on_or_off - device - fan.entity - on - off - None", + } async def test_if_fires_on_state_change_with_for(hass, calls): diff --git a/tests/components/homekit/test_type_triggers.py b/tests/components/homekit/test_type_triggers.py index b6d46b8cda5..ad970b56ad4 100644 --- a/tests/components/homekit/test_type_triggers.py +++ b/tests/components/homekit/test_type_triggers.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock from homeassistant.components.device_automation import DeviceAutomationType +from homeassistant.components.homekit.const import CHAR_PROGRAMMABLE_SWITCH_EVENT from homeassistant.components.homekit.type_triggers import DeviceTriggerAccessory from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component @@ -50,11 +51,16 @@ async def test_programmable_switch_button_fires_on_trigger( hk_driver.publish.reset_mock() hass.states.async_set("light.ceiling_lights", STATE_ON) await hass.async_block_till_done() - hk_driver.publish.assert_called_once() + assert len(hk_driver.publish.mock_calls) == 2 # one for on, one for toggle + for call in hk_driver.publish.mock_calls: + char = acc.get_characteristic(call.args[0]["aid"], call.args[0]["iid"]) + assert char.display_name == CHAR_PROGRAMMABLE_SWITCH_EVENT hk_driver.publish.reset_mock() hass.states.async_set("light.ceiling_lights", STATE_OFF) await hass.async_block_till_done() - hk_driver.publish.assert_called_once() - + assert len(hk_driver.publish.mock_calls) == 2 # one for on, one for toggle + for call in hk_driver.publish.mock_calls: + char = acc.get_characteristic(call.args[0]["aid"], call.args[0]["iid"]) + assert char.display_name == CHAR_PROGRAMMABLE_SWITCH_EVENT await acc.stop() diff --git a/tests/components/humidifier/test_device_trigger.py b/tests/components/humidifier/test_device_trigger.py index e11702a3468..7fa27dacada 100644 --- a/tests/components/humidifier/test_device_trigger.py +++ b/tests/components/humidifier/test_device_trigger.py @@ -84,6 +84,13 @@ async def test_get_triggers(hass, device_reg, entity_reg): "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, + { + "platform": "device", + "domain": DOMAIN, + "type": "toggled", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, ] triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id @@ -200,6 +207,30 @@ async def test_if_fires_on_state_change(hass, calls): }, }, }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "humidifier.entity", + "type": "toggled", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_on_or_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, ] }, ) @@ -225,18 +256,20 @@ async def test_if_fires_on_state_change(hass, calls): # Fake turn off hass.states.async_set("humidifier.entity", STATE_OFF, {const.ATTR_HUMIDITY: 37}) await hass.async_block_till_done() - assert len(calls) == 4 - assert ( - calls[3].data["some"] == "turn_off device - humidifier.entity - on - off - None" - ) + assert len(calls) == 5 + assert {calls[3].data["some"], calls[4].data["some"]} == { + "turn_off device - humidifier.entity - on - off - None", + "turn_on_or_off device - humidifier.entity - on - off - None", + } # Fake turn on hass.states.async_set("humidifier.entity", STATE_ON, {const.ATTR_HUMIDITY: 37}) await hass.async_block_till_done() - assert len(calls) == 5 - assert ( - calls[4].data["some"] == "turn_on device - humidifier.entity - off - on - None" - ) + assert len(calls) == 7 + assert {calls[5].data["some"], calls[6].data["some"]} == { + "turn_on device - humidifier.entity - off - on - None", + "turn_on_or_off device - humidifier.entity - off - on - None", + } async def test_invalid_config(hass, calls): diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index d6e906abd74..3dd9f846ec5 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -51,6 +51,13 @@ async def test_get_triggers(hass, device_reg, entity_reg): ) entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "toggled", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, { "platform": "device", "domain": DOMAIN, @@ -161,6 +168,30 @@ async def test_if_fires_on_state_change(hass, calls, enable_custom_integrations) }, }, }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "toggled", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_on_or_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, ] }, ) @@ -170,17 +201,19 @@ async def test_if_fires_on_state_change(hass, calls, enable_custom_integrations) hass.states.async_set(ent1.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "turn_off device - {} - on - off - None".format( - ent1.entity_id - ) + assert len(calls) == 2 + assert {calls[0].data["some"], calls[1].data["some"]} == { + f"turn_off device - {ent1.entity_id} - on - off - None", + f"turn_on_or_off device - {ent1.entity_id} - on - off - None", + } hass.states.async_set(ent1.entity_id, STATE_ON) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format( - ent1.entity_id - ) + assert len(calls) == 4 + assert {calls[2].data["some"], calls[3].data["some"]} == { + f"turn_on device - {ent1.entity_id} - off - on - None", + f"turn_on_or_off device - {ent1.entity_id} - off - on - None", + } async def test_if_fires_on_state_change_with_for( diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py index 00c444b232e..a3a0d049e34 100644 --- a/tests/components/remote/test_device_trigger.py +++ b/tests/components/remote/test_device_trigger.py @@ -51,6 +51,13 @@ async def test_get_triggers(hass, device_reg, entity_reg): ) entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "toggled", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, { "platform": "device", "domain": DOMAIN, @@ -159,6 +166,30 @@ async def test_if_fires_on_state_change(hass, calls, enable_custom_integrations) }, }, }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "toggled", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_on_or_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, ] }, ) @@ -168,17 +199,19 @@ async def test_if_fires_on_state_change(hass, calls, enable_custom_integrations) hass.states.async_set(ent1.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "turn_off device - {} - on - off - None".format( - ent1.entity_id - ) + assert len(calls) == 2 + assert {calls[0].data["some"], calls[1].data["some"]} == { + f"turn_off device - {ent1.entity_id} - on - off - None", + f"turn_on_or_off device - {ent1.entity_id} - on - off - None", + } hass.states.async_set(ent1.entity_id, STATE_ON) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format( - ent1.entity_id - ) + assert len(calls) == 4 + assert {calls[2].data["some"], calls[3].data["some"]} == { + f"turn_on device - {ent1.entity_id} - off - on - None", + f"turn_on_or_off device - {ent1.entity_id} - off - on - None", + } async def test_if_fires_on_state_change_with_for( diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 6697e9e1949..dbbf1eeeec7 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -51,6 +51,13 @@ async def test_get_triggers(hass, device_reg, entity_reg): ) entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "toggled", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, { "platform": "device", "domain": DOMAIN, @@ -159,6 +166,30 @@ async def test_if_fires_on_state_change(hass, calls, enable_custom_integrations) }, }, }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "toggled", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_on_or_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, ] }, ) @@ -168,17 +199,19 @@ async def test_if_fires_on_state_change(hass, calls, enable_custom_integrations) hass.states.async_set(ent1.entity_id, STATE_OFF) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "turn_off device - {} - on - off - None".format( - ent1.entity_id - ) + assert len(calls) == 2 + assert {calls[0].data["some"], calls[1].data["some"]} == { + f"turn_off device - {ent1.entity_id} - on - off - None", + f"turn_on_or_off device - {ent1.entity_id} - on - off - None", + } hass.states.async_set(ent1.entity_id, STATE_ON) await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format( - ent1.entity_id - ) + assert len(calls) == 4 + assert {calls[2].data["some"], calls[3].data["some"]} == { + f"turn_on device - {ent1.entity_id} - off - on - None", + f"turn_on_or_off device - {ent1.entity_id} - off - on - None", + } async def test_if_fires_on_state_change_with_for( diff --git a/tests/components/wemo/test_device_trigger.py b/tests/components/wemo/test_device_trigger.py index 0d9c537b24d..2e3ba3f6034 100644 --- a/tests/components/wemo/test_device_trigger.py +++ b/tests/components/wemo/test_device_trigger.py @@ -66,6 +66,13 @@ async def test_get_triggers(hass, wemo_entity): CONF_PLATFORM: "device", CONF_TYPE: EVENT_TYPE_LONG_PRESS, }, + { + CONF_DEVICE_ID: wemo_entity.device_id, + CONF_DOMAIN: Platform.SWITCH, + CONF_ENTITY_ID: wemo_entity.entity_id, + CONF_PLATFORM: "device", + CONF_TYPE: "toggled", + }, { CONF_DEVICE_ID: wemo_entity.device_id, CONF_DOMAIN: Platform.SWITCH,