Exemplo n.º 1
0
def get_service_info_mock_without_an_address(
    service_type: str, name: str
) -> AsyncServiceInfo:
    """Return service info for get_service_info without any addresses."""
    return AsyncServiceInfo(
        service_type,
        name,
        addresses=[],
        port=80,
        weight=0,
        priority=0,
        server="name.local.",
        properties=PROPERTIES,
    )
Exemplo n.º 2
0
 def mock_homekit_info(service_type, name):
     return AsyncServiceInfo(
         service_type,
         name,
         addresses=[b"\n\x00\x00\x14"],
         port=80,
         weight=0,
         priority=0,
         server="name.local.",
         properties={
             b"md": model.encode(),
             b"sf": pairing_status
         },
     )
Exemplo n.º 3
0
def get_service_info_mock(
    service_type: str, name: str, *args: Any, **kwargs: Any
) -> AsyncServiceInfo:
    """Return service info for get_service_info."""
    return AsyncServiceInfo(
        service_type,
        name,
        addresses=[b"\n\x00\x00\x14"],
        port=80,
        weight=0,
        priority=0,
        server="name.local.",
        properties=PROPERTIES,
    )
Exemplo n.º 4
0
async def test_find_with_device(mock_asynczeroconf):
    desc = {b"id": b"00:00:02:00:00:02", b"c#": b"1", b"md": b"any"}
    info = AsyncServiceInfo(
        "_hap._tcp.local.",
        "foo1._hap._tcp.local.",
        addresses=[socket.inet_aton("127.0.0.1")],
        port=1234,
        properties=desc,
        weight=0,
        priority=0,
    )
    with patch("aiohomekit.zeroconf.AsyncServiceInfo", return_value=info):
        result = await async_find_device_ip_and_port("00:00:02:00:00:02", 1)
    assert result == ("127.0.0.1", 1234)
Exemplo n.º 5
0
    async def _get_service_info(self, zeroconf: Zeroconf, service_type: str, name: str) -> None:
        """Get service information, if IP matches."""
        service_info = AsyncServiceInfo(service_type, name)
        question_type = DNSQuestionType.QM if self._multicast else DNSQuestionType.QU
        with suppress(RuntimeError):
            await service_info.async_request(zeroconf, timeout=1000, question_type=question_type)

        if (
            service_info is None
            or not service_info.addresses
            or str(ipaddress.ip_address(service_info.addresses[0])) != self.ip
        ):
            return  # No need to continue, if there are no relevant service information

        self._logger.debug("Adding service info of %s to %s", service_type, service_info.server_key)
        self._info[service_type] = self.info_from_service(service_info)
Exemplo n.º 6
0
    async def _process_service_update(self, zeroconf: HaZeroconf,
                                      service_type: str, name: str) -> None:
        """Process a zeroconf update."""
        async_service_info = AsyncServiceInfo(service_type, name)
        await async_service_info.async_request(zeroconf, 3000)

        info = info_from_service(async_service_info)
        if not info:
            # Prevent the browser thread from collapsing
            _LOGGER.debug("Failed to get addresses for device %s", name)
            return

        _LOGGER.debug("Discovered new device %s %s", name, info)
        props: dict[str, str] = info.properties

        # If we can handle it as a HomeKit discovery, we do that here.
        if service_type in HOMEKIT_TYPES and (
                domain := async_get_homekit_discovery_domain(
                    self.homekit_models, props)):
            discovery_flow.async_create_flow(
                self.hass, domain, {"source": config_entries.SOURCE_HOMEKIT},
                info)
            # Continue on here as homekit_controller
            # still needs to get updates on devices
            # so it can see when the 'c#' field is updated.
            #
            # We only send updates to homekit_controller
            # if the device is already paired in order to avoid
            # offering a second discovery for the same device
            if not is_homekit_paired(props):
                integration: Integration = await async_get_integration(
                    self.hass, domain)
                # Since we prefer local control, if the integration that is being discovered
                # is cloud AND the homekit device is UNPAIRED we still want to discovery it.
                #
                # Additionally if the integration is polling, HKC offers a local push
                # experience for the user to control the device so we want to offer that
                # as well.
                #
                # As soon as the device becomes paired, the config flow will be dismissed
                # in the event the user does not want to pair with Home Assistant.
                #
                if not integration.iot_class or (
                        not integration.iot_class.startswith("cloud")
                        and "polling" not in integration.iot_class):
                    return
