2019-05-27 02:48:27 +00:00
|
|
|
"""Test the SSDP integration."""
|
2021-09-11 23:38:16 +00:00
|
|
|
from datetime import datetime, timedelta
|
2021-05-29 02:18:59 +00:00
|
|
|
from ipaddress import IPv4Address, IPv6Address
|
2021-09-11 23:38:16 +00:00
|
|
|
from unittest.mock import ANY, AsyncMock, patch
|
2019-05-27 02:48:27 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
from async_upnp_client.ssdp import udn_from_headers
|
|
|
|
from async_upnp_client.ssdp_listener import SsdpListener
|
2021-05-29 02:18:59 +00:00
|
|
|
from async_upnp_client.utils import CaseInsensitiveDict
|
2019-05-30 23:23:42 +00:00
|
|
|
import pytest
|
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
import homeassistant
|
2021-04-25 09:27:40 +00:00
|
|
|
from homeassistant import config_entries
|
2019-05-27 02:48:27 +00:00
|
|
|
from homeassistant.components import ssdp
|
2021-06-03 18:26:37 +00:00
|
|
|
from homeassistant.const import (
|
|
|
|
EVENT_HOMEASSISTANT_STARTED,
|
|
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
|
|
MATCH_ALL,
|
|
|
|
)
|
2021-04-14 20:23:15 +00:00
|
|
|
from homeassistant.setup import async_setup_component
|
|
|
|
import homeassistant.util.dt as dt_util
|
2019-05-27 02:48:27 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
from tests.common import async_fire_time_changed
|
2019-05-27 02:48:27 +00:00
|
|
|
|
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
def _ssdp_headers(headers):
|
2021-10-08 15:57:49 +00:00
|
|
|
ssdp_headers = CaseInsensitiveDict(headers, _timestamp=datetime(2021, 1, 1, 12, 00))
|
|
|
|
ssdp_headers["_udn"] = udn_from_headers(ssdp_headers)
|
|
|
|
return ssdp_headers
|
2021-02-18 10:00:11 +00:00
|
|
|
|
2021-05-29 02:18:59 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
async def init_ssdp_component(hass: homeassistant) -> SsdpListener:
|
|
|
|
"""Initialize ssdp component and get SsdpListener."""
|
|
|
|
await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
return hass.data[ssdp.DOMAIN]._ssdp_listeners[0]
|
2021-05-29 02:18:59 +00:00
|
|
|
|
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
@patch(
|
|
|
|
"homeassistant.components.ssdp.async_get_ssdp",
|
|
|
|
return_value={"mock-domain": [{"st": "mock-st"}]},
|
|
|
|
)
|
|
|
|
@pytest.mark.usefixtures("mock_get_source_ip")
|
|
|
|
async def test_ssdp_flow_dispatched_on_st(mock_get_ssdp, hass, caplog, mock_flow_init):
|
2021-05-29 02:18:59 +00:00
|
|
|
"""Test matching based on ST."""
|
2021-09-11 23:38:16 +00:00
|
|
|
mock_ssdp_search_response = _ssdp_headers(
|
|
|
|
{
|
|
|
|
"st": "mock-st",
|
2021-10-08 15:57:49 +00:00
|
|
|
"location": "http://1.1.1.1",
|
2021-09-11 23:38:16 +00:00
|
|
|
"usn": "uuid:mock-udn::mock-st",
|
|
|
|
"server": "mock-server",
|
|
|
|
"ext": "",
|
|
|
|
}
|
|
|
|
)
|
|
|
|
ssdp_listener = await init_ssdp_component(hass)
|
|
|
|
await ssdp_listener._on_search(mock_ssdp_search_response)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
|
|
await hass.async_block_till_done()
|
2019-05-27 02:48:27 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
assert len(mock_flow_init.mock_calls) == 1
|
|
|
|
assert mock_flow_init.mock_calls[0][1][0] == "mock-domain"
|
|
|
|
assert mock_flow_init.mock_calls[0][2]["context"] == {
|
2021-04-25 09:27:40 +00:00
|
|
|
"source": config_entries.SOURCE_SSDP
|
|
|
|
}
|
2021-09-11 23:38:16 +00:00
|
|
|
assert mock_flow_init.mock_calls[0][2]["data"] == {
|
2020-09-14 07:18:09 +00:00
|
|
|
ssdp.ATTR_SSDP_ST: "mock-st",
|
2021-10-08 15:57:49 +00:00
|
|
|
ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1",
|
2021-09-11 23:38:16 +00:00
|
|
|
ssdp.ATTR_SSDP_USN: "uuid:mock-udn::mock-st",
|
2020-09-14 07:18:09 +00:00
|
|
|
ssdp.ATTR_SSDP_SERVER: "mock-server",
|
|
|
|
ssdp.ATTR_SSDP_EXT: "",
|
2021-09-11 23:38:16 +00:00
|
|
|
ssdp.ATTR_UPNP_UDN: "uuid:mock-udn",
|
Config-flow for DLNA-DMR integration (#55267)
* Modernize dlna_dmr component: configflow, test, types
* Support config-flow with ssdp discovery
* Add unit tests
* Enforce strict typing
* Gracefully handle network devices (dis)appearing
* Fix Aiohttp mock response headers type to match actual response class
* Fixes from code review
* Fixes from code review
* Import device config in flow if unavailable at hass start
* Support SSDP advertisements
* Ignore bad BOOTID, fix ssdp:byebye handling
* Only listen for events on interface connected to device
* Release all listeners when entities are removed
* Warn about deprecated dlna_dmr configuration
* Use sublogger for dlna_dmr.config_flow for easier filtering
* Tests for dlna_dmr.data module
* Rewrite DMR tests for HA style
* Fix DMR strings: "Digital Media *Renderer*"
* Update DMR entity state and device info when changed
* Replace deprecated async_upnp_client State with TransportState
* supported_features are dynamic, based on current device state
* Cleanup fully when subscription fails
* Log warnings when device connection fails unexpectedly
* Set PARALLEL_UPDATES to unlimited
* Fix spelling
* Fixes from code review
* Simplify has & can checks to just can, which includes has
* Treat transitioning state as playing (not idle) to reduce UI jerking
* Test if device is usable
* Handle ssdp:update messages properly
* Fix _remove_ssdp_callbacks being shared by all DlnaDmrEntity instances
* Fix tests for transitioning state
* Mock DmrDevice.is_profile_device (added to support embedded devices)
* Use ST & NT SSDP headers to find DMR devices, not deviceType
The deviceType is extracted from the device's description XML, and will not
be what we want when dealing with embedded devices.
* Use UDN from SSDP headers, not device description, as unique_id
The SSDP headers have the UDN of the embedded device that we're interested
in, whereas the device description (`ATTR_UPNP_UDN`) field will always be
for the root device.
* Fix DMR string English localization
* Test config flow with UDN from SSDP headers
* Bump async-upnp-client==0.22.1, fix flake8 error
* fix test for remapping
* DMR HA Device connections based on root and embedded UDN
* DmrDevice's UpnpDevice is now named profile_device
* Use device type from SSDP headers, not device description
* Mark dlna_dmr constants as Final
* Use embedded device UDN and type for unique ID when connected via URL
* More informative connection error messages
* Also match SSDP messages on NT headers
The NT header is to ssdp:alive messages what ST is to M-SEARCH responses.
* Bump async-upnp-client==0.22.2
* fix merge
* Bump async-upnp-client==0.22.3
Co-authored-by: Steven Looman <steven.looman@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2021-09-27 20:47:01 +00:00
|
|
|
ssdp.ATTR_SSDP_UDN: ANY,
|
2021-09-11 23:38:16 +00:00
|
|
|
"_timestamp": ANY,
|
2021-10-22 20:26:33 +00:00
|
|
|
ssdp.ATTR_HA_MATCHING_DOMAINS: {"mock-domain"},
|
2020-09-14 07:18:09 +00:00
|
|
|
}
|
2020-11-13 08:40:25 +00:00
|
|
|
assert "Failed to fetch ssdp data" not in caplog.text
|
2019-05-27 02:48:27 +00:00
|
|
|
|
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
@pytest.mark.usefixtures("mock_get_source_ip")
|
|
|
|
@patch(
|
|
|
|
"homeassistant.components.ssdp.async_get_ssdp",
|
|
|
|
return_value={"mock-domain": [{"manufacturer": "Paulus"}]},
|
2019-12-19 17:28:03 +00:00
|
|
|
)
|
2021-09-11 23:38:16 +00:00
|
|
|
async def test_scan_match_upnp_devicedesc_manufacturer(
|
|
|
|
mock_get_ssdp, hass, aioclient_mock, mock_flow_init
|
2021-09-02 18:44:50 +00:00
|
|
|
):
|
2019-11-02 19:30:09 +00:00
|
|
|
"""Test matching based on UPnP device description data."""
|
2019-07-31 19:25:30 +00:00
|
|
|
aioclient_mock.get(
|
|
|
|
"http://1.1.1.1",
|
2021-09-11 23:38:16 +00:00
|
|
|
text="""
|
2019-05-27 02:48:27 +00:00
|
|
|
<root>
|
|
|
|
<device>
|
2021-09-11 23:38:16 +00:00
|
|
|
<manufacturer>Paulus</manufacturer>
|
2019-05-27 02:48:27 +00:00
|
|
|
</device>
|
|
|
|
</root>
|
2019-07-31 19:25:30 +00:00
|
|
|
""",
|
|
|
|
)
|
2021-09-11 23:38:16 +00:00
|
|
|
mock_ssdp_search_response = _ssdp_headers(
|
|
|
|
{
|
|
|
|
"st": "mock-st",
|
|
|
|
"location": "http://1.1.1.1",
|
|
|
|
"usn": "uuid:mock-udn::mock-st",
|
|
|
|
}
|
|
|
|
)
|
|
|
|
ssdp_listener = await init_ssdp_component(hass)
|
|
|
|
await ssdp_listener._on_search(mock_ssdp_search_response)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
|
2021-06-11 11:35:03 +00:00
|
|
|
# If we get duplicate response, ensure we only look it up once
|
2021-04-14 20:23:15 +00:00
|
|
|
assert len(aioclient_mock.mock_calls) == 1
|
2021-09-11 23:38:16 +00:00
|
|
|
assert len(mock_flow_init.mock_calls) == 1
|
|
|
|
assert mock_flow_init.mock_calls[0][1][0] == "mock-domain"
|
|
|
|
assert mock_flow_init.mock_calls[0][2]["context"] == {
|
2021-04-25 09:27:40 +00:00
|
|
|
"source": config_entries.SOURCE_SSDP
|
|
|
|
}
|
2019-05-27 02:48:27 +00:00
|
|
|
|
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
@pytest.mark.usefixtures("mock_get_source_ip")
|
|
|
|
@patch(
|
|
|
|
"homeassistant.components.ssdp.async_get_ssdp",
|
|
|
|
return_value={"mock-domain": [{"deviceType": "Paulus"}]},
|
|
|
|
)
|
|
|
|
async def test_scan_match_upnp_devicedesc_devicetype(
|
|
|
|
mock_get_ssdp, hass, aioclient_mock, mock_flow_init
|
|
|
|
):
|
|
|
|
"""Test matching based on UPnP device description data."""
|
2019-07-31 19:25:30 +00:00
|
|
|
aioclient_mock.get(
|
|
|
|
"http://1.1.1.1",
|
2020-04-30 19:37:58 +00:00
|
|
|
text="""
|
2019-05-27 02:48:27 +00:00
|
|
|
<root>
|
|
|
|
<device>
|
|
|
|
<deviceType>Paulus</deviceType>
|
|
|
|
</device>
|
|
|
|
</root>
|
2019-07-31 19:25:30 +00:00
|
|
|
""",
|
|
|
|
)
|
2021-09-11 23:38:16 +00:00
|
|
|
mock_ssdp_search_response = _ssdp_headers(
|
|
|
|
{
|
|
|
|
"st": "mock-st",
|
|
|
|
"location": "http://1.1.1.1",
|
|
|
|
"usn": "uuid:mock-udn::mock-st",
|
|
|
|
}
|
|
|
|
)
|
|
|
|
ssdp_listener = await init_ssdp_component(hass)
|
|
|
|
await ssdp_listener._on_search(mock_ssdp_search_response)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
|
|
# If we get duplicate response, ensure we only look it up once
|
|
|
|
assert len(aioclient_mock.mock_calls) == 1
|
|
|
|
assert len(mock_flow_init.mock_calls) == 1
|
|
|
|
assert mock_flow_init.mock_calls[0][1][0] == "mock-domain"
|
|
|
|
assert mock_flow_init.mock_calls[0][2]["context"] == {
|
|
|
|
"source": config_entries.SOURCE_SSDP
|
2021-05-29 02:18:59 +00:00
|
|
|
}
|
2021-09-11 23:38:16 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_get_source_ip")
|
|
|
|
@patch(
|
|
|
|
"homeassistant.components.ssdp.async_get_ssdp",
|
|
|
|
return_value={
|
2021-05-29 02:18:59 +00:00
|
|
|
"mock-domain": [
|
2021-04-14 20:23:15 +00:00
|
|
|
{
|
2021-05-29 02:18:59 +00:00
|
|
|
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
|
|
|
|
ssdp.ATTR_UPNP_MANUFACTURER: "Paulus",
|
2021-04-14 20:23:15 +00:00
|
|
|
}
|
2021-05-29 02:18:59 +00:00
|
|
|
]
|
2021-09-11 23:38:16 +00:00
|
|
|
},
|
|
|
|
)
|
|
|
|
async def test_scan_not_all_present(
|
|
|
|
mock_get_ssdp, hass, aioclient_mock, mock_flow_init
|
|
|
|
):
|
|
|
|
"""Test match fails if some specified attributes are not present."""
|
2019-11-02 19:30:09 +00:00
|
|
|
aioclient_mock.get(
|
|
|
|
"http://1.1.1.1",
|
2020-04-30 19:37:58 +00:00
|
|
|
text="""
|
2019-11-02 19:30:09 +00:00
|
|
|
<root>
|
|
|
|
<device>
|
|
|
|
<deviceType>Paulus</deviceType>
|
|
|
|
</device>
|
|
|
|
</root>
|
|
|
|
""",
|
|
|
|
)
|
2021-09-11 23:38:16 +00:00
|
|
|
mock_ssdp_search_response = _ssdp_headers(
|
2021-05-30 03:50:48 +00:00
|
|
|
{
|
2021-09-11 23:38:16 +00:00
|
|
|
"st": "mock-st",
|
|
|
|
"location": "http://1.1.1.1",
|
2021-05-30 03:50:48 +00:00
|
|
|
}
|
2021-09-11 23:38:16 +00:00
|
|
|
)
|
|
|
|
ssdp_listener = await init_ssdp_component(hass)
|
|
|
|
await ssdp_listener._on_search(mock_ssdp_search_response)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
|
|
await hass.async_block_till_done()
|
2021-05-30 03:50:48 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
assert not mock_flow_init.mock_calls
|
2019-05-30 23:23:42 +00:00
|
|
|
|
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
@pytest.mark.usefixtures("mock_get_source_ip")
|
|
|
|
@patch(
|
|
|
|
"homeassistant.components.ssdp.async_get_ssdp",
|
|
|
|
return_value={
|
2021-05-29 02:18:59 +00:00
|
|
|
"mock-domain": [
|
2021-04-14 20:23:15 +00:00
|
|
|
{
|
2021-05-29 02:18:59 +00:00
|
|
|
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
|
2021-09-11 23:38:16 +00:00
|
|
|
ssdp.ATTR_UPNP_MANUFACTURER: "Not-Paulus",
|
2021-04-14 20:23:15 +00:00
|
|
|
}
|
2021-05-29 02:18:59 +00:00
|
|
|
]
|
2021-09-11 23:38:16 +00:00
|
|
|
},
|
|
|
|
)
|
|
|
|
async def test_scan_not_all_match(mock_get_ssdp, hass, aioclient_mock, mock_flow_init):
|
|
|
|
"""Test match fails if some specified attribute values differ."""
|
2021-01-31 16:59:14 +00:00
|
|
|
aioclient_mock.get(
|
|
|
|
"http://1.1.1.1",
|
|
|
|
text="""
|
|
|
|
<root>
|
|
|
|
<device>
|
2021-09-11 23:38:16 +00:00
|
|
|
<deviceType>Paulus</deviceType>
|
|
|
|
<manufacturer>Paulus</manufacturer>
|
2021-01-31 16:59:14 +00:00
|
|
|
</device>
|
|
|
|
</root>
|
|
|
|
""",
|
|
|
|
)
|
2021-09-11 23:38:16 +00:00
|
|
|
mock_ssdp_search_response = _ssdp_headers(
|
|
|
|
{
|
|
|
|
"st": "mock-st",
|
|
|
|
"location": "http://1.1.1.1",
|
|
|
|
"usn": "uuid:mock-udn::mock-st",
|
|
|
|
}
|
|
|
|
)
|
|
|
|
ssdp_listener = await init_ssdp_component(hass)
|
|
|
|
await ssdp_listener._on_search(mock_ssdp_search_response)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
|
|
await hass.async_block_till_done()
|
2021-01-31 16:59:14 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
assert not mock_flow_init.mock_calls
|
2021-04-14 20:23:15 +00:00
|
|
|
|
|
|
|
|
2021-09-27 06:39:22 +00:00
|
|
|
@pytest.mark.usefixtures("mock_get_source_ip")
|
|
|
|
@patch(
|
|
|
|
"homeassistant.components.ssdp.async_get_ssdp",
|
|
|
|
return_value={"mock-domain": [{"deviceType": "Paulus"}]},
|
|
|
|
)
|
|
|
|
async def test_flow_start_only_alive(
|
|
|
|
mock_get_ssdp, hass, aioclient_mock, mock_flow_init
|
|
|
|
):
|
|
|
|
"""Test config flow is only started for alive devices."""
|
|
|
|
aioclient_mock.get(
|
|
|
|
"http://1.1.1.1",
|
|
|
|
text="""
|
|
|
|
<root>
|
|
|
|
<device>
|
|
|
|
<deviceType>Paulus</deviceType>
|
|
|
|
</device>
|
|
|
|
</root>
|
|
|
|
""",
|
|
|
|
)
|
|
|
|
ssdp_listener = await init_ssdp_component(hass)
|
|
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
|
|
# Search should start a flow
|
|
|
|
mock_ssdp_search_response = _ssdp_headers(
|
|
|
|
{
|
|
|
|
"st": "mock-st",
|
|
|
|
"location": "http://1.1.1.1",
|
|
|
|
"usn": "uuid:mock-udn::mock-st",
|
|
|
|
}
|
|
|
|
)
|
|
|
|
await ssdp_listener._on_search(mock_ssdp_search_response)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
|
|
mock_flow_init.assert_awaited_once_with(
|
|
|
|
"mock-domain", context={"source": config_entries.SOURCE_SSDP}, data=ANY
|
|
|
|
)
|
|
|
|
|
|
|
|
# ssdp:alive advertisement should start a flow
|
|
|
|
mock_flow_init.reset_mock()
|
|
|
|
mock_ssdp_advertisement = _ssdp_headers(
|
|
|
|
{
|
|
|
|
"location": "http://1.1.1.1",
|
|
|
|
"usn": "uuid:mock-udn::mock-st",
|
|
|
|
"nt": "upnp:rootdevice",
|
|
|
|
"nts": "ssdp:alive",
|
|
|
|
}
|
|
|
|
)
|
|
|
|
await ssdp_listener._on_alive(mock_ssdp_advertisement)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
mock_flow_init.assert_awaited_once_with(
|
|
|
|
"mock-domain", context={"source": config_entries.SOURCE_SSDP}, data=ANY
|
|
|
|
)
|
|
|
|
|
|
|
|
# ssdp:byebye advertisement should not start a flow
|
|
|
|
mock_flow_init.reset_mock()
|
|
|
|
mock_ssdp_advertisement["nts"] = "ssdp:byebye"
|
|
|
|
await ssdp_listener._on_byebye(mock_ssdp_advertisement)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
mock_flow_init.assert_not_called()
|
|
|
|
|
|
|
|
# ssdp:update advertisement should start a flow
|
|
|
|
mock_flow_init.reset_mock()
|
|
|
|
mock_ssdp_advertisement["nts"] = "ssdp:update"
|
|
|
|
await ssdp_listener._on_update(mock_ssdp_advertisement)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
mock_flow_init.assert_awaited_once_with(
|
|
|
|
"mock-domain", context={"source": config_entries.SOURCE_SSDP}, data=ANY
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-10-21 20:57:34 +00:00
|
|
|
@pytest.mark.usefixtures("mock_get_source_ip")
|
|
|
|
@patch(
|
|
|
|
"homeassistant.components.ssdp.async_get_ssdp",
|
|
|
|
return_value={},
|
|
|
|
)
|
|
|
|
async def test_discovery_from_advertisement_sets_ssdp_st(
|
|
|
|
mock_get_ssdp, hass, aioclient_mock, mock_flow_init
|
|
|
|
):
|
|
|
|
"""Test discovery from advertisement sets `ssdp_st` for more compatibility."""
|
|
|
|
aioclient_mock.get(
|
|
|
|
"http://1.1.1.1",
|
|
|
|
text="""
|
|
|
|
<root>
|
|
|
|
<device>
|
|
|
|
<deviceType>Paulus</deviceType>
|
|
|
|
</device>
|
|
|
|
</root>
|
|
|
|
""",
|
|
|
|
)
|
|
|
|
ssdp_listener = await init_ssdp_component(hass)
|
|
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
|
|
mock_ssdp_advertisement = _ssdp_headers(
|
|
|
|
{
|
|
|
|
"nt": "mock-st",
|
|
|
|
"nts": "ssdp:alive",
|
|
|
|
"location": "http://1.1.1.1",
|
|
|
|
"usn": "uuid:mock-udn::mock-st",
|
|
|
|
}
|
|
|
|
)
|
|
|
|
await ssdp_listener._on_alive(mock_ssdp_advertisement)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
|
|
discovery_info = await ssdp.async_get_discovery_info_by_udn(hass, "uuid:mock-udn")
|
|
|
|
assert discovery_info == [
|
|
|
|
{
|
|
|
|
ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1",
|
|
|
|
ssdp.ATTR_SSDP_NT: "mock-st",
|
|
|
|
ssdp.ATTR_SSDP_ST: "mock-st", # Set by ssdp component, not in original advertisement.
|
|
|
|
ssdp.ATTR_SSDP_USN: "uuid:mock-udn::mock-st",
|
|
|
|
ssdp.ATTR_UPNP_UDN: "uuid:mock-udn",
|
|
|
|
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
|
|
|
|
ssdp.ATTR_SSDP_UDN: ANY,
|
|
|
|
"nts": "ssdp:alive",
|
|
|
|
"_timestamp": ANY,
|
|
|
|
}
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
@patch( # XXX TODO: Isn't this duplicate with mock_get_source_ip?
|
|
|
|
"homeassistant.components.ssdp.Scanner._async_build_source_set",
|
|
|
|
return_value={IPv4Address("192.168.1.1")},
|
|
|
|
)
|
|
|
|
@pytest.mark.usefixtures("mock_get_source_ip")
|
|
|
|
async def test_start_stop_scanner(mock_source_set, hass):
|
2021-04-14 20:23:15 +00:00
|
|
|
"""Test we start and stop the scanner."""
|
2021-09-11 23:38:16 +00:00
|
|
|
ssdp_listener = await init_ssdp_component(hass)
|
2021-04-14 20:23:15 +00:00
|
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
|
|
await hass.async_block_till_done()
|
2021-09-11 23:38:16 +00:00
|
|
|
|
2021-04-14 20:23:15 +00:00
|
|
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
|
|
|
|
await hass.async_block_till_done()
|
2021-09-11 23:38:16 +00:00
|
|
|
assert ssdp_listener.async_start.call_count == 1
|
|
|
|
assert ssdp_listener.async_search.call_count == 4
|
|
|
|
assert ssdp_listener.async_stop.call_count == 0
|
2021-04-14 20:23:15 +00:00
|
|
|
|
|
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
|
|
|
|
await hass.async_block_till_done()
|
2021-09-11 23:38:16 +00:00
|
|
|
assert ssdp_listener.async_start.call_count == 1
|
|
|
|
assert ssdp_listener.async_search.call_count == 4
|
|
|
|
assert ssdp_listener.async_stop.call_count == 1
|
2021-05-29 02:18:59 +00:00
|
|
|
|
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
@pytest.mark.usefixtures("mock_get_source_ip")
|
|
|
|
@patch("homeassistant.components.ssdp.async_get_ssdp", return_value={})
|
2021-09-02 18:44:50 +00:00
|
|
|
async def test_scan_with_registered_callback(
|
2021-09-11 23:38:16 +00:00
|
|
|
mock_get_ssdp, hass, aioclient_mock, caplog
|
2021-09-02 18:44:50 +00:00
|
|
|
):
|
2021-05-29 02:18:59 +00:00
|
|
|
"""Test matching based on callback."""
|
|
|
|
aioclient_mock.get(
|
|
|
|
"http://1.1.1.1",
|
|
|
|
text="""
|
|
|
|
<root>
|
|
|
|
<device>
|
|
|
|
<deviceType>Paulus</deviceType>
|
|
|
|
</device>
|
|
|
|
</root>
|
|
|
|
""",
|
|
|
|
)
|
2021-09-11 23:38:16 +00:00
|
|
|
mock_ssdp_search_response = _ssdp_headers(
|
|
|
|
{
|
|
|
|
"st": "mock-st",
|
|
|
|
"location": "http://1.1.1.1",
|
|
|
|
"usn": "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::mock-st",
|
|
|
|
"server": "mock-server",
|
|
|
|
"x-rincon-bootseq": "55",
|
|
|
|
"ext": "",
|
|
|
|
}
|
|
|
|
)
|
|
|
|
ssdp_listener = await init_ssdp_component(hass)
|
2021-05-29 02:18:59 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
async_exception_callback = AsyncMock(side_effect=ValueError)
|
|
|
|
await ssdp.async_register_callback(hass, async_exception_callback, {})
|
2021-05-29 02:18:59 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
async_integration_callback = AsyncMock()
|
|
|
|
await ssdp.async_register_callback(
|
|
|
|
hass, async_integration_callback, {"st": "mock-st"}
|
|
|
|
)
|
2021-05-29 02:18:59 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
async_integration_match_all_callback1 = AsyncMock()
|
|
|
|
await ssdp.async_register_callback(
|
|
|
|
hass, async_integration_match_all_callback1, {"x-rincon-bootseq": MATCH_ALL}
|
|
|
|
)
|
2021-05-29 02:18:59 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
async_integration_match_all_not_present_callback1 = AsyncMock()
|
|
|
|
await ssdp.async_register_callback(
|
|
|
|
hass,
|
|
|
|
async_integration_match_all_not_present_callback1,
|
|
|
|
{"x-not-there": MATCH_ALL},
|
|
|
|
)
|
2021-05-29 02:18:59 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
async_not_matching_integration_callback1 = AsyncMock()
|
|
|
|
await ssdp.async_register_callback(
|
|
|
|
hass, async_not_matching_integration_callback1, {"st": "not-match-mock-st"}
|
|
|
|
)
|
2021-05-29 02:18:59 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
async_match_any_callback1 = AsyncMock()
|
|
|
|
await ssdp.async_register_callback(hass, async_match_any_callback1)
|
2021-05-29 02:18:59 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
await hass.async_block_till_done()
|
|
|
|
await ssdp_listener._on_search(mock_ssdp_search_response)
|
|
|
|
|
|
|
|
assert async_integration_callback.call_count == 1
|
|
|
|
assert async_integration_match_all_callback1.call_count == 1
|
|
|
|
assert async_integration_match_all_not_present_callback1.call_count == 0
|
|
|
|
assert async_match_any_callback1.call_count == 1
|
|
|
|
assert async_not_matching_integration_callback1.call_count == 0
|
|
|
|
assert async_integration_callback.call_args[0] == (
|
|
|
|
{
|
|
|
|
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
|
|
|
|
ssdp.ATTR_SSDP_EXT: "",
|
|
|
|
ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1",
|
|
|
|
ssdp.ATTR_SSDP_SERVER: "mock-server",
|
|
|
|
ssdp.ATTR_SSDP_ST: "mock-st",
|
|
|
|
ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::mock-st",
|
|
|
|
ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL",
|
|
|
|
"x-rincon-bootseq": "55",
|
Config-flow for DLNA-DMR integration (#55267)
* Modernize dlna_dmr component: configflow, test, types
* Support config-flow with ssdp discovery
* Add unit tests
* Enforce strict typing
* Gracefully handle network devices (dis)appearing
* Fix Aiohttp mock response headers type to match actual response class
* Fixes from code review
* Fixes from code review
* Import device config in flow if unavailable at hass start
* Support SSDP advertisements
* Ignore bad BOOTID, fix ssdp:byebye handling
* Only listen for events on interface connected to device
* Release all listeners when entities are removed
* Warn about deprecated dlna_dmr configuration
* Use sublogger for dlna_dmr.config_flow for easier filtering
* Tests for dlna_dmr.data module
* Rewrite DMR tests for HA style
* Fix DMR strings: "Digital Media *Renderer*"
* Update DMR entity state and device info when changed
* Replace deprecated async_upnp_client State with TransportState
* supported_features are dynamic, based on current device state
* Cleanup fully when subscription fails
* Log warnings when device connection fails unexpectedly
* Set PARALLEL_UPDATES to unlimited
* Fix spelling
* Fixes from code review
* Simplify has & can checks to just can, which includes has
* Treat transitioning state as playing (not idle) to reduce UI jerking
* Test if device is usable
* Handle ssdp:update messages properly
* Fix _remove_ssdp_callbacks being shared by all DlnaDmrEntity instances
* Fix tests for transitioning state
* Mock DmrDevice.is_profile_device (added to support embedded devices)
* Use ST & NT SSDP headers to find DMR devices, not deviceType
The deviceType is extracted from the device's description XML, and will not
be what we want when dealing with embedded devices.
* Use UDN from SSDP headers, not device description, as unique_id
The SSDP headers have the UDN of the embedded device that we're interested
in, whereas the device description (`ATTR_UPNP_UDN`) field will always be
for the root device.
* Fix DMR string English localization
* Test config flow with UDN from SSDP headers
* Bump async-upnp-client==0.22.1, fix flake8 error
* fix test for remapping
* DMR HA Device connections based on root and embedded UDN
* DmrDevice's UpnpDevice is now named profile_device
* Use device type from SSDP headers, not device description
* Mark dlna_dmr constants as Final
* Use embedded device UDN and type for unique ID when connected via URL
* More informative connection error messages
* Also match SSDP messages on NT headers
The NT header is to ssdp:alive messages what ST is to M-SEARCH responses.
* Bump async-upnp-client==0.22.2
* fix merge
* Bump async-upnp-client==0.22.3
Co-authored-by: Steven Looman <steven.looman@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2021-09-27 20:47:01 +00:00
|
|
|
ssdp.ATTR_SSDP_UDN: ANY,
|
2021-09-11 23:38:16 +00:00
|
|
|
"_timestamp": ANY,
|
2021-10-22 20:26:33 +00:00
|
|
|
ssdp.ATTR_HA_MATCHING_DOMAINS: set(),
|
2021-09-11 23:38:16 +00:00
|
|
|
},
|
|
|
|
ssdp.SsdpChange.ALIVE,
|
|
|
|
)
|
2021-05-29 02:18:59 +00:00
|
|
|
assert "Failed to callback info" in caplog.text
|
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
async_integration_callback_from_cache = AsyncMock()
|
|
|
|
await ssdp.async_register_callback(
|
|
|
|
hass, async_integration_callback_from_cache, {"st": "mock-st"}
|
2021-06-05 07:23:51 +00:00
|
|
|
)
|
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
assert async_integration_callback_from_cache.call_count == 1
|
2021-06-05 07:23:51 +00:00
|
|
|
|
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
@pytest.mark.usefixtures("mock_get_source_ip")
|
|
|
|
@patch(
|
|
|
|
"homeassistant.components.ssdp.async_get_ssdp",
|
|
|
|
return_value={"mock-domain": [{"st": "mock-st"}]},
|
|
|
|
)
|
2021-10-13 15:37:14 +00:00
|
|
|
async def test_getting_existing_headers(
|
|
|
|
mock_get_ssdp, hass, aioclient_mock, mock_flow_init
|
|
|
|
):
|
2021-09-11 23:38:16 +00:00
|
|
|
"""Test getting existing/previously scanned headers."""
|
2021-05-29 02:18:59 +00:00
|
|
|
aioclient_mock.get(
|
|
|
|
"http://1.1.1.1",
|
|
|
|
text="""
|
|
|
|
<root>
|
|
|
|
<device>
|
|
|
|
<deviceType>Paulus</deviceType>
|
|
|
|
</device>
|
|
|
|
</root>
|
|
|
|
""",
|
|
|
|
)
|
2021-09-11 23:38:16 +00:00
|
|
|
mock_ssdp_search_response = _ssdp_headers(
|
|
|
|
{
|
2021-05-29 02:18:59 +00:00
|
|
|
"ST": "mock-st",
|
|
|
|
"LOCATION": "http://1.1.1.1",
|
|
|
|
"USN": "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3",
|
|
|
|
"SERVER": "mock-server",
|
|
|
|
"EXT": "",
|
|
|
|
}
|
|
|
|
)
|
2021-09-11 23:38:16 +00:00
|
|
|
ssdp_listener = await init_ssdp_component(hass)
|
|
|
|
await ssdp_listener._on_search(mock_ssdp_search_response)
|
2021-05-29 02:18:59 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
discovery_info_by_st = await ssdp.async_get_discovery_info_by_st(hass, "mock-st")
|
|
|
|
assert discovery_info_by_st == [
|
|
|
|
{
|
|
|
|
ssdp.ATTR_SSDP_EXT: "",
|
|
|
|
ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1",
|
|
|
|
ssdp.ATTR_SSDP_SERVER: "mock-server",
|
|
|
|
ssdp.ATTR_SSDP_ST: "mock-st",
|
|
|
|
ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3",
|
|
|
|
ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL",
|
|
|
|
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
|
Config-flow for DLNA-DMR integration (#55267)
* Modernize dlna_dmr component: configflow, test, types
* Support config-flow with ssdp discovery
* Add unit tests
* Enforce strict typing
* Gracefully handle network devices (dis)appearing
* Fix Aiohttp mock response headers type to match actual response class
* Fixes from code review
* Fixes from code review
* Import device config in flow if unavailable at hass start
* Support SSDP advertisements
* Ignore bad BOOTID, fix ssdp:byebye handling
* Only listen for events on interface connected to device
* Release all listeners when entities are removed
* Warn about deprecated dlna_dmr configuration
* Use sublogger for dlna_dmr.config_flow for easier filtering
* Tests for dlna_dmr.data module
* Rewrite DMR tests for HA style
* Fix DMR strings: "Digital Media *Renderer*"
* Update DMR entity state and device info when changed
* Replace deprecated async_upnp_client State with TransportState
* supported_features are dynamic, based on current device state
* Cleanup fully when subscription fails
* Log warnings when device connection fails unexpectedly
* Set PARALLEL_UPDATES to unlimited
* Fix spelling
* Fixes from code review
* Simplify has & can checks to just can, which includes has
* Treat transitioning state as playing (not idle) to reduce UI jerking
* Test if device is usable
* Handle ssdp:update messages properly
* Fix _remove_ssdp_callbacks being shared by all DlnaDmrEntity instances
* Fix tests for transitioning state
* Mock DmrDevice.is_profile_device (added to support embedded devices)
* Use ST & NT SSDP headers to find DMR devices, not deviceType
The deviceType is extracted from the device's description XML, and will not
be what we want when dealing with embedded devices.
* Use UDN from SSDP headers, not device description, as unique_id
The SSDP headers have the UDN of the embedded device that we're interested
in, whereas the device description (`ATTR_UPNP_UDN`) field will always be
for the root device.
* Fix DMR string English localization
* Test config flow with UDN from SSDP headers
* Bump async-upnp-client==0.22.1, fix flake8 error
* fix test for remapping
* DMR HA Device connections based on root and embedded UDN
* DmrDevice's UpnpDevice is now named profile_device
* Use device type from SSDP headers, not device description
* Mark dlna_dmr constants as Final
* Use embedded device UDN and type for unique ID when connected via URL
* More informative connection error messages
* Also match SSDP messages on NT headers
The NT header is to ssdp:alive messages what ST is to M-SEARCH responses.
* Bump async-upnp-client==0.22.2
* fix merge
* Bump async-upnp-client==0.22.3
Co-authored-by: Steven Looman <steven.looman@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2021-09-27 20:47:01 +00:00
|
|
|
ssdp.ATTR_SSDP_UDN: ANY,
|
2021-09-11 23:38:16 +00:00
|
|
|
"_timestamp": ANY,
|
|
|
|
}
|
|
|
|
]
|
2021-05-29 02:18:59 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
discovery_info_by_udn = await ssdp.async_get_discovery_info_by_udn(
|
|
|
|
hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL"
|
|
|
|
)
|
|
|
|
assert discovery_info_by_udn == [
|
|
|
|
{
|
|
|
|
ssdp.ATTR_SSDP_EXT: "",
|
|
|
|
ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1",
|
|
|
|
ssdp.ATTR_SSDP_SERVER: "mock-server",
|
|
|
|
ssdp.ATTR_SSDP_ST: "mock-st",
|
|
|
|
ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3",
|
|
|
|
ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL",
|
|
|
|
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
|
Config-flow for DLNA-DMR integration (#55267)
* Modernize dlna_dmr component: configflow, test, types
* Support config-flow with ssdp discovery
* Add unit tests
* Enforce strict typing
* Gracefully handle network devices (dis)appearing
* Fix Aiohttp mock response headers type to match actual response class
* Fixes from code review
* Fixes from code review
* Import device config in flow if unavailable at hass start
* Support SSDP advertisements
* Ignore bad BOOTID, fix ssdp:byebye handling
* Only listen for events on interface connected to device
* Release all listeners when entities are removed
* Warn about deprecated dlna_dmr configuration
* Use sublogger for dlna_dmr.config_flow for easier filtering
* Tests for dlna_dmr.data module
* Rewrite DMR tests for HA style
* Fix DMR strings: "Digital Media *Renderer*"
* Update DMR entity state and device info when changed
* Replace deprecated async_upnp_client State with TransportState
* supported_features are dynamic, based on current device state
* Cleanup fully when subscription fails
* Log warnings when device connection fails unexpectedly
* Set PARALLEL_UPDATES to unlimited
* Fix spelling
* Fixes from code review
* Simplify has & can checks to just can, which includes has
* Treat transitioning state as playing (not idle) to reduce UI jerking
* Test if device is usable
* Handle ssdp:update messages properly
* Fix _remove_ssdp_callbacks being shared by all DlnaDmrEntity instances
* Fix tests for transitioning state
* Mock DmrDevice.is_profile_device (added to support embedded devices)
* Use ST & NT SSDP headers to find DMR devices, not deviceType
The deviceType is extracted from the device's description XML, and will not
be what we want when dealing with embedded devices.
* Use UDN from SSDP headers, not device description, as unique_id
The SSDP headers have the UDN of the embedded device that we're interested
in, whereas the device description (`ATTR_UPNP_UDN`) field will always be
for the root device.
* Fix DMR string English localization
* Test config flow with UDN from SSDP headers
* Bump async-upnp-client==0.22.1, fix flake8 error
* fix test for remapping
* DMR HA Device connections based on root and embedded UDN
* DmrDevice's UpnpDevice is now named profile_device
* Use device type from SSDP headers, not device description
* Mark dlna_dmr constants as Final
* Use embedded device UDN and type for unique ID when connected via URL
* More informative connection error messages
* Also match SSDP messages on NT headers
The NT header is to ssdp:alive messages what ST is to M-SEARCH responses.
* Bump async-upnp-client==0.22.2
* fix merge
* Bump async-upnp-client==0.22.3
Co-authored-by: Steven Looman <steven.looman@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2021-09-27 20:47:01 +00:00
|
|
|
ssdp.ATTR_SSDP_UDN: ANY,
|
2021-09-11 23:38:16 +00:00
|
|
|
"_timestamp": ANY,
|
|
|
|
}
|
|
|
|
]
|
2021-05-29 02:18:59 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
discovery_info_by_udn_st = await ssdp.async_get_discovery_info_by_udn_st(
|
|
|
|
hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", "mock-st"
|
|
|
|
)
|
|
|
|
assert discovery_info_by_udn_st == {
|
2021-05-29 02:18:59 +00:00
|
|
|
ssdp.ATTR_SSDP_EXT: "",
|
|
|
|
ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1",
|
|
|
|
ssdp.ATTR_SSDP_SERVER: "mock-server",
|
|
|
|
ssdp.ATTR_SSDP_ST: "mock-st",
|
|
|
|
ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3",
|
|
|
|
ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL",
|
|
|
|
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
|
Config-flow for DLNA-DMR integration (#55267)
* Modernize dlna_dmr component: configflow, test, types
* Support config-flow with ssdp discovery
* Add unit tests
* Enforce strict typing
* Gracefully handle network devices (dis)appearing
* Fix Aiohttp mock response headers type to match actual response class
* Fixes from code review
* Fixes from code review
* Import device config in flow if unavailable at hass start
* Support SSDP advertisements
* Ignore bad BOOTID, fix ssdp:byebye handling
* Only listen for events on interface connected to device
* Release all listeners when entities are removed
* Warn about deprecated dlna_dmr configuration
* Use sublogger for dlna_dmr.config_flow for easier filtering
* Tests for dlna_dmr.data module
* Rewrite DMR tests for HA style
* Fix DMR strings: "Digital Media *Renderer*"
* Update DMR entity state and device info when changed
* Replace deprecated async_upnp_client State with TransportState
* supported_features are dynamic, based on current device state
* Cleanup fully when subscription fails
* Log warnings when device connection fails unexpectedly
* Set PARALLEL_UPDATES to unlimited
* Fix spelling
* Fixes from code review
* Simplify has & can checks to just can, which includes has
* Treat transitioning state as playing (not idle) to reduce UI jerking
* Test if device is usable
* Handle ssdp:update messages properly
* Fix _remove_ssdp_callbacks being shared by all DlnaDmrEntity instances
* Fix tests for transitioning state
* Mock DmrDevice.is_profile_device (added to support embedded devices)
* Use ST & NT SSDP headers to find DMR devices, not deviceType
The deviceType is extracted from the device's description XML, and will not
be what we want when dealing with embedded devices.
* Use UDN from SSDP headers, not device description, as unique_id
The SSDP headers have the UDN of the embedded device that we're interested
in, whereas the device description (`ATTR_UPNP_UDN`) field will always be
for the root device.
* Fix DMR string English localization
* Test config flow with UDN from SSDP headers
* Bump async-upnp-client==0.22.1, fix flake8 error
* fix test for remapping
* DMR HA Device connections based on root and embedded UDN
* DmrDevice's UpnpDevice is now named profile_device
* Use device type from SSDP headers, not device description
* Mark dlna_dmr constants as Final
* Use embedded device UDN and type for unique ID when connected via URL
* More informative connection error messages
* Also match SSDP messages on NT headers
The NT header is to ssdp:alive messages what ST is to M-SEARCH responses.
* Bump async-upnp-client==0.22.2
* fix merge
* Bump async-upnp-client==0.22.3
Co-authored-by: Steven Looman <steven.looman@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2021-09-27 20:47:01 +00:00
|
|
|
ssdp.ATTR_SSDP_UDN: ANY,
|
2021-09-11 23:38:16 +00:00
|
|
|
"_timestamp": ANY,
|
2021-05-29 02:18:59 +00:00
|
|
|
}
|
2021-04-14 20:23:15 +00:00
|
|
|
|
2021-05-29 02:18:59 +00:00
|
|
|
assert (
|
2021-09-11 23:38:16 +00:00
|
|
|
await ssdp.async_get_discovery_info_by_udn_st(hass, "wrong", "mock-st") is None
|
2021-05-29 02:18:59 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
_ADAPTERS_WITH_MANUAL_CONFIG = [
|
|
|
|
{
|
|
|
|
"auto": True,
|
|
|
|
"default": False,
|
|
|
|
"enabled": True,
|
|
|
|
"ipv4": [],
|
|
|
|
"ipv6": [
|
|
|
|
{
|
|
|
|
"address": "2001:db8::",
|
|
|
|
"network_prefix": 8,
|
|
|
|
"flowinfo": 1,
|
|
|
|
"scope_id": 1,
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"name": "eth0",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"auto": True,
|
|
|
|
"default": False,
|
|
|
|
"enabled": True,
|
|
|
|
"ipv4": [{"address": "192.168.1.5", "network_prefix": 23}],
|
|
|
|
"ipv6": [],
|
|
|
|
"name": "eth1",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"auto": False,
|
|
|
|
"default": False,
|
|
|
|
"enabled": False,
|
|
|
|
"ipv4": [{"address": "169.254.3.2", "network_prefix": 16}],
|
|
|
|
"ipv6": [],
|
|
|
|
"name": "vtun0",
|
|
|
|
},
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
@pytest.mark.usefixtures("mock_get_source_ip")
|
|
|
|
@patch(
|
|
|
|
"homeassistant.components.ssdp.async_get_ssdp",
|
|
|
|
return_value={
|
2021-05-29 02:18:59 +00:00
|
|
|
"mock-domain": [
|
|
|
|
{
|
|
|
|
ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC",
|
|
|
|
}
|
|
|
|
]
|
2021-09-11 23:38:16 +00:00
|
|
|
},
|
|
|
|
)
|
|
|
|
@patch(
|
|
|
|
"homeassistant.components.ssdp.network.async_get_adapters",
|
|
|
|
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
|
|
|
|
) # XXX TODO: Isn't this duplicate with mock_get_source_ip?
|
|
|
|
async def test_async_detect_interfaces_setting_empty_route(
|
|
|
|
mock_get_adapters, mock_get_ssdp, hass
|
|
|
|
):
|
|
|
|
"""Test without default interface config and the route returns nothing."""
|
|
|
|
await init_ssdp_component(hass)
|
2021-07-11 21:03:48 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
ssdp_listeners = hass.data[ssdp.DOMAIN]._ssdp_listeners
|
|
|
|
source_ips = {ssdp_listener.source_ip for ssdp_listener in ssdp_listeners}
|
|
|
|
assert source_ips == {IPv6Address("2001:db8::"), IPv4Address("192.168.1.5")}
|
2021-07-29 16:07:52 +00:00
|
|
|
|
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
@pytest.mark.usefixtures("mock_get_source_ip")
|
|
|
|
@patch(
|
|
|
|
"homeassistant.components.ssdp.async_get_ssdp",
|
|
|
|
return_value={
|
2021-07-29 16:07:52 +00:00
|
|
|
"mock-domain": [
|
|
|
|
{
|
|
|
|
ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC",
|
|
|
|
}
|
|
|
|
]
|
2021-09-11 23:38:16 +00:00
|
|
|
},
|
|
|
|
)
|
|
|
|
@patch(
|
|
|
|
"homeassistant.components.ssdp.network.async_get_adapters",
|
|
|
|
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
|
|
|
|
) # XXX TODO: Isn't this duplicate with mock_get_source_ip?
|
|
|
|
async def test_bind_failure_skips_adapter(
|
|
|
|
mock_get_adapters, mock_get_ssdp, hass, caplog
|
|
|
|
):
|
|
|
|
"""Test that an adapter with a bind failure is skipped."""
|
2021-07-29 16:07:52 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
async def _async_start(self):
|
|
|
|
if self.source_ip == IPv6Address("2001:db8::"):
|
|
|
|
raise OSError
|
2021-07-29 16:07:52 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
SsdpListener.async_start = _async_start
|
|
|
|
await init_ssdp_component(hass)
|
2021-07-29 16:07:52 +00:00
|
|
|
|
|
|
|
assert "Failed to setup listener for" in caplog.text
|
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
ssdp_listeners = hass.data[ssdp.DOMAIN]._ssdp_listeners
|
|
|
|
source_ips = {ssdp_listener.source_ip for ssdp_listener in ssdp_listeners}
|
|
|
|
assert source_ips == {
|
|
|
|
IPv4Address("192.168.1.5")
|
|
|
|
} # Note no SsdpListener for IPv6 address.
|
2021-08-13 16:13:25 +00:00
|
|
|
|
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
@pytest.mark.usefixtures("mock_get_source_ip")
|
|
|
|
@patch(
|
|
|
|
"homeassistant.components.ssdp.async_get_ssdp",
|
|
|
|
return_value={
|
2021-08-13 16:13:25 +00:00
|
|
|
"mock-domain": [
|
|
|
|
{
|
|
|
|
ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC",
|
|
|
|
}
|
|
|
|
]
|
2021-09-11 23:38:16 +00:00
|
|
|
},
|
|
|
|
)
|
|
|
|
@patch(
|
|
|
|
"homeassistant.components.ssdp.network.async_get_adapters",
|
|
|
|
return_value=_ADAPTERS_WITH_MANUAL_CONFIG,
|
|
|
|
) # XXX TODO: Isn't this duplicate with mock_get_source_ip?
|
|
|
|
async def test_ipv4_does_additional_search_for_sonos(
|
|
|
|
mock_get_adapters, mock_get_ssdp, hass
|
|
|
|
):
|
|
|
|
"""Test that only ipv4 does an additional search for Sonos."""
|
|
|
|
ssdp_listener = await init_ssdp_component(hass)
|
2021-08-13 16:13:25 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
|
|
|
|
await hass.async_block_till_done()
|
2021-08-13 16:13:25 +00:00
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
assert ssdp_listener.async_search.call_count == 6
|
|
|
|
assert ssdp_listener.async_search.call_args[0] == (
|
2021-08-13 16:13:25 +00:00
|
|
|
(
|
2021-09-11 23:38:16 +00:00
|
|
|
"255.255.255.255",
|
|
|
|
1900,
|
2021-08-13 16:13:25 +00:00
|
|
|
),
|
2021-08-27 16:53:29 +00:00
|
|
|
)
|
2021-09-11 23:38:16 +00:00
|
|
|
assert ssdp_listener.async_search.call_args[1] == {}
|