diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index beddd915fc0..6a9fb126c21 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity import Entity from .config_flow import normalize_hkid from .connection import HKDevice -from .const import CONTROLLER, DOMAIN, ENTITY_MAP, KNOWN_DEVICES +from .const import CONTROLLER, DOMAIN, ENTITY_MAP, KNOWN_DEVICES, TRIGGERS from .storage import EntityMapStorage _LOGGER = logging.getLogger(__name__) @@ -200,6 +200,7 @@ async def async_setup(hass, config): zeroconf_instance = await zeroconf.async_get_instance(hass) hass.data[CONTROLLER] = aiohomekit.Controller(zeroconf_instance=zeroconf_instance) hass.data[KNOWN_DEVICES] = {} + hass.data[TRIGGERS] = {} return True diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 6a59f98f3dc..ed2aaaa4656 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -16,6 +16,7 @@ from homeassistant.core import callback from homeassistant.helpers.event import async_track_time_interval from .const import CONTROLLER, DOMAIN, ENTITY_MAP, HOMEKIT_ACCESSORY_DISPATCH +from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60) RETRY_INTERVAL = 60 # seconds @@ -237,6 +238,9 @@ class HKDevice: await self.async_create_devices() + # Load any triggers for this config entry + await async_setup_triggers_for_entry(self.hass, self.config_entry) + self.add_entities() if self.watchable_characteristics: @@ -377,6 +381,9 @@ class HKDevice: """Process events from accessory into HA state.""" self.available = True + # Process any stateless events (via device_triggers) + async_fire_triggers(self, new_values_dict) + for (aid, cid), value in new_values_dict.items(): accessory = self.current_state.setdefault(aid, {}) accessory[cid] = value diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 2a8a106a296..b1e32417137 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -4,6 +4,7 @@ DOMAIN = "homekit_controller" KNOWN_DEVICES = f"{DOMAIN}-devices" CONTROLLER = f"{DOMAIN}-controller" ENTITY_MAP = f"{DOMAIN}-entity-map" +TRIGGERS = f"{DOMAIN}-triggers" HOMEKIT_DIR = ".homekit" PAIRING_FILE = "pairing.json" diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py new file mode 100644 index 00000000000..76b82eec597 --- /dev/null +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -0,0 +1,268 @@ +"""Provides device automations for homekit devices.""" +from typing import List + +from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.characteristics.const import InputEventValues +from aiohomekit.model.services import ServicesTypes +from aiohomekit.utils import clamp_enum_to_char +import voluptuous as vol + +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN, KNOWN_DEVICES, TRIGGERS + +TRIGGER_TYPES = { + "button1", + "button2", + "button3", + "button4", + "button5", + "button6", + "button7", + "button8", + "button9", + "button10", +} +TRIGGER_SUBTYPES = {"single_press", "double_press", "long_press"} + +CONF_IID = "iid" +CONF_SUBTYPE = "subtype" + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + vol.Required(CONF_SUBTYPE): vol.In(TRIGGER_SUBTYPES), + } +) + +HK_TO_HA_INPUT_EVENT_VALUES = { + InputEventValues.SINGLE_PRESS: "single_press", + InputEventValues.DOUBLE_PRESS: "double_press", + InputEventValues.LONG_PRESS: "long_press", +} + + +class TriggerSource: + """Represents a stateless source of event data from HomeKit.""" + + def __init__(self, connection, aid, triggers): + """Initialize a set of triggers for a device.""" + self._hass = connection.hass + self._connection = connection + self._aid = aid + self._triggers = {} + for trigger in triggers: + self._triggers[(trigger["type"], trigger["subtype"])] = trigger + self._callbacks = {} + + def fire(self, iid, value): + """Process events that have been received from a HomeKit accessory.""" + for event_handler in self._callbacks.get(iid, []): + event_handler(value) + + def async_get_triggers(self): + """List device triggers for homekit devices.""" + yield from self._triggers + + async def async_attach_trigger( + self, + config: TRIGGER_SCHEMA, + action: AutomationActionType, + automation_info: dict, + ) -> CALLBACK_TYPE: + """Attach a trigger.""" + + def event_handler(char): + if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]: + return + self._hass.async_create_task(action({"trigger": config})) + + trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]] + iid = trigger["characteristic"] + + self._connection.add_watchable_characteristics([(self._aid, iid)]) + self._callbacks.setdefault(iid, []).append(event_handler) + + def async_remove_handler(): + if iid in self._callbacks: + self._callbacks[iid].remove(event_handler) + + return async_remove_handler + + +def enumerate_stateless_switch(service): + """Enumerate a stateless switch, like a single button.""" + + # A stateless switch that has a SERVICE_LABEL_INDEX is part of a group + # And is handled separately + if service.has(CharacteristicsTypes.SERVICE_LABEL_INDEX): + if len(service.linked) > 0: + return [] + + char = service[CharacteristicsTypes.INPUT_EVENT] + + # HomeKit itself supports single, double and long presses. But the + # manufacturer might not - clamp options to what they say. + all_values = clamp_enum_to_char(InputEventValues, char) + + results = [] + for event_type in all_values: + results.append( + { + "characteristic": char.iid, + "value": event_type, + "type": "button1", + "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type], + } + ) + return results + + +def enumerate_stateless_switch_group(service): + """Enumerate a group of stateless switches, like a remote control.""" + switches = list( + service.accessory.services.filter( + service_type=ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH, + child_service=service, + order_by=[CharacteristicsTypes.SERVICE_LABEL_INDEX], + ) + ) + + results = [] + for idx, switch in enumerate(switches): + char = switch[CharacteristicsTypes.INPUT_EVENT] + + # HomeKit itself supports single, double and long presses. But the + # manufacturer might not - clamp options to what they say. + all_values = clamp_enum_to_char(InputEventValues, char) + + for event_type in all_values: + results.append( + { + "characteristic": char.iid, + "value": event_type, + "type": f"button{idx + 1}", + "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type], + } + ) + return results + + +def enumerate_doorbell(service): + """Enumerate doorbell buttons.""" + input_event = service[CharacteristicsTypes.INPUT_EVENT] + + # HomeKit itself supports single, double and long presses. But the + # manufacturer might not - clamp options to what they say. + all_values = clamp_enum_to_char(InputEventValues, input_event) + + results = [] + for event_type in all_values: + results.append( + { + "characteristic": input_event.iid, + "value": event_type, + "type": "doorbell", + "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type], + } + ) + return results + + +TRIGGER_FINDERS = { + "service-label": enumerate_stateless_switch_group, + "stateless-programmable-switch": enumerate_stateless_switch, + "doorbell": enumerate_doorbell, +} + + +async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry): + """Triggers aren't entities as they have no state, but we still need to set them up for a config entry.""" + hkid = config_entry.data["AccessoryPairingID"] + conn = hass.data[KNOWN_DEVICES][hkid] + + @callback + def async_add_service(aid, service_dict): + service_type = service_dict["stype"] + + # If not a known service type then we can't handle any stateless events for it + if service_type not in TRIGGER_FINDERS: + return False + + # We can't have multiple trigger sources for the same device id + # Can't have a doorbell and a remote control in the same accessory + # They have to be different accessories (they can be on the same bridge) + # In practice, this is inline with what iOS actually supports AFAWCT. + device_id = conn.devices[aid] + if device_id in hass.data[TRIGGERS]: + return False + + # At the moment add_listener calls us with the raw service dict, rather than + # a service model. So turn it into a service ourselves. + accessory = conn.entity_map.aid(aid) + service = accessory.services.iid(service_dict["iid"]) + + # Just because we recognise the service type doesn't mean we can actually + # extract any triggers - so only proceed if we can + triggers = TRIGGER_FINDERS[service_type](service) + if len(triggers) == 0: + return False + + trigger = TriggerSource(conn, aid, triggers) + hass.data[TRIGGERS][device_id] = trigger + + return True + + conn.add_listener(async_add_service) + + +def async_fire_triggers(conn, events): + """Process events generated by a HomeKit accessory into automation triggers.""" + for (aid, iid), ev in events.items(): + if aid in conn.devices: + device_id = conn.devices[aid] + if device_id in conn.hass.data[TRIGGERS]: + source = conn.hass.data[TRIGGERS][device_id] + source.fire(iid, ev) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for homekit devices.""" + + if device_id not in hass.data.get(TRIGGERS, {}): + return [] + + device = hass.data[TRIGGERS][device_id] + + triggers = [] + + for trigger, subtype in device.async_get_triggers(): + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: trigger, + CONF_SUBTYPE: subtype, + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + device_id = config[CONF_DEVICE_ID] + device = hass.data[TRIGGERS][device_id] + return await device.async_attach_trigger(config, action, automation_info) diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index bc07b71fa75..babfac05718 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -46,5 +46,25 @@ "accessory_not_found_error": "Cannot add pairing as device can no longer be found.", "already_in_progress": "Config flow for device is already in progress." } + }, + "device_automation": { + "trigger_type": { + "single_press": "\"{subtype}\" pressed", + "double_press": "\"{subtype}\" pressed twice", + "long_press": "\"{subtype}\" pressed and held" + }, + "trigger_subtype": { + "doorbell": "Doorbell", + "button1": "Button 1", + "button2": "Button 2", + "button3": "Button 3", + "button4": "Button 4", + "button5": "Button 5", + "button6": "Button 6", + "button7": "Button 7", + "button8": "Button 8", + "button9": "Button 9", + "button10": "Button 10" + } } } diff --git a/homeassistant/components/homekit_controller/translations/en.json b/homeassistant/components/homekit_controller/translations/en.json index 544c86391be..afa790dd222 100644 --- a/homeassistant/components/homekit_controller/translations/en.json +++ b/homeassistant/components/homekit_controller/translations/en.json @@ -53,5 +53,26 @@ } } }, - "title": "HomeKit Controller" -} \ No newline at end of file + "title": "HomeKit Controller", + "device_automation": { + "trigger_type": { + "single_press": "\"{subtype}\" pressed", + "double_press": "\"{subtype}\" pressed twice", + "long_press": "\"{subtype}\" pressed and held" + }, + "trigger_subtype": { + "doorbell": "Doorbell", + "button1": "Button 1", + "button2": "Button 2", + "button3": "Button 3", + "button4": "Button 4", + "button5": "Button 5", + "button6": "Button 6", + "button7": "Button 7", + "button8": "Button 8", + "button9": "Button 9", + "button10": "Button 10" + } + } +} + diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py new file mode 100644 index 00000000000..a9744fb7bfc --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py @@ -0,0 +1,52 @@ +""" +Regression tests for Aqara AR004. + +This device has a non-standard programmable stateless switch service that has a +service-label-index despite not being linked to a service-label. + +https://github.com/home-assistant/core/pull/39090 +""" + +from tests.common import assert_lists_same, async_get_device_automations +from tests.components.homekit_controller.common import ( + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_aqara_switch_setup(hass): + """Test that a Aqara Switch can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "aqara_switch.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + battery_id = "sensor.programmable_switch_battery" + battery = entity_registry.async_get(battery_id) + assert battery.unique_id == "homekit-111a1111a1a111-5" + + # The fixture file has 1 button and a battery + + expected = [ + { + "device_id": battery.device_id, + "domain": "sensor", + "entity_id": "sensor.programmable_switch_battery", + "platform": "device", + "type": "battery_level", + } + ] + + for subtype in ("single_press", "double_press", "long_press"): + expected.append( + { + "device_id": battery.device_id, + "domain": "homekit_controller", + "platform": "device", + "type": "button1", + "subtype": subtype, + } + ) + + triggers = await async_get_device_automations(hass, "trigger", battery.device_id) + assert_lists_same(triggers, expected) diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py index 0b6ebc00eba..67b7508eb94 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -1,5 +1,6 @@ """Tests for handling accessories on a Hue bridge via HomeKit.""" +from tests.common import assert_lists_same, async_get_device_automations from tests.components.homekit_controller.common import ( Helper, setup_accessories_from_file, @@ -34,3 +35,32 @@ async def test_hue_bridge_setup(hass): assert device.name == "Hue dimmer switch" assert device.model == "RWL021" assert device.sw_version == "45.1.17846" + + # The fixture file has 1 dimmer, which is a remote with 4 buttons + # It (incorrectly) claims to support single, double and long press events + # It also has a battery + + expected = [ + { + "device_id": device.id, + "domain": "sensor", + "entity_id": "sensor.hue_dimmer_switch_battery", + "platform": "device", + "type": "battery_level", + } + ] + + for button in ("button1", "button2", "button3", "button4"): + for subtype in ("single_press", "double_press", "long_press"): + expected.append( + { + "device_id": device.id, + "domain": "homekit_controller", + "platform": "device", + "type": button, + "subtype": subtype, + } + ) + + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert_lists_same(triggers, expected) diff --git a/tests/components/homekit_controller/specific_devices/test_lg_tv.py b/tests/components/homekit_controller/specific_devices/test_lg_tv.py index acebac95006..cd3f57137bf 100644 --- a/tests/components/homekit_controller/specific_devices/test_lg_tv.py +++ b/tests/components/homekit_controller/specific_devices/test_lg_tv.py @@ -1,12 +1,12 @@ """Make sure that handling real world LG HomeKit characteristics isn't broken.""" - from homeassistant.components.media_player.const import ( SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_SELECT_SOURCE, ) +from tests.common import async_get_device_automations from tests.components.homekit_controller.common import ( Helper, setup_accessories_from_file, @@ -62,3 +62,7 @@ async def test_lg_tv(hass): assert device.model == "OLED55B9PUA" assert device.sw_version == "04.71.04" assert device.via_device_id is None + + # A TV doesn't have any triggers + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert triggers == [] diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py new file mode 100644 index 00000000000..c8ef2cbef38 --- /dev/null +++ b/tests/components/homekit_controller/test_device_trigger.py @@ -0,0 +1,298 @@ +"""Test homekit_controller stateless triggers.""" +from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes +import pytest + +import homeassistant.components.automation as automation +from homeassistant.components.homekit_controller.const import DOMAIN +from homeassistant.setup import async_setup_component + +from tests.common import ( + assert_lists_same, + async_get_device_automations, + async_mock_service, +) +from tests.components.homekit_controller.common import setup_test_component + + +# pylint: disable=redefined-outer-name +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +def create_remote(accessory): + """Define characteristics for a button (that is inn a group).""" + service_label = accessory.add_service(ServicesTypes.SERVICE_LABEL) + + char = service_label.add_char(CharacteristicsTypes.SERVICE_LABEL_NAMESPACE) + char.value = 1 + + for i in range(4): + button = accessory.add_service(ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH) + button.linked.append(service_label) + + char = button.add_char(CharacteristicsTypes.INPUT_EVENT) + char.value = 0 + char.perms = ["pw", "pr", "ev"] + + char = button.add_char(CharacteristicsTypes.NAME) + char.value = f"Button {i + 1}" + + char = button.add_char(CharacteristicsTypes.SERVICE_LABEL_INDEX) + char.value = i + + battery = accessory.add_service(ServicesTypes.BATTERY_SERVICE) + battery.add_char(CharacteristicsTypes.BATTERY_LEVEL) + + +def create_button(accessory): + """Define a button (that is not in a group).""" + button = accessory.add_service(ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH) + + char = button.add_char(CharacteristicsTypes.INPUT_EVENT) + char.value = 0 + char.perms = ["pw", "pr", "ev"] + + char = button.add_char(CharacteristicsTypes.NAME) + char.value = "Button 1" + + battery = accessory.add_service(ServicesTypes.BATTERY_SERVICE) + battery.add_char(CharacteristicsTypes.BATTERY_LEVEL) + + +def create_doorbell(accessory): + """Define a button (that is not in a group).""" + button = accessory.add_service(ServicesTypes.DOORBELL) + + char = button.add_char(CharacteristicsTypes.INPUT_EVENT) + char.value = 0 + char.perms = ["pw", "pr", "ev"] + + char = button.add_char(CharacteristicsTypes.NAME) + char.value = "Doorbell" + + battery = accessory.add_service(ServicesTypes.BATTERY_SERVICE) + battery.add_char(CharacteristicsTypes.BATTERY_LEVEL) + + +async def test_enumerate_remote(hass, utcnow): + """Test that remote is correctly enumerated.""" + await setup_test_component(hass, create_remote) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry = entity_registry.async_get("sensor.testdevice_battery") + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + expected = [ + { + "device_id": device.id, + "domain": "sensor", + "entity_id": "sensor.testdevice_battery", + "platform": "device", + "type": "battery_level", + } + ] + + for button in ("button1", "button2", "button3", "button4"): + for subtype in ("single_press", "double_press", "long_press"): + expected.append( + { + "device_id": device.id, + "domain": "homekit_controller", + "platform": "device", + "type": button, + "subtype": subtype, + } + ) + + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert_lists_same(triggers, expected) + + +async def test_enumerate_button(hass, utcnow): + """Test that a button is correctly enumerated.""" + await setup_test_component(hass, create_button) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry = entity_registry.async_get("sensor.testdevice_battery") + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + expected = [ + { + "device_id": device.id, + "domain": "sensor", + "entity_id": "sensor.testdevice_battery", + "platform": "device", + "type": "battery_level", + } + ] + + for subtype in ("single_press", "double_press", "long_press"): + expected.append( + { + "device_id": device.id, + "domain": "homekit_controller", + "platform": "device", + "type": "button1", + "subtype": subtype, + } + ) + + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert_lists_same(triggers, expected) + + +async def test_enumerate_doorbell(hass, utcnow): + """Test that a button is correctly enumerated.""" + await setup_test_component(hass, create_doorbell) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry = entity_registry.async_get("sensor.testdevice_battery") + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + expected = [ + { + "device_id": device.id, + "domain": "sensor", + "entity_id": "sensor.testdevice_battery", + "platform": "device", + "type": "battery_level", + } + ] + + for subtype in ("single_press", "double_press", "long_press"): + expected.append( + { + "device_id": device.id, + "domain": "homekit_controller", + "platform": "device", + "type": "doorbell", + "subtype": subtype, + } + ) + + triggers = await async_get_device_automations(hass, "trigger", device.id) + assert_lists_same(triggers, expected) + + +async def test_handle_events(hass, utcnow, calls): + """Test that events are handled.""" + helper = await setup_test_component(hass, create_remote) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry = entity_registry.async_get("sensor.testdevice_battery") + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "alias": "single_press", + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "button1", + "subtype": "single_press", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "{{ trigger.platform}} - " + "{{ trigger.type }} - {{ trigger.subtype }}" + ) + }, + }, + }, + { + "alias": "long_press", + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device.id, + "type": "button2", + "subtype": "long_press", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "{{ trigger.platform}} - " + "{{ trigger.type }} - {{ trigger.subtype }}" + ) + }, + }, + }, + ] + }, + ) + + # Make sure first automation (only) fires for single press + helper.pairing.testing.update_named_service( + "Button 1", {CharacteristicsTypes.INPUT_EVENT: 0} + ) + + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "device - button1 - single_press" + + # Make sure automation doesn't trigger for long press + helper.pairing.testing.update_named_service( + "Button 1", {CharacteristicsTypes.INPUT_EVENT: 1} + ) + + await hass.async_block_till_done() + assert len(calls) == 1 + + # Make sure automation doesn't trigger for double press + helper.pairing.testing.update_named_service( + "Button 1", {CharacteristicsTypes.INPUT_EVENT: 2} + ) + + await hass.async_block_till_done() + assert len(calls) == 1 + + # Make sure second automation fires for long press + helper.pairing.testing.update_named_service( + "Button 2", {CharacteristicsTypes.INPUT_EVENT: 2} + ) + + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "device - button2 - long_press" + + # Turn the automations off + await hass.services.async_call( + "automation", + "turn_off", + {"entity_id": "automation.long_press"}, + blocking=True, + ) + + await hass.services.async_call( + "automation", + "turn_off", + {"entity_id": "automation.single_press"}, + blocking=True, + ) + + # Make sure event no longer fires + helper.pairing.testing.update_named_service( + "Button 2", {CharacteristicsTypes.INPUT_EVENT: 2} + ) + + await hass.async_block_till_done() + assert len(calls) == 2 diff --git a/tests/fixtures/homekit_controller/aqara_switch.json b/tests/fixtures/homekit_controller/aqara_switch.json new file mode 100644 index 00000000000..320478343f4 --- /dev/null +++ b/tests/fixtures/homekit_controller/aqara_switch.json @@ -0,0 +1,209 @@ +[ + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "bool", + "iid": 65537, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "ev": false, + "format": "string", + "iid": 65538, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Aqara" + }, + { + "ev": false, + "format": "string", + "iid": 65539, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "AR004" + }, + { + "ev": false, + "format": "string", + "iid": 65540, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Programmable Switch" + }, + { + "ev": false, + "format": "string", + "iid": 65541, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "111a1111a1a111" + }, + { + "ev": false, + "format": "string", + "iid": 65542, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "9" + }, + { + "ev": false, + "format": "string", + "iid": 65543, + "perms": [ + "pr" + ], + "type": "00000053-0000-1000-8000-0026BB765291", + "value": "1.0" + } + ], + "hidden": false, + "iid": 1, + "linked": [], + "primary": false, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": false, + "format": "string", + "iid": 262146, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Programmable Switch" + }, + { + "ev": false, + "format": "uint8", + "iid": 262147, + "maxValue": 2, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "00000073-0000-1000-8000-0026BB765291", + "valid-values": [ + 0, + 1, + 2 + ], + "value": null + }, + { + "ev": false, + "format": "uint8", + "iid": 262148, + "maxValue": 255, + "minStep": 1, + "minValue": 1, + "perms": [ + "pr" + ], + "type": "000000CB-0000-1000-8000-0026BB765291", + "value": 1 + } + ], + "hidden": false, + "iid": 4, + "linked": [], + "primary": true, + "stype": "stateless-programmable-switch", + "type": "00000089-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": false, + "format": "string", + "iid": 327682, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Battery Sensor" + }, + { + "ev": true, + "format": "uint8", + "iid": 327683, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "00000068-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "ev": true, + "format": "uint8", + "iid": 327685, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "00000079-0000-1000-8000-0026BB765291", + "valid-values": [ + 0, + 1 + ], + "value": 0 + }, + { + "ev": true, + "format": "uint8", + "iid": 327684, + "maxValue": 2, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "0000008F-0000-1000-8000-0026BB765291", + "valid-values": [ + 0, + 1, + 2 + ], + "value": 2 + } + ], + "hidden": false, + "iid": 5, + "linked": [], + "primary": false, + "stype": "battery", + "type": "00000096-0000-1000-8000-0026BB765291" + } + ] + } +] \ No newline at end of file