コード例 #1
0
 def async_matching_domains(self, info_with_desc: CaseInsensitiveDict) -> set[str]:
     """Find domains matching the passed CaseInsensitiveDict."""
     assert self._match_by_key is not None
     domains = set()
     for key, matchers_by_key in self._match_by_key.items():
         if not (match_value := info_with_desc.get(key)):
             continue
         for domain, matcher in matchers_by_key.get(match_value, []):
             if domain in domains:
                 continue
             if all(info_with_desc.get(k) == v for (k, v) in matcher.items()):
                 domains.add(domain)
コード例 #2
0
def decode_ssdp_packet(
        data: bytes,
        addr: AddressTupleVXType) -> Tuple[str, CaseInsensitiveDict]:
    """Decode a message."""
    lines = data.split(b"\n")

    # request_line
    request_line = lines[0].strip().decode()

    # parse headers
    header_lines = b"\n".join(lines[1:])
    email_headers = email.message_from_bytes(header_lines)
    headers = CaseInsensitiveDict(**dict(email_headers.items()))

    # adjust some headers
    if "location" in headers:
        headers["_location_original"] = headers["location"]
        headers["location"] = get_adjusted_url(headers["location"], addr)

    # own data
    headers["_timestamp"] = datetime.now()
    headers["_host"] = get_host_string(addr)
    headers["_port"] = addr[1]

    if "usn" in headers and "uuid:" in headers["usn"]:
        parts = str(headers["usn"]).split("::")
        headers["_udn"] = parts[0]

    return request_line, headers
コード例 #3
0
def decode_ssdp_packet(
        data: bytes,
        addr: AddressTupleVXType) -> Tuple[str, CaseInsensitiveDict]:
    """Decode a message."""
    lines = data.replace(b"\r\n", b"\n").split(b"\n")

    # request_line
    request_line = lines[0].strip().decode()

    if lines and lines[-1] != b"":
        lines.append(b"")

    parsed_headers, _ = HeadersParser().parse_headers(lines)
    headers = CaseInsensitiveDict(**parsed_headers)

    # adjust some headers
    if "location" in headers:
        headers["_location_original"] = headers["location"]
        headers["location"] = get_adjusted_url(headers["location"], addr)

    # own data
    headers["_timestamp"] = datetime.now()
    headers["_host"] = get_host_string(addr)
    headers["_port"] = addr[1]

    if "usn" in headers and "uuid:" in headers["usn"]:
        parts = str(headers["usn"]).split("::")
        headers["_udn"] = parts[0]

    return request_line, headers
コード例 #4
0
def test_case_insensitive_dict():
    """Test CaseInsensitiveDict."""
    ci_dict = CaseInsensitiveDict()
    ci_dict["Key"] = "value"
    assert ci_dict["Key"] == "value"
    assert ci_dict["key"] == "value"
    assert ci_dict["KEY"] == "value"
コード例 #5
0
    async def _ssdp_listener_callback(self, ssdp_device: SsdpDevice,
                                      dst: DeviceOrServiceType,
                                      source: SsdpSource) -> None:
        """Handle a device/service change."""
        _LOGGER.debug("Change, ssdp_device: %s, dst: %s, source: %s",
                      ssdp_device, dst, source)

        location = ssdp_device.location
        info_desc = await self._async_get_description_dict(location) or {}
        combined_headers = ssdp_device.combined_headers(dst)
        info_with_desc = CaseInsensitiveDict(combined_headers, **info_desc)
        discovery_info = discovery_info_from_headers_and_description(
            info_with_desc)

        callbacks = self._async_get_matching_callbacks(combined_headers)
        ssdp_change = SSDP_SOURCE_SSDP_CHANGE_MAPPING[source]
        await _async_process_callbacks(callbacks, discovery_info, ssdp_change)

        for domain in self._async_matching_domains(info_with_desc):
            _LOGGER.debug("Discovered %s at %s", domain, location)

            flow: SSDPFlow = {
                "domain": domain,
                "context": {
                    "source": config_entries.SOURCE_SSDP
                },
                "data": discovery_info,
            }
            assert self._flow_dispatcher is not None
            self._flow_dispatcher.create(flow)
コード例 #6
0
ファイル: __init__.py プロジェクト: pgenera/home-assistant
 def _async_matching_domains(self, info_with_req: CaseInsensitiveDict) -> set[str]:
     domains = set()
     for domain, matchers in self._integration_matchers.items():
         for matcher in matchers:
             if all(info_with_req.get(k) == v for (k, v) in matcher.items()):
                 domains.add(domain)
     return domains
コード例 #7
0
def test_case_insensitive_dict_dict_equality():
    """Test CaseInsensitiveDict against dict equality."""
    ci_dict = CaseInsensitiveDict()
    ci_dict["Key"] = "value"

    assert ci_dict == {"Key": "value"}
    assert ci_dict == {"key": "value"}
    assert ci_dict == {"KEY": "value"}
