From 853d6cda257c0524ac47b6bef9ee1abebde4a73e Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 26 Feb 2020 18:35:53 +0000 Subject: [PATCH] Make homekit_controller a local push integration (#32213) --- .../components/homekit_controller/__init__.py | 7 +++ .../homekit_controller/config_flow.py | 2 +- .../homekit_controller/connection.py | 20 +++++++ tests/components/homekit_controller/common.py | 5 ++ .../homekit_controller/test_light.py | 57 ++++++++++++++++++- 5 files changed, 89 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 3477e23897a2d..d94405f23e3a6 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -46,10 +46,12 @@ async def async_added_to_hass(self): ) self._accessory.add_pollable_characteristics(self.pollable_characteristics) + self._accessory.add_watchable_characteristics(self.watchable_characteristics) async def async_will_remove_from_hass(self): """Prepare to be removed from hass.""" self._accessory.remove_pollable_characteristics(self._aid) + self._accessory.remove_watchable_characteristics(self._aid) for signal_remove in self._signals: signal_remove() @@ -71,6 +73,7 @@ def setup(self): characteristic_types = [get_uuid(c) for c in self.get_characteristic_types()] self.pollable_characteristics = [] + self.watchable_characteristics = [] self._chars = {} self._char_names = {} @@ -98,6 +101,10 @@ def _setup_characteristic(self, char): if "pr" in char["perms"]: self.pollable_characteristics.append((self._aid, char["iid"])) + # Build up a list of (aid, iid) tuples to subscribe to + if "ev" in char["perms"]: + self.watchable_characteristics.append((self._aid, char["iid"])) + # Build a map of ctype -> iid short_name = CharacteristicsTypes.get_short(char["type"]) self._chars[short_name] = char["iid"] diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 4b713636beb9b..dbfb8dcbcd9b7 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -81,7 +81,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): """Handle a HomeKit config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize the homekit_controller flow.""" diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index f49480fc69da5..493ef3ccb866d 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -108,6 +108,10 @@ def __init__(self, hass, config_entry, pairing_data): self._polling_lock = asyncio.Lock() self._polling_lock_warned = False + self.watchable_characteristics = [] + + self.pairing.dispatcher_connect(self.process_new_events) + def add_pollable_characteristics(self, characteristics): """Add (aid, iid) pairs that we need to poll.""" self.pollable_characteristics.extend(characteristics) @@ -118,6 +122,17 @@ def remove_pollable_characteristics(self, accessory_id): char for char in self.pollable_characteristics if char[0] != accessory_id ] + def add_watchable_characteristics(self, characteristics): + """Add (aid, iid) pairs that we need to poll.""" + self.watchable_characteristics.extend(characteristics) + self.hass.add_job(self.pairing.subscribe(characteristics)) + + def remove_watchable_characteristics(self, accessory_id): + """Remove all pollable characteristics by accessory id.""" + self.watchable_characteristics = [ + char for char in self.watchable_characteristics if char[0] != accessory_id + ] + @callback def async_set_unavailable(self): """Mark state of all entities on this connection as unavailable.""" @@ -163,6 +178,9 @@ async def async_process_entity_map(self): self.add_entities() + if self.watchable_characteristics: + await self.pairing.subscribe(self.watchable_characteristics) + await self.async_update() return True @@ -172,6 +190,8 @@ async def async_unload(self): if self._polling_interval_remover: self._polling_interval_remover() + await self.pairing.unsubscribe(self.watchable_characteristics) + unloads = [] for platform in self.platforms: unloads.append( diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 4a6515a2503c9..0a57450a740d5 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -40,6 +40,11 @@ def __init__(self, hass, entity_id, pairing, accessory, config_entry): char_name = CharacteristicsTypes.get_short(char.type) self.characteristics[(service_name, char_name)] = char + async def update_named_service(self, service, characteristics): + """Update a service.""" + self.pairing.testing.update_named_service(service, characteristics) + await self.hass.async_block_till_done() + async def poll_and_get_state(self): """Trigger a time based poll and return the current entity state.""" await time_changed(self.hass, 60) diff --git a/tests/components/homekit_controller/test_light.py b/tests/components/homekit_controller/test_light.py index d9e1d21e2fe07..e443e36b91071 100644 --- a/tests/components/homekit_controller/test_light.py +++ b/tests/components/homekit_controller/test_light.py @@ -6,6 +6,9 @@ from tests.components.homekit_controller.common import setup_test_component +LIGHT_BULB_NAME = "Light Bulb" +LIGHT_BULB_ENTITY_ID = "light.testdevice" + LIGHT_ON = ("lightbulb", "on") LIGHT_BRIGHTNESS = ("lightbulb", "brightness") LIGHT_HUE = ("lightbulb", "hue") @@ -15,7 +18,7 @@ def create_lightbulb_service(accessory): """Define lightbulb characteristics.""" - service = accessory.add_service(ServicesTypes.LIGHTBULB) + service = accessory.add_service(ServicesTypes.LIGHTBULB, name=LIGHT_BULB_NAME) on_char = service.add_char(CharacteristicsTypes.ON) on_char.value = 0 @@ -110,6 +113,35 @@ async def test_switch_read_light_state(hass, utcnow): assert state.state == "off" +async def test_switch_push_light_state(hass, utcnow): + """Test that we can read the state of a HomeKit light accessory.""" + helper = await setup_test_component(hass, create_lightbulb_service_with_hs) + + # Initial state is that the light is off + state = hass.states.get(LIGHT_BULB_ENTITY_ID) + assert state.state == "off" + + await helper.update_named_service( + LIGHT_BULB_NAME, + { + CharacteristicsTypes.ON: True, + CharacteristicsTypes.BRIGHTNESS: 100, + CharacteristicsTypes.HUE: 4, + CharacteristicsTypes.SATURATION: 5, + }, + ) + + state = hass.states.get(LIGHT_BULB_ENTITY_ID) + assert state.state == "on" + assert state.attributes["brightness"] == 255 + assert state.attributes["hs_color"] == (4, 5) + + # Simulate that device switched off in the real world not via HA + await helper.update_named_service(LIGHT_BULB_NAME, {CharacteristicsTypes.ON: False}) + state = hass.states.get(LIGHT_BULB_ENTITY_ID) + assert state.state == "off" + + async def test_switch_read_light_state_color_temp(hass, utcnow): """Test that we can read the color_temp of a light accessory.""" helper = await setup_test_component(hass, create_lightbulb_service_with_color_temp) @@ -129,6 +161,29 @@ async def test_switch_read_light_state_color_temp(hass, utcnow): assert state.attributes["color_temp"] == 400 +async def test_switch_push_light_state_color_temp(hass, utcnow): + """Test that we can read the state of a HomeKit light accessory.""" + helper = await setup_test_component(hass, create_lightbulb_service_with_color_temp) + + # Initial state is that the light is off + state = hass.states.get(LIGHT_BULB_ENTITY_ID) + assert state.state == "off" + + await helper.update_named_service( + LIGHT_BULB_NAME, + { + CharacteristicsTypes.ON: True, + CharacteristicsTypes.BRIGHTNESS: 100, + CharacteristicsTypes.COLOR_TEMPERATURE: 400, + }, + ) + + state = hass.states.get(LIGHT_BULB_ENTITY_ID) + assert state.state == "on" + assert state.attributes["brightness"] == 255 + assert state.attributes["color_temp"] == 400 + + async def test_light_becomes_unavailable_but_recovers(hass, utcnow): """Test transition to and from unavailable state.""" helper = await setup_test_component(hass, create_lightbulb_service_with_color_temp)