Use dataclass for SsdpServiceInfo (#59931)

Co-authored-by: epenet <epenet@users.noreply.github.com>
This commit is contained in:
epenet 2021-11-29 17:10:07 +01:00 committed by GitHub
parent 7ece86ee8d
commit ec1c52d945
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 295 additions and 175 deletions

View File

@ -6,7 +6,7 @@ from urllib.parse import urlsplit
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import dhcp, zeroconf from homeassistant.components import dhcp, ssdp, zeroconf
from homeassistant.config_entries import SOURCE_IGNORE from homeassistant.config_entries import SOURCE_IGNORE
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
@ -163,7 +163,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN):
} }
) )
async def async_step_ssdp(self, discovery_info: dict): async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo):
"""Prepare configuration for a SSDP discovered Axis device.""" """Prepare configuration for a SSDP discovered Axis device."""
url = urlsplit(discovery_info["presentationURL"]) url = urlsplit(discovery_info["presentationURL"])
return await self._process_discovered_device( return await self._process_discovered_device(

View File

@ -8,13 +8,13 @@ from urllib.parse import urlparse
from directv import DIRECTV, DIRECTVError from directv import DIRECTV, DIRECTVError
import voluptuous as vol import voluptuous as vol
from homeassistant.components import ssdp
from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_SERIAL from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_SERIAL
from homeassistant.config_entries import ConfigFlow from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import DiscoveryInfoType
from .const import CONF_RECEIVER_ID, DOMAIN from .const import CONF_RECEIVER_ID, DOMAIN
@ -67,7 +67,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) return self.async_create_entry(title=user_input[CONF_HOST], data=user_input)
async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
"""Handle SSDP discovery.""" """Handle SSDP discovery."""
host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname
receiver_id = None receiver_id = None

View File

@ -25,7 +25,6 @@ from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import IntegrationError from homeassistant.exceptions import IntegrationError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import DiscoveryInfoType
from .const import ( from .const import (
CONF_CALLBACK_URL_OVERRIDE, CONF_CALLBACK_URL_OVERRIDE,
@ -57,7 +56,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize flow.""" """Initialize flow."""
self._discoveries: dict[str, Mapping[str, Any]] = {} self._discoveries: dict[str, ssdp.SsdpServiceInfo] = {}
self._location: str | None = None self._location: str | None = None
self._udn: str | None = None self._udn: str | None = None
self._device_type: str | None = None self._device_type: str | None = None
@ -205,7 +204,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self._set_confirm_only() self._set_confirm_only()
return self.async_show_form(step_id="import_turn_on", errors=errors) return self.async_show_form(step_id="import_turn_on", errors=errors)
async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
"""Handle a flow initialized by SSDP discovery.""" """Handle a flow initialized by SSDP discovery."""
LOGGER.debug("async_step_ssdp: discovery_info %s", pformat(discovery_info)) LOGGER.debug("async_step_ssdp: discovery_info %s", pformat(discovery_info))
@ -330,7 +329,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title=title, data=data, options=self._options) return self.async_create_entry(title=title, data=data, options=self._options)
async def _async_set_info_from_discovery( async def _async_set_info_from_discovery(
self, discovery_info: Mapping[str, Any], abort_if_configured: bool = True self, discovery_info: ssdp.SsdpServiceInfo, abort_if_configured: bool = True
) -> None: ) -> None:
"""Set information required for a config entry from the SSDP discovery.""" """Set information required for a config entry from the SSDP discovery."""
LOGGER.debug( LOGGER.debug(
@ -361,12 +360,12 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
or DEFAULT_NAME or DEFAULT_NAME
) )
async def _async_get_discoveries(self) -> list[Mapping[str, Any]]: async def _async_get_discoveries(self) -> list[ssdp.SsdpServiceInfo]:
"""Get list of unconfigured DLNA devices discovered by SSDP.""" """Get list of unconfigured DLNA devices discovered by SSDP."""
LOGGER.debug("_get_discoveries") LOGGER.debug("_get_discoveries")
# Get all compatible devices from ssdp's cache # Get all compatible devices from ssdp's cache
discoveries: list[Mapping[str, Any]] = [] discoveries: list[ssdp.SsdpServiceInfo] = []
for udn_st in DmrDevice.DEVICE_TYPES: for udn_st in DmrDevice.DEVICE_TYPES:
st_discoveries = await ssdp.async_get_discovery_info_by_st( st_discoveries = await ssdp.async_get_discovery_info_by_st(
self.hass, udn_st self.hass, udn_st
@ -454,7 +453,7 @@ class DlnaDmrOptionsFlowHandler(config_entries.OptionsFlow):
) )
def _is_ignored_device(discovery_info: Mapping[str, Any]) -> bool: def _is_ignored_device(discovery_info: ssdp.SsdpServiceInfo) -> bool:
"""Return True if this device should be ignored for discovery. """Return True if this device should be ignored for discovery.
These devices are supported better by other integrations, so don't bug These devices are supported better by other integrations, so don't bug

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Mapping, Sequence from collections.abc import Sequence
import contextlib import contextlib
from datetime import datetime, timedelta from datetime import datetime, timedelta
import functools import functools
@ -241,7 +241,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
await self._device_disconnect() await self._device_disconnect()
async def async_ssdp_callback( async def async_ssdp_callback(
self, info: Mapping[str, Any], change: ssdp.SsdpChange self, info: ssdp.SsdpServiceInfo, change: ssdp.SsdpChange
) -> None: ) -> None:
"""Handle notification from SSDP of device state change.""" """Handle notification from SSDP of device state change."""
_LOGGER.debug( _LOGGER.debug(

View File

@ -9,6 +9,7 @@ from urllib.parse import ParseResult, urlparse
from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError
import voluptuous as vol import voluptuous as vol
from homeassistant.components import ssdp
from homeassistant.components.device_tracker.const import ( from homeassistant.components.device_tracker.const import (
CONF_CONSIDER_HOME, CONF_CONSIDER_HOME,
DEFAULT_CONSIDER_HOME, DEFAULT_CONSIDER_HOME,
@ -22,7 +23,6 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.typing import DiscoveryInfoType
from .common import FritzBoxTools from .common import FritzBoxTools
from .const import ( from .const import (
@ -115,7 +115,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
}, },
) )
async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
"""Handle a flow initialized by discovery.""" """Handle a flow initialized by discovery."""
ssdp_location: ParseResult = urlparse(discovery_info[ATTR_SSDP_LOCATION]) ssdp_location: ParseResult = urlparse(discovery_info[ATTR_SSDP_LOCATION])
self._host = ssdp_location.hostname self._host = ssdp_location.hostname

View File

@ -8,6 +8,7 @@ from pyfritzhome import Fritzhome, LoginError
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
import voluptuous as vol import voluptuous as vol
from homeassistant.components import ssdp
from homeassistant.components.ssdp import ( from homeassistant.components.ssdp import (
ATTR_SSDP_LOCATION, ATTR_SSDP_LOCATION,
ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_FRIENDLY_NAME,
@ -16,7 +17,6 @@ from homeassistant.components.ssdp import (
from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.typing import DiscoveryInfoType
from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN
@ -119,7 +119,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors
) )
async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
"""Handle a flow initialized by discovery.""" """Handle a flow initialized by discovery."""
host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname
assert isinstance(host, str) assert isinstance(host, str)

View File

@ -31,7 +31,6 @@ from homeassistant.const import (
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.typing import DiscoveryInfoType
from .const import ( from .const import (
CONF_TRACK_WIRED_CLIENTS, CONF_TRACK_WIRED_CLIENTS,
@ -202,7 +201,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title=title, data=user_input) return self.async_create_entry(title=title, data=user_input)
async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
"""Handle SSDP initiated config flow.""" """Handle SSDP initiated config flow."""
await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN]) await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN])
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()

View File

@ -18,7 +18,7 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
CONF_ALLOW_HUE_GROUPS, CONF_ALLOW_HUE_GROUPS,
@ -186,7 +186,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
}, },
) )
async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
"""Handle a discovered Hue bridge. """Handle a discovered Hue bridge.
This flow is triggered by the SSDP component. It will check if the This flow is triggered by the SSDP component. It will check if the
@ -213,7 +213,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="not_hue_bridge") return self.async_abort(reason="not_hue_bridge")
host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname
bridge = await self._get_bridge(host, discovery_info[ssdp.ATTR_UPNP_SERIAL]) # type: ignore[arg-type] bridge = await self._get_bridge(host, discovery_info[ssdp.ATTR_UPNP_SERIAL])
await self.async_set_unique_id(bridge.id) await self.async_set_unique_id(bridge.id)
self._abort_if_unique_id_configured( self._abort_if_unique_id_configured(

View File

@ -10,6 +10,7 @@ from urllib.parse import urlparse
from hyperion import client, const from hyperion import client, const
import voluptuous as vol import voluptuous as vol
from homeassistant.components import ssdp
from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_SERIAL from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_SERIAL
from homeassistant.config_entries import ( from homeassistant.config_entries import (
SOURCE_REAUTH, SOURCE_REAUTH,
@ -151,7 +152,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="cannot_connect") return self.async_abort(reason="cannot_connect")
return await self._advance_to_auth_step_if_necessary(hyperion_client) return await self._advance_to_auth_step_if_necessary(hyperion_client)
async def async_step_ssdp(self, discovery_info: dict[str, Any]) -> FlowResult: async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
"""Handle a flow initiated by SSDP.""" """Handle a flow initiated by SSDP."""
# Sample data provided by SSDP: { # Sample data provided by SSDP: {
# 'ssdp_location': 'http://192.168.0.1:8090/description.xml', # 'ssdp_location': 'http://192.168.0.1:8090/description.xml',

View File

@ -9,11 +9,10 @@ from aionanoleaf import InvalidToken, Nanoleaf, Unauthorized, Unavailable
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import zeroconf from homeassistant.components import ssdp, zeroconf
from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.const import CONF_HOST, CONF_TOKEN
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import DiscoveryInfoType
from homeassistant.util.json import load_json, save_json from homeassistant.util.json import load_json, save_json
from .const import DOMAIN from .const import DOMAIN
@ -114,7 +113,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
discovery_info[zeroconf.ATTR_PROPERTIES][zeroconf.ATTR_PROPERTIES_ID], discovery_info[zeroconf.ATTR_PROPERTIES][zeroconf.ATTR_PROPERTIES_ID],
) )
async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
"""Handle Nanoleaf SSDP discovery.""" """Handle Nanoleaf SSDP discovery."""
_LOGGER.debug("SSDP discovered: %s", discovery_info) _LOGGER.debug("SSDP discovered: %s", discovery_info)
return await self._async_discovery_handler( return await self._async_discovery_handler(

View File

@ -7,7 +7,7 @@ from urllib.parse import urlparse
from rokuecp import Roku, RokuError from rokuecp import Roku, RokuError
import voluptuous as vol import voluptuous as vol
from homeassistant.components import zeroconf from homeassistant.components import ssdp, zeroconf
from homeassistant.components.ssdp import ( from homeassistant.components.ssdp import (
ATTR_SSDP_LOCATION, ATTR_SSDP_LOCATION,
ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_FRIENDLY_NAME,
@ -18,7 +18,6 @@ from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import DiscoveryInfoType
from .const import DOMAIN from .const import DOMAIN
@ -115,7 +114,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
return await self.async_step_discovery_confirm() return await self.async_step_discovery_confirm()
async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
"""Handle a flow initialized by discovery.""" """Handle a flow initialized by discovery."""
host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname
name = discovery_info[ATTR_UPNP_FRIENDLY_NAME] name = discovery_info[ATTR_UPNP_FRIENDLY_NAME]

View File

@ -11,7 +11,7 @@ import getmac
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, data_entry_flow from homeassistant import config_entries, data_entry_flow
from homeassistant.components import dhcp, zeroconf from homeassistant.components import dhcp, ssdp, zeroconf
from homeassistant.components.ssdp import ( from homeassistant.components.ssdp import (
ATTR_SSDP_LOCATION, ATTR_SSDP_LOCATION,
ATTR_UPNP_MANUFACTURER, ATTR_UPNP_MANUFACTURER,
@ -28,7 +28,6 @@ from homeassistant.const import (
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.typing import DiscoveryInfoType
from .bridge import ( from .bridge import (
SamsungTVBridge, SamsungTVBridge,
@ -266,7 +265,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED)
async def async_step_ssdp( async def async_step_ssdp(
self, discovery_info: DiscoveryInfoType self, discovery_info: ssdp.SsdpServiceInfo
) -> data_entry_flow.FlowResult: ) -> data_entry_flow.FlowResult:
"""Handle a flow initialized by ssdp discovery.""" """Handle a flow initialized by ssdp discovery."""
LOGGER.debug("Samsung device found via SSDP: %s", discovery_info) LOGGER.debug("Samsung device found via SSDP: %s", discovery_info)

View File

@ -2,13 +2,13 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Awaitable, Iterator from collections.abc import Awaitable, Mapping
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import timedelta from datetime import timedelta
from enum import Enum from enum import Enum
from ipaddress import IPv4Address, IPv6Address from ipaddress import IPv4Address, IPv6Address
import logging import logging
from typing import Any, Callable, Mapping from typing import Any, Callable
from async_upnp_client.aiohttp import AiohttpSessionRequester from async_upnp_client.aiohttp import AiohttpSessionRequester
from async_upnp_client.const import DeviceOrServiceType, SsdpHeaders, SsdpSource from async_upnp_client.const import DeviceOrServiceType, SsdpHeaders, SsdpSource
@ -63,28 +63,6 @@ ATTR_HA_MATCHING_DOMAINS = "x_homeassistant_matching_domains"
PRIMARY_MATCH_KEYS = [ATTR_UPNP_MANUFACTURER, "st", ATTR_UPNP_DEVICE_TYPE, "nt"] PRIMARY_MATCH_KEYS = [ATTR_UPNP_MANUFACTURER, "st", ATTR_UPNP_DEVICE_TYPE, "nt"]
DISCOVERY_MAPPING = {
"usn": ATTR_SSDP_USN,
"ext": ATTR_SSDP_EXT,
"server": ATTR_SSDP_SERVER,
"st": ATTR_SSDP_ST,
"location": ATTR_SSDP_LOCATION,
"_udn": ATTR_SSDP_UDN,
"nt": ATTR_SSDP_NT,
}
SsdpChange = Enum("SsdpChange", "ALIVE BYEBYE UPDATE")
SsdpCallback = Callable[[Mapping[str, Any], SsdpChange], Awaitable]
SSDP_SOURCE_SSDP_CHANGE_MAPPING: Mapping[SsdpSource, SsdpChange] = {
SsdpSource.SEARCH_ALIVE: SsdpChange.ALIVE,
SsdpSource.SEARCH_CHANGED: SsdpChange.ALIVE,
SsdpSource.ADVERTISEMENT_ALIVE: SsdpChange.ALIVE,
SsdpSource.ADVERTISEMENT_BYEBYE: SsdpChange.BYEBYE,
SsdpSource.ADVERTISEMENT_UPDATE: SsdpChange.UPDATE,
}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -106,13 +84,14 @@ class _SsdpServiceDescription:
ssdp_udn: str | None = None ssdp_udn: str | None = None
ssdp_ext: str | None = None ssdp_ext: str | None = None
ssdp_server: str | None = None ssdp_server: str | None = None
ssdp_headers: Mapping[str, Any] = field(default_factory=dict)
@dataclass @dataclass
class _UpnpServiceDescription: class _UpnpServiceDescription:
"""UPnP info.""" """UPnP info."""
upnp: dict[str, Any] upnp: Mapping[str, Any]
@dataclass @dataclass
@ -136,7 +115,7 @@ class SsdpServiceInfo(
if not self._warning_logged: if not self._warning_logged:
report( report(
f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6", f"accessed discovery_info['{name}'] instead of discovery_info.{name}; this will fail in version 2022.6",
exclude_integrations={"ssdp"}, exclude_integrations={DOMAIN},
error_if_core=False, error_if_core=False,
level=logging.DEBUG, level=logging.DEBUG,
) )
@ -144,41 +123,58 @@ class SsdpServiceInfo(
# Use a property if it is available, fallback to upnp data # Use a property if it is available, fallback to upnp data
if hasattr(self, name): if hasattr(self, name):
return getattr(self, name) return getattr(self, name)
if name in self.ssdp_headers and name not in self.upnp:
return self.ssdp_headers.get(name)
return self.upnp[name] return self.upnp[name]
def get(self, name: str, default: Any = None) -> Any: def get(self, name: str, default: Any = None) -> Any:
""" """
Allow property access by name for compatibility reason. Enable method for compatibility reason.
Deprecated, and will be removed in version 2022.6. Deprecated, and will be removed in version 2022.6.
""" """
if not self._warning_logged: if not self._warning_logged:
report( report(
f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6", f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; this will fail in version 2022.6",
exclude_integrations={"ssdp"}, exclude_integrations={DOMAIN},
error_if_core=False, error_if_core=False,
level=logging.DEBUG, level=logging.DEBUG,
) )
self._warning_logged = True self._warning_logged = True
if hasattr(self, name): if hasattr(self, name):
return getattr(self, name) return getattr(self, name)
return self.upnp.get(name, default) return self.upnp.get(name, self.ssdp_headers.get(name, default))
def __iter__(self) -> Iterator[str]: def __contains__(self, name: str) -> bool:
""" """
Implement iter(self) on upnp data. Enable method for compatibility reason.
Deprecated, and will be removed in version 2022.6. Deprecated, and will be removed in version 2022.6.
""" """
if not self._warning_logged: if not self._warning_logged:
report( report(
"accessed discovery_info.__iter__() instead of discovery_info.upnp.__iter__(); this will fail in version 2022.6", "accessed discovery_info.__contains__() instead of discovery_info.upnp.__contains__() "
exclude_integrations={"ssdp"}, "or discovery_info.ssdp_headers.__contains__(); this will fail in version 2022.6",
exclude_integrations={DOMAIN},
error_if_core=False, error_if_core=False,
level=logging.DEBUG, level=logging.DEBUG,
) )
self._warning_logged = True self._warning_logged = True
return self.upnp.__iter__() if hasattr(self, name):
return getattr(self, name) is not None
return name in self.upnp or name in self.ssdp_headers
SsdpChange = Enum("SsdpChange", "ALIVE BYEBYE UPDATE")
SsdpCallback = Callable[[SsdpServiceInfo, SsdpChange], Awaitable]
SSDP_SOURCE_SSDP_CHANGE_MAPPING: Mapping[SsdpSource, SsdpChange] = {
SsdpSource.SEARCH_ALIVE: SsdpChange.ALIVE,
SsdpSource.SEARCH_CHANGED: SsdpChange.ALIVE,
SsdpSource.ADVERTISEMENT_ALIVE: SsdpChange.ALIVE,
SsdpSource.ADVERTISEMENT_BYEBYE: SsdpChange.BYEBYE,
SsdpSource.ADVERTISEMENT_UPDATE: SsdpChange.UPDATE,
}
@bind_hass @bind_hass
@ -198,7 +194,7 @@ async def async_register_callback(
@bind_hass @bind_hass
async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name
hass: HomeAssistant, udn: str, st: str hass: HomeAssistant, udn: str, st: str
) -> dict[str, str] | None: ) -> SsdpServiceInfo | None:
"""Fetch the discovery info cache.""" """Fetch the discovery info cache."""
scanner: Scanner = hass.data[DOMAIN] scanner: Scanner = hass.data[DOMAIN]
return await scanner.async_get_discovery_info_by_udn_st(udn, st) return await scanner.async_get_discovery_info_by_udn_st(udn, st)
@ -207,7 +203,7 @@ async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name
@bind_hass @bind_hass
async def async_get_discovery_info_by_st( # pylint: disable=invalid-name async def async_get_discovery_info_by_st( # pylint: disable=invalid-name
hass: HomeAssistant, st: str hass: HomeAssistant, st: str
) -> list[dict[str, str]]: ) -> list[SsdpServiceInfo]:
"""Fetch all the entries matching the st.""" """Fetch all the entries matching the st."""
scanner: Scanner = hass.data[DOMAIN] scanner: Scanner = hass.data[DOMAIN]
return await scanner.async_get_discovery_info_by_st(st) return await scanner.async_get_discovery_info_by_st(st)
@ -216,7 +212,7 @@ async def async_get_discovery_info_by_st( # pylint: disable=invalid-name
@bind_hass @bind_hass
async def async_get_discovery_info_by_udn( async def async_get_discovery_info_by_udn(
hass: HomeAssistant, udn: str hass: HomeAssistant, udn: str
) -> list[dict[str, str]]: ) -> list[SsdpServiceInfo]:
"""Fetch all the entries matching the udn.""" """Fetch all the entries matching the udn."""
scanner: Scanner = hass.data[DOMAIN] scanner: Scanner = hass.data[DOMAIN]
return await scanner.async_get_discovery_info_by_udn(udn) return await scanner.async_get_discovery_info_by_udn(udn)
@ -237,7 +233,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def _async_process_callbacks( async def _async_process_callbacks(
callbacks: list[SsdpCallback], callbacks: list[SsdpCallback],
discovery_info: dict[str, str], discovery_info: SsdpServiceInfo,
ssdp_change: SsdpChange, ssdp_change: SsdpChange,
) -> None: ) -> None:
for callback in callbacks: for callback in callbacks:
@ -496,8 +492,10 @@ class Scanner:
if not callbacks and not matching_domains: if not callbacks and not matching_domains:
return return
discovery_info = discovery_info_from_headers_and_description(info_with_desc) discovery_info = discovery_info_from_headers_and_description(
discovery_info[ATTR_HA_MATCHING_DOMAINS] = matching_domains combined_headers, info_desc
)
discovery_info.x_homeassistant_matching_domains = matching_domains
ssdp_change = SSDP_SOURCE_SSDP_CHANGE_MAPPING[source] ssdp_change = SSDP_SOURCE_SSDP_CHANGE_MAPPING[source]
await _async_process_callbacks(callbacks, discovery_info, ssdp_change) await _async_process_callbacks(callbacks, discovery_info, ssdp_change)
@ -505,6 +503,8 @@ class Scanner:
if ssdp_change == SsdpChange.BYEBYE: if ssdp_change == SsdpChange.BYEBYE:
return return
_LOGGER.debug("Discovery info: %s", discovery_info)
for domain in matching_domains: for domain in matching_domains:
_LOGGER.debug("Discovered %s at %s", domain, location) _LOGGER.debug("Discovered %s at %s", domain, location)
discovery_flow.async_create_flow( discovery_flow.async_create_flow(
@ -523,7 +523,7 @@ class Scanner:
async def _async_headers_to_discovery_info( async def _async_headers_to_discovery_info(
self, headers: Mapping[str, Any] self, headers: Mapping[str, Any]
) -> dict[str, Any]: ) -> SsdpServiceInfo:
"""Combine the headers and description into discovery_info. """Combine the headers and description into discovery_info.
Building this is a bit expensive so we only do it on demand. Building this is a bit expensive so we only do it on demand.
@ -533,13 +533,11 @@ class Scanner:
info_desc = ( info_desc = (
await self._description_cache.async_get_description_dict(location) or {} await self._description_cache.async_get_description_dict(location) or {}
) )
return discovery_info_from_headers_and_description( return discovery_info_from_headers_and_description(headers, info_desc)
CaseInsensitiveDict(headers, **info_desc)
)
async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name
self, udn: str, st: str self, udn: str, st: str
) -> dict[str, Any] | None: ) -> SsdpServiceInfo | None:
"""Return discovery_info for a udn and st.""" """Return discovery_info for a udn and st."""
if headers := self._all_headers_from_ssdp_devices.get((udn, st)): if headers := self._all_headers_from_ssdp_devices.get((udn, st)):
return await self._async_headers_to_discovery_info(headers) return await self._async_headers_to_discovery_info(headers)
@ -547,7 +545,7 @@ class Scanner:
async def async_get_discovery_info_by_st( # pylint: disable=invalid-name async def async_get_discovery_info_by_st( # pylint: disable=invalid-name
self, st: str self, st: str
) -> list[dict[str, Any]]: ) -> list[SsdpServiceInfo]:
"""Return matching discovery_infos for a st.""" """Return matching discovery_infos for a st."""
return [ return [
await self._async_headers_to_discovery_info(headers) await self._async_headers_to_discovery_info(headers)
@ -555,7 +553,7 @@ class Scanner:
if udn_st[1] == st if udn_st[1] == st
] ]
async def async_get_discovery_info_by_udn(self, udn: str) -> list[dict[str, Any]]: async def async_get_discovery_info_by_udn(self, udn: str) -> list[SsdpServiceInfo]:
"""Return matching discovery_infos for a udn.""" """Return matching discovery_infos for a udn."""
return [ return [
await self._async_headers_to_discovery_info(headers) await self._async_headers_to_discovery_info(headers)
@ -565,23 +563,36 @@ class Scanner:
def discovery_info_from_headers_and_description( def discovery_info_from_headers_and_description(
info_with_desc: CaseInsensitiveDict, combined_headers: Mapping[str, Any],
) -> dict[str, Any]: info_desc: Mapping[str, Any],
) -> SsdpServiceInfo:
"""Convert headers and description to discovery_info.""" """Convert headers and description to discovery_info."""
info = { ssdp_usn = combined_headers["usn"]
DISCOVERY_MAPPING.get(k.lower(), k): v ssdp_st = combined_headers.get("st")
for k, v in info_with_desc.as_dict().items() upnp_info = {**info_desc}
}
if ATTR_UPNP_UDN not in info and ATTR_SSDP_USN in info: # Increase compatibility: depending on the message type,
if udn := _udn_from_usn(info[ATTR_SSDP_USN]): # either the ST (Search Target, from M-SEARCH messages)
info[ATTR_UPNP_UDN] = udn # or NT (Notification Type, from NOTIFY messages) header is mandatory
if not ssdp_st:
ssdp_st = combined_headers["nt"]
# Increase compatibility. # Ensure UPnP "udn" is set
if ATTR_SSDP_ST not in info and ATTR_SSDP_NT in info: if ATTR_UPNP_UDN not in upnp_info:
info[ATTR_SSDP_ST] = info[ATTR_SSDP_NT] if udn := _udn_from_usn(ssdp_usn):
upnp_info[ATTR_UPNP_UDN] = udn
return info return SsdpServiceInfo(
ssdp_usn=ssdp_usn,
ssdp_st=ssdp_st,
ssdp_ext=combined_headers.get("ext"),
ssdp_server=combined_headers.get("server"),
ssdp_location=combined_headers.get("location"),
ssdp_udn=combined_headers.get("_udn"),
ssdp_nt=combined_headers.get("nt"),
ssdp_headers=combined_headers,
upnp=upnp_info,
)
def _udn_from_usn(usn: str | None) -> str | None: def _udn_from_usn(usn: str | None) -> str | None:

View File

@ -237,7 +237,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
return self._show_form(step) return self._show_form(step)
return await self.async_validate_input_create_entry(user_input, step_id=step) return await self.async_validate_input_create_entry(user_input, step_id=step)
async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
"""Handle a discovered synology_dsm.""" """Handle a discovered synology_dsm."""
parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION])
friendly_name = ( friendly_name = (

View File

@ -35,6 +35,7 @@ import homeassistant.util.uuid as uuid_util
if TYPE_CHECKING: if TYPE_CHECKING:
from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.dhcp import DhcpServiceInfo
from homeassistant.components.mqtt.discovery import MqttServiceInfo from homeassistant.components.mqtt.discovery import MqttServiceInfo
from homeassistant.components.ssdp import SsdpServiceInfo
from homeassistant.components.usb import UsbServiceInfo from homeassistant.components.usb import UsbServiceInfo
from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.components.zeroconf import ZeroconfServiceInfo
@ -1370,10 +1371,10 @@ class ConfigFlow(data_entry_flow.FlowHandler):
return await self.async_step_discovery(dataclasses.asdict(discovery_info)) return await self.async_step_discovery(dataclasses.asdict(discovery_info))
async def async_step_ssdp( async def async_step_ssdp(
self, discovery_info: DiscoveryInfoType self, discovery_info: SsdpServiceInfo
) -> data_entry_flow.FlowResult: ) -> data_entry_flow.FlowResult:
"""Handle a flow initialized by SSDP discovery.""" """Handle a flow initialized by SSDP discovery."""
return await self.async_step_discovery(discovery_info) return await self.async_step_discovery(dataclasses.asdict(discovery_info))
async def async_step_zeroconf( async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo self, discovery_info: ZeroconfServiceInfo

View File

@ -5,7 +5,7 @@ import logging
from typing import Any, Awaitable, Callable, Union from typing import Any, Awaitable, Callable, Union
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import dhcp, zeroconf from homeassistant.components import dhcp, ssdp, zeroconf
from homeassistant.components.mqtt import discovery as mqtt from homeassistant.components.mqtt import discovery as mqtt
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
@ -123,7 +123,14 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow):
return await self.async_step_confirm() return await self.async_step_confirm()
async_step_ssdp = async_step_discovery async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
"""Handle a flow initialized by Ssdp discovery."""
if self._async_in_progress() or self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
await self.async_set_unique_id(self._domain)
return await self.async_step_confirm()
async def async_step_import(self, _: dict[str, Any] | None) -> FlowResult: async def async_step_import(self, _: dict[str, Any] | None) -> FlowResult:
"""Handle a flow initialized by import.""" """Handle a flow initialized by import."""

View File

@ -1,4 +1,6 @@
"""Test the SSDP integration.""" """Test the SSDP integration."""
# pylint: disable=protected-access
from datetime import datetime, timedelta from datetime import datetime, timedelta
from ipaddress import IPv4Address, IPv6Address from ipaddress import IPv4Address, IPv6Address
from unittest.mock import ANY, AsyncMock, patch from unittest.mock import ANY, AsyncMock, patch
@ -62,18 +64,28 @@ async def test_ssdp_flow_dispatched_on_st(mock_get_ssdp, hass, caplog, mock_flow
assert mock_flow_init.mock_calls[0][2]["context"] == { assert mock_flow_init.mock_calls[0][2]["context"] == {
"source": config_entries.SOURCE_SSDP "source": config_entries.SOURCE_SSDP
} }
assert mock_flow_init.mock_calls[0][2]["data"] == { mock_call_data: ssdp.SsdpServiceInfo = mock_flow_init.mock_calls[0][2]["data"]
ssdp.ATTR_SSDP_ST: "mock-st", assert mock_call_data.ssdp_st == "mock-st"
ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", assert mock_call_data.ssdp_location == "http://1.1.1.1"
ssdp.ATTR_SSDP_USN: "uuid:mock-udn::mock-st", assert mock_call_data.ssdp_usn == "uuid:mock-udn::mock-st"
ssdp.ATTR_SSDP_SERVER: "mock-server", assert mock_call_data.ssdp_server == "mock-server"
ssdp.ATTR_SSDP_EXT: "", assert mock_call_data.ssdp_ext == ""
ssdp.ATTR_UPNP_UDN: "uuid:mock-udn", assert mock_call_data.ssdp_udn == ANY
ssdp.ATTR_SSDP_UDN: ANY, assert mock_call_data.ssdp_headers["_timestamp"] == ANY
"_timestamp": ANY, assert mock_call_data.x_homeassistant_matching_domains == {"mock-domain"}
ssdp.ATTR_HA_MATCHING_DOMAINS: {"mock-domain"}, assert mock_call_data.upnp == {ssdp.ATTR_UPNP_UDN: "uuid:mock-udn"}
}
assert "Failed to fetch ssdp data" not in caplog.text assert "Failed to fetch ssdp data" not in caplog.text
# Compatibility with old dict access (to be removed after 2022.6)
assert mock_call_data[ssdp.ATTR_SSDP_ST] == "mock-st"
assert mock_call_data[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1"
assert mock_call_data[ssdp.ATTR_SSDP_USN] == "uuid:mock-udn::mock-st"
assert mock_call_data[ssdp.ATTR_SSDP_SERVER] == "mock-server"
assert mock_call_data[ssdp.ATTR_SSDP_EXT] == ""
assert mock_call_data[ssdp.ATTR_UPNP_UDN] == "uuid:mock-udn"
assert mock_call_data[ssdp.ATTR_SSDP_UDN] == ANY
assert mock_call_data["_timestamp"] == ANY
assert mock_call_data[ssdp.ATTR_HA_MATCHING_DOMAINS] == {"mock-domain"}
# End compatibility checks
@pytest.mark.usefixtures("mock_get_source_ip") @pytest.mark.usefixtures("mock_get_source_ip")
@ -347,19 +359,31 @@ async def test_discovery_from_advertisement_sets_ssdp_st(
await hass.async_block_till_done() await hass.async_block_till_done()
discovery_info = await ssdp.async_get_discovery_info_by_udn(hass, "uuid:mock-udn") discovery_info = await ssdp.async_get_discovery_info_by_udn(hass, "uuid:mock-udn")
assert discovery_info == [ discovery_info = discovery_info[0]
{ assert discovery_info.ssdp_location == "http://1.1.1.1"
ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", assert discovery_info.ssdp_nt == "mock-st"
ssdp.ATTR_SSDP_NT: "mock-st", # Set by ssdp component, not in original advertisement.
ssdp.ATTR_SSDP_ST: "mock-st", # Set by ssdp component, not in original advertisement. assert discovery_info.ssdp_st == "mock-st"
ssdp.ATTR_SSDP_USN: "uuid:mock-udn::mock-st", assert discovery_info.ssdp_usn == "uuid:mock-udn::mock-st"
ssdp.ATTR_UPNP_UDN: "uuid:mock-udn", assert discovery_info.ssdp_udn == ANY
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", assert discovery_info.ssdp_headers["nts"] == "ssdp:alive"
ssdp.ATTR_SSDP_UDN: ANY, assert discovery_info.ssdp_headers["_timestamp"] == ANY
"nts": "ssdp:alive", assert discovery_info.upnp == {
"_timestamp": ANY, ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
} ssdp.ATTR_UPNP_UDN: "uuid:mock-udn",
] }
# Compatibility with old dict access (to be removed after 2022.6)
assert discovery_info[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1"
assert discovery_info[ssdp.ATTR_SSDP_NT] == "mock-st"
# Set by ssdp component, not in original advertisement.
assert discovery_info[ssdp.ATTR_SSDP_ST] == "mock-st"
assert discovery_info[ssdp.ATTR_SSDP_USN] == "uuid:mock-udn::mock-st"
assert discovery_info[ssdp.ATTR_UPNP_UDN] == "uuid:mock-udn"
assert discovery_info[ssdp.ATTR_UPNP_DEVICE_TYPE] == "Paulus"
assert discovery_info[ssdp.ATTR_SSDP_UDN] == ANY
assert discovery_info["nts"] == "ssdp:alive"
assert discovery_info["_timestamp"] == ANY
# End compatibility checks
@patch( # XXX TODO: Isn't this duplicate with mock_get_source_ip? @patch( # XXX TODO: Isn't this duplicate with mock_get_source_ip?
@ -452,29 +476,48 @@ async def test_scan_with_registered_callback(
assert async_integration_match_all_not_present_callback1.call_count == 0 assert async_integration_match_all_not_present_callback1.call_count == 0
assert async_match_any_callback1.call_count == 1 assert async_match_any_callback1.call_count == 1
assert async_not_matching_integration_callback1.call_count == 0 assert async_not_matching_integration_callback1.call_count == 0
assert async_integration_callback.call_args[0] == ( assert async_integration_callback.call_args[0][1] == ssdp.SsdpChange.ALIVE
{ mock_call_data: ssdp.SsdpServiceInfo = async_integration_callback.call_args[0][0]
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", assert mock_call_data.ssdp_ext == ""
ssdp.ATTR_SSDP_EXT: "", assert mock_call_data.ssdp_location == "http://1.1.1.1"
ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", assert mock_call_data.ssdp_server == "mock-server"
ssdp.ATTR_SSDP_SERVER: "mock-server", assert mock_call_data.ssdp_st == "mock-st"
ssdp.ATTR_SSDP_ST: "mock-st", assert (
ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::mock-st", mock_call_data.ssdp_usn == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::mock-st"
ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL",
"x-rincon-bootseq": "55",
ssdp.ATTR_SSDP_UDN: ANY,
"_timestamp": ANY,
ssdp.ATTR_HA_MATCHING_DOMAINS: set(),
},
ssdp.SsdpChange.ALIVE,
) )
assert mock_call_data.ssdp_headers["x-rincon-bootseq"] == "55"
assert mock_call_data.ssdp_udn == ANY
assert mock_call_data.ssdp_headers["_timestamp"] == ANY
assert mock_call_data.x_homeassistant_matching_domains == set()
assert mock_call_data.upnp == {
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL",
}
# Compatibility with old dict access (to be removed after 2022.6)
assert mock_call_data[ssdp.ATTR_UPNP_DEVICE_TYPE] == "Paulus"
assert mock_call_data[ssdp.ATTR_SSDP_EXT] == ""
assert mock_call_data[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1"
assert mock_call_data[ssdp.ATTR_SSDP_SERVER] == "mock-server"
assert mock_call_data[ssdp.ATTR_SSDP_ST] == "mock-st"
assert (
mock_call_data[ssdp.ATTR_SSDP_USN]
== "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::mock-st"
)
assert (
mock_call_data[ssdp.ATTR_UPNP_UDN]
== "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL"
)
assert mock_call_data["x-rincon-bootseq"] == "55"
assert mock_call_data[ssdp.ATTR_SSDP_UDN] == ANY
assert mock_call_data["_timestamp"] == ANY
assert mock_call_data[ssdp.ATTR_HA_MATCHING_DOMAINS] == set()
# End of compatibility checks
assert "Failed to callback info" in caplog.text assert "Failed to callback info" in caplog.text
async_integration_callback_from_cache = AsyncMock() async_integration_callback_from_cache = AsyncMock()
await ssdp.async_register_callback( await ssdp.async_register_callback(
hass, async_integration_callback_from_cache, {"st": "mock-st"} hass, async_integration_callback_from_cache, {"st": "mock-st"}
) )
assert async_integration_callback_from_cache.call_count == 1 assert async_integration_callback_from_cache.call_count == 1
@ -510,51 +553,109 @@ async def test_getting_existing_headers(
await ssdp_listener._on_search(mock_ssdp_search_response) await ssdp_listener._on_search(mock_ssdp_search_response)
discovery_info_by_st = await ssdp.async_get_discovery_info_by_st(hass, "mock-st") discovery_info_by_st = await ssdp.async_get_discovery_info_by_st(hass, "mock-st")
assert discovery_info_by_st == [ discovery_info_by_st = discovery_info_by_st[0]
{ assert discovery_info_by_st.ssdp_ext == ""
ssdp.ATTR_SSDP_EXT: "", assert discovery_info_by_st.ssdp_location == "http://1.1.1.1"
ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", assert discovery_info_by_st.ssdp_server == "mock-server"
ssdp.ATTR_SSDP_SERVER: "mock-server", assert discovery_info_by_st.ssdp_st == "mock-st"
ssdp.ATTR_SSDP_ST: "mock-st", assert (
ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", discovery_info_by_st.ssdp_usn
ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3"
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", )
ssdp.ATTR_SSDP_UDN: ANY, assert discovery_info_by_st.ssdp_udn == ANY
"_timestamp": ANY, assert discovery_info_by_st.ssdp_headers["_timestamp"] == ANY
} assert discovery_info_by_st.upnp == {
] ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL",
}
# Compatibility with old dict access (to be removed after 2022.6)
assert discovery_info_by_st[ssdp.ATTR_SSDP_EXT] == ""
assert discovery_info_by_st[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1"
assert discovery_info_by_st[ssdp.ATTR_SSDP_SERVER] == "mock-server"
assert discovery_info_by_st[ssdp.ATTR_SSDP_ST] == "mock-st"
assert (
discovery_info_by_st[ssdp.ATTR_SSDP_USN]
== "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3"
)
assert (
discovery_info_by_st[ssdp.ATTR_UPNP_UDN]
== "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL"
)
assert discovery_info_by_st[ssdp.ATTR_UPNP_DEVICE_TYPE] == "Paulus"
assert discovery_info_by_st[ssdp.ATTR_SSDP_UDN] == ANY
assert discovery_info_by_st["_timestamp"] == ANY
# End of compatibility checks
discovery_info_by_udn = await ssdp.async_get_discovery_info_by_udn( discovery_info_by_udn = await ssdp.async_get_discovery_info_by_udn(
hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL"
) )
assert discovery_info_by_udn == [ discovery_info_by_udn = discovery_info_by_udn[0]
{ assert discovery_info_by_udn.ssdp_ext == ""
ssdp.ATTR_SSDP_EXT: "", assert discovery_info_by_udn.ssdp_location == "http://1.1.1.1"
ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", assert discovery_info_by_udn.ssdp_server == "mock-server"
ssdp.ATTR_SSDP_SERVER: "mock-server", assert discovery_info_by_udn.ssdp_st == "mock-st"
ssdp.ATTR_SSDP_ST: "mock-st", assert (
ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", discovery_info_by_udn.ssdp_usn
ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3"
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", )
ssdp.ATTR_SSDP_UDN: ANY, assert discovery_info_by_udn.ssdp_udn == ANY
"_timestamp": ANY, assert discovery_info_by_udn.ssdp_headers["_timestamp"] == ANY
} assert discovery_info_by_udn.upnp == {
] ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL",
}
# Compatibility with old dict access (to be removed after 2022.6)
assert discovery_info_by_udn[ssdp.ATTR_SSDP_EXT] == ""
assert discovery_info_by_udn[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1"
assert discovery_info_by_udn[ssdp.ATTR_SSDP_SERVER] == "mock-server"
assert discovery_info_by_udn[ssdp.ATTR_SSDP_ST] == "mock-st"
assert (
discovery_info_by_udn[ssdp.ATTR_SSDP_USN]
== "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3"
)
assert (
discovery_info_by_udn[ssdp.ATTR_UPNP_UDN]
== "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL"
)
assert discovery_info_by_udn[ssdp.ATTR_UPNP_DEVICE_TYPE] == "Paulus"
assert discovery_info_by_udn[ssdp.ATTR_SSDP_UDN] == ANY
assert discovery_info_by_udn["_timestamp"] == ANY
# End of compatibility checks
discovery_info_by_udn_st = await ssdp.async_get_discovery_info_by_udn_st( discovery_info_by_udn_st = await ssdp.async_get_discovery_info_by_udn_st(
hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", "mock-st" hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", "mock-st"
) )
assert discovery_info_by_udn_st == { assert discovery_info_by_udn_st.ssdp_ext == ""
ssdp.ATTR_SSDP_EXT: "", assert discovery_info_by_udn_st.ssdp_location == "http://1.1.1.1"
ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", assert discovery_info_by_udn_st.ssdp_server == "mock-server"
ssdp.ATTR_SSDP_SERVER: "mock-server", assert discovery_info_by_udn_st.ssdp_st == "mock-st"
ssdp.ATTR_SSDP_ST: "mock-st", assert (
ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", discovery_info_by_udn_st.ssdp_usn
ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3"
)
assert discovery_info_by_udn_st.ssdp_udn == ANY
assert discovery_info_by_udn_st.ssdp_headers["_timestamp"] == ANY
assert discovery_info_by_udn_st.upnp == {
ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
ssdp.ATTR_SSDP_UDN: ANY, ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL",
"_timestamp": ANY,
} }
# Compatibility with old dict access (to be removed after 2022.6)
assert discovery_info_by_udn_st[ssdp.ATTR_SSDP_EXT] == ""
assert discovery_info_by_udn_st[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1"
assert discovery_info_by_udn_st[ssdp.ATTR_SSDP_SERVER] == "mock-server"
assert discovery_info_by_udn_st[ssdp.ATTR_SSDP_ST] == "mock-st"
assert (
discovery_info_by_udn_st[ssdp.ATTR_SSDP_USN]
== "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3"
)
assert (
discovery_info_by_udn_st[ssdp.ATTR_UPNP_UDN]
== "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL"
)
assert discovery_info_by_udn_st[ssdp.ATTR_UPNP_DEVICE_TYPE] == "Paulus"
assert discovery_info_by_udn_st[ssdp.ATTR_SSDP_UDN] == ANY
assert discovery_info_by_udn_st["_timestamp"] == ANY
# End of compatibility checks
assert ( assert (
await ssdp.async_get_discovery_info_by_udn_st(hass, "wrong", "mock-st") is None await ssdp.async_get_discovery_info_by_udn_st(hass, "wrong", "mock-st") is None

View File

@ -237,7 +237,9 @@ async def test_abort_discovered_multiple(hass, flow_handler, local_impl):
) )
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
TEST_DOMAIN, context={"source": config_entries.SOURCE_SSDP} TEST_DOMAIN,
context={"source": config_entries.SOURCE_SSDP},
data=data_entry_flow.BaseServiceInfo(),
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
@ -267,7 +269,9 @@ async def test_abort_discovered_existing_entries(hass, flow_handler, local_impl)
entry.add_to_hass(hass) entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
TEST_DOMAIN, context={"source": config_entries.SOURCE_SSDP} TEST_DOMAIN,
context={"source": config_entries.SOURCE_SSDP},
data=data_entry_flow.BaseServiceInfo(),
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT

View File

@ -2351,7 +2351,7 @@ async def test_async_setup_update_entry(hass):
"discovery_source", "discovery_source",
( (
(config_entries.SOURCE_DISCOVERY, {}), (config_entries.SOURCE_DISCOVERY, {}),
(config_entries.SOURCE_SSDP, {}), (config_entries.SOURCE_SSDP, BaseServiceInfo()),
(config_entries.SOURCE_USB, BaseServiceInfo()), (config_entries.SOURCE_USB, BaseServiceInfo()),
(config_entries.SOURCE_HOMEKIT, BaseServiceInfo()), (config_entries.SOURCE_HOMEKIT, BaseServiceInfo()),
(config_entries.SOURCE_DHCP, BaseServiceInfo()), (config_entries.SOURCE_DHCP, BaseServiceInfo()),