コード例 #8
0
ファイル: __init__.py プロジェクト: pgenera/home-assistant
def discovery_info_from_headers_and_request(
    info_with_req: CaseInsensitiveDict,
) -> dict[str, str]:
    """Convert headers and description to discovery_info."""
    info = {DISCOVERY_MAPPING.get(k.lower(), k): v for k, v in info_with_req.items()}

    if ATTR_UPNP_UDN not in info and ATTR_SSDP_USN in info:
        if udn := _udn_from_usn(info[ATTR_SSDP_USN]):
            info[ATTR_UPNP_UDN] = udn
コード例 #9
0
ファイル: __init__.py プロジェクト: andynbaker/core
def _async_headers_match(headers: CaseInsensitiveDict,
                         lower_match_dict: dict[str, str]) -> bool:
    for header, val in lower_match_dict.items():
        if val == MATCH_ALL:
            if header not in headers:
                return False
        elif headers.get_lower(header) != val:
            return False
    return True
コード例 #10
0
def test_case_insensitive_dict():
    ci = CaseInsensitiveDict()
    ci['Key'] = 'value'
    assert ci['Key'] == 'value'
    assert ci['key'] == 'value'
    assert ci['KEY'] == 'value'

    assert ci == {'Key': 'value'}
    assert ci == {'key': 'value'}
    assert ci == {'KEY': 'value'}

    source_dict = {'key': 'value'}
    ci = CaseInsensitiveDict(**source_dict)
    assert ci == {'key': 'value'}

    cis = set()
    cis.add(CaseInsensitiveDict(key='value'))
    cis.add(CaseInsensitiveDict(KEY='value'))
    assert len(cis) == 1
コード例 #11
0
    def _async_headers_to_discovery_info(
            self, headers: Mapping[str, str]) -> dict[str, str]:
        """Combine the headers and description into discovery_info.

        Building this is a bit expensive so we only do it on demand.
        """
        assert self.description_manager is not None
        location = headers["location"]
        info_req = self.description_manager.async_cached_description(
            location) or {}
        return discovery_info_from_headers_and_request(
            CaseInsensitiveDict(**headers, **info_req))
コード例 #12
0
ファイル: __init__.py プロジェクト: antsar/home-assistant
    async def _async_headers_to_discovery_info(
            self, headers: Mapping[str, Any]) -> dict[str, Any]:
        """Combine the headers and description into discovery_info.

        Building this is a bit expensive so we only do it on demand.
        """
        assert self._description_cache is not None
        location = headers["location"]
        info_desc = (
            await self._description_cache.async_get_description_dict(location)
            or {})
        return discovery_info_from_headers_and_description(
            CaseInsensitiveDict(headers, **info_desc))
コード例 #13
0
    async def _ssdp_listener_callback(
        self,
        ssdp_device: SsdpDevice,
        dst: DeviceOrServiceType,
        source: SsdpSource,
    ) -> None:
        """Handle a device/service change."""
        _LOGGER.debug(
            "SSDP: ssdp_device: %s, dst: %s, source: %s", ssdp_device, dst, source
        )

        location = ssdp_device.location
        info_desc = None
        combined_headers = ssdp_device.combined_headers(dst)
        callbacks = self._async_get_matching_callbacks(combined_headers)
        matching_domains: set[str] = set()

        # If there are no changes from a search, do not trigger a config flow
        if source != SsdpSource.SEARCH_ALIVE:
            info_desc = await self._async_get_description_dict(location) or {}
            assert isinstance(combined_headers, CaseInsensitiveDict)
            matching_domains = self.integration_matchers.async_matching_domains(
                CaseInsensitiveDict({**combined_headers.as_dict(), **info_desc})
            )

        if not callbacks and not matching_domains:
            return

        if info_desc is None:
            info_desc = await self._async_get_description_dict(location) or {}
        discovery_info = discovery_info_from_headers_and_description(
            combined_headers, info_desc
        )
        discovery_info.x_homeassistant_matching_domains = matching_domains
        ssdp_change = SSDP_SOURCE_SSDP_CHANGE_MAPPING[source]
        await _async_process_callbacks(callbacks, discovery_info, ssdp_change)

        # Config flows should only be created for alive/update messages from alive devices
        if ssdp_change == SsdpChange.BYEBYE:
            return

        _LOGGER.debug("Discovery info: %s", discovery_info)

        for domain in matching_domains:
            _LOGGER.debug("Discovered %s at %s", domain, location)
            discovery_flow.async_create_flow(
                self.hass,
                domain,
                {"source": config_entries.SOURCE_SSDP},
                discovery_info,
            )
コード例 #14
0
ファイル: __init__.py プロジェクト: home-ha/home-assistant
    async def _ssdp_listener_callback(
        self,
        ssdp_device: SsdpDevice,
        dst: DeviceOrServiceType,
        source: SsdpSource,
    ) -> None:
        """Handle a device/service change."""
        _LOGGER.debug("SSDP: ssdp_device: %s, dst: %s, source: %s",
                      ssdp_device, dst, source)

        location = ssdp_device.location
        info_desc = await self._async_get_description_dict(location) or {}
        combined_headers = ssdp_device.combined_headers(dst)
        info_with_desc = CaseInsensitiveDict(combined_headers, **info_desc)

        callbacks = self._async_get_matching_callbacks(combined_headers)
        matching_domains: set[str] = set()

        # If there are no changes from a search, do not trigger a config flow
        if source != SsdpSource.SEARCH_ALIVE:
            matching_domains = self.integration_matchers.async_matching_domains(
                info_with_desc)

        if not callbacks and not matching_domains:
            return

        discovery_info = discovery_info_from_headers_and_description(
            info_with_desc)
        ssdp_change = SSDP_SOURCE_SSDP_CHANGE_MAPPING[source]
        await _async_process_callbacks(callbacks, discovery_info, ssdp_change)

        # Config flows should only be created for alive/update messages from alive devices
        if ssdp_change == SsdpChange.BYEBYE:
            return

        for domain in matching_domains:
            _LOGGER.debug("Discovered %s at %s", domain, location)
            flow: SSDPFlow = {
                "domain": domain,
                "context": {
                    "source": config_entries.SOURCE_SSDP
                },
                "data": discovery_info,
            }
            assert self._flow_dispatcher is not None
            self._flow_dispatcher.create(flow)
コード例 #15
0
def decode_ssdp_packet(data: bytes, addr: str) -> Tuple[str, CaseInsensitiveDict]:
    """Decode a message."""
    lines = data.split(b'\n')

    # request_line
    request_line = lines[0].strip().decode()

    # parse headers
    header_lines = b'\n'.join(lines[1:])
    email_headers = email.message_from_bytes(header_lines)
    headers = CaseInsensitiveDict(**dict(email_headers.items()))

    # own data
    headers['_timestamp'] = datetime.now()
    headers['_address'] = addr
    if 'usn' in headers and 'uuid:' in headers['usn']:
        parts = str(headers['usn']).split('::')
        headers['_udn'] = parts[0]

    return request_line, headers
コード例 #16
0
ファイル: __init__.py プロジェクト: andynbaker/core
def discovery_info_from_headers_and_description(
    combined_headers: CaseInsensitiveDict,
    info_desc: Mapping[str, Any],
) -> SsdpServiceInfo:
    """Convert headers and description to discovery_info."""
    ssdp_usn = combined_headers["usn"]
    ssdp_st = combined_headers.get("st")
    if isinstance(info_desc, CaseInsensitiveDict):
        upnp_info = {**info_desc.as_dict()}
    else:
        upnp_info = {**info_desc}

    # Increase compatibility: depending on the message type,
    # either the ST (Search Target, from M-SEARCH messages)
    # or NT (Notification Type, from NOTIFY messages) header is mandatory
    if not ssdp_st:
        ssdp_st = combined_headers["nt"]

    # Ensure UPnP "udn" is set
    if ATTR_UPNP_UDN not in upnp_info:
        if udn := _udn_from_usn(ssdp_usn):
            upnp_info[ATTR_UPNP_UDN] = udn
コード例 #17
0
def test_case_insensitive_dict_equality():
    """Test CaseInsensitiveDict equality."""
    assert CaseInsensitiveDict(key="value") == CaseInsensitiveDict(KEY="value")
