Create Fritz device and connectivity sensor (#49699)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Simone Chemelli 2021-05-04 05:11:21 +02:00 committed by GitHub
parent 0df9454310
commit 55c96ae86f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 138 additions and 45 deletions

View File

@ -330,6 +330,7 @@ omit =
homeassistant/components/freebox/sensor.py homeassistant/components/freebox/sensor.py
homeassistant/components/freebox/switch.py homeassistant/components/freebox/switch.py
homeassistant/components/fritz/__init__.py homeassistant/components/fritz/__init__.py
homeassistant/components/fritz/binary_sensor.py
homeassistant/components/fritz/common.py homeassistant/components/fritz/common.py
homeassistant/components/fritz/const.py homeassistant/components/fritz/const.py
homeassistant/components/fritz/device_tracker.py homeassistant/components/fritz/device_tracker.py

View File

@ -13,7 +13,6 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.typing import ConfigType
from .common import FritzBoxTools, FritzData from .common import FritzBoxTools, FritzData
from .const import DATA_FRITZ, DOMAIN, PLATFORMS from .const import DATA_FRITZ, DOMAIN, PLATFORMS
@ -59,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True 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.""" """Unload FRITZ!Box Tools config entry."""
fritzbox: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] fritzbox: FritzBoxTools = hass.data[DOMAIN][entry.entry_id]
fritzbox.async_unload() fritzbox.async_unload()

View File

@ -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

View File

@ -12,6 +12,7 @@ from fritzconnection.lib.fritzhosts import FritzHosts
from fritzconnection.lib.fritzstatus import FritzStatus from fritzconnection.lib.fritzstatus import FritzStatus
from homeassistant.core import callback 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.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -49,7 +50,6 @@ class FritzBoxTools:
): ):
"""Initialize FritzboxTools class.""" """Initialize FritzboxTools class."""
self._cancel_scan = None self._cancel_scan = None
self._device_info = None
self._devices: dict[str, Any] = {} self._devices: dict[str, Any] = {}
self._unique_id = None self._unique_id = None
self.connection = None self.connection = None
@ -60,6 +60,9 @@ class FritzBoxTools:
self.password = password self.password = password
self.port = port self.port = port
self.username = username self.username = username
self.mac = None
self.model = None
self.sw_version = None
async def async_setup(self): async def async_setup(self):
"""Wrap up FritzboxTools class setup.""" """Wrap up FritzboxTools class setup."""
@ -76,12 +79,13 @@ class FritzBoxTools:
) )
self.fritzstatus = FritzStatus(fc=self.connection) self.fritzstatus = FritzStatus(fc=self.connection)
info = self.connection.call_action("DeviceInfo:1", "GetInfo")
if self._unique_id is None: if self._unique_id is None:
self._unique_id = self.connection.call_action("DeviceInfo:1", "GetInfo")[ self._unique_id = info["NewSerialNumber"]
"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): async def async_start(self):
"""Start FritzHosts connection.""" """Start FritzHosts connection."""
@ -106,16 +110,6 @@ class FritzBoxTools:
"""Return unique id.""" """Return unique id."""
return self._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 @property
def devices(self) -> dict[str, Any]: def devices(self) -> dict[str, Any]:
"""Return devices.""" """Return devices."""
@ -163,33 +157,13 @@ class FritzBoxTools:
if new_device: if new_device:
async_dispatcher_send(self.hass, self.signal_device_new) 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: class FritzData:
"""Storage class for platform global data.""" """Storage class for platform global data."""
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the data.""" """Initialize the data."""
self.tracked = {} self.tracked: dict = {}
class FritzDevice: class FritzDevice:
@ -241,3 +215,30 @@ class FritzDevice:
def last_activity(self): def last_activity(self):
"""Return device last activity.""" """Return device last activity."""
return self._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,
}

View File

@ -1,4 +1,6 @@
"""Config flow to configure the FRITZ!Box Tools integration.""" """Config flow to configure the FRITZ!Box Tools integration."""
from __future__ import annotations
import logging import logging
from urllib.parse import urlparse from urllib.parse import urlparse
@ -65,7 +67,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
return None return None
async def async_check_configured_entry(self) -> ConfigEntry: async def async_check_configured_entry(self) -> ConfigEntry | None:
"""Check if entry is configured.""" """Check if entry is configured."""
for entry in self._async_current_entries(include_ignore=False): for entry in self._async_current_entries(include_ignore=False):
if entry.data[CONF_HOST] == self._host: if entry.data[CONF_HOST] == self._host:
@ -170,7 +172,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
self._password = user_input[CONF_PASSWORD] self._password = user_input[CONF_PASSWORD]
if not (error := await self.fritz_tools_init()): 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(): if await self.async_check_configured_entry():
error = "already_configured" error = "already_configured"

View File

@ -2,7 +2,7 @@
DOMAIN = "fritz" DOMAIN = "fritz"
PLATFORMS = ["device_tracker"] PLATFORMS = ["binary_sensor", "device_tracker"]
DATA_FRITZ = "fritz_data" DATA_FRITZ = "fritz_data"

View File

@ -17,7 +17,6 @@ from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .common import FritzBoxTools from .common import FritzBoxTools
@ -116,7 +115,7 @@ class FritzBoxTracker(ScannerEntity):
self._mac = device.mac_address self._mac = device.mac_address
self._name = device.hostname or DEFAULT_DEVICE_NAME self._name = device.hostname or DEFAULT_DEVICE_NAME
self._active = False self._active = False
self._attrs = {} self._attrs: dict = {}
@property @property
def is_connected(self): def is_connected(self):
@ -154,7 +153,7 @@ class FritzBoxTracker(ScannerEntity):
return SOURCE_TYPE_ROUTER return SOURCE_TYPE_ROUTER
@property @property
def device_info(self) -> DeviceInfo: def device_info(self):
"""Return the device information.""" """Return the device information."""
return { return {
"connections": {(CONNECTION_NETWORK_MAC, self._mac)}, "connections": {(CONNECTION_NETWORK_MAC, self._mac)},
@ -162,6 +161,10 @@ class FritzBoxTracker(ScannerEntity):
"name": self.name, "name": self.name,
"manufacturer": "AVM", "manufacturer": "AVM",
"model": "FRITZ!Box Tracked device", "model": "FRITZ!Box Tracked device",
"via_device": (
DOMAIN,
self._router.unique_id,
),
} }
@property @property

View File

@ -49,7 +49,7 @@ class FritzConnectionMock: # pylint: disable=too-few-public-methods
"NewBytesReceived": 12045, "NewBytesReceived": 12045,
}, },
("DeviceInfo:1", "GetInfo"): { ("DeviceInfo:1", "GetInfo"): {
"NewSerialNumber": 1234, "NewSerialNumber": "abcdefgh",
"NewName": "TheName", "NewName": "TheName",
"NewModelName": "FRITZ!Box 7490", "NewModelName": "FRITZ!Box 7490",
}, },