From 0dc0706eb20657ea7069fa5a7dcbca0b1d305aaa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 7 Jun 2019 22:59:51 -0700 Subject: [PATCH] Add more HomeKit models for discovery (#24391) * Add more HomeKit models for discovery * Discover Tradfri with HomeKit * Add Wemo device info * Allow full match for HomeKit model * Fix tests --- .../homekit_controller/config_flow.py | 2 - homeassistant/components/hue/config_flow.py | 16 ++++++++ homeassistant/components/hue/manifest.json | 5 +++ .../components/tradfri/config_flow.py | 2 + .../components/tradfri/manifest.json | 5 +++ homeassistant/components/wemo/manifest.json | 5 +++ homeassistant/components/wemo/switch.py | 10 ++++- homeassistant/components/zeroconf/__init__.py | 2 +- homeassistant/generated/zeroconf.py | 5 ++- script/hassfest/ssdp.py | 2 +- script/hassfest/zeroconf.py | 7 +--- .../homekit_controller/test_config_flow.py | 2 +- tests/components/hue/test_config_flow.py | 35 +++++++++++++++++ tests/components/zeroconf/test_init.py | 39 +++++++++++++++---- 14 files changed, 118 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 2ce8c0db6b7..9ddb144ec9a 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -13,9 +13,7 @@ from .connection import get_bridge_information, get_accessory_name HOMEKIT_IGNORE = [ - 'BSB002', 'Home Assistant Bridge', - 'TRADFRI gateway', ] HOMEKIT_DIR = '.homekit' PAIRING_FILE = 'pairing.json' diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 9c81d144d1c..d57706f7ac8 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -175,6 +175,22 @@ class HueFlowHandler(config_entries.ConfigFlow): 'path': 'phue-{}.conf'.format(serial) }) + async def async_step_homekit(self, homekit_info): + """Handle HomeKit discovery.""" + # pylint: disable=unsupported-assignment-operation + host = self.context['host'] = homekit_info.get('host') + + if any(host == flow['context']['host'] + for flow in self._async_in_progress()): + return self.async_abort(reason='already_in_progress') + + if host in configured_hosts(self.hass): + return self.async_abort(reason='already_configured') + + return await self.async_step_import({ + 'host': host, + }) + async def async_step_import(self, import_info): """Import a new bridge as a config entry. diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index d16988529b1..c0c7c462f90 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -11,6 +11,11 @@ "Royal Philips Electronics" ] }, + "homekit": { + "models": [ + "BSB002" + ] + }, "dependencies": [], "codeowners": [ "@balloob" diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 76f6a8f5764..bfabf4fd12a 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -87,6 +87,8 @@ class FlowHandler(config_entries.ConfigFlow): self._host = user_input['host'] return await self.async_step_auth() + async_step_homekit = async_step_zeroconf + async def async_step_import(self, user_input): """Import a config entry.""" for entry in self._async_current_entries(): diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index aba3805a4aa..ba6b21e0028 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -6,6 +6,11 @@ "requirements": [ "pytradfri[async]==6.0.1" ], + "homekit": { + "models": [ + "TRADFRI" + ] + }, "dependencies": [], "zeroconf": ["_coap._udp.local."], "codeowners": [ diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index c610c28da39..1902df1060b 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -11,6 +11,11 @@ "Belkin International Inc." ] }, + "homekit": { + "models": [ + "Wemo" + ] + }, "dependencies": [], "codeowners": [ "@sqldiablo" diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index b8967cead3b..79f941d8bcf 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -12,7 +12,7 @@ from homeassistant.util import convert from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN) -from . import SUBSCRIPTION_REGISTRY +from . import SUBSCRIPTION_REGISTRY, DOMAIN as WEMO_DOMAIN SCAN_INTERVAL = timedelta(seconds=10) @@ -93,6 +93,14 @@ class WemoSwitch(SwitchDevice): """Return the name of the switch if any.""" return self._name + @property + def device_info(self): + """Return the device info.""" + return { + 'name': self._name, + 'identifiers': {(WEMO_DOMAIN, self._serialnumber)}, + } + @property def device_state_attributes(self): """Return the state attributes of the device.""" diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 289aba6ef56..6011712c2f9 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -112,7 +112,7 @@ def handle_homekit(hass, info) -> bool: return False for test_model in HOMEKIT: - if not model.startswith(test_model): + if model != test_model and not model.startswith(test_model + " "): continue hass.add_job( diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 716b212e4c6..1bc00d08314 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -20,5 +20,8 @@ ZEROCONF = { } HOMEKIT = { - "LIFX ": "lifx" + "BSB002": "hue", + "LIFX": "lifx", + "TRADFRI": "tradfri", + "Wemo": "wemo" } diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py index 9c745e5b033..308491dfa35 100644 --- a/script/hassfest/ssdp.py +++ b/script/hassfest/ssdp.py @@ -44,7 +44,7 @@ def generate_and_validate(integrations: Dict[str, Integration]): try: with open(str(integration.path / "config_flow.py")) as fp: content = fp.read() - if (' async_step_ssdp(' not in content and + if (' async_step_ssdp' not in content and 'register_discovery_flow' not in content): integration.add_error( 'ssdp', 'Config flow has no async_step_ssdp') diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index 895ae4ab790..ad2b5b4e295 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -42,13 +42,13 @@ def generate_and_validate(integrations: Dict[str, Integration]): uses_discovery_flow = 'register_discovery_flow' in content if (service_types and not uses_discovery_flow and - ' async_step_zeroconf(' not in content): + ' async_step_zeroconf' not in content): integration.add_error( 'zeroconf', 'Config flow has no async_step_zeroconf') continue if (homekit_models and not uses_discovery_flow and - ' async_step_homekit(' not in content): + ' async_step_homekit' not in content): integration.add_error( 'zeroconf', 'Config flow has no async_step_homekit') continue @@ -64,9 +64,6 @@ def generate_and_validate(integrations: Dict[str, Integration]): service_type_dict[service_type].append(domain) for model in homekit_models: - # We add a space, as we want to test for it to be model + space. - model += " " - if model in homekit_dict: integration.add_error( 'zeroconf', diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index b5f923dd55e..99562f60045 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -283,7 +283,7 @@ async def test_discovery_ignored_model(hass): 'host': '127.0.0.1', 'port': 8080, 'properties': { - 'md': 'BSB002', + 'md': config_flow.HOMEKIT_IGNORE[0], 'id': '00:00:00:00:00:00', 'c#': 1, 'sf': 1, diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index b7736e62390..a4524dfd48d 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -371,3 +371,38 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): # We did not process the result of this entry but already removed the old # ones. So we should have 0 entries. assert len(hass.config_entries.async_entries('hue')) == 0 + + +async def test_bridge_homekit(hass): + """Test a bridge being discovered via HomeKit.""" + flow = config_flow.HueFlowHandler() + flow.hass = hass + flow.context = {} + + with patch.object(config_flow, 'get_bridge', + side_effect=errors.AuthenticationRequired): + result = await flow.async_step_homekit({ + 'host': '0.0.0.0', + 'serial': '1234', + 'manufacturerURL': config_flow.HUE_MANUFACTURERURL + }) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + + +async def test_bridge_homekit_already_configured(hass): + """Test if a HomeKit discovered bridge has already been configured.""" + MockConfigEntry(domain='hue', data={ + 'host': '0.0.0.0' + }).add_to_hass(hass) + + flow = config_flow.HueFlowHandler() + flow.hass = hass + flow.context = {} + + result = await flow.async_step_homekit({ + 'host': '0.0.0.0', + }) + + assert result['type'] == 'abort' diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 27c1dc75749..e67d9063b0a 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -31,12 +31,15 @@ def get_service_info_mock(service_type, name): properties={b'macaddress': b'ABCDEF012345'}) -def get_homekit_info_mock(service_type, name): +def get_homekit_info_mock(model): """Return homekit info for get_service_info.""" - return ServiceInfo( - service_type, name, address=b'\n\x00\x00\x14', port=80, weight=0, - priority=0, server='name.local.', - properties={b'md': b'LIFX Bulb'}) + def mock_homekit_info(service_type, name): + return ServiceInfo( + service_type, name, address=b'\n\x00\x00\x14', port=80, weight=0, + priority=0, server='name.local.', + properties={b'md': model.encode()}) + + return mock_homekit_info async def test_setup(hass, mock_zeroconf): @@ -54,7 +57,7 @@ async def test_setup(hass, mock_zeroconf): assert len(mock_config_flow.mock_calls) == len(zc_gen.ZEROCONF) * 2 -async def test_homekit(hass, mock_zeroconf): +async def test_homekit_match_partial(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( zc_gen.ZEROCONF, { @@ -65,10 +68,32 @@ async def test_homekit(hass, mock_zeroconf): ) as mock_config_flow, patch.object( zeroconf, 'ServiceBrowser', side_effect=service_update_mock ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock + mock_zeroconf.get_service_info.side_effect = \ + get_homekit_info_mock("LIFX bulb") assert await async_setup_component( hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert len(mock_service_browser.mock_calls) == 1 assert len(mock_config_flow.mock_calls) == 2 assert mock_config_flow.mock_calls[0][1][0] == 'lifx' + + +async def test_homekit_match_full(hass, mock_zeroconf): + """Test configured options for a device are loaded via config entry.""" + with patch.dict( + zc_gen.ZEROCONF, { + zeroconf.HOMEKIT_TYPE: ["homekit_controller"] + }, clear=True + ), patch.object( + hass.config_entries, 'flow' + ) as mock_config_flow, patch.object( + zeroconf, 'ServiceBrowser', side_effect=service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = \ + get_homekit_info_mock("BSB002") + assert await async_setup_component( + hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 2 + assert mock_config_flow.mock_calls[0][1][0] == 'hue'