From 16cc4aed0634f452fd66b9f53208deafcfe64161 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 May 2020 14:59:29 -0500 Subject: [PATCH] Ensure homekit_controller recieves zeroconf c# updates (#35545) * Ensure homekit_controller recieves zeroconf c# updates If an integration has a homekit config flow step homekit controller would not see updates for devices that were paired with it and would not rescan for changes. * Only send updates to homekit controller if the device is paired This avoids the device showing up a second time. * remove debug * fix refactor error --- homeassistant/components/zeroconf/__init__.py | 30 +++++++-- tests/components/zeroconf/test_init.py | 65 +++++++++++++++++-- 2 files changed, 84 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 343897a44c0..8376ed09e6c 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -43,6 +43,10 @@ HOMEKIT_TYPE = "_hap._tcp.local." CONF_DEFAULT_INTERFACE = "default_interface" DEFAULT_DEFAULT_INTERFACE = False +HOMEKIT_PROPERTIES = "properties" +HOMEKIT_PAIRED_STATUS_FLAG = "sf" +HOMEKIT_MODEL = "md" + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -178,8 +182,26 @@ def setup(hass, config): _LOGGER.debug("Discovered new device %s %s", name, info) # If we can handle it as a HomeKit discovery, we do that here. - if service_type == HOMEKIT_TYPE and handle_homekit(hass, info): - return + if service_type == HOMEKIT_TYPE: + handle_homekit(hass, info) + # Continue on here as homekit_controller + # still needs to get updates on devices + # so it can see when the 'c#' field is updated. + # + # We only send updates to homekit_controller + # if the device is already paired in order to avoid + # offering a second discovery for the same device + if ( + HOMEKIT_PROPERTIES in info + and HOMEKIT_PAIRED_STATUS_FLAG in info[HOMEKIT_PROPERTIES] + ): + try: + if not int(info[HOMEKIT_PROPERTIES][HOMEKIT_PAIRED_STATUS_FLAG]): + return + except ValueError: + # HomeKit pairing status unknown + # likely bad homekit data + return for domain in ZEROCONF[service_type]: hass.add_job( @@ -203,10 +225,10 @@ def handle_homekit(hass, info) -> bool: Return if discovery was forwarded. """ model = None - props = info.get("properties", {}) + props = info.get(HOMEKIT_PROPERTIES, {}) for key in props: - if key.lower() == "md": + if key.lower() == HOMEKIT_MODEL: model = props[key] break diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 66a4a5bf44c..9d6d08d5b27 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -18,6 +18,9 @@ PROPERTIES = { NON_ASCII_KEY: None, } +HOMEKIT_STATUS_UNPAIRED = b"0" +HOMEKIT_STATUS_PAIRED = b"1" + @pytest.fixture def mock_zeroconf(): @@ -45,8 +48,8 @@ def get_service_info_mock(service_type, name): ) -def get_homekit_info_mock(model): - """Return homekit info for get_service_info.""" +def get_homekit_info_mock(model, pairing_status): + """Return homekit info for get_service_info for an homekit device.""" def mock_homekit_info(service_type, name): return ServiceInfo( @@ -57,7 +60,7 @@ def get_homekit_info_mock(model): weight=0, priority=0, server="name.local.", - properties={b"md": model.encode()}, + properties={b"md": model.encode(), b"sf": pairing_status}, ) return mock_homekit_info @@ -119,7 +122,9 @@ async def test_homekit_match_partial_space(hass, mock_zeroconf): ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock("LIFX bulb") + mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( + "LIFX bulb", HOMEKIT_STATUS_UNPAIRED + ) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert len(mock_service_browser.mock_calls) == 1 @@ -137,7 +142,7 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf): zeroconf, "HaServiceBrowser", side_effect=service_update_mock ) as mock_service_browser: mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( - "Rachio-fa46ba" + "Rachio-fa46ba", HOMEKIT_STATUS_UNPAIRED ) assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -155,12 +160,58 @@ async def test_homekit_match_full(hass, mock_zeroconf): ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock("BSB002") + mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( + "BSB002", HOMEKIT_STATUS_UNPAIRED + ) + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + + homekit_mock = get_homekit_info_mock("BSB002", HOMEKIT_STATUS_UNPAIRED) + info = homekit_mock("_hap._tcp.local.", "BSB002._hap._tcp.local.") + import pprint + + pprint.pprint(["homekit", info]) + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "hue" + + +async def test_homekit_already_paired(hass, mock_zeroconf): + """Test that an already paired device is sent to homekit_controller.""" + with patch.dict( + zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( + "tado", HOMEKIT_STATUS_PAIRED + ) + 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] == "tado" + assert mock_config_flow.mock_calls[1][1][0] == "homekit_controller" + + +async def test_homekit_invalid_paring_status(hass, mock_zeroconf): + """Test that missing paring data is not sent to homekit_controller.""" + with patch.dict( + zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( + "tado", b"invalid" + ) 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) == 1 - assert mock_config_flow.mock_calls[0][1][0] == "hue" + assert mock_config_flow.mock_calls[0][1][0] == "tado" async def test_info_from_service_non_utf8(hass):