diff --git a/homeassistant/components/sfr_box/sensor.py b/homeassistant/components/sfr_box/sensor.py index 2da8cbe55ef..f14296e7253 100644 --- a/homeassistant/components/sfr_box/sensor.py +++ b/homeassistant/components/sfr_box/sensor.py @@ -1,6 +1,8 @@ """SFR Box sensor platform.""" -from collections.abc import Callable +from collections.abc import Callable, Iterable from dataclasses import dataclass +from itertools import chain +from typing import Generic, TypeVar from sfrbox_api.models import DslInfo, SystemInfo @@ -11,7 +13,12 @@ from homeassistant.components.sensor import ( SensorStateClass, ) 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.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -22,42 +29,44 @@ from .const import DOMAIN from .coordinator import SFRDataUpdateCoordinator from .models import DomainData +_T = TypeVar("_T") + @dataclass -class SFRBoxSensorMixin: +class SFRBoxSensorMixin(Generic[_T]): """Mixin for SFR Box sensors.""" - value_fn: Callable[[DslInfo], StateType] + value_fn: Callable[[_T], StateType] @dataclass -class SFRBoxSensorEntityDescription(SensorEntityDescription, SFRBoxSensorMixin): +class SFRBoxSensorEntityDescription(SensorEntityDescription, SFRBoxSensorMixin[_T]): """Description for SFR Box sensors.""" -SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( - SFRBoxSensorEntityDescription( +DSL_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[DslInfo], ...] = ( + SFRBoxSensorEntityDescription[DslInfo]( key="linemode", name="Line mode", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda x: x.linemode, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="counter", name="Counter", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda x: x.counter, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="crc", name="CRC", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda x: x.crc, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="noise_down", name="Noise down", device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -67,7 +76,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value_fn=lambda x: x.noise_down, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="noise_up", name="Noise up", device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -77,7 +86,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value_fn=lambda x: x.noise_up, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="attenuation_down", name="Attenuation down", device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -87,7 +96,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value_fn=lambda x: x.attenuation_down, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="attenuation_up", name="Attenuation up", device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -97,7 +106,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value_fn=lambda x: x.attenuation_up, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="rate_down", name="Rate down", device_class=SensorDeviceClass.DATA_RATE, @@ -105,7 +114,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value_fn=lambda x: x.rate_down, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="rate_up", name="Rate up", device_class=SensorDeviceClass.DATA_RATE, @@ -113,7 +122,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value_fn=lambda x: x.rate_up, ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="line_status", name="Line status", device_class=SensorDeviceClass.ENUM, @@ -130,7 +139,7 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( translation_key="line_status", value_fn=lambda x: x.line_status.lower().replace(" ", "_"), ), - SFRBoxSensorEntityDescription( + SFRBoxSensorEntityDescription[DslInfo]( key="training", name="Training", device_class=SensorDeviceClass.ENUM, @@ -152,6 +161,40 @@ SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription, ...] = ( 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( @@ -160,29 +203,38 @@ async def async_setup_entry( """Set up the sensors.""" data: DomainData = hass.data[DOMAIN][entry.entry_id] - entities = [ - SFRBoxSensor(data.dsl, description, data.system.data) - for description in SENSOR_TYPES - ] + entities: Iterable[SFRBoxSensor] = chain( + ( + 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) -class SFRBoxSensor(CoordinatorEntity[SFRDataUpdateCoordinator[DslInfo]], SensorEntity): +class SFRBoxSensor(CoordinatorEntity[SFRDataUpdateCoordinator[_T]], SensorEntity): """SFR Box sensor.""" - entity_description: SFRBoxSensorEntityDescription + entity_description: SFRBoxSensorEntityDescription[_T] _attr_has_entity_name = True def __init__( self, - coordinator: SFRDataUpdateCoordinator[DslInfo], + coordinator: SFRDataUpdateCoordinator[_T], description: SFRBoxSensorEntityDescription, system_info: SystemInfo, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) 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)}} @property diff --git a/homeassistant/components/sfr_box/strings.json b/homeassistant/components/sfr_box/strings.json index c9b9f62bb33..3cd7f42f725 100644 --- a/homeassistant/components/sfr_box/strings.json +++ b/homeassistant/components/sfr_box/strings.json @@ -26,6 +26,13 @@ "unknown": "Unknown" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS" + } + }, "training": { "state": { "idle": "Idle", diff --git a/homeassistant/components/sfr_box/translations/en.json b/homeassistant/components/sfr_box/translations/en.json index 646483c1501..6e10ba3b78c 100644 --- a/homeassistant/components/sfr_box/translations/en.json +++ b/homeassistant/components/sfr_box/translations/en.json @@ -27,6 +27,13 @@ "unknown": "Unknown" } }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS" + } + }, "training": { "state": { "g_922_channel_analysis": "G.922 Channel Analysis", diff --git a/tests/components/sfr_box/const.py b/tests/components/sfr_box/const.py index 6b121402e9f..9682e7002b5 100644 --- a/tests/components/sfr_box/const.py +++ b/tests/components/sfr_box/const.py @@ -1,6 +1,6 @@ """Constants for SFR Box tests.""" -from homeassistant.components.select.const import ATTR_OPTIONS from homeassistant.components.sensor import ( + ATTR_OPTIONS, ATTR_STATE_CLASS, SensorDeviceClass, SensorStateClass, @@ -18,6 +18,8 @@ from homeassistant.const import ( SIGNAL_STRENGTH_DECIBELS, Platform, UnitOfDataRate, + UnitOfElectricPotential, + UnitOfTemperature, ) ATTR_DEFAULT_DISABLED = "default_disabled" @@ -37,6 +39,30 @@ EXPECTED_ENTITIES = { ATTR_SW_VERSION: "NB6VAC-MAIN-R4.0.44k", }, 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_ENTITY_ID: "sensor.sfr_box_line_mode", diff --git a/tests/components/sfr_box/test_sensor.py b/tests/components/sfr_box/test_sensor.py index ceb2e38d692..2f6bc9bac84 100644 --- a/tests/components/sfr_box/test_sensor.py +++ b/tests/components/sfr_box/test_sensor.py @@ -33,6 +33,7 @@ def _check_and_enable_disabled_entities( if expected_entity.get(ATTR_DEFAULT_DISABLED): entity_id = expected_entity[ATTR_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_by is RegistryEntryDisabler.INTEGRATION entity_registry.async_update_entity(entity_id, **{"disabled_by": None})