Add strict typing to device_tracker (#50930)

* Add strict typing to device_tracker

* Update homeassistant/components/device_tracker/legacy.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Ruslan Sayfutdinov 2021-05-22 09:15:15 +01:00 committed by GitHub
parent 2e316f6fd5
commit b704f0e729
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 157 additions and 120 deletions

View File

@ -21,6 +21,7 @@ homeassistant.components.camera.*
homeassistant.components.canary.* homeassistant.components.canary.*
homeassistant.components.cover.* homeassistant.components.cover.*
homeassistant.components.device_automation.* homeassistant.components.device_automation.*
homeassistant.components.device_tracker.*
homeassistant.components.elgato.* homeassistant.components.elgato.*
homeassistant.components.fitbit.* homeassistant.components.fitbit.*
homeassistant.components.fritzbox.* homeassistant.components.fritzbox.*

View File

@ -37,12 +37,12 @@ from .legacy import ( # noqa: F401
@bind_hass @bind_hass
def is_on(hass: HomeAssistant, entity_id: str): def is_on(hass: HomeAssistant, entity_id: str) -> bool:
"""Return the state if any or a specified device is home.""" """Return the state if any or a specified device is home."""
return hass.states.is_state(entity_id, STATE_HOME) return hass.states.is_state(entity_id, STATE_HOME)
async def async_setup(hass: HomeAssistant, config: ConfigType): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the device tracker.""" """Set up the device tracker."""
await async_setup_legacy_integration(hass, config) await async_setup_legacy_integration(hass, config)

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from typing import final from typing import final
from homeassistant.components import zone from homeassistant.components import zone
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_BATTERY_LEVEL,
ATTR_GPS_ACCURACY, ATTR_GPS_ACCURACY,
@ -12,13 +13,15 @@ from homeassistant.const import (
STATE_HOME, STATE_HOME,
STATE_NOT_HOME, STATE_NOT_HOME,
) )
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import StateType
from .const import ATTR_HOST_NAME, ATTR_IP, ATTR_MAC, ATTR_SOURCE_TYPE, DOMAIN, LOGGER from .const import ATTR_HOST_NAME, ATTR_IP, ATTR_MAC, ATTR_SOURCE_TYPE, DOMAIN, LOGGER
async def async_setup_entry(hass, entry): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up an entry.""" """Set up an entry."""
component: EntityComponent | None = hass.data.get(DOMAIN) component: EntityComponent | None = hass.data.get(DOMAIN)
@ -28,16 +31,17 @@ async def async_setup_entry(hass, entry):
return await component.async_setup_entry(entry) return await component.async_setup_entry(entry)
async def async_unload_entry(hass, entry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload an entry.""" """Unload an entry."""
return await hass.data[DOMAIN].async_unload_entry(entry) component: EntityComponent = hass.data[DOMAIN]
return await component.async_unload_entry(entry)
class BaseTrackerEntity(Entity): class BaseTrackerEntity(Entity):
"""Represent a tracked device.""" """Represent a tracked device."""
@property @property
def battery_level(self): def battery_level(self) -> int | None:
"""Return the battery level of the device. """Return the battery level of the device.
Percentage from 0-100. Percentage from 0-100.
@ -45,16 +49,16 @@ class BaseTrackerEntity(Entity):
return None return None
@property @property
def source_type(self): def source_type(self) -> str:
"""Return the source type, eg gps or router, of the device.""" """Return the source type, eg gps or router, of the device."""
raise NotImplementedError raise NotImplementedError
@property @property
def state_attributes(self): def state_attributes(self) -> dict[str, StateType]:
"""Return the device state attributes.""" """Return the device state attributes."""
attr = {ATTR_SOURCE_TYPE: self.source_type} attr: dict[str, StateType] = {ATTR_SOURCE_TYPE: self.source_type}
if self.battery_level: if self.battery_level is not None:
attr[ATTR_BATTERY_LEVEL] = self.battery_level attr[ATTR_BATTERY_LEVEL] = self.battery_level
return attr return attr
@ -64,17 +68,17 @@ class TrackerEntity(BaseTrackerEntity):
"""Base class for a tracked device.""" """Base class for a tracked device."""
@property @property
def should_poll(self): def should_poll(self) -> bool:
"""No polling for entities that have location pushed.""" """No polling for entities that have location pushed."""
return False return False
@property @property
def force_update(self): def force_update(self) -> bool:
"""All updates need to be written to the state machine if we're not polling.""" """All updates need to be written to the state machine if we're not polling."""
return not self.should_poll return not self.should_poll
@property @property
def location_accuracy(self): def location_accuracy(self) -> int:
"""Return the location accuracy of the device. """Return the location accuracy of the device.
Value in meters. Value in meters.
@ -97,9 +101,9 @@ class TrackerEntity(BaseTrackerEntity):
raise NotImplementedError raise NotImplementedError
@property @property
def state(self): def state(self) -> str | None:
"""Return the state of the device.""" """Return the state of the device."""
if self.location_name: if self.location_name is not None:
return self.location_name return self.location_name
if self.latitude is not None and self.longitude is not None: if self.latitude is not None and self.longitude is not None:
@ -118,11 +122,11 @@ class TrackerEntity(BaseTrackerEntity):
@final @final
@property @property
def state_attributes(self): def state_attributes(self) -> dict[str, StateType]:
"""Return the device state attributes.""" """Return the device state attributes."""
attr = {} attr: dict[str, StateType] = {}
attr.update(super().state_attributes) attr.update(super().state_attributes)
if self.latitude is not None: if self.latitude is not None and self.longitude is not None:
attr[ATTR_LATITUDE] = self.latitude attr[ATTR_LATITUDE] = self.latitude
attr[ATTR_LONGITUDE] = self.longitude attr[ATTR_LONGITUDE] = self.longitude
attr[ATTR_GPS_ACCURACY] = self.location_accuracy attr[ATTR_GPS_ACCURACY] = self.location_accuracy
@ -162,9 +166,9 @@ class ScannerEntity(BaseTrackerEntity):
@final @final
@property @property
def state_attributes(self): def state_attributes(self) -> dict[str, StateType]:
"""Return the device state attributes.""" """Return the device state attributes."""
attr = {} attr: dict[str, StateType] = {}
attr.update(super().state_attributes) attr.update(super().state_attributes)
if self.ip_address is not None: if self.ip_address is not None:
attr[ATTR_IP] = self.ip_address attr[ATTR_IP] = self.ip_address

View File

@ -1,37 +1,38 @@
"""Device tracker constants.""" """Device tracker constants."""
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Final
LOGGER = logging.getLogger(__package__) LOGGER: Final = logging.getLogger(__package__)
DOMAIN = "device_tracker" DOMAIN: Final = "device_tracker"
PLATFORM_TYPE_LEGACY = "legacy" PLATFORM_TYPE_LEGACY: Final = "legacy"
PLATFORM_TYPE_ENTITY = "entity_platform" PLATFORM_TYPE_ENTITY: Final = "entity_platform"
SOURCE_TYPE_GPS = "gps" SOURCE_TYPE_GPS: Final = "gps"
SOURCE_TYPE_ROUTER = "router" SOURCE_TYPE_ROUTER: Final = "router"
SOURCE_TYPE_BLUETOOTH = "bluetooth" SOURCE_TYPE_BLUETOOTH: Final = "bluetooth"
SOURCE_TYPE_BLUETOOTH_LE = "bluetooth_le" SOURCE_TYPE_BLUETOOTH_LE: Final = "bluetooth_le"
CONF_SCAN_INTERVAL = "interval_seconds" CONF_SCAN_INTERVAL: Final = "interval_seconds"
SCAN_INTERVAL = timedelta(seconds=12) SCAN_INTERVAL: Final = timedelta(seconds=12)
CONF_TRACK_NEW = "track_new_devices" CONF_TRACK_NEW: Final = "track_new_devices"
DEFAULT_TRACK_NEW = True DEFAULT_TRACK_NEW: Final = True
CONF_CONSIDER_HOME = "consider_home" CONF_CONSIDER_HOME: Final = "consider_home"
DEFAULT_CONSIDER_HOME = timedelta(seconds=180) DEFAULT_CONSIDER_HOME: Final = timedelta(seconds=180)
CONF_NEW_DEVICE_DEFAULTS = "new_device_defaults" CONF_NEW_DEVICE_DEFAULTS: Final = "new_device_defaults"
ATTR_ATTRIBUTES = "attributes" ATTR_ATTRIBUTES: Final = "attributes"
ATTR_BATTERY = "battery" ATTR_BATTERY: Final = "battery"
ATTR_DEV_ID = "dev_id" ATTR_DEV_ID: Final = "dev_id"
ATTR_GPS = "gps" ATTR_GPS: Final = "gps"
ATTR_HOST_NAME = "host_name" ATTR_HOST_NAME: Final = "host_name"
ATTR_LOCATION_NAME = "location_name" ATTR_LOCATION_NAME: Final = "location_name"
ATTR_MAC = "mac" ATTR_MAC: Final = "mac"
ATTR_SOURCE_TYPE = "source_type" ATTR_SOURCE_TYPE: Final = "source_type"
ATTR_CONSIDER_HOME = "consider_home" ATTR_CONSIDER_HOME: Final = "consider_home"
ATTR_IP = "ip" ATTR_IP: Final = "ip"

View File

@ -1,6 +1,8 @@
"""Provides device automations for Device Tracker.""" """Provides device automations for Device Tracker."""
from __future__ import annotations from __future__ import annotations
from typing import Final
import voluptuous as vol import voluptuous as vol
from homeassistant.components.automation import AutomationActionType from homeassistant.components.automation import AutomationActionType
@ -21,9 +23,9 @@ from homeassistant.helpers.typing import ConfigType
from . import DOMAIN from . import DOMAIN
TRIGGER_TYPES = {"enters", "leaves"} TRIGGER_TYPES: Final[set[str]] = {"enters", "leaves"}
TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( TRIGGER_SCHEMA: Final = TRIGGER_BASE_SCHEMA.extend(
{ {
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
@ -88,7 +90,9 @@ async def async_attach_trigger(
) )
async def async_get_trigger_capabilities(hass: HomeAssistant, config: ConfigType): async def async_get_trigger_capabilities(
hass: HomeAssistant, config: ConfigType
) -> dict[str, vol.Schema]:
"""List trigger capabilities.""" """List trigger capabilities."""
zones = { zones = {
ent.entity_id: ent.name ent.entity_id: ent.name

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Sequence from collections.abc import Coroutine, Sequence
from datetime import timedelta from datetime import timedelta
import hashlib import hashlib
from types import ModuleType from types import ModuleType
@ -28,7 +28,7 @@ from homeassistant.const import (
STATE_HOME, STATE_HOME,
STATE_NOT_HOME, STATE_NOT_HOME,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers import config_per_platform, discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -38,7 +38,7 @@ from homeassistant.helpers.event import (
async_track_utc_time_change, async_track_utc_time_change,
) )
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, GPSType from homeassistant.helpers.typing import ConfigType, GPSType, StateType
from homeassistant.setup import async_prepare_setup_platform, async_start_setup from homeassistant.setup import async_prepare_setup_platform, async_start_setup
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.yaml import dump from homeassistant.util.yaml import dump
@ -69,9 +69,9 @@ from .const import (
SOURCE_TYPE_ROUTER, SOURCE_TYPE_ROUTER,
) )
SERVICE_SEE = "see" SERVICE_SEE: Final = "see"
SOURCE_TYPES = ( SOURCE_TYPES: Final[tuple[str, ...]] = (
SOURCE_TYPE_GPS, SOURCE_TYPE_GPS,
SOURCE_TYPE_ROUTER, SOURCE_TYPE_ROUTER,
SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH,
@ -92,9 +92,11 @@ PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA.extend(
vol.Optional(CONF_NEW_DEVICE_DEFAULTS, default={}): NEW_DEVICE_DEFAULTS_SCHEMA, vol.Optional(CONF_NEW_DEVICE_DEFAULTS, default={}): NEW_DEVICE_DEFAULTS_SCHEMA,
} }
) )
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) PLATFORM_SCHEMA_BASE: Final[vol.Schema] = cv.PLATFORM_SCHEMA_BASE.extend(
PLATFORM_SCHEMA.schema
)
SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema( SERVICE_SEE_PAYLOAD_SCHEMA: Final[vol.Schema] = vol.Schema(
vol.All( vol.All(
cv.has_at_least_one_key(ATTR_MAC, ATTR_DEV_ID), cv.has_at_least_one_key(ATTR_MAC, ATTR_DEV_ID),
{ {
@ -115,23 +117,23 @@ SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema(
) )
) )
YAML_DEVICES = "known_devices.yaml" YAML_DEVICES: Final = "known_devices.yaml"
EVENT_NEW_DEVICE = "device_tracker_new_device" EVENT_NEW_DEVICE: Final = "device_tracker_new_device"
def see( def see(
hass: HomeAssistant, hass: HomeAssistant,
mac: str = None, mac: str | None = None,
dev_id: str = None, dev_id: str | None = None,
host_name: str = None, host_name: str | None = None,
location_name: str = None, location_name: str | None = None,
gps: GPSType = None, gps: GPSType | None = None,
gps_accuracy=None, gps_accuracy: int | None = None,
battery: int = None, battery: int | None = None,
attributes: dict = None, attributes: dict | None = None,
): ) -> None:
"""Call service to notify you see device.""" """Call service to notify you see device."""
data = { data: dict[str, Any] = {
key: value key: value
for key, value in ( for key, value in (
(ATTR_MAC, mac), (ATTR_MAC, mac),
@ -144,7 +146,7 @@ def see(
) )
if value is not None if value is not None
} }
if attributes: if attributes is not None:
data[ATTR_ATTRIBUTES] = attributes data[ATTR_ATTRIBUTES] = attributes
hass.services.call(DOMAIN, SERVICE_SEE, data) hass.services.call(DOMAIN, SERVICE_SEE, data)
@ -163,7 +165,9 @@ async def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> No
if setup_tasks: if setup_tasks:
await asyncio.wait(setup_tasks) await asyncio.wait(setup_tasks)
async def async_platform_discovered(p_type, info): async def async_platform_discovered(
p_type: str, info: dict[str, Any] | None
) -> None:
"""Load a platform.""" """Load a platform."""
platform = await async_create_platform_type(hass, config, p_type, {}) platform = await async_create_platform_type(hass, config, p_type, {})
@ -179,7 +183,7 @@ async def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> No
hass, tracker.async_update_stale, second=range(0, 60, 5) hass, tracker.async_update_stale, second=range(0, 60, 5)
) )
async def async_see_service(call): async def async_see_service(call: ServiceCall) -> None:
"""Service to see a device.""" """Service to see a device."""
# Temp workaround for iOS, introduced in 0.65 # Temp workaround for iOS, introduced in 0.65
data = dict(call.data) data = dict(call.data)
@ -199,7 +203,7 @@ async def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> No
class DeviceTrackerPlatform: class DeviceTrackerPlatform:
"""Class to hold platform information.""" """Class to hold platform information."""
LEGACY_SETUP = ( LEGACY_SETUP: Final[tuple[str, ...]] = (
"async_get_scanner", "async_get_scanner",
"get_scanner", "get_scanner",
"async_setup_scanner", "async_setup_scanner",
@ -211,17 +215,22 @@ class DeviceTrackerPlatform:
config: dict = attr.ib() config: dict = attr.ib()
@property @property
def type(self): def type(self) -> str | None:
"""Return platform type.""" """Return platform type."""
for methods, platform_type in ((self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY),): methods, platform_type = self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY
for meth in methods: for method in methods:
if hasattr(self.platform, meth): if hasattr(self.platform, method):
return platform_type return platform_type
return None return None
async def async_setup_legacy(self, hass, tracker, discovery_info=None): async def async_setup_legacy(
self,
hass: HomeAssistant,
tracker: DeviceTracker,
discovery_info: dict[str, Any] | None = None,
) -> None:
"""Set up a legacy platform.""" """Set up a legacy platform."""
assert self.type == PLATFORM_TYPE_LEGACY
full_name = f"{DOMAIN}.{self.name}" full_name = f"{DOMAIN}.{self.name}"
LOGGER.info("Setting up %s", full_name) LOGGER.info("Setting up %s", full_name)
with async_start_setup(hass, [full_name]): with async_start_setup(hass, [full_name]):
@ -229,20 +238,22 @@ class DeviceTrackerPlatform:
scanner = None scanner = None
setup = None setup = None
if hasattr(self.platform, "async_get_scanner"): if hasattr(self.platform, "async_get_scanner"):
scanner = await self.platform.async_get_scanner( scanner = await self.platform.async_get_scanner( # type: ignore[attr-defined]
hass, {DOMAIN: self.config} hass, {DOMAIN: self.config}
) )
elif hasattr(self.platform, "get_scanner"): elif hasattr(self.platform, "get_scanner"):
scanner = await hass.async_add_executor_job( scanner = await hass.async_add_executor_job(
self.platform.get_scanner, hass, {DOMAIN: self.config} self.platform.get_scanner, # type: ignore[attr-defined]
hass,
{DOMAIN: self.config},
) )
elif hasattr(self.platform, "async_setup_scanner"): elif hasattr(self.platform, "async_setup_scanner"):
setup = await self.platform.async_setup_scanner( setup = await self.platform.async_setup_scanner( # type: ignore[attr-defined]
hass, self.config, tracker.async_see, discovery_info hass, self.config, tracker.async_see, discovery_info
) )
elif hasattr(self.platform, "setup_scanner"): elif hasattr(self.platform, "setup_scanner"):
setup = await hass.async_add_executor_job( setup = await hass.async_add_executor_job(
self.platform.setup_scanner, self.platform.setup_scanner, # type: ignore[attr-defined]
hass, hass,
self.config, self.config,
tracker.see, tracker.see,
@ -251,12 +262,12 @@ class DeviceTrackerPlatform:
else: else:
raise HomeAssistantError("Invalid legacy device_tracker platform.") raise HomeAssistantError("Invalid legacy device_tracker platform.")
if scanner: if scanner is not None:
async_setup_scanner_platform( async_setup_scanner_platform(
hass, self.config, scanner, tracker.async_see, self.type hass, self.config, scanner, tracker.async_see, self.type
) )
if not setup and not scanner: if setup is None and scanner is None:
LOGGER.error( LOGGER.error(
"Error setting up platform %s %s", self.type, self.name "Error setting up platform %s %s", self.type, self.name
) )
@ -270,9 +281,11 @@ class DeviceTrackerPlatform:
) )
async def async_extract_config(hass, config): async def async_extract_config(
hass: HomeAssistant, config: ConfigType
) -> list[DeviceTrackerPlatform]:
"""Extract device tracker config and split between legacy and modern.""" """Extract device tracker config and split between legacy and modern."""
legacy = [] legacy: list[DeviceTrackerPlatform] = []
for platform in await asyncio.gather( for platform in await asyncio.gather(
*( *(
@ -294,7 +307,7 @@ async def async_extract_config(hass, config):
async def async_create_platform_type( async def async_create_platform_type(
hass, config, p_type, p_config hass: HomeAssistant, config: ConfigType, p_type: str, p_config: dict
) -> DeviceTrackerPlatform | None: ) -> DeviceTrackerPlatform | None:
"""Determine type of platform.""" """Determine type of platform."""
platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type) platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type)
@ -310,9 +323,9 @@ def async_setup_scanner_platform(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config: ConfigType,
scanner: DeviceScanner, scanner: DeviceScanner,
async_see_device: Callable, async_see_device: Callable[..., Coroutine[None, None, None]],
platform: str, platform: str,
): ) -> None:
"""Set up the connect scanner-based platform to device tracker. """Set up the connect scanner-based platform to device tracker.
This method must be run in the event loop. This method must be run in the event loop.
@ -324,7 +337,7 @@ def async_setup_scanner_platform(
# Initial scan of each mac we also tell about host name for config # Initial scan of each mac we also tell about host name for config
seen: Any = set() seen: Any = set()
async def async_device_tracker_scan(now: dt_util.dt.datetime | None): async def async_device_tracker_scan(now: dt_util.dt.datetime | None) -> None:
"""Handle interval matches.""" """Handle interval matches."""
if update_lock.locked(): if update_lock.locked():
LOGGER.warning( LOGGER.warning(
@ -350,7 +363,7 @@ def async_setup_scanner_platform(
except NotImplementedError: except NotImplementedError:
extra_attributes = {} extra_attributes = {}
kwargs = { kwargs: dict[str, Any] = {
"mac": mac, "mac": mac,
"host_name": host_name, "host_name": host_name,
"source_type": SOURCE_TYPE_ROUTER, "source_type": SOURCE_TYPE_ROUTER,
@ -361,7 +374,7 @@ def async_setup_scanner_platform(
} }
zone_home = hass.states.get(hass.components.zone.ENTITY_ID_HOME) zone_home = hass.states.get(hass.components.zone.ENTITY_ID_HOME)
if zone_home: if zone_home is not None:
kwargs["gps"] = [ kwargs["gps"] = [
zone_home.attributes[ATTR_LATITUDE], zone_home.attributes[ATTR_LATITUDE],
zone_home.attributes[ATTR_LONGITUDE], zone_home.attributes[ATTR_LONGITUDE],
@ -374,7 +387,7 @@ def async_setup_scanner_platform(
hass.async_create_task(async_device_tracker_scan(None)) hass.async_create_task(async_device_tracker_scan(None))
async def get_tracker(hass, config): async def get_tracker(hass: HomeAssistant, config: ConfigType) -> DeviceTracker:
"""Create a tracker.""" """Create a tracker."""
yaml_path = hass.config.path(YAML_DEVICES) yaml_path = hass.config.path(YAML_DEVICES)
@ -400,12 +413,12 @@ class DeviceTracker:
hass: HomeAssistant, hass: HomeAssistant,
consider_home: timedelta, consider_home: timedelta,
track_new: bool, track_new: bool,
defaults: dict, defaults: dict[str, Any],
devices: Sequence, devices: Sequence[Device],
) -> None: ) -> None:
"""Initialize a device tracker.""" """Initialize a device tracker."""
self.hass = hass self.hass = hass
self.devices = {dev.dev_id: dev for dev in devices} self.devices: dict[str, Device] = {dev.dev_id: dev for dev in devices}
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac} self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
self.consider_home = consider_home self.consider_home = consider_home
self.track_new = ( self.track_new = (
@ -436,7 +449,7 @@ class DeviceTracker:
picture: str | None = None, picture: str | None = None,
icon: str | None = None, icon: str | None = None,
consider_home: timedelta | None = None, consider_home: timedelta | None = None,
): ) -> None:
"""Notify the device tracker that you see a device.""" """Notify the device tracker that you see a device."""
self.hass.create_task( self.hass.create_task(
self.async_see( self.async_see(
@ -556,7 +569,7 @@ class DeviceTracker:
) )
) )
async def async_update_config(self, path, dev_id, device): async def async_update_config(self, path: str, dev_id: str, device: Device) -> None:
"""Add device to YAML configuration file. """Add device to YAML configuration file.
This method is a coroutine. This method is a coroutine.
@ -567,7 +580,7 @@ class DeviceTracker:
) )
@callback @callback
def async_update_stale(self, now: dt_util.dt.datetime): def async_update_stale(self, now: dt_util.dt.datetime) -> None:
"""Update stale devices. """Update stale devices.
This method must be run in the event loop. This method must be run in the event loop.
@ -576,18 +589,18 @@ class DeviceTracker:
if (device.track and device.last_update_home) and device.stale(now): if (device.track and device.last_update_home) and device.stale(now):
self.hass.async_create_task(device.async_update_ha_state(True)) self.hass.async_create_task(device.async_update_ha_state(True))
async def async_setup_tracked_device(self): async def async_setup_tracked_device(self) -> None:
"""Set up all not exists tracked devices. """Set up all not exists tracked devices.
This method is a coroutine. This method is a coroutine.
""" """
async def async_init_single_device(dev): async def async_init_single_device(dev: Device) -> None:
"""Init a single device_tracker entity.""" """Init a single device_tracker entity."""
await dev.async_added_to_hass() await dev.async_added_to_hass()
dev.async_write_ha_state() dev.async_write_ha_state()
tasks = [] tasks: list[asyncio.Task] = []
for device in self.devices.values(): for device in self.devices.values():
if device.track and not device.last_seen: if device.track and not device.last_seen:
tasks.append( tasks.append(
@ -610,8 +623,8 @@ class Device(RestoreEntity):
attributes: dict | None = None attributes: dict | None = None
# Track if the last update of this device was HOME. # Track if the last update of this device was HOME.
last_update_home = False last_update_home: bool = False
_state = STATE_NOT_HOME _state: str = STATE_NOT_HOME
def __init__( def __init__(
self, self,
@ -644,6 +657,7 @@ class Device(RestoreEntity):
self.config_name = name self.config_name = name
# Configured picture # Configured picture
self.config_picture: str | None
if gravatar is not None: if gravatar is not None:
self.config_picture = get_gravatar_for_email(gravatar) self.config_picture = get_gravatar_for_email(gravatar)
else: else:
@ -656,32 +670,32 @@ class Device(RestoreEntity):
self._attributes: dict[str, Any] = {} self._attributes: dict[str, Any] = {}
@property @property
def name(self): def name(self) -> str:
"""Return the name of the entity.""" """Return the name of the entity."""
return self.config_name or self.host_name or self.dev_id or DEVICE_DEFAULT_NAME return self.config_name or self.host_name or self.dev_id or DEVICE_DEFAULT_NAME
@property @property
def state(self): def state(self) -> str:
"""Return the state of the device.""" """Return the state of the device."""
return self._state return self._state
@property @property
def entity_picture(self): def entity_picture(self) -> str | None:
"""Return the picture of the device.""" """Return the picture of the device."""
return self.config_picture return self.config_picture
@final @final
@property @property
def state_attributes(self): def state_attributes(self) -> dict[str, StateType]:
"""Return the device state attributes.""" """Return the device state attributes."""
attributes = {ATTR_SOURCE_TYPE: self.source_type} attributes: dict[str, StateType] = {ATTR_SOURCE_TYPE: self.source_type}
if self.gps: if self.gps is not None:
attributes[ATTR_LATITUDE] = self.gps[0] attributes[ATTR_LATITUDE] = self.gps[0]
attributes[ATTR_LONGITUDE] = self.gps[1] attributes[ATTR_LONGITUDE] = self.gps[1]
attributes[ATTR_GPS_ACCURACY] = self.gps_accuracy attributes[ATTR_GPS_ACCURACY] = self.gps_accuracy
if self.battery: if self.battery is not None:
attributes[ATTR_BATTERY] = self.battery attributes[ATTR_BATTERY] = self.battery
return attributes return attributes
@ -742,13 +756,13 @@ class Device(RestoreEntity):
or (now or dt_util.utcnow()) - self.last_seen > self.consider_home or (now or dt_util.utcnow()) - self.last_seen > self.consider_home
) )
def mark_stale(self): def mark_stale(self) -> None:
"""Mark the device state as stale.""" """Mark the device state as stale."""
self._state = STATE_NOT_HOME self._state = STATE_NOT_HOME
self.gps = None self.gps = None
self.last_update_home = False self.last_update_home = False
async def async_update(self): async def async_update(self) -> None:
"""Update state of entity. """Update state of entity.
This method is a coroutine. This method is a coroutine.
@ -773,7 +787,7 @@ class Device(RestoreEntity):
self._state = STATE_HOME self._state = STATE_HOME
self.last_update_home = True self.last_update_home = True
async def async_added_to_hass(self): async def async_added_to_hass(self) -> None:
"""Add an entity.""" """Add an entity."""
await super().async_added_to_hass() await super().async_added_to_hass()
state = await self.async_get_last_state() state = await self.async_get_last_state()
@ -807,7 +821,7 @@ class DeviceScanner:
"""Scan for devices.""" """Scan for devices."""
raise NotImplementedError() raise NotImplementedError()
async def async_scan_devices(self) -> Any: async def async_scan_devices(self) -> list[str]:
"""Scan for devices.""" """Scan for devices."""
assert ( assert (
self.hass is not None self.hass is not None
@ -829,7 +843,7 @@ class DeviceScanner:
"""Get the extra attributes of a device.""" """Get the extra attributes of a device."""
raise NotImplementedError() raise NotImplementedError()
async def async_get_extra_attributes(self, device: str) -> Any: async def async_get_extra_attributes(self, device: str) -> dict:
"""Get the extra attributes of a device.""" """Get the extra attributes of a device."""
assert ( assert (
self.hass is not None self.hass is not None
@ -837,7 +851,9 @@ class DeviceScanner:
return await self.hass.async_add_executor_job(self.get_extra_attributes, device) return await self.hass.async_add_executor_job(self.get_extra_attributes, device)
async def async_load_config(path: str, hass: HomeAssistant, consider_home: timedelta): async def async_load_config(
path: str, hass: HomeAssistant, consider_home: timedelta
) -> list[Device]:
"""Load devices from YAML configuration file. """Load devices from YAML configuration file.
This method is a coroutine. This method is a coroutine.
@ -857,7 +873,7 @@ async def async_load_config(path: str, hass: HomeAssistant, consider_home: timed
), ),
} }
) )
result = [] result: list[Device] = []
try: try:
devices = await hass.async_add_executor_job(load_yaml_config_file, path) devices = await hass.async_add_executor_job(load_yaml_config_file, path)
except HomeAssistantError as err: except HomeAssistantError as err:
@ -880,7 +896,7 @@ async def async_load_config(path: str, hass: HomeAssistant, consider_home: timed
return result return result
def update_config(path: str, dev_id: str, device: Device): def update_config(path: str, dev_id: str, device: Device) -> None:
"""Add device to YAML configuration file.""" """Add device to YAML configuration file."""
with open(path, "a") as out: with open(path, "a") as out:
device_config = { device_config = {
@ -896,7 +912,7 @@ def update_config(path: str, dev_id: str, device: Device):
out.write(dump(device_config)) out.write(dump(device_config))
def get_gravatar_for_email(email: str): def get_gravatar_for_email(email: str) -> str:
"""Return an 80px Gravatar for the given email address. """Return an 80px Gravatar for the given email address.
Async friendly. Async friendly.

View File

@ -242,6 +242,17 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.device_tracker.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.elgato.*] [mypy-homeassistant.components.elgato.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true