From b75476e84466f184e584d21d941fb1dbd7efe2ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Nov 2021 07:44:15 -0800 Subject: [PATCH] Add support for matching the zeroconf model property (#58922) --- homeassistant/components/zeroconf/__init__.py | 10 ++++ script/hassfest/manifest.py | 1 + tests/components/zeroconf/test_init.py | 51 +++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index f49fc708a1f..44efc77d222 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -397,6 +397,11 @@ class ZeroconfDiscovery: else: lowercase_manufacturer = None + if "model" in info[ATTR_PROPERTIES]: + lowercase_model: str | None = info[ATTR_PROPERTIES]["model"].lower() + else: + lowercase_model = None + # Not all homekit types are currently used for discovery # so not all service type exist in zeroconf_types for matcher in self.zeroconf_types.get(service_type, []): @@ -418,6 +423,11 @@ class ZeroconfDiscovery: ) ): continue + if "model" in matcher and ( + lowercase_model is None + or not fnmatch.fnmatch(lowercase_model, matcher["model"]) + ): + continue discovery_flow.async_create_flow( self.hass, diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 0bec9e51436..ecc00142e30 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -187,6 +187,7 @@ MANIFEST_SCHEMA = vol.Schema( str, verify_uppercase, verify_wildcard ), vol.Optional("manufacturer"): vol.All(str, verify_lowercase), + vol.Optional("model"): vol.All(str, verify_lowercase), vol.Optional("name"): vol.All(str, verify_lowercase), } ), diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index edf29a32f69..d04ddd3dd4b 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -126,6 +126,24 @@ def get_zeroconf_info_mock_manufacturer(manufacturer): return mock_zc_info +def get_zeroconf_info_mock_model(model): + """Return info for get_service_info for an zeroconf device.""" + + def mock_zc_info(service_type, name): + return AsyncServiceInfo( + service_type, + name, + addresses=[b"\n\x00\x00\x14"], + port=80, + weight=0, + priority=0, + server="name.local.", + properties={b"model": model.encode()}, + ) + + return mock_zc_info + + async def test_setup(hass, mock_async_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.object( @@ -330,6 +348,39 @@ async def test_zeroconf_match_manufacturer(hass, mock_async_zeroconf): assert mock_config_flow.mock_calls[0][1][0] == "samsungtv" +async def test_zeroconf_match_model(hass, mock_async_zeroconf): + """Test matching a specific model in zeroconf.""" + + def http_only_service_update_mock(ipv6, zeroconf, services, handlers): + """Call service update handler.""" + handlers[0]( + zeroconf, + "_airplay._tcp.local.", + "s1000._airplay._tcp.local.", + ServiceStateChange.Added, + ) + + with patch.dict( + zc_gen.ZEROCONF, + {"_airplay._tcp.local.": [{"domain": "appletv", "model": "appletv*"}]}, + clear=True, + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, "HaAsyncServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_zeroconf_info_mock_model("appletv"), + ): + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + 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] == "appletv" + + async def test_zeroconf_match_manufacturer_not_present(hass, mock_async_zeroconf): """Test matchers reject when a property is missing."""