From 55c96ae86f7743c9912e0119af3b8ef95566ac2f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 4 May 2021 05:11:21 +0200 Subject: [PATCH] Create Fritz device and connectivity sensor (#49699) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + homeassistant/components/fritz/__init__.py | 3 +- .../components/fritz/binary_sensor.py | 87 +++++++++++++++++++ homeassistant/components/fritz/common.py | 73 ++++++++-------- homeassistant/components/fritz/config_flow.py | 6 +- homeassistant/components/fritz/const.py | 2 +- .../components/fritz/device_tracker.py | 9 +- tests/components/fritz/__init__.py | 2 +- 8 files changed, 138 insertions(+), 45 deletions(-) create mode 100644 homeassistant/components/fritz/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index deadd4e0b19..d692794c6e4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -330,6 +330,7 @@ omit = homeassistant/components/freebox/sensor.py homeassistant/components/freebox/switch.py homeassistant/components/fritz/__init__.py + homeassistant/components/fritz/binary_sensor.py homeassistant/components/fritz/common.py homeassistant/components/fritz/const.py homeassistant/components/fritz/device_tracker.py diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index afa3229c585..5cbaa23c1b5 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -13,7 +13,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.typing import ConfigType from .common import FritzBoxTools, FritzData from .const import DATA_FRITZ, DOMAIN, PLATFORMS @@ -59,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload FRITZ!Box Tools config entry.""" fritzbox: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] fritzbox.async_unload() diff --git a/homeassistant/components/fritz/binary_sensor.py b/homeassistant/components/fritz/binary_sensor.py new file mode 100644 index 00000000000..493f1bc0d42 --- /dev/null +++ b/homeassistant/components/fritz/binary_sensor.py @@ -0,0 +1,87 @@ +"""AVM FRITZ!Box connectivitiy sensor.""" +import logging + +from fritzconnection.core.exceptions import FritzConnectionException + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .common import FritzBoxBaseEntity, FritzBoxTools +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +) -> None: + """Set up entry.""" + _LOGGER.debug("Setting up FRITZ!Box binary sensors") + fritzbox_tools = hass.data[DOMAIN][entry.entry_id] + + if "WANIPConn1" in fritzbox_tools.connection.services: + # Only routers are supported at the moment + async_add_entities( + [FritzBoxConnectivitySensor(fritzbox_tools, entry.title)], True + ) + + +class FritzBoxConnectivitySensor(FritzBoxBaseEntity, BinarySensorEntity): + """Define FRITZ!Box connectivity class.""" + + def __init__(self, fritzbox_tools: FritzBoxTools, device_friendlyname: str) -> None: + """Init FRITZ!Box connectivity class.""" + self._unique_id = f"{fritzbox_tools.unique_id}-connectivity" + self._name = f"{device_friendlyname} Connectivity" + self._is_on = True + self._is_available = True + super().__init__(fritzbox_tools, device_friendlyname) + + @property + def name(self): + """Return name.""" + return self._name + + @property + def device_class(self): + """Return device class.""" + return DEVICE_CLASS_CONNECTIVITY + + @property + def is_on(self) -> bool: + """Return status.""" + return self._is_on + + @property + def unique_id(self): + """Return unique id.""" + return self._unique_id + + @property + def available(self) -> bool: + """Return availability.""" + return self._is_available + + def update(self) -> None: + """Update data.""" + _LOGGER.debug("Updating FRITZ!Box binary sensors") + self._is_on = True + try: + if "WANCommonInterfaceConfig1" in self._fritzbox_tools.connection.services: + link_props = self._fritzbox_tools.connection.call_action( + "WANCommonInterfaceConfig1", "GetCommonLinkProperties" + ) + is_up = link_props["NewPhysicalLinkStatus"] + self._is_on = is_up == "Up" + else: + self._is_on = self._fritzbox_tools.fritzstatus.is_connected + + self._is_available = True + + except FritzConnectionException: + _LOGGER.error("Error getting the state from the FRITZ!Box", exc_info=True) + self._is_available = False diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 6a6f0b4a7d9..3a6bf132fb6 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -12,6 +12,7 @@ from fritzconnection.lib.fritzhosts import FritzHosts from fritzconnection.lib.fritzstatus import FritzStatus from homeassistant.core import callback +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt as dt_util @@ -49,7 +50,6 @@ class FritzBoxTools: ): """Initialize FritzboxTools class.""" self._cancel_scan = None - self._device_info = None self._devices: dict[str, Any] = {} self._unique_id = None self.connection = None @@ -60,6 +60,9 @@ class FritzBoxTools: self.password = password self.port = port self.username = username + self.mac = None + self.model = None + self.sw_version = None async def async_setup(self): """Wrap up FritzboxTools class setup.""" @@ -76,12 +79,13 @@ class FritzBoxTools: ) self.fritzstatus = FritzStatus(fc=self.connection) + info = self.connection.call_action("DeviceInfo:1", "GetInfo") if self._unique_id is None: - self._unique_id = self.connection.call_action("DeviceInfo:1", "GetInfo")[ - "NewSerialNumber" - ] + self._unique_id = info["NewSerialNumber"] - self._device_info = self._fetch_device_info() + self.model = info.get("NewModelName") + self.sw_version = info.get("NewSoftwareVersion") + self.mac = self.unique_id async def async_start(self): """Start FritzHosts connection.""" @@ -106,16 +110,6 @@ class FritzBoxTools: """Return unique id.""" return self._unique_id - @property - def fritzbox_model(self): - """Return model.""" - return self._device_info["model"].replace("FRITZ!Box ", "") - - @property - def device_info(self): - """Return device info.""" - return self._device_info - @property def devices(self) -> dict[str, Any]: """Return devices.""" @@ -163,33 +157,13 @@ class FritzBoxTools: if new_device: async_dispatcher_send(self.hass, self.signal_device_new) - def _fetch_device_info(self): - """Fetch device info.""" - info = self.connection.call_action("DeviceInfo:1", "GetInfo") - - dev_info = {} - dev_info["identifiers"] = { - # Serial numbers are unique identifiers within a specific domain - (DOMAIN, self.unique_id) - } - dev_info["manufacturer"] = "AVM" - - if dev_name := info.get("NewName"): - dev_info["name"] = dev_name - if dev_model := info.get("NewModelName"): - dev_info["model"] = dev_model - if dev_sw_ver := info.get("NewSoftwareVersion"): - dev_info["sw_version"] = dev_sw_ver - - return dev_info - class FritzData: """Storage class for platform global data.""" def __init__(self) -> None: """Initialize the data.""" - self.tracked = {} + self.tracked: dict = {} class FritzDevice: @@ -241,3 +215,30 @@ class FritzDevice: def last_activity(self): """Return device last activity.""" return self._last_activity + + +class FritzBoxBaseEntity: + """Fritz host entity base class.""" + + def __init__(self, fritzbox_tools: FritzBoxTools, device_name: str) -> None: + """Init device info class.""" + self._fritzbox_tools = fritzbox_tools + self._device_name = device_name + + @property + def mac_address(self) -> str: + """Return the mac address of the main device.""" + return self._fritzbox_tools.mac + + @property + def device_info(self): + """Return the device information.""" + + return { + "connections": {(CONNECTION_NETWORK_MAC, self.mac_address)}, + "identifiers": {(DOMAIN, self._fritzbox_tools.unique_id)}, + "name": self._device_name, + "manufacturer": "AVM", + "model": self._fritzbox_tools.model, + "sw_version": self._fritzbox_tools.sw_version, + } diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index a8afff6e41e..23e713f7966 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -1,4 +1,6 @@ """Config flow to configure the FRITZ!Box Tools integration.""" +from __future__ import annotations + import logging from urllib.parse import urlparse @@ -65,7 +67,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): return None - async def async_check_configured_entry(self) -> ConfigEntry: + async def async_check_configured_entry(self) -> ConfigEntry | None: """Check if entry is configured.""" for entry in self._async_current_entries(include_ignore=False): if entry.data[CONF_HOST] == self._host: @@ -170,7 +172,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): self._password = user_input[CONF_PASSWORD] if not (error := await self.fritz_tools_init()): - self._name = self.fritz_tools.device_info["model"] + self._name = self.fritz_tools.model if await self.async_check_configured_entry(): error = "already_configured" diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index 1a3b176deb7..fff55a276e1 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -2,7 +2,7 @@ DOMAIN = "fritz" -PLATFORMS = ["device_tracker"] +PLATFORMS = ["binary_sensor", "device_tracker"] DATA_FRITZ = "fritz_data" diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index 23657429f68..646a8cc986e 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -17,7 +17,6 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType from .common import FritzBoxTools @@ -116,7 +115,7 @@ class FritzBoxTracker(ScannerEntity): self._mac = device.mac_address self._name = device.hostname or DEFAULT_DEVICE_NAME self._active = False - self._attrs = {} + self._attrs: dict = {} @property def is_connected(self): @@ -154,7 +153,7 @@ class FritzBoxTracker(ScannerEntity): return SOURCE_TYPE_ROUTER @property - def device_info(self) -> DeviceInfo: + def device_info(self): """Return the device information.""" return { "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, @@ -162,6 +161,10 @@ class FritzBoxTracker(ScannerEntity): "name": self.name, "manufacturer": "AVM", "model": "FRITZ!Box Tracked device", + "via_device": ( + DOMAIN, + self._router.unique_id, + ), } @property diff --git a/tests/components/fritz/__init__.py b/tests/components/fritz/__init__.py index 5a9b6cb1652..27ec391c092 100644 --- a/tests/components/fritz/__init__.py +++ b/tests/components/fritz/__init__.py @@ -49,7 +49,7 @@ class FritzConnectionMock: # pylint: disable=too-few-public-methods "NewBytesReceived": 12045, }, ("DeviceInfo:1", "GetInfo"): { - "NewSerialNumber": 1234, + "NewSerialNumber": "abcdefgh", "NewName": "TheName", "NewModelName": "FRITZ!Box 7490", },