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:
parent
256a2de7ce
commit
edccb7eb58
6 changed files with 74 additions and 43 deletions
|
@ -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
|
||||||
|
|
|
@ -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.*
|
||||||
|
|
12
homeassistant/components/actiontec/const.py
Normal file
12
homeassistant/components/actiontec/const.py
Normal 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"
|
||||||
|
)
|
|
@ -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
|
||||||
|
|
11
homeassistant/components/actiontec/model.py
Normal file
11
homeassistant/components/actiontec/model.py
Normal 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
|
11
mypy.ini
11
mypy.ini
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue