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, )
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, )
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], ), )
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, }, )
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 }, )
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), )
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={}, ), )
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
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, ), )
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}, )
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), )
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"}}, ) ]
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, }, )
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, }, )
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
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 )
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"}}, ) ]
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), )
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 }, )
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
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, )
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, )
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, )