Ensure ssdp can callback messages that do not have an ST (#51436)
* Ensure ssdp can callback messages that do not have an ST Sonos sends unsolicited messages when the device reboots. We want to capture these to ensure we can recover the subscriptions as soon as the device reboots * Update homeassistant/components/ssdp/__init__.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
909140a7c6
commit
12ac4109f4
2 changed files with 107 additions and 10 deletions
|
@ -153,7 +153,7 @@ class Scanner:
|
|||
) -> None:
|
||||
"""Initialize class."""
|
||||
self.hass = hass
|
||||
self.seen: set[tuple[str, str]] = set()
|
||||
self.seen: set[tuple[str, str | None]] = set()
|
||||
self.cache: dict[tuple[str, str], Mapping[str, str]] = {}
|
||||
self._integration_matchers = integration_matchers
|
||||
self._cancel_scan: Callable[[], None] | None = None
|
||||
|
@ -268,20 +268,28 @@ class Scanner:
|
|||
domains.add(domain)
|
||||
return domains
|
||||
|
||||
def _async_seen(self, header_st: str | None, header_location: str | None) -> bool:
|
||||
"""Check if we have seen a specific st and optional location."""
|
||||
if header_st is None:
|
||||
return True
|
||||
return (header_st, header_location) in self.seen
|
||||
|
||||
def _async_see(self, header_st: str | None, header_location: str | None) -> None:
|
||||
"""Mark a specific st and optional location as seen."""
|
||||
if header_st is not None:
|
||||
self.seen.add((header_st, header_location))
|
||||
|
||||
async def _async_process_entry(self, headers: Mapping[str, str]) -> None:
|
||||
"""Process SSDP entries."""
|
||||
_LOGGER.debug("_async_process_entry: %s", headers)
|
||||
if "st" not in headers or "location" not in headers:
|
||||
return
|
||||
h_st = headers["st"]
|
||||
h_location = headers["location"]
|
||||
key = (h_st, h_location)
|
||||
h_st = headers.get("st")
|
||||
h_location = headers.get("location")
|
||||
|
||||
if udn := _udn_from_usn(headers.get("usn")):
|
||||
if h_st and (udn := _udn_from_usn(headers.get("usn"))):
|
||||
self.cache[(udn, h_st)] = headers
|
||||
|
||||
callbacks = self._async_get_matching_callbacks(headers)
|
||||
if key in self.seen and not callbacks:
|
||||
if self._async_seen(h_st, h_location) and not callbacks:
|
||||
return
|
||||
|
||||
assert self.description_manager is not None
|
||||
|
@ -290,9 +298,10 @@ class Scanner:
|
|||
discovery_info = discovery_info_from_headers_and_request(info_with_req)
|
||||
|
||||
_async_process_callbacks(callbacks, discovery_info)
|
||||
if key in self.seen:
|
||||
|
||||
if self._async_seen(h_st, h_location):
|
||||
return
|
||||
self.seen.add(key)
|
||||
self._async_see(h_st, h_location)
|
||||
|
||||
for domain in self._async_matching_domains(info_with_req):
|
||||
_LOGGER.debug("Discovered %s at %s", domain, h_location)
|
||||
|
|
|
@ -478,6 +478,94 @@ async def test_scan_with_registered_callback(hass, aioclient_mock, caplog):
|
|||
assert "Failed to callback info" in caplog.text
|
||||
|
||||
|
||||
async def test_unsolicited_ssdp_registered_callback(hass, aioclient_mock, caplog):
|
||||
"""Test matching based on callback can handle unsolicited ssdp traffic without st."""
|
||||
aioclient_mock.get(
|
||||
"http://10.6.9.12:1400/xml/device_description.xml",
|
||||
text="""
|
||||
<root>
|
||||
<device>
|
||||
<deviceType>Paulus</deviceType>
|
||||
</device>
|
||||
</root>
|
||||
""",
|
||||
)
|
||||
mock_ssdp_response = {
|
||||
"location": "http://10.6.9.12:1400/xml/device_description.xml",
|
||||
"nt": "uuid:RINCON_1111BB963FD801400",
|
||||
"nts": "ssdp:alive",
|
||||
"server": "Linux UPnP/1.0 Sonos/63.2-88230 (ZPS12)",
|
||||
"usn": "uuid:RINCON_1111BB963FD801400",
|
||||
"x-rincon-household": "Sonos_dfjfkdghjhkjfhkdjfhkd",
|
||||
"x-rincon-bootseq": "250",
|
||||
"bootid.upnp.org": "250",
|
||||
"x-rincon-wifimode": "0",
|
||||
"x-rincon-variant": "1",
|
||||
"household.smartspeaker.audio": "Sonos_v3294823948542543534",
|
||||
}
|
||||
intergration_callbacks = []
|
||||
|
||||
@callback
|
||||
def _async_intergration_callbacks(info):
|
||||
intergration_callbacks.append(info)
|
||||
|
||||
def _generate_fake_ssdp_listener(*args, **kwargs):
|
||||
listener = SSDPListener(*args, **kwargs)
|
||||
|
||||
async def _async_callback(*_):
|
||||
await listener.async_callback(mock_ssdp_response)
|
||||
|
||||
@callback
|
||||
def _callback(*_):
|
||||
hass.async_create_task(listener.async_callback(mock_ssdp_response))
|
||||
|
||||
listener.async_start = _async_callback
|
||||
listener.async_search = _callback
|
||||
return listener
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.ssdp.SSDPListener",
|
||||
new=_generate_fake_ssdp_listener,
|
||||
):
|
||||
hass.state = CoreState.stopped
|
||||
assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
ssdp.async_register_callback(
|
||||
hass,
|
||||
_async_intergration_callbacks,
|
||||
{"nts": "ssdp:alive", "x-rincon-bootseq": MATCH_ALL},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
|
||||
await hass.async_block_till_done()
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
hass.state = CoreState.running
|
||||
await hass.async_block_till_done()
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
|
||||
await hass.async_block_till_done()
|
||||
assert hass.state == CoreState.running
|
||||
|
||||
assert (
|
||||
len(intergration_callbacks) == 2
|
||||
) # unsolicited callbacks without st are not cached
|
||||
assert intergration_callbacks[0] == {
|
||||
"UDN": "uuid:RINCON_1111BB963FD801400",
|
||||
"bootid.upnp.org": "250",
|
||||
"deviceType": "Paulus",
|
||||
"household.smartspeaker.audio": "Sonos_v3294823948542543534",
|
||||
"nt": "uuid:RINCON_1111BB963FD801400",
|
||||
"nts": "ssdp:alive",
|
||||
"ssdp_location": "http://10.6.9.12:1400/xml/device_description.xml",
|
||||
"ssdp_server": "Linux UPnP/1.0 Sonos/63.2-88230 (ZPS12)",
|
||||
"ssdp_usn": "uuid:RINCON_1111BB963FD801400",
|
||||
"x-rincon-bootseq": "250",
|
||||
"x-rincon-household": "Sonos_dfjfkdghjhkjfhkdjfhkd",
|
||||
"x-rincon-variant": "1",
|
||||
"x-rincon-wifimode": "0",
|
||||
}
|
||||
assert "Failed to callback info" not in caplog.text
|
||||
|
||||
|
||||
async def test_scan_second_hit(hass, aioclient_mock, caplog):
|
||||
"""Test matching on second scan."""
|
||||
aioclient_mock.get(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue