diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 38544798b9b..d2eaa6ca766 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -279,6 +279,13 @@ async def _async_start_zeroconf_browser( else: uppercase_mac = None + if "manufacturer" in info["properties"]: + lowercase_manufacturer: str | None = info["properties"][ + "manufacturer" + ].lower() + else: + lowercase_manufacturer = None + # Not all homekit types are currently used for discovery # so not all service type exist in zeroconf_types for entry in zeroconf_types.get(service_type, []): @@ -295,6 +302,14 @@ async def _async_start_zeroconf_browser( and not fnmatch.fnmatch(lowercase_name, entry["name"]) ): continue + if ( + lowercase_manufacturer is not None + and "manufacturer" in entry + and not fnmatch.fnmatch( + lowercase_manufacturer, entry["manufacturer"] + ) + ): + continue hass.add_job( hass.config_entries.flow.async_init( diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 7bd670fb4c9..e7a30abc73f 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -104,6 +104,24 @@ def get_zeroconf_info_mock(macaddress): return mock_zc_info +def get_zeroconf_info_mock_manufacturer(manufacturer): + """Return info for get_service_info for an zeroconf device.""" + + def mock_zc_info(service_type, name): + return ServiceInfo( + service_type, + name, + addresses=[b"\n\x00\x00\x14"], + port=80, + weight=0, + priority=0, + server="name.local.", + properties={b"manufacturer": manufacturer.encode()}, + ) + + return mock_zc_info + + async def test_setup(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.object( @@ -237,7 +255,7 @@ async def test_service_with_invalid_name(hass, mock_zeroconf, caplog): assert "Failed to get info for device name" in caplog.text -async def test_zeroconf_match(hass, mock_zeroconf): +async def test_zeroconf_match_macaddress(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" def http_only_service_update_mock(zeroconf, services, handlers): @@ -274,6 +292,39 @@ async def test_zeroconf_match(hass, mock_zeroconf): assert mock_config_flow.mock_calls[0][1][0] == "shelly" +async def test_zeroconf_match_manufacturer(hass, mock_zeroconf): + """Test configured options for a device are loaded via config entry.""" + + def http_only_service_update_mock(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": "samsungtv", "manufacturer": "samsung*"}]}, + clear=True, + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = ( + get_zeroconf_info_mock_manufacturer("Samsung Electronics") + ) + 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] == "samsungtv" + + async def test_zeroconf_no_match(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" @@ -306,6 +357,38 @@ async def test_zeroconf_no_match(hass, mock_zeroconf): assert len(mock_config_flow.mock_calls) == 0 +async def test_zeroconf_no_match_manufacturer(hass, mock_zeroconf): + """Test configured options for a device are loaded via config entry.""" + + def http_only_service_update_mock(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": "samsungtv", "manufacturer": "samsung*"}]}, + clear=True, + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = ( + get_zeroconf_info_mock_manufacturer("Not Samsung Electronics") + ) + 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) == 0 + + async def test_homekit_match_partial_space(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict(