Exemple #1
0
    def scanner_adv_received(self,
                             service_info: BluetoothServiceInfoBleak) -> None:
        """Handle a new advertisement from any scanner.

        Callbacks from all the scanners arrive here.

        In the future we will only process callbacks if

        - The device is not in the history
        - The RSSI is above a certain threshold better than
          than the source from the history or the timestamp
          in the history is older than 180s
        """
        device = service_info.device
        connectable = service_info.connectable
        address = device.address
        all_history = self._connectable_history if connectable else self._history
        old_service_info = all_history.get(address)
        if old_service_info and _prefer_previous_adv(old_service_info,
                                                     service_info):
            return

        self._history[address] = service_info
        advertisement_data = service_info.advertisement
        source = service_info.source

        if connectable:
            self._connectable_history[address] = service_info
            # Bleak callbacks must get a connectable device

            for callback_filters in self._bleak_callbacks:
                _dispatch_bleak_callback(*callback_filters, device,
                                         advertisement_data)

        matched_domains = self._integration_matcher.match_domains(service_info)
        _LOGGER.debug(
            "%s: %s %s connectable: %s match: %s",
            source,
            address,
            advertisement_data,
            connectable,
            matched_domains,
        )

        for match in self._callback_index.match_callbacks(service_info):
            callback = match[CALLBACK]
            try:
                callback(service_info, BluetoothChange.ADVERTISEMENT)
            except Exception:  # pylint: disable=broad-except
                _LOGGER.exception("Error in bluetooth callback")

        for domain in matched_domains:
            discovery_flow.async_create_flow(
                self.hass,
                domain,
                {"source": config_entries.SOURCE_BLUETOOTH},
                service_info,
            )
Exemple #2
0
def async_discovery(hass: HomeAssistant,
                    discovery: YaleXSBLEDiscovery) -> None:
    """Update keys for the yalexs-ble integration if available."""
    discovery_flow.async_create_flow(
        hass,
        DOMAIN,
        context={"source": SOURCE_INTEGRATION_DISCOVERY},
        data=discovery,
    )
Exemple #3
0
    def async_process_client(self, ip_address, hostname, mac_address):
        """Process a client."""
        made_ip_address = make_ip_address(ip_address)

        if (
            is_link_local(made_ip_address)
            or is_loopback(made_ip_address)
            or is_invalid(made_ip_address)
        ):
            # Ignore self assigned addresses, loopback, invalid
            return

        data = self._address_data.get(ip_address)
        if (
            data
            and data[MAC_ADDRESS] == mac_address
            and data[HOSTNAME].startswith(hostname)
        ):
            # If the address data is the same no need
            # to process it
            return

        data = {MAC_ADDRESS: mac_address, HOSTNAME: hostname}
        self._address_data[ip_address] = data

        lowercase_hostname = data[HOSTNAME].lower()
        uppercase_mac = data[MAC_ADDRESS].upper()

        _LOGGER.debug(
            "Processing updated address data for %s: mac=%s hostname=%s",
            ip_address,
            uppercase_mac,
            lowercase_hostname,
        )

        for entry in self._integration_matchers:
            if MAC_ADDRESS in entry and not fnmatch.fnmatch(
                uppercase_mac, entry[MAC_ADDRESS]
            ):
                continue

            if HOSTNAME in entry and not fnmatch.fnmatch(
                lowercase_hostname, entry[HOSTNAME]
            ):
                continue

            _LOGGER.debug("Matched %s against %s", data, entry)
            discovery_flow.async_create_flow(
                self.hass,
                entry["domain"],
                {"source": config_entries.SOURCE_DHCP},
                DhcpServiceInfo(
                    ip=ip_address,
                    hostname=lowercase_hostname,
                    macaddress=data[MAC_ADDRESS],
                ),
            )
Exemple #4
0
 def _discovered_server(server):
     discovery_flow.async_create_flow(
         hass,
         DOMAIN,
         context={"source": SOURCE_INTEGRATION_DISCOVERY},
         data={
             CONF_HOST: server.host,
             CONF_PORT: int(server.port),
             "uuid": server.uuid,
         },
     )
Exemple #5
0
def async_init_discovery_flow(hass: HomeAssistant, host: str,
                              serial: str) -> None:
    """Start discovery of devices."""
    discovery_flow.async_create_flow(
        hass,
        DOMAIN,
        context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
        data={
            CONF_HOST: host,
            CONF_SERIAL: serial
        },
    )
Exemple #6
0
def async_trigger_discovery(
    hass: HomeAssistant,
    discovered_devices: list[ElkSystem],
) -> None:
    """Trigger config flows for discovered devices."""
    for device in discovered_devices:
        discovery_flow.async_create_flow(
            hass,
            DOMAIN,
            context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
            data=asdict(device),
        )
Exemple #7
0
 def _async_start_flow(*_) -> None:
     discovery_flow.async_create_flow(
         self._hass,
         DOMAIN,
         context={"source": config_entries.SOURCE_SSDP},
         data=ssdp.SsdpServiceInfo(
             ssdp_usn="",
             ssdp_st=SSDP_ST,
             ssdp_headers=response,
             upnp={},
         ),
     )
Exemple #8
0
async def test_async_create_flow_checks_existing_flows(hass, mock_flow_init):
    """Test existing flows prevent an identical one from being creates."""
    with patch(
        "homeassistant.data_entry_flow.FlowManager.async_has_matching_flow",
        return_value=True,
    ):
        discovery_flow.async_create_flow(
            hass,
            "hue",
            {"source": config_entries.SOURCE_HOMEKIT},
            {"properties": {"id": "aa:bb:cc:dd:ee:ff"}},
        )
        assert not mock_flow_init.mock_calls
Exemple #9
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,
            )
    def _async_process_discovered_usb_device(self, device: USBDevice) -> None:
        """Process a USB discovery."""
        _LOGGER.debug("Discovered USB Device: %s", device)
        device_tuple = dataclasses.astuple(device)
        if device_tuple in self.seen:
            return
        self.seen.add(device_tuple)
        matched = []
        for matcher in self.usb:
            if "vid" in matcher and device.vid != matcher["vid"]:
                continue
            if "pid" in matcher and device.pid != matcher["pid"]:
                continue
            if "serial_number" in matcher and not _fnmatch_lower(
                    device.serial_number, matcher["serial_number"]):
                continue
            if "manufacturer" in matcher and not _fnmatch_lower(
                    device.manufacturer, matcher["manufacturer"]):
                continue
            if "description" in matcher and not _fnmatch_lower(
                    device.description, matcher["description"]):
                continue
            matched.append(matcher)

        if not matched:
            return

        sorted_by_most_targeted = sorted(matched, key=lambda item: -len(item))
        most_matched_fields = len(sorted_by_most_targeted[0])

        for matcher in sorted_by_most_targeted:
            # If there is a less targeted match, we only
            # want the most targeted match
            if len(matcher) < most_matched_fields:
                break

            discovery_flow.async_create_flow(
                self.hass,
                matcher["domain"],
                {"source": config_entries.SOURCE_USB},
                UsbServiceInfo(
                    device=device.device,
                    vid=device.vid,
                    pid=device.pid,
                    serial_number=device.serial_number,
                    manufacturer=device.manufacturer,
                    description=device.description,
                ),
            )
Exemple #11
0
def async_trigger_discovery(
    hass: HomeAssistant,
    discovered_devices: list[SensemeDevice],
) -> None:
    """Trigger config flows for discovered devices."""
    for device in discovered_devices:
        if device.uuid:
            discovery_flow.async_create_flow(
                hass,
                DOMAIN,
                context={
                    "source": config_entries.SOURCE_INTEGRATION_DISCOVERY
                },
                data={CONF_ID: device.uuid},
            )
Exemple #12
0
def async_trigger_discovery(
    hass: HomeAssistant,
    discovered_devices: list[UnifiDevice],
) -> None:
    """Trigger config flows for discovered devices."""
    for device in discovered_devices:
        if device.services[UnifiService.Protect] and device.hw_addr:
            discovery_flow.async_create_flow(
                hass,
                DOMAIN,
                context={
                    "source": config_entries.SOURCE_INTEGRATION_DISCOVERY
                },
                data=asdict(device),
            )
Exemple #13
0
async def test_async_create_flow(hass, mock_flow_init):
    """Test we can create a flow."""
    discovery_flow.async_create_flow(
        hass,
        "hue",
        {"source": config_entries.SOURCE_HOMEKIT},
        {"properties": {"id": "aa:bb:cc:dd:ee:ff"}},
    )
    assert mock_flow_init.mock_calls == [
        call(
            "hue",
            context={"source": "homekit"},
            data={"properties": {"id": "aa:bb:cc:dd:ee:ff"}},
        )
    ]
Exemple #14
0
def async_trigger_discovery(
    hass: HomeAssistant,
    discovered_devices: dict[str, SmartDevice],
) -> None:
    """Trigger config flows for discovered devices."""
    for formatted_mac, device in discovered_devices.items():
        discovery_flow.async_create_flow(
            hass,
            DOMAIN,
            context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
            data={
                CONF_NAME: device.alias,
                CONF_HOST: device.host,
                CONF_MAC: formatted_mac,
            },
        )
Exemple #15
0
def async_trigger_discovery(
    hass: HomeAssistant,
    discovered_devices: list[Device30303],
) -> None:
    """Trigger config flows for discovered devices."""
    for device in discovered_devices:
        discovery_flow.async_create_flow(
            hass,
            DOMAIN,
            context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
            data={
                "ipaddress": device.ipaddress,
                "name": device.name,
                "mac": device.mac,
                "hostname": device.hostname,
            },
        )
Exemple #16
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
Exemple #17
0
    async def new_service_found(service, info):
        """Handle a new service if one is found."""
        if service in MIGRATED_SERVICE_HANDLERS:
            return

        if service in ignored_platforms:
            logger.info("Ignoring service: %s %s", service, info)
            return

        discovery_hash = json.dumps([service, info], sort_keys=True)
        if discovery_hash in already_discovered:
            logger.debug("Already discovered service %s %s.", service, info)
            return

        already_discovered.add(discovery_hash)

        if service in CONFIG_ENTRY_HANDLERS:
            discovery_flow.async_create_flow(
                hass,
                CONFIG_ENTRY_HANDLERS[service],
                context={"source": config_entries.SOURCE_DISCOVERY},
                data=info,
            )
            return

        service_details = SERVICE_HANDLERS.get(service)

        if not service_details and service in enabled_platforms:
            service_details = OPTIONAL_SERVICE_HANDLERS[service]

        # We do not know how to handle this service.
        if not service_details:
            logger.debug("Unknown service discovered: %s %s", service, info)
            return

        logger.info("Found new service: %s %s", service, info)

        if service_details.platform is None:
            await async_discover(hass, service, info, service_details.component, config)
        else:
            await async_load_platform(
                hass, service_details.component, service_details.platform, info, config
            )
Exemple #18
0
async def test_async_create_flow_deferred_until_started(hass, mock_flow_init):
    """Test flows are deferred until started."""
    hass.state = CoreState.stopped
    discovery_flow.async_create_flow(
        hass,
        "hue",
        {"source": config_entries.SOURCE_HOMEKIT},
        {"properties": {"id": "aa:bb:cc:dd:ee:ff"}},
    )
    assert not mock_flow_init.mock_calls
    hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
    await hass.async_block_till_done()
    assert mock_flow_init.mock_calls == [
        call(
            "hue",
            context={"source": "homekit"},
            data={"properties": {"id": "aa:bb:cc:dd:ee:ff"}},
        )
    ]
Exemple #19
0
    async def async_process_new(self, data):
        """Process add discovery entry."""
        service = data[ATTR_SERVICE]
        config_data = data[ATTR_CONFIG]

        # Read additional Add-on info
        try:
            addon_info = await self.hassio.get_addon_info(data[ATTR_ADDON])
        except HassioAPIError as err:
            _LOGGER.error("Can't read add-on info: %s", err)
            return
        config_data[ATTR_ADDON] = addon_info[ATTR_NAME]

        # Use config flow
        discovery_flow.async_create_flow(
            self.hass,
            service,
            context={"source": config_entries.SOURCE_HASSIO},
            data=HassioServiceInfo(config=config_data),
        )
