Add strict type annotations to actiontect (#50672)

* add strict type annotations

* fix pylint, add coverage omit

* apply suggestions

* fix rebase conflict

* import PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA

* correct get_device_name() return annotation
This commit is contained in:
Michael 2021-05-15 23:59:57 +02:00 committed by GitHub
parent 256a2de7ce
commit edccb7eb58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 74 additions and 43 deletions

View file

@ -9,7 +9,9 @@ omit =
# omit pieces of code that rely on external devices being present # omit pieces of code that rely on external devices being present
homeassistant/components/acer_projector/* homeassistant/components/acer_projector/*
homeassistant/components/actiontec/const.py
homeassistant/components/actiontec/device_tracker.py homeassistant/components/actiontec/device_tracker.py
homeassistant/components/actiontec/model.py
homeassistant/components/acmeda/__init__.py homeassistant/components/acmeda/__init__.py
homeassistant/components/acmeda/base.py homeassistant/components/acmeda/base.py
homeassistant/components/acmeda/const.py homeassistant/components/acmeda/const.py

View file

@ -4,6 +4,7 @@
homeassistant.components homeassistant.components
homeassistant.components.acer_projector.* homeassistant.components.acer_projector.*
homeassistant.components.actiontec.*
homeassistant.components.aftership.* homeassistant.components.aftership.*
homeassistant.components.airly.* homeassistant.components.airly.*
homeassistant.components.aladdin_connect.* homeassistant.components.aladdin_connect.*

View file

@ -0,0 +1,12 @@
"""Support for Actiontec MI424WR (Verizon FIOS) routers."""
from __future__ import annotations
import re
from typing import Final
LEASES_REGEX: Final[re.Pattern] = re.compile(
r"(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})"
+ r"\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))"
+ r"\svalid\sfor:\s(?P<timevalid>(-?\d+))"
+ r"\ssec"
)

View file

@ -1,30 +1,28 @@
"""Support for Actiontec MI424WR (Verizon FIOS) routers.""" """Support for Actiontec MI424WR (Verizon FIOS) routers."""
from collections import namedtuple from __future__ import annotations
import logging import logging
import re
import telnetlib import telnetlib
from typing import Final
import voluptuous as vol import voluptuous as vol
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
DOMAIN, DOMAIN,
PLATFORM_SCHEMA, PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA,
DeviceScanner, DeviceScanner,
) )
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__) from .const import LEASES_REGEX
from .model import Device
_LEASES_REGEX = re.compile( _LOGGER: Final = logging.getLogger(__name__)
r"(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})"
+ r"\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))"
+ r"\svalid\sfor:\s(?P<timevalid>(-?\d+))"
+ r"\ssec"
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend(
{ {
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
@ -33,43 +31,40 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
) )
def get_scanner(hass, config): def get_scanner(
hass: HomeAssistant, config: ConfigType
) -> ActiontecDeviceScanner | None:
"""Validate the configuration and return an Actiontec scanner.""" """Validate the configuration and return an Actiontec scanner."""
scanner = ActiontecDeviceScanner(config[DOMAIN]) scanner = ActiontecDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
Device = namedtuple("Device", ["mac", "ip", "last_update"])
class ActiontecDeviceScanner(DeviceScanner): class ActiontecDeviceScanner(DeviceScanner):
"""This class queries an actiontec router for connected devices.""" """This class queries an actiontec router for connected devices."""
def __init__(self, config): def __init__(self, config: ConfigType) -> None:
"""Initialize the scanner.""" """Initialize the scanner."""
self.host = config[CONF_HOST] self.host: str = config[CONF_HOST]
self.username = config[CONF_USERNAME] self.username: str = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD] self.password: str = config[CONF_PASSWORD]
self.last_results = [] self.last_results: list[Device] = []
data = self.get_actiontec_data() data = self.get_actiontec_data()
self.success_init = data is not None self.success_init = data is not None
_LOGGER.info("Scanner initialized") _LOGGER.info("Scanner initialized")
def scan_devices(self): def scan_devices(self) -> list[str]:
"""Scan for new devices and return a list with found device IDs.""" """Scan for new devices and return a list with found device IDs."""
self._update_info() self._update_info()
return [client.mac for client in self.last_results] return [client.mac_address for client in self.last_results]
def get_device_name(self, device): def get_device_name(self, device: str) -> str | None: # type: ignore[override]
"""Return the name of the given device or None if we don't know.""" """Return the name of the given device or None if we don't know."""
if not self.last_results:
return None
for client in self.last_results: for client in self.last_results:
if client.mac == device: if client.mac_address == device:
return client.ip return client.ip_address
return None return None
def _update_info(self): def _update_info(self) -> bool:
"""Ensure the information from the router is up to date. """Ensure the information from the router is up to date.
Return boolean if scanning successful. Return boolean if scanning successful.
@ -78,19 +73,16 @@ class ActiontecDeviceScanner(DeviceScanner):
if not self.success_init: if not self.success_init:
return False return False
now = dt_util.now()
actiontec_data = self.get_actiontec_data() actiontec_data = self.get_actiontec_data()
if not actiontec_data: if actiontec_data is None:
return False return False
self.last_results = [ self.last_results = [
Device(data["mac"], name, now) device for device in actiontec_data if device.timevalid > -60
for name, data in actiontec_data.items()
if data["timevalid"] > -60
] ]
_LOGGER.info("Scan successful") _LOGGER.info("Scan successful")
return True return True
def get_actiontec_data(self): def get_actiontec_data(self) -> list[Device] | None:
"""Retrieve data from Actiontec MI424WR and return parsed result.""" """Retrieve data from Actiontec MI424WR and return parsed result."""
try: try:
telnet = telnetlib.Telnet(self.host) telnet = telnetlib.Telnet(self.host)
@ -106,18 +98,20 @@ class ActiontecDeviceScanner(DeviceScanner):
telnet.write(b"exit\n") telnet.write(b"exit\n")
except EOFError: except EOFError:
_LOGGER.exception("Unexpected response from router") _LOGGER.exception("Unexpected response from router")
return return None
except ConnectionRefusedError: except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router. Telnet enabled?") _LOGGER.exception("Connection refused by router. Telnet enabled?")
return None return None
devices = {} devices: list[Device] = []
for lease in leases_result: for lease in leases_result:
match = _LEASES_REGEX.search(lease.decode("utf-8")) match = LEASES_REGEX.search(lease.decode("utf-8"))
if match is not None: if match is not None:
devices[match.group("ip")] = { devices.append(
"ip": match.group("ip"), Device(
"mac": match.group("mac").upper(), match.group("ip"),
"timevalid": int(match.group("timevalid")), match.group("mac").upper(),
} int(match.group("timevalid")),
)
)
return devices return devices

View file

@ -0,0 +1,11 @@
"""Model definitions for Actiontec MI424WR (Verizon FIOS) routers."""
from dataclasses import dataclass
@dataclass
class Device:
"""Actiontec device class."""
ip_address: str
mac_address: str
timevalid: int

View file

@ -55,6 +55,17 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.actiontec.*]
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.aftership.*] [mypy-homeassistant.components.aftership.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true