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
homeassistant/components/acer_projector/*
homeassistant/components/actiontec/const.py
homeassistant/components/actiontec/device_tracker.py
homeassistant/components/actiontec/model.py
homeassistant/components/acmeda/__init__.py
homeassistant/components/acmeda/base.py
homeassistant/components/acmeda/const.py

View File

@ -4,6 +4,7 @@
homeassistant.components
homeassistant.components.acer_projector.*
homeassistant.components.actiontec.*
homeassistant.components.aftership.*
homeassistant.components.airly.*
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."""
from collections import namedtuple
from __future__ import annotations
import logging
import re
import telnetlib
from typing import Final
import voluptuous as vol
from homeassistant.components.device_tracker import (
DOMAIN,
PLATFORM_SCHEMA,
PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA,
DeviceScanner,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
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(
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"
)
_LOGGER: Final = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): 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."""
scanner = ActiontecDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
Device = namedtuple("Device", ["mac", "ip", "last_update"])
class ActiontecDeviceScanner(DeviceScanner):
"""This class queries an actiontec router for connected devices."""
def __init__(self, config):
def __init__(self, config: ConfigType) -> None:
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.last_results = []
self.host: str = config[CONF_HOST]
self.username: str = config[CONF_USERNAME]
self.password: str = config[CONF_PASSWORD]
self.last_results: list[Device] = []
data = self.get_actiontec_data()
self.success_init = data is not None
_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."""
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."""
if not self.last_results:
return None
for client in self.last_results:
if client.mac == device:
return client.ip
if client.mac_address == device:
return client.ip_address
return None
def _update_info(self):
def _update_info(self) -> bool:
"""Ensure the information from the router is up to date.
Return boolean if scanning successful.
@ -78,19 +73,16 @@ class ActiontecDeviceScanner(DeviceScanner):
if not self.success_init:
return False
now = dt_util.now()
actiontec_data = self.get_actiontec_data()
if not actiontec_data:
if actiontec_data is None:
return False
self.last_results = [
Device(data["mac"], name, now)
for name, data in actiontec_data.items()
if data["timevalid"] > -60
device for device in actiontec_data if device.timevalid > -60
]
_LOGGER.info("Scan successful")
return True
def get_actiontec_data(self):
def get_actiontec_data(self) -> list[Device] | None:
"""Retrieve data from Actiontec MI424WR and return parsed result."""
try:
telnet = telnetlib.Telnet(self.host)
@ -106,18 +98,20 @@ class ActiontecDeviceScanner(DeviceScanner):
telnet.write(b"exit\n")
except EOFError:
_LOGGER.exception("Unexpected response from router")
return
return None
except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router. Telnet enabled?")
return None
devices = {}
devices: list[Device] = []
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:
devices[match.group("ip")] = {
"ip": match.group("ip"),
"mac": match.group("mac").upper(),
"timevalid": int(match.group("timevalid")),
}
devices.append(
Device(
match.group("ip"),
match.group("mac").upper(),
int(match.group("timevalid")),
)
)
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_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.*]
check_untyped_defs = true
disallow_incomplete_defs = true