diff --git a/.coveragerc b/.coveragerc index 460d3d2bc76..6f6fa152fa7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -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 diff --git a/.strict-typing b/.strict-typing index 389ff5261f8..7abbfbf7051 100644 --- a/.strict-typing +++ b/.strict-typing @@ -4,6 +4,7 @@ homeassistant.components homeassistant.components.acer_projector.* +homeassistant.components.actiontec.* homeassistant.components.aftership.* homeassistant.components.airly.* homeassistant.components.aladdin_connect.* diff --git a/homeassistant/components/actiontec/const.py b/homeassistant/components/actiontec/const.py new file mode 100644 index 00000000000..1043bd1bdb6 --- /dev/null +++ b/homeassistant/components/actiontec/const.py @@ -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([0-9]{1,3}[\.]){3}[0-9]{1,3})" + + r"\smac:\s(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))" + + r"\svalid\sfor:\s(?P(-?\d+))" + + r"\ssec" +) diff --git a/homeassistant/components/actiontec/device_tracker.py b/homeassistant/components/actiontec/device_tracker.py index c88ed546b9d..3783ad881e2 100644 --- a/homeassistant/components/actiontec/device_tracker.py +++ b/homeassistant/components/actiontec/device_tracker.py @@ -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([0-9]{1,3}[\.]){3}[0-9]{1,3})" - + r"\smac:\s(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))" - + r"\svalid\sfor:\s(?P(-?\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 diff --git a/homeassistant/components/actiontec/model.py b/homeassistant/components/actiontec/model.py new file mode 100644 index 00000000000..ff28d6d4ac6 --- /dev/null +++ b/homeassistant/components/actiontec/model.py @@ -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 diff --git a/mypy.ini b/mypy.ini index 8dd42547fad..1506a06e839 100644 --- a/mypy.ini +++ b/mypy.ini @@ -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