コード例 #18
0
async def test_location_change_with_overlapping_udn_st_combinations(
        hass, aioclient_mock):
    """Test handling when a UDN and ST broadcast multiple locations."""
    mock_get_ssdp = {
        "test_integration": [{
            "manufacturer": "test_manufacturer",
            "modelName": "test_model"
        }]
    }

    hue_response = """
<root xmlns="urn:schemas-upnp-org:device-1-0">
<device>
<manufacturer>test_manufacturer</manufacturer>
<modelName>test_model</modelName>
</device>
</root>
    """

    aioclient_mock.get(
        "http://192.168.72.1:49154/wps_device.xml",
        text=hue_response.format(ip_address="192.168.72.1"),
    )
    aioclient_mock.get(
        "http://192.168.72.1:49152/wps_device.xml",
        text=hue_response.format(ip_address="192.168.72.1"),
    )
    ssdp_response_without_location = {
        "ST": "upnp:rootdevice",
        "_udn": "uuid:a793d3cc-e802-44fb-84f4-5a30f33115b6",
        "USN": "uuid:a793d3cc-e802-44fb-84f4-5a30f33115b6::upnp:rootdevice",
        "EXT": "",
    }

    port_49154_response = CaseInsensitiveDict(
        **ssdp_response_without_location,
        **{"LOCATION": "http://192.168.72.1:49154/wps_device.xml"},
    )
    port_49152_response = CaseInsensitiveDict(
        **ssdp_response_without_location,
        **{"LOCATION": "http://192.168.72.1:49152/wps_device.xml"},
    )
    mock_ssdp_response = port_49154_response

    def _generate_fake_ssdp_listener(*args, **kwargs):
        listener = SSDPListener(*args, **kwargs)

        async def _async_callback(*_):
            pass

        @callback
        def _callback(*_):
            hass.async_create_task(listener.async_callback(mock_ssdp_response))

        listener.async_start = _async_callback
        listener.async_search = _callback
        return listener

    with patch(
            "homeassistant.components.ssdp.async_get_ssdp",
            return_value=mock_get_ssdp,
    ), patch(
            "homeassistant.components.ssdp.SSDPListener",
            new=_generate_fake_ssdp_listener,
    ), patch.object(hass.config_entries.flow, "async_init") as mock_init:
        assert await async_setup_component(hass, ssdp.DOMAIN,
                                           {ssdp.DOMAIN: {}})
        await hass.async_block_till_done()
        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()
        assert len(mock_init.mock_calls) == 1
        assert mock_init.mock_calls[0][1][0] == "test_integration"
        assert mock_init.mock_calls[0][2]["context"] == {
            "source": config_entries.SOURCE_SSDP
        }
        assert (mock_init.mock_calls[0][2]["data"][ssdp.ATTR_SSDP_LOCATION] ==
                port_49154_response["location"])

        mock_init.reset_mock()
        mock_ssdp_response = port_49152_response
        async_fire_time_changed(hass,
                                dt_util.utcnow() + timedelta(seconds=400))
        await hass.async_block_till_done()
        assert mock_init.mock_calls[0][1][0] == "test_integration"
        assert mock_init.mock_calls[0][2]["context"] == {
            "source": config_entries.SOURCE_SSDP
        }
        assert (mock_init.mock_calls[0][2]["data"][ssdp.ATTR_SSDP_LOCATION] ==
                port_49152_response["location"])

        mock_init.reset_mock()
        mock_ssdp_response = port_49154_response
        async_fire_time_changed(hass,
                                dt_util.utcnow() + timedelta(seconds=600))
        await hass.async_block_till_done()
        assert mock_init.mock_calls[0][1][0] == "test_integration"
        assert mock_init.mock_calls[0][2]["context"] == {
            "source": config_entries.SOURCE_SSDP
        }
        assert (mock_init.mock_calls[0][2]["data"][ssdp.ATTR_SSDP_LOCATION] ==
                port_49154_response["location"])
コード例 #19
0
def _ssdp_headers(headers):
    return CaseInsensitiveDict(headers,
                               _timestamp=datetime(2021, 1, 1, 12, 00),
                               _udn=udn_from_headers(headers))
コード例 #20
0
ファイル: test_init.py プロジェクト: pgenera/home-assistant
async def test_location_change_evicts_prior_location_from_cache(hass, aioclient_mock):
    """Test that a location change for a UDN will evict the prior location from the cache."""
    mock_get_ssdp = {
        "hue": [{"manufacturer": "Signify", "modelName": "Philips hue bridge 2015"}]
    }

    hue_response = """
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<URLBase>http://{ip_address}:80/</URLBase>
<device>
<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>
<friendlyName>Philips hue ({ip_address})</friendlyName>
<manufacturer>Signify</manufacturer>
<manufacturerURL>http://www.philips-hue.com</manufacturerURL>
<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>
<modelName>Philips hue bridge 2015</modelName>
<modelNumber>BSB002</modelNumber>
<modelURL>http://www.philips-hue.com</modelURL>
<serialNumber>001788a36abf</serialNumber>
<UDN>uuid:2f402f80-da50-11e1-9b23-001788a36abf</UDN>
</device>
</root>
    """

    aioclient_mock.get(
        "http://192.168.212.23/description.xml",
        text=hue_response.format(ip_address="192.168.212.23"),
    )
    aioclient_mock.get(
        "http://169.254.8.155/description.xml",
        text=hue_response.format(ip_address="169.254.8.155"),
    )
    ssdp_response_without_location = {
        "ST": "uuid:2f402f80-da50-11e1-9b23-001788a36abf",
        "_udn": "uuid:2f402f80-da50-11e1-9b23-001788a36abf",
        "USN": "uuid:2f402f80-da50-11e1-9b23-001788a36abf",
        "SERVER": "Hue/1.0 UPnP/1.0 IpBridge/1.44.0",
        "hue-bridgeid": "001788FFFEA36ABF",
        "EXT": "",
    }

    mock_good_ip_ssdp_response = CaseInsensitiveDict(
        **ssdp_response_without_location,
        **{"LOCATION": "http://192.168.212.23/description.xml"},
    )
    mock_link_local_ip_ssdp_response = CaseInsensitiveDict(
        **ssdp_response_without_location,
        **{"LOCATION": "http://169.254.8.155/description.xml"},
    )
    mock_ssdp_response = mock_good_ip_ssdp_response

    def _generate_fake_ssdp_listener(*args, **kwargs):
        listener = SSDPListener(*args, **kwargs)

        async def _async_callback(*_):
            pass

        @callback
        def _callback(*_):
            import pprint

            pprint.pprint(mock_ssdp_response)
            hass.async_create_task(listener.async_callback(mock_ssdp_response))

        listener.async_start = _async_callback
        listener.async_search = _callback
        return listener

    with patch(
        "homeassistant.components.ssdp.async_get_ssdp",
        return_value=mock_get_ssdp,
    ), patch(
        "homeassistant.components.ssdp.SSDPListener",
        new=_generate_fake_ssdp_listener,
    ), patch.object(
        hass.config_entries.flow, "async_init"
    ) as mock_init:
        assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
        await hass.async_block_till_done()
        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()
        assert len(mock_init.mock_calls) == 1
        assert mock_init.mock_calls[0][1][0] == "hue"
        assert mock_init.mock_calls[0][2]["context"] == {
            "source": config_entries.SOURCE_SSDP
        }
        assert (
            mock_init.mock_calls[0][2]["data"][ssdp.ATTR_SSDP_LOCATION]
            == mock_good_ip_ssdp_response["location"]
        )

        mock_init.reset_mock()
        mock_ssdp_response = mock_link_local_ip_ssdp_response
        async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=400))
        await hass.async_block_till_done()
        assert mock_init.mock_calls[0][1][0] == "hue"
        assert mock_init.mock_calls[0][2]["context"] == {
            "source": config_entries.SOURCE_SSDP
        }
        assert (
            mock_init.mock_calls[0][2]["data"][ssdp.ATTR_SSDP_LOCATION]
            == mock_link_local_ip_ssdp_response["location"]
        )

        mock_init.reset_mock()
        mock_ssdp_response = mock_good_ip_ssdp_response
        async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=600))
        await hass.async_block_till_done()
        assert mock_init.mock_calls[0][1][0] == "hue"
        assert mock_init.mock_calls[0][2]["context"] == {
            "source": config_entries.SOURCE_SSDP
        }
        assert (
            mock_init.mock_calls[0][2]["data"][ssdp.ATTR_SSDP_LOCATION]
            == mock_good_ip_ssdp_response["location"]
        )
コード例 #21
0
ファイル: test_init.py プロジェクト: pgenera/home-assistant
async def test_scan_second_hit(hass, aioclient_mock, caplog):
    """Test matching on second scan."""
    aioclient_mock.get(
        "http://1.1.1.1",
        text="""
<root>
  <device>
    <deviceType>Paulus</deviceType>
  </device>
</root>
    """,
    )

    mock_ssdp_response = CaseInsensitiveDict(
        **{
            "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": "",
        }
    )
    mock_get_ssdp = {"mock-domain": [{"st": "mock-st"}]}
    integration_callbacks = []

    @callback
    def _async_integration_callbacks(info):
        integration_callbacks.append(info)

    def _generate_fake_ssdp_listener(*args, **kwargs):
        listener = SSDPListener(*args, **kwargs)

        async def _async_callback(*_):
            pass

        @callback
        def _callback(*_):
            hass.async_create_task(listener.async_callback(mock_ssdp_response))

        listener.async_start = _async_callback
        listener.async_search = _callback
        return listener

    with patch(
        "homeassistant.components.ssdp.async_get_ssdp",
        return_value=mock_get_ssdp,
    ), patch(
        "homeassistant.components.ssdp.SSDPListener",
        new=_generate_fake_ssdp_listener,
    ), patch.object(
        hass.config_entries.flow, "async_init", return_value=mock_coro()
    ) as mock_init:
        assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
        await hass.async_block_till_done()
        remove = ssdp.async_register_callback(
            hass,
            _async_integration_callbacks,
            {"st": "mock-st"},
        )
        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()
        async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
        await hass.async_block_till_done()
        remove()
        async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
        await hass.async_block_till_done()

    assert len(integration_callbacks) == 4
    assert integration_callbacks[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::urn:mdx-netflix-com:service:target:3",
        ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL",
    }
    assert len(mock_init.mock_calls) == 1
    assert mock_init.mock_calls[0][1][0] == "mock-domain"
    assert mock_init.mock_calls[0][2]["context"] == {
        "source": config_entries.SOURCE_SSDP
    }
    assert mock_init.mock_calls[0][2]["data"] == {
        ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus",
        ssdp.ATTR_SSDP_ST: "mock-st",
        ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1",
        ssdp.ATTR_SSDP_SERVER: "mock-server",
        ssdp.ATTR_SSDP_EXT: "",
        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",
    }
    assert "Failed to fetch ssdp data" not in caplog.text
    udn_discovery_info = ssdp.async_get_discovery_info_by_st(hass, "mock-st")
    discovery_info = udn_discovery_info[0]
    assert discovery_info[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1"
    assert discovery_info[ssdp.ATTR_SSDP_ST] == "mock-st"
    assert (
        discovery_info[ssdp.ATTR_UPNP_UDN]
        == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL"
    )
    assert (
        discovery_info[ssdp.ATTR_SSDP_USN]
        == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3"
    )

    st_discovery_info = ssdp.async_get_discovery_info_by_udn(
        hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL"
    )
    discovery_info = st_discovery_info[0]
    assert discovery_info[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1"
    assert discovery_info[ssdp.ATTR_SSDP_ST] == "mock-st"
    assert (
        discovery_info[ssdp.ATTR_UPNP_UDN]
        == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL"
    )
    assert (
        discovery_info[ssdp.ATTR_SSDP_USN]
        == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3"
    )

    discovery_info = ssdp.async_get_discovery_info_by_udn_st(
        hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", "mock-st"
    )
    assert discovery_info[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1"
    assert discovery_info[ssdp.ATTR_SSDP_ST] == "mock-st"
    assert (
        discovery_info[ssdp.ATTR_UPNP_UDN]
        == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL"
    )
    assert (
        discovery_info[ssdp.ATTR_SSDP_USN]
        == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3"
    )

    assert ssdp.async_get_discovery_info_by_udn_st(hass, "wrong", "mock-st") is None
コード例 #22
0
class Scanner:
    """Class to manage SSDP scanning."""
    def __init__(
            self, hass: HomeAssistant,
            integration_matchers: dict[str, list[dict[str, str]]]) -> None:
        """Initialize class."""
        self.hass = hass
        self.seen: set[tuple[str, str]] = set()
        self.cache: dict[tuple[str, str], Mapping[str, str]] = {}
        self._integration_matchers = integration_matchers
        self._cancel_scan: Callable[[], None] | None = None
        self._ssdp_listeners: list[SSDPListener] = []
        self._callbacks: list[tuple[Callable[[dict], None], dict[str,
                                                                 str]]] = []
        self.flow_dispatcher: FlowDispatcher | None = None
        self.description_manager: DescriptionManager | None = None

    @core_callback
    def async_register_callback(
            self,
            callback: Callable[[dict], None],
            match_dict: None | dict[str, str] = None) -> Callable[[], None]:
        """Register a callback."""
        if match_dict is None:
            match_dict = {}

        # Make sure any entries that happened
        # before the callback was registered are fired
        if self.hass.state != CoreState.running:
            for headers in self.cache.values():
                self._async_callback_if_match(callback, headers, match_dict)

        callback_entry = (callback, match_dict)
        self._callbacks.append(callback_entry)

        @core_callback
        def _async_remove_callback() -> None:
            self._callbacks.remove(callback_entry)

        return _async_remove_callback

    @core_callback
    def _async_callback_if_match(
        self,
        callback: Callable[[dict], None],
        headers: Mapping[str, str],
        match_dict: dict[str, str],
    ) -> None:
        """Fire a callback if info matches the match dict."""
        if not all(headers.get(k) == v for (k, v) in match_dict.items()):
            return
        _async_process_callbacks(
            [callback], self._async_headers_to_discovery_info(headers))

    @core_callback
    def async_stop(self, *_: Any) -> None:
        """Stop the scanner."""
        assert self._cancel_scan is not None
        self._cancel_scan()
        for listener in self._ssdp_listeners:
            listener.async_stop()
        self._ssdp_listeners = []

    async def _async_build_source_set(self) -> set[IPv4Address | IPv6Address]:
        """Build the list of ssdp sources."""
        adapters = await network.async_get_adapters(self.hass)
        sources: set[IPv4Address | IPv6Address] = set()
        if _async_use_default_interface(adapters):
            sources.add(IPv4Address("0.0.0.0"))
            return sources

        for adapter in adapters:
            if not adapter["enabled"]:
                continue
            if adapter["ipv4"]:
                ipv4 = adapter["ipv4"][0]
                sources.add(IPv4Address(ipv4["address"]))
            if adapter["ipv6"]:
                ipv6 = adapter["ipv6"][0]
                # With python 3.9 add scope_ids can be
                # added by enumerating adapter["ipv6"]s
                # IPv6Address(f"::%{ipv6['scope_id']}")
                sources.add(IPv6Address(ipv6["address"]))

        return sources

    @core_callback
    def async_scan(self, *_: Any) -> None:
        """Scan for new entries."""
        for listener in self._ssdp_listeners:
            listener.async_search()

    async def async_start(self) -> None:
        """Start the scanner."""
        self.description_manager = DescriptionManager(self.hass)
        self.flow_dispatcher = FlowDispatcher(self.hass)
        for source_ip in await self._async_build_source_set():
            self._ssdp_listeners.append(
                SSDPListener(async_callback=self._async_process_entry,
                             source_ip=source_ip))

        self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
                                        self.async_stop)
        self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED,
                                        self.flow_dispatcher.async_start)
        await asyncio.gather(
            *[listener.async_start() for listener in self._ssdp_listeners])
        self._cancel_scan = async_track_time_interval(self.hass,
                                                      self.async_scan,
                                                      SCAN_INTERVAL)

    @core_callback
    def _async_get_matching_callbacks(
            self, headers: Mapping[str, str]) -> list[Callable[[dict], None]]:
        """Return a list of callbacks that match."""
        return [
            callback for callback, match_dict in self._callbacks if all(
                headers.get(k) == v for (k, v) in match_dict.items())
        ]

    @core_callback
    def _async_matching_domains(
            self, info_with_req: CaseInsensitiveDict) -> set[str]:
        domains = set()
        for domain, matchers in self._integration_matchers.items():
            for matcher in matchers:
                if all(
                        info_with_req.get(k) == v
                        for (k, v) in matcher.items()):
                    domains.add(domain)
        return domains

    async def _async_process_entry(self, headers: Mapping[str, str]) -> None:
        """Process SSDP entries."""
        _LOGGER.debug("_async_process_entry: %s", headers)
        if "st" not in headers or "location" not in headers:
            return
        h_st = headers["st"]
        h_location = headers["location"]
        key = (h_st, h_location)

        if udn := _udn_from_usn(headers.get("usn")):
            self.cache[(udn, h_st)] = headers

        callbacks = self._async_get_matching_callbacks(headers)
        if key in self.seen and not callbacks:
            return

        assert self.description_manager is not None
        info_req = await self.description_manager.fetch_description(h_location
                                                                    ) or {}
        info_with_req = CaseInsensitiveDict(**headers, **info_req)
        discovery_info = discovery_info_from_headers_and_request(info_with_req)

        _async_process_callbacks(callbacks, discovery_info)
        if key in self.seen:
            return
        self.seen.add(key)

        for domain in self._async_matching_domains(info_with_req):
            _LOGGER.debug("Discovered %s at %s", domain, h_location)
            flow: SSDPFlow = {
                "domain": domain,
                "context": {
                    "source": config_entries.SOURCE_SSDP
                },
                "data": discovery_info,
            }
            assert self.flow_dispatcher is not None
            self.flow_dispatcher.create(flow)
コード例 #23
0
def _ssdp_headers(headers):
    ssdp_headers = CaseInsensitiveDict(headers, _timestamp=datetime.now())
    ssdp_headers["_udn"] = udn_from_headers(ssdp_headers)
    return ssdp_headers
コード例 #24
0
class Scanner:
    """Class to manage SSDP scanning."""
    def __init__(
            self, hass: HomeAssistant,
            integration_matchers: dict[str, list[dict[str, str]]]) -> None:
        """Initialize class."""
        self.hass = hass
        self.seen: set[tuple[str, str | None]] = set()
        self.cache: dict[tuple[str, str], Mapping[str, str]] = {}
        self._integration_matchers = integration_matchers
        self._cancel_scan: Callable[[], None] | None = None
        self._ssdp_listeners: list[SSDPListener] = []
        self._callbacks: list[tuple[Callable[[dict], None], dict[str,
                                                                 str]]] = []
        self.flow_dispatcher: FlowDispatcher | None = None
        self.description_manager: DescriptionManager | None = None

    @core_callback
    def async_register_callback(
            self,
            callback: Callable[[dict], None],
            match_dict: None | dict[str, str] = None) -> Callable[[], None]:
        """Register a callback."""
        if match_dict is None:
            match_dict = {}

        # Make sure any entries that happened
        # before the callback was registered are fired
        if self.hass.state != CoreState.running:
            for headers in self.cache.values():
                if _async_headers_match(headers, match_dict):
                    _async_process_callbacks(
                        [callback],
                        self._async_headers_to_discovery_info(headers))

        callback_entry = (callback, match_dict)
        self._callbacks.append(callback_entry)

        @core_callback
        def _async_remove_callback() -> None:
            self._callbacks.remove(callback_entry)

        return _async_remove_callback

    @core_callback
    def async_stop(self, *_: Any) -> None:
        """Stop the scanner."""
        assert self._cancel_scan is not None
        self._cancel_scan()
        for listener in self._ssdp_listeners:
            listener.async_stop()
        self._ssdp_listeners = []

    async def _async_build_source_set(self) -> set[IPv4Address | IPv6Address]:
        """Build the list of ssdp sources."""
        adapters = await network.async_get_adapters(self.hass)
        sources: set[IPv4Address | IPv6Address] = set()
        if _async_use_default_interface(adapters):
            sources.add(IPv4Address("0.0.0.0"))
            return sources

        for adapter in adapters:
            if not adapter["enabled"]:
                continue
            if adapter["ipv4"]:
                ipv4 = adapter["ipv4"][0]
                sources.add(IPv4Address(ipv4["address"]))
            if adapter["ipv6"]:
                ipv6 = adapter["ipv6"][0]
                # With python 3.9 add scope_ids can be
                # added by enumerating adapter["ipv6"]s
                # IPv6Address(f"::%{ipv6['scope_id']}")
                sources.add(IPv6Address(ipv6["address"]))

        return sources

    @core_callback
    def async_scan(self, *_: Any) -> None:
        """Scan for new entries."""
        for listener in self._ssdp_listeners:
            listener.async_search()

    async def async_start(self) -> None:
        """Start the scanner."""
        self.description_manager = DescriptionManager(self.hass)
        self.flow_dispatcher = FlowDispatcher(self.hass)
        for source_ip in await self._async_build_source_set():
            self._ssdp_listeners.append(
                SSDPListener(async_callback=self._async_process_entry,
                             source_ip=source_ip))
            try:
                IPv4Address(source_ip)
            except ValueError:
                continue
            # Some sonos devices only seem to respond if we send to the broadcast
            # address. This matches pysonos' behavior
            # https://github.com/amelchio/pysonos/blob/d4329b4abb657d106394ae69357805269708c996/pysonos/discovery.py#L120
            self._ssdp_listeners.append(
                SSDPListener(
                    async_callback=self._async_process_entry,
                    source_ip=source_ip,
                    target_ip=IPV4_BROADCAST,
                ))
        self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
                                        self.async_stop)
        self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED,
                                        self.flow_dispatcher.async_start)
        await asyncio.gather(
            *[listener.async_start() for listener in self._ssdp_listeners])
        self._cancel_scan = async_track_time_interval(self.hass,
                                                      self.async_scan,
                                                      SCAN_INTERVAL)

    @core_callback
    def _async_get_matching_callbacks(
            self, headers: Mapping[str, str]) -> list[Callable[[dict], None]]:
        """Return a list of callbacks that match."""
        return [
            callback for callback, match_dict in self._callbacks
            if _async_headers_match(headers, match_dict)
        ]

    @core_callback
    def _async_matching_domains(
            self, info_with_req: CaseInsensitiveDict) -> set[str]:
        domains = set()
        for domain, matchers in self._integration_matchers.items():
            for matcher in matchers:
                if all(
                        info_with_req.get(k) == v
                        for (k, v) in matcher.items()):
                    domains.add(domain)
        return domains

    def _async_seen(self, header_st: str | None,
                    header_location: str | None) -> bool:
        """Check if we have seen a specific st and optional location."""
        if header_st is None:
            return True
        return (header_st, header_location) in self.seen

    def _async_see(self, header_st: str | None,
                   header_location: str | None) -> None:
        """Mark a specific st and optional location as seen."""
        if header_st is not None:
            self.seen.add((header_st, header_location))

    async def _async_process_entry(self, headers: Mapping[str, str]) -> None:
        """Process SSDP entries."""
        _LOGGER.debug("_async_process_entry: %s", headers)
        h_st = headers.get("st")
        h_location = headers.get("location")

        if h_st and (udn := _udn_from_usn(headers.get("usn"))):
            self.cache[(udn, h_st)] = headers

        callbacks = self._async_get_matching_callbacks(headers)
        if self._async_seen(h_st, h_location) and not callbacks:
            return

        assert self.description_manager is not None
        info_req = await self.description_manager.fetch_description(h_location
                                                                    ) or {}
        info_with_req = CaseInsensitiveDict(**headers, **info_req)
        discovery_info = discovery_info_from_headers_and_request(info_with_req)

        _async_process_callbacks(callbacks, discovery_info)

        if self._async_seen(h_st, h_location):
            return
        self._async_see(h_st, h_location)

        for domain in self._async_matching_domains(info_with_req):
            _LOGGER.debug("Discovered %s at %s", domain, h_location)
            flow: SSDPFlow = {
                "domain": domain,
                "context": {
                    "source": config_entries.SOURCE_SSDP
                },
                "data": discovery_info,
            }
            assert self.flow_dispatcher is not None
            self.flow_dispatcher.create(flow)
コード例 #25
0
ファイル: __init__.py プロジェクト: pgenera/home-assistant
        if h_st and (udn := _udn_from_usn(headers.get("usn"))):
            cache_key = (udn, h_st)
            if old_headers := self.cache.get(cache_key):
                old_h_location = old_headers.get("location")
                if h_location != old_h_location:
                    self._async_unsee(old_headers.get("st"), old_h_location)
            self.cache[cache_key] = headers

        callbacks = self._async_get_matching_callbacks(headers)
        if self._async_seen(h_st, h_location) and not callbacks:
            return

        assert self.description_manager is not None
        info_req = await self.description_manager.fetch_description(h_location) or {}
        info_with_req = CaseInsensitiveDict(**headers, **info_req)
        discovery_info = discovery_info_from_headers_and_request(info_with_req)

        _async_process_callbacks(callbacks, discovery_info)

        if self._async_seen(h_st, h_location):
            return
        self._async_see(h_st, h_location)

        for domain in self._async_matching_domains(info_with_req):
            _LOGGER.debug("Discovered %s at %s", domain, h_location)
            flow: SSDPFlow = {
                "domain": domain,
                "context": {"source": config_entries.SOURCE_SSDP},
                "data": discovery_info,
            }