Exemple #20
0
async def async_discover_adapters(
    hass: HomeAssistant,
    adapters: dict[str, AdapterDetails],
) -> None:
    """Discover adapters and start flows."""
    if platform.system() == "Windows":
        # We currently do not have a good way to detect if a bluetooth device is
        # available on Windows. We will just assume that it is not unless they
        # actively add it.
        return

    for adapter, details in adapters.items():
        discovery_flow.async_create_flow(
            hass,
            DOMAIN,
            context={"source": SOURCE_INTEGRATION_DISCOVERY},
            data={
                CONF_ADAPTER: adapter,
                CONF_DETAILS: details
            },
        )
Exemple #21
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
Exemple #22
0
    def _async_process_discovered_usb_device(self, device: USBDevice) -> None:
        """Process a USB discovery."""
        _LOGGER.debug("Discovered USB Device: %s", device)
        device_tuple = dataclasses.astuple(device)
        if device_tuple in self.seen:
            return
        self.seen.add(device_tuple)

        matched = [matcher for matcher in self.usb if _is_matching(device, matcher)]
        if not matched:
            return

        service_info = UsbServiceInfo(
            device=device.device,
            vid=device.vid,
            pid=device.pid,
            serial_number=device.serial_number,
            manufacturer=device.manufacturer,
            description=device.description,
        )

        sorted_by_most_targeted = sorted(matched, key=lambda item: -len(item))
        most_matched_fields = len(sorted_by_most_targeted[0])

        for matcher in sorted_by_most_targeted:
            # If there is a less targeted match, we only
            # want the most targeted match
            if len(matcher) < most_matched_fields:
                break

            discovery_flow.async_create_flow(
                self.hass,
                matcher["domain"],
                {"source": config_entries.SOURCE_USB},
                service_info,
            )
Exemple #23
0
async def async_setup_entry(
    hass: HomeAssistant,
    entry: ConfigEntry,
    async_add_entities: entity_platform.AddEntitiesCallback,
) -> None:
    """Set up Ezviz cameras based on a config entry."""

    coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][
        entry.entry_id][DATA_COORDINATOR]

    camera_entities = []

    for camera, value in coordinator.data.items():

        camera_rtsp_entry = [
            item for item in hass.config_entries.async_entries(DOMAIN)
            if item.unique_id == camera and item.source != SOURCE_IGNORE
        ]

        # There seem to be a bug related to localRtspPort in Ezviz API.
        local_rtsp_port = (value["local_rtsp_port"]
                           if value["local_rtsp_port"] != 0 else
                           DEFAULT_RTSP_PORT)

        if camera_rtsp_entry:

            ffmpeg_arguments = camera_rtsp_entry[0].options[
                CONF_FFMPEG_ARGUMENTS]
            camera_username = camera_rtsp_entry[0].data[CONF_USERNAME]
            camera_password = camera_rtsp_entry[0].data[CONF_PASSWORD]

            camera_rtsp_stream = f"rtsp://{camera_username}:{camera_password}@{value['local_ip']}:{local_rtsp_port}{ffmpeg_arguments}"
            _LOGGER.debug(
                "Configuring Camera %s with ip: %s rtsp port: %s ffmpeg arguments: %s",
                camera,
                value["local_ip"],
                local_rtsp_port,
                ffmpeg_arguments,
            )

        else:

            discovery_flow.async_create_flow(
                hass,
                DOMAIN,
                context={"source": SOURCE_INTEGRATION_DISCOVERY},
                data={
                    ATTR_SERIAL: camera,
                    CONF_IP_ADDRESS: value["local_ip"],
                },
            )

            _LOGGER.warning(
                "Found camera with serial %s without configuration. Please go to integration to complete setup",
                camera,
            )

            ffmpeg_arguments = DEFAULT_FFMPEG_ARGUMENTS
            camera_username = DEFAULT_CAMERA_USERNAME
            camera_password = None
            camera_rtsp_stream = ""

        camera_entities.append(
            EzvizCamera(
                hass,
                coordinator,
                camera,
                camera_username,
                camera_password,
                camera_rtsp_stream,
                local_rtsp_port,
                ffmpeg_arguments,
            ))

    async_add_entities(camera_entities)

    platform = entity_platform.async_get_current_platform()

    platform.async_register_entity_service(
        SERVICE_PTZ,
        {
            vol.Required(ATTR_DIRECTION):
            vol.In([DIR_UP, DIR_DOWN, DIR_LEFT, DIR_RIGHT]),
            vol.Required(ATTR_SPEED):
            cv.positive_int,
        },
        "perform_ptz",
    )

    platform.async_register_entity_service(
        SERVICE_ALARM_TRIGGER,
        {
            vol.Required(ATTR_ENABLE): cv.positive_int,
        },
        "perform_sound_alarm",
    )

    platform.async_register_entity_service(SERVICE_WAKE_DEVICE, {},
                                           "perform_wake_device")

    platform.async_register_entity_service(
        SERVICE_ALARM_SOUND,
        {vol.Required(ATTR_LEVEL): cv.positive_int},
        "perform_alarm_sound",
    )

    platform.async_register_entity_service(
        SERVICE_DETECTION_SENSITIVITY,
        {
            vol.Required(ATTR_LEVEL): cv.positive_int,
            vol.Required(ATTR_TYPE): cv.positive_int,
        },
        "perform_set_alarm_detection_sensibility",
    )
                continue

            if (matcher_hostname :=
                    matcher.get(HOSTNAME)) is not None and not fnmatch.fnmatch(
                        lowercase_hostname, matcher_hostname):
                continue

            _LOGGER.debug("Matched %s against %s", data, matcher)
            matched_domains.add(domain)

        for domain in matched_domains:
            discovery_flow.async_create_flow(
                self.hass,
                domain,
                {"source": config_entries.SOURCE_DHCP},
                DhcpServiceInfo(
                    ip=ip_address,
                    hostname=lowercase_hostname,
                    macaddress=mac_address,
                ),
            )


class NetworkWatcher(WatcherBase):
    """Class to query ptr records routers."""
    def __init__(
        self,
        hass: HomeAssistant,
        address_data: dict[str, dict[str, str]],
        integration_matchers: list[DHCPMatcher],
    ) -> None:
        """Initialize class."""
class ZeroconfDiscovery:
    """Discovery via zeroconf."""
    def __init__(
        self,
        hass: HomeAssistant,
        zeroconf: HaZeroconf,
        zeroconf_types: dict[str, list[dict[str, str | dict[str, str]]]],
        homekit_models: dict[str, str],
        ipv6: bool,
    ) -> None:
        """Init discovery."""
        self.hass = hass
        self.zeroconf = zeroconf
        self.zeroconf_types = zeroconf_types
        self.homekit_models = homekit_models
        self.ipv6 = ipv6

        self.async_service_browser: HaAsyncServiceBrowser | None = None

    async def async_setup(self) -> None:
        """Start discovery."""
        types = list(self.zeroconf_types)
        # We want to make sure we know about other HomeAssistant
        # instances as soon as possible to avoid name conflicts
        # so we always browse for ZEROCONF_TYPE
        for hk_type in (ZEROCONF_TYPE, *HOMEKIT_TYPES):
            if hk_type not in self.zeroconf_types:
                types.append(hk_type)
        _LOGGER.debug("Starting Zeroconf browser for: %s", types)
        self.async_service_browser = HaAsyncServiceBrowser(
            self.ipv6,
            self.zeroconf,
            types,
            handlers=[self.async_service_update])

    async def async_stop(self) -> None:
        """Cancel the service browser and stop processing the queue."""
        if self.async_service_browser:
            await self.async_service_browser.async_cancel()

    @callback
    def async_service_update(
        self,
        zeroconf: HaZeroconf,
        service_type: str,
        name: str,
        state_change: ServiceStateChange,
    ) -> None:
        """Service state changed."""
        _LOGGER.debug(
            "service_update: type=%s name=%s state_change=%s",
            service_type,
            name,
            state_change,
        )

        if state_change == ServiceStateChange.Removed:
            return

        asyncio.create_task(
            self._process_service_update(zeroconf, service_type, name))

    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.
                #
                # 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"):
                    return

        match_data: dict[str, str] = {}
        for key in LOWER_MATCH_ATTRS:
            attr_value: str = getattr(info, key)
            match_data[key] = attr_value.lower()

        # Not all homekit types are currently used for discovery
        # so not all service type exist in zeroconf_types
        for matcher in self.zeroconf_types.get(service_type, []):
            if len(matcher) > 1:
                if not _match_against_data(matcher, match_data):
                    continue
                if ATTR_PROPERTIES in matcher:
                    matcher_props = matcher[ATTR_PROPERTIES]
                    assert isinstance(matcher_props, dict)
                    if not _match_against_props(matcher_props, props):
                        continue

            matcher_domain = matcher["domain"]
            assert isinstance(matcher_domain, str)
            discovery_flow.async_create_flow(
                self.hass,
                matcher_domain,
                {"source": config_entries.SOURCE_ZEROCONF},
                info,
            )