Exemplo n.º 7
0
async def _async_homekit_devices_from_cache(
        aiozc: AsyncZeroconf,
        filter_func: Callable = None) -> list[dict[str, Any]]:
    """Return all homekit devices in the cache, updating any missing data as needed."""
    infos = [
        AsyncServiceInfo(HAP_TYPE, record.alias)
        for record in aiozc.zeroconf.cache.get_all_by_details(
            HAP_TYPE, TYPE_PTR, CLASS_IN)
    ]
    tasks = [info.async_request(aiozc.zeroconf, _TIMEOUT_MS) for info in infos]
    await asyncio.gather(*tasks)

    devices = []
    for info in infos:
        if not _service_info_is_homekit_device(info):
            continue
        if filter_func and not filter_func(info):
            continue
        devices.append(_build_data_from_service_info(info))
    return devices
Exemplo n.º 8
0
def test_is_homekit_device_case_insensitive():
    desc = {
        b"C#": b"1",
        b"id": b"00:00:01:00:00:02",
        b"md": b"unittest",
        b"s#": b"1",
        b"ci": b"5",
        b"sf": b"0",
    }
    info = AsyncServiceInfo(
        "_hap._tcp.local.",
        "foo2._hap._tcp.local.",
        addresses=[socket.inet_aton("127.0.0.1")],
        port=1234,
        properties=desc,
        weight=0,
        priority=0,
    )

    assert _service_info_is_homekit_device(info)
Exemplo n.º 9
0
async def test_async_find_data_for_upper_case_device_id_matches(mock_asynczeroconf):
    desc = {
        b"c#": b"1",
        b"id": b"AA:00:01:00:00:02",
        b"md": b"unittest",
        b"s#": b"1",
        b"ci": b"5",
        b"sf": b"0",
    }
    info = AsyncServiceInfo(
        "_hap._tcp.local.",
        "foo2._hap._tcp.local.",
        addresses=[socket.inet_aton("127.0.0.1")],
        port=1234,
        properties=desc,
        weight=0,
        priority=0,
    )
    with patch("aiohomekit.zeroconf.AsyncServiceInfo", return_value=info):
        result = await async_find_data_for_device_id(
            device_id="aa:00:01:00:00:02",
            max_seconds=1,
            async_zeroconf_instance=mock_asynczeroconf,
        )

    assert result == {
        "address": "127.0.0.1",
        "c#": "1",
        "category": "Lightbulb",
        "ci": "5",
        "ff": 0,
        "flags": FeatureFlags(0),
        "id": "AA:00:01:00:00:02",
        "md": "unittest",
        "name": "foo2._hap._tcp.local.",
        "port": 1234,
        "pv": "1.0",
        "s#": "1",
        "sf": "0",
        "statusflags": "Accessory has been paired.",
    }
Exemplo n.º 10
0
async def test_async_discover_homekit_devices_missing_md(mock_asynczeroconf):
    desc = {
        b"c#": b"1",
        b"id": b"00:00:01:00:00:02",
        b"s#": b"1",
        b"ci": b"5",
        b"sf": b"0",
    }
    info = AsyncServiceInfo(
        "_hap._tcp.local.",
        "foo2._hap._tcp.local.",
        addresses=[socket.inet_aton("127.0.0.1")],
        port=1234,
        properties=desc,
        weight=0,
        priority=0,
    )
    with patch("aiohomekit.zeroconf.AsyncServiceInfo", return_value=info):
        result = await async_discover_homekit_devices(max_seconds=1)

    assert result == []
Exemplo n.º 11
0
 def _build_service_info_queries(
     self, ) -> List[Union[AsyncServiceInfo, AsyncDeviceInfoServiceInfo]]:
     """Build AsyncServiceInfo queries from the requested types."""
     infos: List[Union[AsyncServiceInfo, AsyncDeviceInfoServiceInfo]] = []
     device_names = set()
     for type_ in (SLEEP_PROXY, *self._services):
         if type_ == DEVICE_INFO:
             continue
         zc_type = f"{type_}."
         for record in self.zeroconf.cache.async_all_by_details(
                 zc_type, _TYPE_PTR, _CLASS_IN):
             ptr_name = cast(DNSPointer, record).alias
             service_info = AsyncServiceInfo(zc_type, ptr_name)
             infos.append(service_info)
             name = _name_without_type(ptr_name, zc_type)
             device_name = self._device_info_name[type_](name)
             if device_name is not None and device_name not in device_names:
                 device_names.add(device_name)
                 device_service_info = AsyncDeviceInfoServiceInfo(
                     DEVICE_INFO_TYPE, f"{device_name}.{DEVICE_INFO_TYPE}")
                 infos.append(device_service_info)
     return infos
Exemplo n.º 12
0
 def create_async_service_infos(self, interface: str, service_name: str,
                                domain_name: str,
                                ip: str) -> AsyncServiceInfo:
     """
     Create A list of AsyncServiceInfo() for the given interface and service
     Each domain results in a new service
     """
     service = self.service_types[service_name]
     try:
         return AsyncServiceInfo(
             f"{service.name}.{service.protocol}.local.",
             f"{domain_name}.{service.name}.{service.protocol}.local.",
             addresses=[socket.inet_aton(ip)],
             port=service.port,
             properties=service.get_properties(),
             server=f"{domain_name}.local.",
         )
     except Exception as e:
         logger.warning(
             f"Error creating AsyncServiceInfo {service.name} at {interface}: {e}"
         )
         raise e
Exemplo n.º 13
0
    async def _process_service_update(self, zeroconf: HaZeroconf,
                                      service_type: str, name: str) -> None:
        """Process a zeroconf update."""
        async_service_info = AsyncServiceInfo(service_type, name)
        await async_service_info.async_request(zeroconf, 3000)

        info = info_from_service(async_service_info)
        if not info:
            # Prevent the browser thread from collapsing
            _LOGGER.debug("Failed to get addresses for device %s", name)
            return

        _LOGGER.debug("Discovered new device %s %s", name, info)

        # If we can handle it as a HomeKit discovery, we do that here.
        if service_type in HOMEKIT_TYPES:
            props = info[ATTR_PROPERTIES]
            if domain := async_get_homekit_discovery_domain(
                    self.homekit_models, props):
                discovery_flow.async_create_flow(
                    self.hass, domain,
                    {"source": config_entries.SOURCE_HOMEKIT}, info)
            # Continue on here as homekit_controller
            # still needs to get updates on devices
            # so it can see when the 'c#' field is updated.
            #
            # We only send updates to homekit_controller
            # if the device is already paired in order to avoid
            # offering a second discovery for the same device
            if domain and HOMEKIT_PAIRED_STATUS_FLAG in props:
                try:
                    # 0 means paired and not discoverable by iOS clients)
                    if int(props[HOMEKIT_PAIRED_STATUS_FLAG]):
                        return
                except ValueError:
                    # HomeKit pairing status unknown
                    # likely bad homekit data
                    return
Exemplo n.º 14
0
def info_from_service(service: AsyncServiceInfo) -> ZeroconfServiceInfo | None:
    """Return prepared info from mDNS entries."""
    properties: dict[str, Any] = {"_raw": {}}

    for key, value in service.properties.items():
        # See https://ietf.org/rfc/rfc6763.html#section-6.4 and
        # https://ietf.org/rfc/rfc6763.html#section-6.5 for expected encodings
        # for property keys and values
        try:
            key = key.decode("ascii")
        except UnicodeDecodeError:
            _LOGGER.debug("Ignoring invalid key provided by [%s]: %s",
                          service.name, key)
            continue

        properties["_raw"][key] = value

        with suppress(UnicodeDecodeError):
            if isinstance(value, bytes):
                properties[key] = value.decode("utf-8")

    if not (addresses := service.addresses or service.parsed_addresses()):
        return None
Exemplo n.º 15
0
    async def _process_service_update(self, zeroconf: HaZeroconf,
                                      service_type: str, name: str) -> None:
        """Process a zeroconf update."""
        async_service_info = AsyncServiceInfo(service_type, name)
        await async_service_info.async_request(zeroconf, 3000)

        info = info_from_service(async_service_info)
        if not info:
            # Prevent the browser thread from collapsing
            _LOGGER.debug("Failed to get addresses for device %s", name)
            return

        _LOGGER.debug("Discovered new device %s %s", name, info)
        assert self.flow_dispatcher is not None

        # If we can handle it as a HomeKit discovery, we do that here.
        if service_type in HOMEKIT_TYPES:
            if pending_flow := handle_homekit(self.hass, self.homekit_models,
                                              info):
                self.flow_dispatcher.async_create(pending_flow)
            # Continue on here as homekit_controller
            # still needs to get updates on devices
            # so it can see when the 'c#' field is updated.
            #
            # We only send updates to homekit_controller
            # if the device is already paired in order to avoid
            # offering a second discovery for the same device
            if pending_flow and HOMEKIT_PAIRED_STATUS_FLAG in info[
                    "properties"]:
                try:
                    # 0 means paired and not discoverable by iOS clients)
                    if int(info["properties"][HOMEKIT_PAIRED_STATUS_FLAG]):
                        return
                except ValueError:
                    # HomeKit pairing status unknown
                    # likely bad homekit data
                    return
Exemplo n.º 16
0
async def test_async_find_data_for_device_id_info_without_id(mock_asynczeroconf):
    desc = {
        b"c#": b"1",
        b"md": b"unittest",
        b"s#": b"1",
        b"ci": b"5",
        b"sf": b"0",
    }
    info = AsyncServiceInfo(
        "_hap._tcp.local.",
        "foo2._hap._tcp.local.",
        addresses=[socket.inet_aton("127.0.0.1")],
        port=1234,
        properties=desc,
        weight=0,
        priority=0,
    )
    with patch("aiohomekit.zeroconf.AsyncServiceInfo", return_value=info):
        with pytest.raises(AccessoryNotFoundError):
            await async_find_data_for_device_id(
                device_id="00:00:01:00:00:02",
                max_seconds=1,
                async_zeroconf_instance=mock_asynczeroconf,
            )
Exemplo n.º 17
0
async def test_async_discover_homekit_devices_with_service_browser_running(
    mock_asynczeroconf,
):
    desc = {
        b"c#": b"1",
        b"id": b"00:00:01:00:00:02",
        b"md": b"unittest",
        b"s#": b"1",
        b"ci": b"5",
        b"sf": b"0",
    }
    info = AsyncServiceInfo(
        "_hap._tcp.local.",
        "foo._hap._tcp.local.",
        addresses=[socket.inet_aton("127.0.0.1")],
        port=1234,
        properties=desc,
        weight=0,
        priority=0,
    )

    info2 = AsyncServiceInfo(
        "_hap._tcp.local.",
        "Foo2._hap._tcp.local.",
        addresses=[socket.inet_aton("127.0.0.1")],
        port=1234,
        properties=desc,
        weight=0,
        priority=0,
    )

    mock_asynczeroconf.zeroconf.cache = MagicMock(
        get_all_by_details=MagicMock(
            return_value=[
                MagicMock(alias="foo._hap._tcp.local."),
                MagicMock(alias="Foo2._hap._tcp.local."),
            ]
        )
    )
    with patch(
        "aiohomekit.zeroconf.AsyncServiceInfo", side_effect=[info, info2]
    ) as asyncserviceinfo_mock, patch(
        "aiohomekit.zeroconf.async_zeroconf_has_hap_service_browser", return_value=True
    ):
        result = await async_discover_homekit_devices(
            max_seconds=1, async_zeroconf_instance=mock_asynczeroconf
        )

    assert result == [
        {
            "address": "127.0.0.1",
            "c#": "1",
            "category": "Lightbulb",
            "ci": "5",
            "ff": 0,
            "flags": FeatureFlags(0),
            "id": "00:00:01:00:00:02",
            "md": "unittest",
            "name": "foo._hap._tcp.local.",
            "port": 1234,
            "pv": "1.0",
            "s#": "1",
            "sf": "0",
            "statusflags": "Accessory has been paired.",
        },
        {
            "address": "127.0.0.1",
            "c#": "1",
            "category": "Lightbulb",
            "ci": "5",
            "ff": 0,
            "flags": FeatureFlags(0),
            "id": "00:00:01:00:00:02",
            "md": "unittest",
            "name": "Foo2._hap._tcp.local.",
            "port": 1234,
            "pv": "1.0",
            "s#": "1",
            "sf": "0",
            "statusflags": "Accessory has been paired.",
        },
    ]

    assert asyncserviceinfo_mock.mock_calls == [
        call("_hap._tcp.local.", "foo._hap._tcp.local."),
        call("_hap._tcp.local.", "Foo2._hap._tcp.local."),
    ]