From a74559471289ec605348990e265de2e7c8bb3d7e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 20 Nov 2020 21:28:00 +0100 Subject: [PATCH] Move legacy device tracker setup to legacy module (#43447) --- .../components/device_tracker/__init__.py | 147 +------- .../components/device_tracker/legacy.py | 337 +++++++++++++++++- .../components/device_tracker/setup.py | 196 ---------- 3 files changed, 341 insertions(+), 339 deletions(-) delete mode 100644 homeassistant/components/device_tracker/setup.py diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 6d8e2307145..d785ee826e8 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -1,24 +1,18 @@ """Provide functionality to keep track of devices.""" -import asyncio - -import voluptuous as vol - -from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_utc_time_change -from homeassistant.helpers.typing import ConfigType, GPSType, HomeAssistantType +from homeassistant.const import ( # noqa: F401 pylint: disable=unused-import + ATTR_GPS_ACCURACY, + STATE_HOME, +) +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.loader import bind_hass -from . import legacy, setup from .config_entry import ( # noqa: F401 pylint: disable=unused-import async_setup_entry, async_unload_entry, ) -from .const import ( +from .const import ( # noqa: F401 pylint: disable=unused-import ATTR_ATTRIBUTES, ATTR_BATTERY, - ATTR_CONSIDER_HOME, ATTR_DEV_ID, ATTR_GPS, ATTR_HOST_NAME, @@ -29,60 +23,21 @@ from .const import ( CONF_NEW_DEVICE_DEFAULTS, CONF_SCAN_INTERVAL, CONF_TRACK_NEW, - DEFAULT_CONSIDER_HOME, - DEFAULT_TRACK_NEW, DOMAIN, - PLATFORM_TYPE_LEGACY, SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER, ) -from .legacy import DeviceScanner # noqa: F401 pylint: disable=unused-import - -SERVICE_SEE = "see" - -SOURCE_TYPES = ( - SOURCE_TYPE_GPS, - SOURCE_TYPE_ROUTER, - SOURCE_TYPE_BLUETOOTH, - SOURCE_TYPE_BLUETOOTH_LE, -) - -NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any( - None, - vol.Schema({vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean}), -) -PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, - vol.Optional(CONF_TRACK_NEW): cv.boolean, - vol.Optional(CONF_CONSIDER_HOME, default=DEFAULT_CONSIDER_HOME): vol.All( - cv.time_period, cv.positive_timedelta - ), - vol.Optional(CONF_NEW_DEVICE_DEFAULTS, default={}): NEW_DEVICE_DEFAULTS_SCHEMA, - } -) -PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) -SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema( - vol.All( - cv.has_at_least_one_key(ATTR_MAC, ATTR_DEV_ID), - { - ATTR_MAC: cv.string, - ATTR_DEV_ID: cv.string, - ATTR_HOST_NAME: cv.string, - ATTR_LOCATION_NAME: cv.string, - ATTR_GPS: cv.gps, - ATTR_GPS_ACCURACY: cv.positive_int, - ATTR_BATTERY: cv.positive_int, - ATTR_ATTRIBUTES: dict, - ATTR_SOURCE_TYPE: vol.In(SOURCE_TYPES), - ATTR_CONSIDER_HOME: cv.time_period, - # Temp workaround for iOS app introduced in 0.65 - vol.Optional("battery_status"): str, - vol.Optional("hostname"): str, - }, - ) +from .legacy import ( # noqa: F401 pylint: disable=unused-import + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, + SERVICE_SEE, + SERVICE_SEE_PAYLOAD_SCHEMA, + SOURCE_TYPES, + DeviceScanner, + async_setup_integration as async_setup_legacy_integration, + see, ) @@ -92,78 +47,8 @@ def is_on(hass: HomeAssistantType, entity_id: str): return hass.states.is_state(entity_id, STATE_HOME) -def see( - hass: HomeAssistantType, - mac: str = None, - dev_id: str = None, - host_name: str = None, - location_name: str = None, - gps: GPSType = None, - gps_accuracy=None, - battery: int = None, - attributes: dict = None, -): - """Call service to notify you see device.""" - data = { - key: value - for key, value in ( - (ATTR_MAC, mac), - (ATTR_DEV_ID, dev_id), - (ATTR_HOST_NAME, host_name), - (ATTR_LOCATION_NAME, location_name), - (ATTR_GPS, gps), - (ATTR_GPS_ACCURACY, gps_accuracy), - (ATTR_BATTERY, battery), - ) - if value is not None - } - if attributes: - data[ATTR_ATTRIBUTES] = attributes - hass.services.call(DOMAIN, SERVICE_SEE, data) - - async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up the device tracker.""" - tracker = await legacy.get_tracker(hass, config) + await async_setup_legacy_integration(hass, config) - legacy_platforms = await setup.async_extract_config(hass, config) - - setup_tasks = [ - legacy_platform.async_setup_legacy(hass, tracker) - for legacy_platform in legacy_platforms - ] - - if setup_tasks: - await asyncio.wait(setup_tasks) - - async def async_platform_discovered(p_type, info): - """Load a platform.""" - platform = await setup.async_create_platform_type(hass, config, p_type, {}) - - if platform is None or platform.type != PLATFORM_TYPE_LEGACY: - return - - await platform.async_setup_legacy(hass, tracker, info) - - discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) - - # Clean up stale devices - async_track_utc_time_change( - hass, tracker.async_update_stale, second=range(0, 60, 5) - ) - - async def async_see_service(call): - """Service to see a device.""" - # Temp workaround for iOS, introduced in 0.65 - data = dict(call.data) - data.pop("hostname", None) - data.pop("battery_status", None) - await tracker.async_see(**data) - - hass.services.async_register( - DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA - ) - - # restore - await tracker.async_setup_tracked_device() return True diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 038fad06680..5f60d84f406 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -2,8 +2,10 @@ import asyncio from datetime import timedelta import hashlib -from typing import Any, List, Sequence +from types import ModuleType +from typing import Any, Callable, Dict, List, Optional, Sequence +import attr import voluptuous as vol from homeassistant import util @@ -25,32 +27,343 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_per_platform, discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers.event import ( + async_track_time_interval, + async_track_utc_time_change, +) from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import GPSType, HomeAssistantType -import homeassistant.util.dt as dt_util +from homeassistant.helpers.typing import ConfigType, GPSType, HomeAssistantType +from homeassistant.setup import async_prepare_setup_platform +from homeassistant.util import dt as dt_util from homeassistant.util.yaml import dump from .const import ( + ATTR_ATTRIBUTES, ATTR_BATTERY, + ATTR_CONSIDER_HOME, + ATTR_DEV_ID, + ATTR_GPS, ATTR_HOST_NAME, + ATTR_LOCATION_NAME, ATTR_MAC, ATTR_SOURCE_TYPE, CONF_CONSIDER_HOME, CONF_NEW_DEVICE_DEFAULTS, + CONF_SCAN_INTERVAL, CONF_TRACK_NEW, DEFAULT_CONSIDER_HOME, DEFAULT_TRACK_NEW, DOMAIN, LOGGER, + PLATFORM_TYPE_LEGACY, + SCAN_INTERVAL, + SOURCE_TYPE_BLUETOOTH, + SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS, + SOURCE_TYPE_ROUTER, +) + +SERVICE_SEE = "see" + +SOURCE_TYPES = ( + SOURCE_TYPE_GPS, + SOURCE_TYPE_ROUTER, + SOURCE_TYPE_BLUETOOTH, + SOURCE_TYPE_BLUETOOTH_LE, +) + +NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any( + None, + vol.Schema({vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean}), +) +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, + vol.Optional(CONF_TRACK_NEW): cv.boolean, + vol.Optional(CONF_CONSIDER_HOME, default=DEFAULT_CONSIDER_HOME): vol.All( + cv.time_period, cv.positive_timedelta + ), + vol.Optional(CONF_NEW_DEVICE_DEFAULTS, default={}): NEW_DEVICE_DEFAULTS_SCHEMA, + } +) +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) + +SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema( + vol.All( + cv.has_at_least_one_key(ATTR_MAC, ATTR_DEV_ID), + { + ATTR_MAC: cv.string, + ATTR_DEV_ID: cv.string, + ATTR_HOST_NAME: cv.string, + ATTR_LOCATION_NAME: cv.string, + ATTR_GPS: cv.gps, + ATTR_GPS_ACCURACY: cv.positive_int, + ATTR_BATTERY: cv.positive_int, + ATTR_ATTRIBUTES: dict, + ATTR_SOURCE_TYPE: vol.In(SOURCE_TYPES), + ATTR_CONSIDER_HOME: cv.time_period, + # Temp workaround for iOS app introduced in 0.65 + vol.Optional("battery_status"): str, + vol.Optional("hostname"): str, + }, + ) ) YAML_DEVICES = "known_devices.yaml" EVENT_NEW_DEVICE = "device_tracker_new_device" +def see( + hass: HomeAssistantType, + mac: str = None, + dev_id: str = None, + host_name: str = None, + location_name: str = None, + gps: GPSType = None, + gps_accuracy=None, + battery: int = None, + attributes: dict = None, +): + """Call service to notify you see device.""" + data = { + key: value + for key, value in ( + (ATTR_MAC, mac), + (ATTR_DEV_ID, dev_id), + (ATTR_HOST_NAME, host_name), + (ATTR_LOCATION_NAME, location_name), + (ATTR_GPS, gps), + (ATTR_GPS_ACCURACY, gps_accuracy), + (ATTR_BATTERY, battery), + ) + if value is not None + } + if attributes: + data[ATTR_ATTRIBUTES] = attributes + hass.services.call(DOMAIN, SERVICE_SEE, data) + + +async def async_setup_integration(hass: HomeAssistantType, config: ConfigType) -> None: + """Set up the legacy integration.""" + tracker = await get_tracker(hass, config) + + legacy_platforms = await async_extract_config(hass, config) + + setup_tasks = [ + legacy_platform.async_setup_legacy(hass, tracker) + for legacy_platform in legacy_platforms + ] + + if setup_tasks: + await asyncio.wait(setup_tasks) + + async def async_platform_discovered(p_type, info): + """Load a platform.""" + platform = await async_create_platform_type(hass, config, p_type, {}) + + if platform is None or platform.type != PLATFORM_TYPE_LEGACY: + return + + await platform.async_setup_legacy(hass, tracker, info) + + discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) + + # Clean up stale devices + async_track_utc_time_change( + hass, tracker.async_update_stale, second=range(0, 60, 5) + ) + + async def async_see_service(call): + """Service to see a device.""" + # Temp workaround for iOS, introduced in 0.65 + data = dict(call.data) + data.pop("hostname", None) + data.pop("battery_status", None) + await tracker.async_see(**data) + + hass.services.async_register( + DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA + ) + + # restore + await tracker.async_setup_tracked_device() + + +@attr.s +class DeviceTrackerPlatform: + """Class to hold platform information.""" + + LEGACY_SETUP = ( + "async_get_scanner", + "get_scanner", + "async_setup_scanner", + "setup_scanner", + ) + + name: str = attr.ib() + platform: ModuleType = attr.ib() + config: Dict = attr.ib() + + @property + def type(self): + """Return platform type.""" + for methods, platform_type in ((self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY),): + for meth in methods: + if hasattr(self.platform, meth): + return platform_type + + return None + + async def async_setup_legacy(self, hass, tracker, discovery_info=None): + """Set up a legacy platform.""" + LOGGER.info("Setting up %s.%s", DOMAIN, self.type) + try: + scanner = None + setup = None + if hasattr(self.platform, "async_get_scanner"): + scanner = await self.platform.async_get_scanner( + hass, {DOMAIN: self.config} + ) + elif hasattr(self.platform, "get_scanner"): + scanner = await hass.async_add_executor_job( + self.platform.get_scanner, hass, {DOMAIN: self.config} + ) + elif hasattr(self.platform, "async_setup_scanner"): + setup = await self.platform.async_setup_scanner( + hass, self.config, tracker.async_see, discovery_info + ) + elif hasattr(self.platform, "setup_scanner"): + setup = await hass.async_add_executor_job( + self.platform.setup_scanner, + hass, + self.config, + tracker.see, + discovery_info, + ) + else: + raise HomeAssistantError("Invalid legacy device_tracker platform.") + + if scanner: + async_setup_scanner_platform( + hass, self.config, scanner, tracker.async_see, self.type + ) + return + + if not setup: + LOGGER.error("Error setting up platform %s", self.type) + return + + except Exception: # pylint: disable=broad-except + LOGGER.exception("Error setting up platform %s", self.type) + + +async def async_extract_config(hass, config): + """Extract device tracker config and split between legacy and modern.""" + legacy = [] + + for platform in await asyncio.gather( + *( + async_create_platform_type(hass, config, p_type, p_config) + for p_type, p_config in config_per_platform(config, DOMAIN) + ) + ): + if platform is None: + continue + + if platform.type == PLATFORM_TYPE_LEGACY: + legacy.append(platform) + else: + raise ValueError( + f"Unable to determine type for {platform.name}: {platform.type}" + ) + + return legacy + + +async def async_create_platform_type( + hass, config, p_type, p_config +) -> Optional[DeviceTrackerPlatform]: + """Determine type of platform.""" + platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type) + + if platform is None: + return None + + return DeviceTrackerPlatform(p_type, platform, p_config) + + +@callback +def async_setup_scanner_platform( + hass: HomeAssistantType, + config: ConfigType, + scanner: Any, + async_see_device: Callable, + platform: str, +): + """Set up the connect scanner-based platform to device tracker. + + This method must be run in the event loop. + """ + interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + update_lock = asyncio.Lock() + scanner.hass = hass + + # Initial scan of each mac we also tell about host name for config + seen: Any = set() + + async def async_device_tracker_scan(now: dt_util.dt.datetime): + """Handle interval matches.""" + if update_lock.locked(): + LOGGER.warning( + "Updating device list from %s took longer than the scheduled " + "scan interval %s", + platform, + interval, + ) + return + + async with update_lock: + found_devices = await scanner.async_scan_devices() + + for mac in found_devices: + if mac in seen: + host_name = None + else: + host_name = await scanner.async_get_device_name(mac) + seen.add(mac) + + try: + extra_attributes = await scanner.async_get_extra_attributes(mac) + except NotImplementedError: + extra_attributes = {} + + kwargs = { + "mac": mac, + "host_name": host_name, + "source_type": SOURCE_TYPE_ROUTER, + "attributes": { + "scanner": scanner.__class__.__name__, + **extra_attributes, + }, + } + + zone_home = hass.states.get(hass.components.zone.ENTITY_ID_HOME) + if zone_home: + kwargs["gps"] = [ + zone_home.attributes[ATTR_LATITUDE], + zone_home.attributes[ATTR_LONGITUDE], + ] + kwargs["gps_accuracy"] = 0 + + hass.async_create_task(async_see_device(**kwargs)) + + async_track_time_interval(hass, async_device_tracker_scan, interval) + hass.async_create_task(async_device_tracker_scan(None)) + + async def get_tracker(hass, config): """Create a tracker.""" yaml_path = hass.config.path(YAML_DEVICES) @@ -349,17 +662,17 @@ class Device(RestoreEntity): @property def state_attributes(self): """Return the device state attributes.""" - attr = {ATTR_SOURCE_TYPE: self.source_type} + attributes = {ATTR_SOURCE_TYPE: self.source_type} if self.gps: - attr[ATTR_LATITUDE] = self.gps[0] - attr[ATTR_LONGITUDE] = self.gps[1] - attr[ATTR_GPS_ACCURACY] = self.gps_accuracy + attributes[ATTR_LATITUDE] = self.gps[0] + attributes[ATTR_LONGITUDE] = self.gps[1] + attributes[ATTR_GPS_ACCURACY] = self.gps_accuracy if self.battery: - attr[ATTR_BATTERY] = self.battery + attributes[ATTR_BATTERY] = self.battery - return attr + return attributes @property def device_state_attributes(self): @@ -453,13 +766,13 @@ class Device(RestoreEntity): self.last_update_home = state.state == STATE_HOME self.last_seen = dt_util.utcnow() - for attr, var in ( + for attribute, var in ( (ATTR_SOURCE_TYPE, "source_type"), (ATTR_GPS_ACCURACY, "gps_accuracy"), (ATTR_BATTERY, "battery"), ): - if attr in state.attributes: - setattr(self, var, state.attributes[attr]) + if attribute in state.attributes: + setattr(self, var, state.attributes[attribute]) if ATTR_LONGITUDE in state.attributes: self.gps = ( diff --git a/homeassistant/components/device_tracker/setup.py b/homeassistant/components/device_tracker/setup.py deleted file mode 100644 index 133ea4eb414..00000000000 --- a/homeassistant/components/device_tracker/setup.py +++ /dev/null @@ -1,196 +0,0 @@ -"""Device tracker helpers.""" -import asyncio -from types import ModuleType -from typing import Any, Callable, Dict, Optional - -import attr - -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE -from homeassistant.core import callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_per_platform -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.setup import async_prepare_setup_platform -from homeassistant.util import dt as dt_util - -from .const import ( - CONF_SCAN_INTERVAL, - DOMAIN, - LOGGER, - PLATFORM_TYPE_LEGACY, - SCAN_INTERVAL, - SOURCE_TYPE_ROUTER, -) - - -@attr.s -class DeviceTrackerPlatform: - """Class to hold platform information.""" - - LEGACY_SETUP = ( - "async_get_scanner", - "get_scanner", - "async_setup_scanner", - "setup_scanner", - ) - - name: str = attr.ib() - platform: ModuleType = attr.ib() - config: Dict = attr.ib() - - @property - def type(self): - """Return platform type.""" - for methods, platform_type in ((self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY),): - for meth in methods: - if hasattr(self.platform, meth): - return platform_type - - return None - - async def async_setup_legacy(self, hass, tracker, discovery_info=None): - """Set up a legacy platform.""" - LOGGER.info("Setting up %s.%s", DOMAIN, self.type) - try: - scanner = None - setup = None - if hasattr(self.platform, "async_get_scanner"): - scanner = await self.platform.async_get_scanner( - hass, {DOMAIN: self.config} - ) - elif hasattr(self.platform, "get_scanner"): - scanner = await hass.async_add_executor_job( - self.platform.get_scanner, hass, {DOMAIN: self.config} - ) - elif hasattr(self.platform, "async_setup_scanner"): - setup = await self.platform.async_setup_scanner( - hass, self.config, tracker.async_see, discovery_info - ) - elif hasattr(self.platform, "setup_scanner"): - setup = await hass.async_add_executor_job( - self.platform.setup_scanner, - hass, - self.config, - tracker.see, - discovery_info, - ) - else: - raise HomeAssistantError("Invalid legacy device_tracker platform.") - - if scanner: - async_setup_scanner_platform( - hass, self.config, scanner, tracker.async_see, self.type - ) - return - - if not setup: - LOGGER.error("Error setting up platform %s", self.type) - return - - except Exception: # pylint: disable=broad-except - LOGGER.exception("Error setting up platform %s", self.type) - - -async def async_extract_config(hass, config): - """Extract device tracker config and split between legacy and modern.""" - legacy = [] - - for platform in await asyncio.gather( - *( - async_create_platform_type(hass, config, p_type, p_config) - for p_type, p_config in config_per_platform(config, DOMAIN) - ) - ): - if platform is None: - continue - - if platform.type == PLATFORM_TYPE_LEGACY: - legacy.append(platform) - else: - raise ValueError( - f"Unable to determine type for {platform.name}: {platform.type}" - ) - - return legacy - - -async def async_create_platform_type( - hass, config, p_type, p_config -) -> Optional[DeviceTrackerPlatform]: - """Determine type of platform.""" - platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type) - - if platform is None: - return None - - return DeviceTrackerPlatform(p_type, platform, p_config) - - -@callback -def async_setup_scanner_platform( - hass: HomeAssistantType, - config: ConfigType, - scanner: Any, - async_see_device: Callable, - platform: str, -): - """Set up the connect scanner-based platform to device tracker. - - This method must be run in the event loop. - """ - interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - update_lock = asyncio.Lock() - scanner.hass = hass - - # Initial scan of each mac we also tell about host name for config - seen: Any = set() - - async def async_device_tracker_scan(now: dt_util.dt.datetime): - """Handle interval matches.""" - if update_lock.locked(): - LOGGER.warning( - "Updating device list from %s took longer than the scheduled " - "scan interval %s", - platform, - interval, - ) - return - - async with update_lock: - found_devices = await scanner.async_scan_devices() - - for mac in found_devices: - if mac in seen: - host_name = None - else: - host_name = await scanner.async_get_device_name(mac) - seen.add(mac) - - try: - extra_attributes = await scanner.async_get_extra_attributes(mac) - except NotImplementedError: - extra_attributes = {} - - kwargs = { - "mac": mac, - "host_name": host_name, - "source_type": SOURCE_TYPE_ROUTER, - "attributes": { - "scanner": scanner.__class__.__name__, - **extra_attributes, - }, - } - - zone_home = hass.states.get(hass.components.zone.ENTITY_ID_HOME) - if zone_home: - kwargs["gps"] = [ - zone_home.attributes[ATTR_LATITUDE], - zone_home.attributes[ATTR_LONGITUDE], - ] - kwargs["gps_accuracy"] = 0 - - hass.async_create_task(async_see_device(**kwargs)) - - async_track_time_interval(hass, async_device_tracker_scan, interval) - hass.async_create_task(async_device_tracker_scan(None))