Exemple #26
0
class ZeroconfDiscovery:
    """Discovery via zeroconf."""
    def __init__(
        self,
        hass: HomeAssistant,
        zeroconf: HaZeroconf,
        zeroconf_types: dict[str, list[dict[str, str]]],
        homekit_models: dict[str, str],
        ipv6: bool,
    ) -> None:
        """Init discovery."""
        self.hass = hass
        self.zeroconf = zeroconf
        self.zeroconf_types = zeroconf_types
        self.homekit_models = homekit_models
        self.ipv6 = ipv6

        self.async_service_browser: HaAsyncServiceBrowser | None = None

    async def async_setup(self) -> None:
        """Start discovery."""
        types = list(self.zeroconf_types)
        # We want to make sure we know about other HomeAssistant
        # instances as soon as possible to avoid name conflicts
        # so we always browse for ZEROCONF_TYPE
        for hk_type in (ZEROCONF_TYPE, *HOMEKIT_TYPES):
            if hk_type not in self.zeroconf_types:
                types.append(hk_type)
        _LOGGER.debug("Starting Zeroconf browser for: %s", types)
        self.async_service_browser = HaAsyncServiceBrowser(
            self.ipv6,
            self.zeroconf,
            types,
            handlers=[self.async_service_update])

    async def async_stop(self) -> None:
        """Cancel the service browser and stop processing the queue."""
        if self.async_service_browser:
            await self.async_service_browser.async_cancel()

    @callback
    def async_service_update(
        self,
        zeroconf: HaZeroconf,
        service_type: str,
        name: str,
        state_change: ServiceStateChange,
    ) -> None:
        """Service state changed."""
        _LOGGER.debug(
            "service_update: type=%s name=%s state_change=%s",
            service_type,
            name,
            state_change,
        )

        if state_change == ServiceStateChange.Removed:
            return

        asyncio.create_task(
            self._process_service_update(zeroconf, service_type, name))

    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

        if ATTR_NAME in info:
            lowercase_name: str | None = info[ATTR_NAME].lower()
        else:
            lowercase_name = None

        if "macaddress" in info[ATTR_PROPERTIES]:
            uppercase_mac: str | None = info[ATTR_PROPERTIES][
                "macaddress"].upper()
        else:
            uppercase_mac = None

        if "manufacturer" in info[ATTR_PROPERTIES]:
            lowercase_manufacturer: str | None = info[ATTR_PROPERTIES][
                "manufacturer"].lower()
        else:
            lowercase_manufacturer = None

        # Not all homekit types are currently used for discovery
        # so not all service type exist in zeroconf_types
        for matcher in self.zeroconf_types.get(service_type, []):
            if len(matcher) > 1:
                if "macaddress" in matcher and (
                        uppercase_mac is None or not fnmatch.fnmatch(
                            uppercase_mac, matcher["macaddress"])):
                    continue
                if "name" in matcher and (
                        lowercase_name is None or
                        not fnmatch.fnmatch(lowercase_name, matcher["name"])):
                    continue
                if "manufacturer" in matcher and (
                        lowercase_manufacturer is None or not fnmatch.fnmatch(
                            lowercase_manufacturer, matcher["manufacturer"])):
                    continue

            discovery_flow.async_create_flow(
                self.hass,
                matcher["domain"],
                {"source": config_entries.SOURCE_ZEROCONF},
                info,
            )