1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-19 23:10:15 +01:00
Files
2026-04-30 21:14:48 +02:00

333 lines
12 KiB
Python

"""Support for Fumis sensor entities."""
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Any
from fumis import FumisInfo, StoveAlert, StoveError, StoveState, StoveStatus
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfPower,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.dt import utcnow
from homeassistant.util.variance import ignore_variance
from .coordinator import FumisConfigEntry, FumisDataUpdateCoordinator
from .entity import FumisEntity
PARALLEL_UPDATES = 0
def _code_to_state(code: StoveAlert | StoveError | None) -> str | None:
"""Convert a stove alert or error code to a sensor state value.
Returns "none" when there is no active alert/error, None when the code
is unknown, or the enum member name in lowercase for known codes.
"""
if code is None:
return "none"
if code.name == "UNKNOWN":
return None
return code.name.lower()
def _code_to_attr(code: StoveAlert | StoveError | None) -> dict[str, str | None]:
"""Convert a stove alert or error code to extra state attributes."""
if code is None or code.name == "UNKNOWN":
return {"code": None}
return {"code": code.value}
@dataclass(frozen=True, kw_only=True)
class FumisSensorEntityDescription(SensorEntityDescription):
"""Describes a Fumis sensor entity."""
attr_fn: Callable[[FumisInfo], dict[str, Any]] | None = None
has_fn: Callable[[FumisInfo], bool] = lambda _: True
value_fn: Callable[[FumisInfo], datetime | float | int | str | None]
SENSORS: tuple[FumisSensorEntityDescription, ...] = (
FumisSensorEntityDescription(
key="alert",
translation_key="alert",
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
options=[
"none",
*(
alert.name.lower()
for alert in StoveAlert
if alert != StoveAlert.UNKNOWN
),
],
value_fn=lambda data: _code_to_state(data.controller.stove_alert),
attr_fn=lambda data: _code_to_attr(data.controller.stove_alert),
),
FumisSensorEntityDescription(
key="combustion_chamber_temperature",
translation_key="combustion_chamber_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
has_fn=lambda data: data.controller.combustion_chamber_temperature is not None,
value_fn=lambda data: data.controller.combustion_chamber_temperature,
),
FumisSensorEntityDescription(
key="detailed_stove_status",
translation_key="detailed_stove_status",
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
options=[
status.name.lower()
for status in StoveStatus
if status != StoveStatus.UNKNOWN
],
value_fn=lambda data: (
None
if data.controller.stove_status is StoveStatus.UNKNOWN
else data.controller.stove_status.name.lower()
),
),
FumisSensorEntityDescription(
key="error",
translation_key="error",
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
options=[
"none",
*(
error.name.lower()
for error in StoveError
if error != StoveError.UNKNOWN
),
],
value_fn=lambda data: _code_to_state(data.controller.stove_error),
attr_fn=lambda data: _code_to_attr(data.controller.stove_error),
),
FumisSensorEntityDescription(
key="fan_1_speed",
translation_key="fan_1_speed",
native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
has_fn=lambda data: data.controller.fan1_speed is not None,
value_fn=lambda data: data.controller.fan1_speed,
),
FumisSensorEntityDescription(
key="fan_2_speed",
translation_key="fan_2_speed",
native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
has_fn=lambda data: data.controller.fan2_speed is not None,
value_fn=lambda data: data.controller.fan2_speed,
),
FumisSensorEntityDescription(
key="fuel_quantity",
translation_key="fuel_quantity",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
has_fn=lambda data: (
len(data.controller.fuels) > 0
and data.controller.fuels[0].quantity_percentage is not None
),
value_fn=lambda data: (
data.controller.fuels[0].quantity_percentage
if data.controller.fuels
else None
),
),
FumisSensorEntityDescription(
key="fuel_used",
translation_key="fuel_used",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.controller.statistic.fuel_quantity_used,
),
FumisSensorEntityDescription(
key="heating_time",
translation_key="heating_time",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_unit_of_measurement=UnitOfTime.HOURS,
value_fn=lambda data: data.controller.statistic.heating_time.total_seconds(),
),
FumisSensorEntityDescription(
key="igniter_starts",
translation_key="igniter_starts",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.controller.statistic.igniter_starts,
),
FumisSensorEntityDescription(
key="misfires",
translation_key="misfires",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.controller.statistic.misfires,
),
FumisSensorEntityDescription(
key="module_temperature",
translation_key="module_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
has_fn=lambda data: data.unit.temperature is not None,
value_fn=lambda data: data.unit.temperature,
),
FumisSensorEntityDescription(
key="overheatings",
translation_key="overheatings",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.controller.statistic.overheatings,
),
FumisSensorEntityDescription(
key="power_output",
translation_key="power_output",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=1,
value_fn=lambda data: data.controller.power.kw,
),
FumisSensorEntityDescription(
key="pressure",
translation_key="pressure",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
has_fn=lambda data: data.controller.pressure is not None,
value_fn=lambda data: data.controller.pressure,
),
FumisSensorEntityDescription(
key="stove_status",
translation_key="stove_status",
device_class=SensorDeviceClass.ENUM,
options=[state.value for state in StoveState if state != StoveState.UNKNOWN],
value_fn=lambda data: (
None
if data.controller.state is StoveState.UNKNOWN
else data.controller.state.value
),
),
FumisSensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
has_fn=lambda data: data.controller.main_temperature is not None,
value_fn=lambda data: (
data.controller.main_temperature.actual
if data.controller.main_temperature
else None
),
),
FumisSensorEntityDescription(
key="time_to_service",
translation_key="time_to_service",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.HOURS,
entity_category=EntityCategory.DIAGNOSTIC,
has_fn=lambda data: data.controller.time_to_service is not None,
value_fn=lambda data: data.controller.time_to_service,
),
FumisSensorEntityDescription(
key="uptime",
translation_key="uptime",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=ignore_variance(
lambda data: (
utcnow().replace(microsecond=0) - data.controller.statistic.uptime
),
timedelta(minutes=5),
),
),
FumisSensorEntityDescription(
key="wifi_rssi",
translation_key="wifi_rssi",
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda data: data.unit.rssi,
),
FumisSensorEntityDescription(
key="wifi_signal_strength",
translation_key="wifi_signal_strength",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.unit.signal_strength,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: FumisConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Fumis sensor entities based on a config entry."""
coordinator = entry.runtime_data
async_add_entities(
FumisSensorEntity(coordinator=coordinator, description=description)
for description in SENSORS
if description.has_fn(coordinator.data)
)
class FumisSensorEntity(FumisEntity, SensorEntity):
"""Defines a Fumis sensor entity."""
entity_description: FumisSensorEntityDescription
def __init__(
self,
coordinator: FumisDataUpdateCoordinator,
description: FumisSensorEntityDescription,
) -> None:
"""Initialize the Fumis sensor entity."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{description.key}"
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return additional state attributes."""
if self.entity_description.attr_fn is None:
return None
return self.entity_description.attr_fn(self.coordinator.data)
@property
def native_value(self) -> datetime | float | int | str | None:
"""Return the sensor value."""
return self.entity_description.value_fn(self.coordinator.data)