Strict typing of UniFi integration (#90278)

* Fix typing of UniFi controller

* Strict typing of unifi.__init__

* Strict typing of UniFi config_flow

* Strict typing of UniFi switch

* Strict typing UniFi sensor

* Strict typing UniFi device tracker

* Strict typing of UniFi

* Fix library issues related to typing
This commit is contained in:
Robert Svensson 2023-03-26 09:57:13 +02:00 committed by GitHub
parent e8f3b9c09a
commit a0b6da33ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 57 additions and 54 deletions

View File

@ -311,7 +311,7 @@ homeassistant.components.trafikverket_train.*
homeassistant.components.trafikverket_weatherstation.* homeassistant.components.trafikverket_weatherstation.*
homeassistant.components.tts.* homeassistant.components.tts.*
homeassistant.components.twentemilieu.* homeassistant.components.twentemilieu.*
homeassistant.components.unifi.update homeassistant.components.unifi.*
homeassistant.components.unifiprotect.* homeassistant.components.unifiprotect.*
homeassistant.components.upcloud.* homeassistant.components.upcloud.*
homeassistant.components.update.* homeassistant.components.update.*

View File

@ -64,7 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
controller = hass.data[UNIFI_DOMAIN].pop(config_entry.entry_id) controller: UniFiController = hass.data[UNIFI_DOMAIN].pop(config_entry.entry_id)
if not hass.data[UNIFI_DOMAIN]: if not hass.data[UNIFI_DOMAIN]:
async_unload_services(hass) async_unload_services(hass)

View File

@ -10,7 +10,7 @@ from typing import Any
from aiohttp import CookieJar from aiohttp import CookieJar
import aiounifi import aiounifi
from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.api_handlers import ItemEvent
from aiounifi.websocket import WebsocketSignal, WebsocketState from aiounifi.websocket import WebsocketState
import async_timeout import async_timeout
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -22,7 +22,7 @@ from homeassistant.const import (
CONF_VERIFY_SSL, CONF_VERIFY_SSL,
Platform, Platform,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
from homeassistant.helpers import ( from homeassistant.helpers import (
aiohttp_client, aiohttp_client,
device_registry as dr, device_registry as dr,
@ -75,31 +75,32 @@ CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1)
class UniFiController: class UniFiController:
"""Manages a single UniFi Network instance.""" """Manages a single UniFi Network instance."""
def __init__(self, hass, config_entry, api): def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, api: aiounifi.Controller
) -> None:
"""Initialize the system.""" """Initialize the system."""
self.hass = hass self.hass = hass
self.config_entry = config_entry self.config_entry = config_entry
self.api = api self.api = api
api.callback = self.async_unifi_signalling_callback api.ws_state_callback = self.async_unifi_ws_state_callback
self.available = True self.available = True
self.wireless_clients = hass.data[UNIFI_WIRELESS_CLIENTS] self.wireless_clients = hass.data[UNIFI_WIRELESS_CLIENTS]
self.site_id: str = "" self.site_id: str = ""
self._site_name = None self._site_name: str | None = None
self._site_role = None self._site_role: str | None = None
self._cancel_heartbeat_check = None self._cancel_heartbeat_check: CALLBACK_TYPE | None = None
self._heartbeat_dispatch = {} self._heartbeat_time: dict[str, datetime] = {}
self._heartbeat_time = {}
self.load_config_entry_options() self.load_config_entry_options()
self.entities = {} self.entities: dict[str, str] = {}
self.known_objects: set[tuple[str, str]] = set() self.known_objects: set[tuple[str, str]] = set()
def load_config_entry_options(self): def load_config_entry_options(self) -> None:
"""Store attributes to avoid property call overhead since they are called frequently.""" """Store attributes to avoid property call overhead since they are called frequently."""
options = self.config_entry.options options = self.config_entry.options
@ -114,7 +115,7 @@ class UniFiController:
CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS
) )
# Config entry option to not track devices. # Config entry option to not track devices.
self.option_track_devices = options.get( self.option_track_devices: bool = options.get(
CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES
) )
# Config entry option listing what SSIDs are being used to track clients. # Config entry option listing what SSIDs are being used to track clients.
@ -133,43 +134,45 @@ class UniFiController:
# Config entry option with list of clients to control network access. # Config entry option with list of clients to control network access.
self.option_block_clients = options.get(CONF_BLOCK_CLIENT, []) self.option_block_clients = options.get(CONF_BLOCK_CLIENT, [])
# Config entry option to control DPI restriction groups. # Config entry option to control DPI restriction groups.
self.option_dpi_restrictions = options.get( self.option_dpi_restrictions: bool = options.get(
CONF_DPI_RESTRICTIONS, DEFAULT_DPI_RESTRICTIONS CONF_DPI_RESTRICTIONS, DEFAULT_DPI_RESTRICTIONS
) )
# Statistics sensor options # Statistics sensor options
# Config entry option to allow bandwidth sensors. # Config entry option to allow bandwidth sensors.
self.option_allow_bandwidth_sensors = options.get( self.option_allow_bandwidth_sensors: bool = options.get(
CONF_ALLOW_BANDWIDTH_SENSORS, DEFAULT_ALLOW_BANDWIDTH_SENSORS CONF_ALLOW_BANDWIDTH_SENSORS, DEFAULT_ALLOW_BANDWIDTH_SENSORS
) )
# Config entry option to allow uptime sensors. # Config entry option to allow uptime sensors.
self.option_allow_uptime_sensors = options.get( self.option_allow_uptime_sensors: bool = options.get(
CONF_ALLOW_UPTIME_SENSORS, DEFAULT_ALLOW_UPTIME_SENSORS CONF_ALLOW_UPTIME_SENSORS, DEFAULT_ALLOW_UPTIME_SENSORS
) )
@property @property
def host(self): def host(self) -> str:
"""Return the host of this controller.""" """Return the host of this controller."""
return self.config_entry.data[CONF_HOST] host: str = self.config_entry.data[CONF_HOST]
return host
@property @property
def site(self): def site(self) -> str:
"""Return the site of this config entry.""" """Return the site of this config entry."""
return self.config_entry.data[CONF_SITE_ID] site_id: str = self.config_entry.data[CONF_SITE_ID]
return site_id
@property @property
def site_name(self): def site_name(self) -> str | None:
"""Return the nice name of site.""" """Return the nice name of site."""
return self._site_name return self._site_name
@property @property
def site_role(self): def site_role(self) -> str | None:
"""Return the site user role of this controller.""" """Return the site user role of this controller."""
return self._site_role return self._site_role
@property @property
def mac(self): def mac(self) -> str | None:
"""Return the mac address of this controller.""" """Return the mac address of this controller."""
for client in self.api.clients.values(): for client in self.api.clients.values():
if self.host == client.ip: if self.host == client.ip:
@ -227,22 +230,21 @@ class UniFiController:
async_load_entities(description) async_load_entities(description)
@callback @callback
def async_unifi_signalling_callback(self, signal, data): def async_unifi_ws_state_callback(self, state: WebsocketState) -> None:
"""Handle messages back from UniFi library.""" """Handle messages back from UniFi library."""
if signal == WebsocketSignal.CONNECTION_STATE: if state == WebsocketState.DISCONNECTED and self.available:
if data == WebsocketState.DISCONNECTED and self.available: LOGGER.warning("Lost connection to UniFi Network")
LOGGER.warning("Lost connection to UniFi Network")
if (data == WebsocketState.RUNNING and not self.available) or ( if (state == WebsocketState.RUNNING and not self.available) or (
data == WebsocketState.DISCONNECTED and self.available state == WebsocketState.DISCONNECTED and self.available
): ):
self.available = data == WebsocketState.RUNNING self.available = state == WebsocketState.RUNNING
async_dispatcher_send(self.hass, self.signal_reachable) async_dispatcher_send(self.hass, self.signal_reachable)
if not self.available: if not self.available:
self.hass.loop.call_later(RETRY_TIMER, self.reconnect, True) self.hass.loop.call_later(RETRY_TIMER, self.reconnect, True)
else: else:
LOGGER.info("Connected to UniFi Network") LOGGER.info("Connected to UniFi Network")
@property @property
def signal_reachable(self) -> str: def signal_reachable(self) -> str:
@ -259,7 +261,7 @@ class UniFiController:
"""Event specific per UniFi device tracker to signal new heartbeat missed.""" """Event specific per UniFi device tracker to signal new heartbeat missed."""
return "unifi-heartbeat-missed" return "unifi-heartbeat-missed"
async def initialize(self): async def initialize(self) -> None:
"""Set up a UniFi Network instance.""" """Set up a UniFi Network instance."""
await self.api.initialize() await self.api.initialize()
@ -291,7 +293,7 @@ class UniFiController:
continue continue
client = self.api.clients_all[mac] client = self.api.clients_all[mac]
self.api.clients.process_raw([client.raw]) self.api.clients.process_raw([dict(client.raw)])
LOGGER.debug( LOGGER.debug(
"Restore disconnected client %s (%s)", "Restore disconnected client %s (%s)",
entry.entity_id, entry.entity_id,
@ -319,7 +321,7 @@ class UniFiController:
del self._heartbeat_time[unique_id] del self._heartbeat_time[unique_id]
@callback @callback
def _async_check_for_stale(self, *_) -> None: def _async_check_for_stale(self, *_: datetime) -> None:
"""Check for any devices scheduled to be marked disconnected.""" """Check for any devices scheduled to be marked disconnected."""
now = dt_util.utcnow() now = dt_util.utcnow()
@ -365,7 +367,7 @@ class UniFiController:
async_dispatcher_send(hass, controller.signal_options_update) async_dispatcher_send(hass, controller.signal_options_update)
@callback @callback
def reconnect(self, log=False) -> None: def reconnect(self, log: bool = False) -> None:
"""Prepare to reconnect UniFi session.""" """Prepare to reconnect UniFi session."""
if log: if log:
LOGGER.info("Will try to reconnect to UniFi Network") LOGGER.info("Will try to reconnect to UniFi Network")
@ -387,14 +389,14 @@ class UniFiController:
self.hass.loop.call_later(RETRY_TIMER, self.reconnect) self.hass.loop.call_later(RETRY_TIMER, self.reconnect)
@callback @callback
def shutdown(self, event) -> None: def shutdown(self, event: Event) -> None:
"""Wrap the call to unifi.close. """Wrap the call to unifi.close.
Used as an argument to EventBus.async_listen_once. Used as an argument to EventBus.async_listen_once.
""" """
self.api.stop_websocket() self.api.stop_websocket()
async def async_reset(self): async def async_reset(self) -> bool:
"""Reset this controller to default state. """Reset this controller to default state.
Will cancel any scheduled setup retry and will unload Will cancel any scheduled setup retry and will unload
@ -421,15 +423,15 @@ async def get_unifi_controller(
config: MappingProxyType[str, Any], config: MappingProxyType[str, Any],
) -> aiounifi.Controller: ) -> aiounifi.Controller:
"""Create a controller object and verify authentication.""" """Create a controller object and verify authentication."""
ssl_context = False ssl_context: ssl.SSLContext | bool = False
if verify_ssl := bool(config.get(CONF_VERIFY_SSL)): if verify_ssl := config.get(CONF_VERIFY_SSL):
session = aiohttp_client.async_get_clientsession(hass) session = aiohttp_client.async_get_clientsession(hass)
if isinstance(verify_ssl, str): if isinstance(verify_ssl, str):
ssl_context = ssl.create_default_context(cafile=verify_ssl) ssl_context = ssl.create_default_context(cafile=verify_ssl)
else: else:
session = aiohttp_client.async_create_clientsession( session = aiohttp_client.async_create_clientsession(
hass, verify_ssl=verify_ssl, cookie_jar=CookieJar(unsafe=True) hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True)
) )
controller = aiounifi.Controller( controller = aiounifi.Controller(

View File

@ -19,7 +19,7 @@ from aiounifi.models.event import Event, EventKey
from homeassistant.components.device_tracker import ScannerEntity, SourceType from homeassistant.components.device_tracker import ScannerEntity, SourceType
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import Event as core_Event, HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -268,7 +268,7 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity):
return self._attr_unique_id return self._attr_unique_id
@callback @callback
def _make_disconnected(self, *_) -> None: def _make_disconnected(self, *_: core_Event) -> None:
"""No heart beat by device.""" """No heart beat by device."""
self._is_connected = False self._is_connected = False
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -8,7 +8,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiounifi"], "loggers": ["aiounifi"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["aiounifi==45"], "requirements": ["aiounifi==46"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "Ubiquiti Networks", "manufacturer": "Ubiquiti Networks",

View File

@ -247,8 +247,9 @@ async def async_setup_entry(
for mac in controller.option_block_clients: for mac in controller.option_block_clients:
if mac not in controller.api.clients and mac in controller.api.clients_all: if mac not in controller.api.clients and mac in controller.api.clients_all:
client = controller.api.clients_all[mac] controller.api.clients.process_raw(
controller.api.clients.process_raw([client.raw]) [dict(controller.api.clients_all[mac].raw)]
)
controller.register_platform_add_entities( controller.register_platform_add_entities(
UnifiSwitchEntity, ENTITY_DESCRIPTIONS, async_add_entities UnifiSwitchEntity, ENTITY_DESCRIPTIONS, async_add_entities

View File

@ -2873,7 +2873,7 @@ disallow_untyped_defs = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.unifi.update] [mypy-homeassistant.components.unifi.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true
disallow_subclassing_any = true disallow_subclassing_any = true

View File

@ -291,7 +291,7 @@ aiosyncthing==0.5.1
aiotractive==0.5.5 aiotractive==0.5.5
# homeassistant.components.unifi # homeassistant.components.unifi
aiounifi==45 aiounifi==46
# homeassistant.components.vlc_telnet # homeassistant.components.vlc_telnet
aiovlc==0.1.0 aiovlc==0.1.0

View File

@ -272,7 +272,7 @@ aiosyncthing==0.5.1
aiotractive==0.5.5 aiotractive==0.5.5
# homeassistant.components.unifi # homeassistant.components.unifi
aiounifi==45 aiounifi==46
# homeassistant.components.vlc_telnet # homeassistant.components.vlc_telnet
aiovlc==0.1.0 aiovlc==0.1.0