Add system diagnostic sensors to SFR Box (#85184)

* Add system diagnostic sensor

* Add tests
This commit is contained in:
epenet 2023-01-12 09:29:12 +01:00 committed by GitHub
parent b0d4b73874
commit 679e971131
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 119 additions and 26 deletions

View File

@ -1,6 +1,8 @@
"""SFR Box sensor platform.""" """SFR Box sensor platform."""
from collections.abc import Callable from collections.abc import Callable, Iterable
from dataclasses import dataclass from dataclasses import dataclass
from itertools import chain
from typing import Generic, TypeVar
from sfrbox_api.models import DslInfo, SystemInfo from sfrbox_api.models import DslInfo, SystemInfo
@ -11,7 +13,12 @@ from homeassistant.components.sensor import (
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import SIGNAL_STRENGTH_DECIBELS, UnitOfDataRate from homeassistant.const import (
SIGNAL_STRENGTH_DECIBELS,
UnitOfDataRate,
UnitOfElectricPotential,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -22,42 +29,44 @@ from .const import DOMAIN
from .coordinator import SFRDataUpdateCoordinator from .coordinator import SFRDataUpdateCoordinator
from .models import DomainData from .models import DomainData
_T = TypeVar("_T")
@dataclass @dataclass
class SFRBoxSensorMixin: class SFRBoxSensorMixin(Generic[_T]):
"""Mixin for SFR Box sensors.""" """Mixin for SFR Box sensors."""
value_fn: Callable[[DslInfo], StateType] value_fn: Callable[[_T], StateType]
@dataclass @dataclass
class SFRBoxSensorEntityDescription(SensorEntityDescription, SFRBoxSensorMixin): class SFRBoxSensorEntityDescription(SensorEntityDescription, SFRBoxSensorMixin[_T]):
"""Description for SFR Box sensors.""" """Description for SFR Box sensors."""
SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( DSL_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[DslInfo], ...] = (
SFRBoxSensorEntityDescription( SFRBoxSensorEntityDescription[DslInfo](
key="linemode", key="linemode",
name="Line mode", name="Line mode",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=lambda x: x.linemode, value_fn=lambda x: x.linemode,
), ),
SFRBoxSensorEntityDescription( SFRBoxSensorEntityDescription[DslInfo](
key="counter", key="counter",
name="Counter", name="Counter",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=lambda x: x.counter, value_fn=lambda x: x.counter,
), ),
SFRBoxSensorEntityDescription( SFRBoxSensorEntityDescription[DslInfo](
key="crc", key="crc",
name="CRC", name="CRC",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=lambda x: x.crc, value_fn=lambda x: x.crc,
), ),
SFRBoxSensorEntityDescription( SFRBoxSensorEntityDescription[DslInfo](
key="noise_down", key="noise_down",
name="Noise down", name="Noise down",
device_class=SensorDeviceClass.SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH,
@ -67,7 +76,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda x: x.noise_down, value_fn=lambda x: x.noise_down,
), ),
SFRBoxSensorEntityDescription( SFRBoxSensorEntityDescription[DslInfo](
key="noise_up", key="noise_up",
name="Noise up", name="Noise up",
device_class=SensorDeviceClass.SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH,
@ -77,7 +86,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda x: x.noise_up, value_fn=lambda x: x.noise_up,
), ),
SFRBoxSensorEntityDescription( SFRBoxSensorEntityDescription[DslInfo](
key="attenuation_down", key="attenuation_down",
name="Attenuation down", name="Attenuation down",
device_class=SensorDeviceClass.SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH,
@ -87,7 +96,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda x: x.attenuation_down, value_fn=lambda x: x.attenuation_down,
), ),
SFRBoxSensorEntityDescription( SFRBoxSensorEntityDescription[DslInfo](
key="attenuation_up", key="attenuation_up",
name="Attenuation up", name="Attenuation up",
device_class=SensorDeviceClass.SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH,
@ -97,7 +106,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda x: x.attenuation_up, value_fn=lambda x: x.attenuation_up,
), ),
SFRBoxSensorEntityDescription( SFRBoxSensorEntityDescription[DslInfo](
key="rate_down", key="rate_down",
name="Rate down", name="Rate down",
device_class=SensorDeviceClass.DATA_RATE, device_class=SensorDeviceClass.DATA_RATE,
@ -105,7 +114,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda x: x.rate_down, value_fn=lambda x: x.rate_down,
), ),
SFRBoxSensorEntityDescription( SFRBoxSensorEntityDescription[DslInfo](
key="rate_up", key="rate_up",
name="Rate up", name="Rate up",
device_class=SensorDeviceClass.DATA_RATE, device_class=SensorDeviceClass.DATA_RATE,
@ -113,7 +122,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda x: x.rate_up, value_fn=lambda x: x.rate_up,
), ),
SFRBoxSensorEntityDescription( SFRBoxSensorEntityDescription[DslInfo](
key="line_status", key="line_status",
name="Line status", name="Line status",
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
@ -130,7 +139,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = (
translation_key="line_status", translation_key="line_status",
value_fn=lambda x: x.line_status.lower().replace(" ", "_"), value_fn=lambda x: x.line_status.lower().replace(" ", "_"),
), ),
SFRBoxSensorEntityDescription( SFRBoxSensorEntityDescription[DslInfo](
key="training", key="training",
name="Training", name="Training",
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
@ -152,6 +161,40 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = (
value_fn=lambda x: x.training.lower().replace(" ", "_").replace(".", "_"), value_fn=lambda x: x.training.lower().replace(" ", "_").replace(".", "_"),
), ),
) )
SYSTEM_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[SystemInfo], ...] = (
SFRBoxSensorEntityDescription[SystemInfo](
key="net_infra",
name="Network infrastructure",
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
options=[
"adsl",
"ftth",
"gprs",
],
translation_key="net_infra",
value_fn=lambda x: x.net_infra,
),
SFRBoxSensorEntityDescription[SystemInfo](
key="alimvoltage",
name="Voltage",
device_class=SensorDeviceClass.VOLTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
value_fn=lambda x: x.alimvoltage,
),
SFRBoxSensorEntityDescription[SystemInfo](
key="temperature",
name="Temperature",
device_class=SensorDeviceClass.TEMPERATURE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda x: x.temperature / 1000,
),
)
async def async_setup_entry( async def async_setup_entry(
@ -160,29 +203,38 @@ async def async_setup_entry(
"""Set up the sensors.""" """Set up the sensors."""
data: DomainData = hass.data[DOMAIN][entry.entry_id] data: DomainData = hass.data[DOMAIN][entry.entry_id]
entities = [ entities: Iterable[SFRBoxSensor] = chain(
SFRBoxSensor(data.dsl, description, data.system.data) (
for description in SENSOR_TYPES SFRBoxSensor(data.dsl, description, data.system.data)
] for description in DSL_SENSOR_TYPES
),
(
SFRBoxSensor(data.system, description, data.system.data)
for description in SYSTEM_SENSOR_TYPES
),
)
async_add_entities(entities) async_add_entities(entities)
class SFRBoxSensor(CoordinatorEntity[SFRDataUpdateCoordinator[DslInfo]], SensorEntity): class SFRBoxSensor(CoordinatorEntity[SFRDataUpdateCoordinator[_T]], SensorEntity):
"""SFR Box sensor.""" """SFR Box sensor."""
entity_description: SFRBoxSensorEntityDescription entity_description: SFRBoxSensorEntityDescription[_T]
_attr_has_entity_name = True _attr_has_entity_name = True
def __init__( def __init__(
self, self,
coordinator: SFRDataUpdateCoordinator[DslInfo], coordinator: SFRDataUpdateCoordinator[_T],
description: SFRBoxSensorEntityDescription, description: SFRBoxSensorEntityDescription,
system_info: SystemInfo, system_info: SystemInfo,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator) super().__init__(coordinator)
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{system_info.mac_addr}_dsl_{description.key}" self._attr_unique_id = (
f"{system_info.mac_addr}_{coordinator.name}_{description.key}"
)
self._attr_device_info = {"identifiers": {(DOMAIN, system_info.mac_addr)}} self._attr_device_info = {"identifiers": {(DOMAIN, system_info.mac_addr)}}
@property @property

View File

@ -26,6 +26,13 @@
"unknown": "Unknown" "unknown": "Unknown"
} }
}, },
"net_infra": {
"state": {
"adsl": "ADSL",
"ftth": "FTTH",
"gprs": "GPRS"
}
},
"training": { "training": {
"state": { "state": {
"idle": "Idle", "idle": "Idle",

View File

@ -27,6 +27,13 @@
"unknown": "Unknown" "unknown": "Unknown"
} }
}, },
"net_infra": {
"state": {
"adsl": "ADSL",
"ftth": "FTTH",
"gprs": "GPRS"
}
},
"training": { "training": {
"state": { "state": {
"g_922_channel_analysis": "G.922 Channel Analysis", "g_922_channel_analysis": "G.922 Channel Analysis",

View File

@ -1,6 +1,6 @@
"""Constants for SFR Box tests.""" """Constants for SFR Box tests."""
from homeassistant.components.select.const import ATTR_OPTIONS
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
ATTR_OPTIONS,
ATTR_STATE_CLASS, ATTR_STATE_CLASS,
SensorDeviceClass, SensorDeviceClass,
SensorStateClass, SensorStateClass,
@ -18,6 +18,8 @@ from homeassistant.const import (
SIGNAL_STRENGTH_DECIBELS, SIGNAL_STRENGTH_DECIBELS,
Platform, Platform,
UnitOfDataRate, UnitOfDataRate,
UnitOfElectricPotential,
UnitOfTemperature,
) )
ATTR_DEFAULT_DISABLED = "default_disabled" ATTR_DEFAULT_DISABLED = "default_disabled"
@ -37,6 +39,30 @@ EXPECTED_ENTITIES = {
ATTR_SW_VERSION: "NB6VAC-MAIN-R4.0.44k", ATTR_SW_VERSION: "NB6VAC-MAIN-R4.0.44k",
}, },
Platform.SENSOR: [ Platform.SENSOR: [
{
ATTR_DEFAULT_DISABLED: True,
ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM,
ATTR_ENTITY_ID: "sensor.sfr_box_network_infrastructure",
ATTR_OPTIONS: ["adsl", "ftth", "gprs"],
ATTR_STATE: "adsl",
ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_system_net_infra",
},
{
ATTR_DEFAULT_DISABLED: True,
ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
ATTR_ENTITY_ID: "sensor.sfr_box_temperature",
ATTR_STATE: "27.56",
ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_system_temperature",
ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS,
},
{
ATTR_DEFAULT_DISABLED: True,
ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE,
ATTR_ENTITY_ID: "sensor.sfr_box_voltage",
ATTR_STATE: "12251",
ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_system_alimvoltage",
ATTR_UNIT_OF_MEASUREMENT: UnitOfElectricPotential.MILLIVOLT,
},
{ {
ATTR_DEFAULT_DISABLED: True, ATTR_DEFAULT_DISABLED: True,
ATTR_ENTITY_ID: "sensor.sfr_box_line_mode", ATTR_ENTITY_ID: "sensor.sfr_box_line_mode",

View File

@ -33,6 +33,7 @@ def _check_and_enable_disabled_entities(
if expected_entity.get(ATTR_DEFAULT_DISABLED): if expected_entity.get(ATTR_DEFAULT_DISABLED):
entity_id = expected_entity[ATTR_ENTITY_ID] entity_id = expected_entity[ATTR_ENTITY_ID]
registry_entry = entity_registry.entities.get(entity_id) registry_entry = entity_registry.entities.get(entity_id)
assert registry_entry, f"Registry entry not found for {entity_id}"
assert registry_entry.disabled assert registry_entry.disabled
assert registry_entry.disabled_by is RegistryEntryDisabler.INTEGRATION assert registry_entry.disabled_by is RegistryEntryDisabler.INTEGRATION
entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) entity_registry.async_update_entity(entity_id, **{"disabled_by": None})