1
0
mirror of https://github.com/home-assistant/core.git synced 2026-07-14 18:14:35 +01:00
Files
2026-07-07 16:41:27 +02:00

381 lines
14 KiB
Python

"""Sensor platform for the NeoPool integration."""
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from typing import Any, override
from neopool_modbus.capabilities import (
has_filtvalve,
has_heating_relay,
has_variable_speed_pump,
is_chlorine_module_present,
is_conductivity_module_present,
is_hydrolysis_present,
is_ionization_present,
is_ph_module_present,
is_redox_module_present,
is_temperature_active,
)
from neopool_modbus.decoders import (
FILTRATION_MODE_LABELS,
FILTRATION_SPEED_STATE_LABELS,
HIDRO_POLARITY_LABELS,
ION_POLARITY_LABELS,
PH_STATUS_ALARM_LABELS,
calculate_next_interval_time,
decode_hidro_polarity,
decode_ion_polarity,
decode_ph_alarm,
decode_ph_pump_status,
is_hydrolysis_in_percent,
ph_pump_options,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
EntityCategory,
UnitOfElectricPotential,
UnitOfRatio,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import NeoPoolConfigEntry
from .coordinator import NeoPoolCoordinator
from .entity import NeoPoolEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class NeoPoolSensorEntityDescription(SensorEntityDescription):
"""Describes a NeoPool sensor entity."""
supported_fn: Callable[[dict[str, Any]], bool] | None = None
value_fn: Callable[[dict[str, Any]], Any] | None = None
options_fn: Callable[[dict[str, Any]], list[str]] | None = None
unit_fn: Callable[[dict[str, Any]], str | None] | None = None
precision_fn: Callable[[dict[str, Any]], int | None] | None = None
SENSOR_DESCRIPTIONS: dict[str, NeoPoolSensorEntityDescription] = {
"MBF_ION_CURRENT": NeoPoolSensorEntityDescription(
key="MBF_ION_CURRENT",
translation_key="ion_current",
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
supported_fn=is_ionization_present,
),
"MBF_HIDRO_CURRENT": NeoPoolSensorEntityDescription(
key="MBF_HIDRO_CURRENT",
translation_key="hidro_current",
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
supported_fn=is_hydrolysis_present,
unit_fn=lambda data: (
UnitOfRatio.PERCENTAGE if is_hydrolysis_in_percent(data) else "g/h"
),
precision_fn=lambda data: 0 if is_hydrolysis_in_percent(data) else 1,
),
"MBF_MEASURE_PH": NeoPoolSensorEntityDescription(
key="MBF_MEASURE_PH",
device_class=SensorDeviceClass.PH,
state_class=SensorStateClass.MEASUREMENT,
supported_fn=is_ph_module_present,
),
"MBF_MEASURE_RX": NeoPoolSensorEntityDescription(
key="MBF_MEASURE_RX",
translation_key="measure_rx",
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
supported_fn=is_redox_module_present,
),
"MBF_MEASURE_CL": NeoPoolSensorEntityDescription(
key="MBF_MEASURE_CL",
translation_key="measure_cl",
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
supported_fn=is_chlorine_module_present,
),
"MBF_MEASURE_CONDUCTIVITY": NeoPoolSensorEntityDescription(
key="MBF_MEASURE_CONDUCTIVITY",
translation_key="measure_conductivity",
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
supported_fn=is_conductivity_module_present,
),
"MBF_MEASURE_TEMPERATURE": NeoPoolSensorEntityDescription(
key="MBF_MEASURE_TEMPERATURE",
translation_key="measure_temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
supported_fn=is_temperature_active,
),
"MBF_HIDRO_VOLTAGE": NeoPoolSensorEntityDescription(
key="MBF_HIDRO_VOLTAGE",
translation_key="hidro_voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=1,
entity_registry_enabled_default=False,
supported_fn=is_hydrolysis_present,
),
"MBF_PAR_FILT_MODE": NeoPoolSensorEntityDescription(
key="MBF_PAR_FILT_MODE",
translation_key="filt_mode",
device_class=SensorDeviceClass.ENUM,
options=list(FILTRATION_MODE_LABELS.values()),
value_fn=lambda data: data.get("filtration_mode"),
),
"MBF_PH_STATUS_ALARM": NeoPoolSensorEntityDescription(
key="MBF_PH_STATUS_ALARM",
translation_key="ph_status_alarm",
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
options=list(PH_STATUS_ALARM_LABELS.values()),
value_fn=decode_ph_alarm,
supported_fn=is_ph_module_present,
),
"HIDRO_POLARITY": NeoPoolSensorEntityDescription(
key="HIDRO_POLARITY",
translation_key="hidro_polarity",
device_class=SensorDeviceClass.ENUM,
options=list(HIDRO_POLARITY_LABELS),
value_fn=decode_hidro_polarity,
supported_fn=is_hydrolysis_present,
),
"ION_POLARITY": NeoPoolSensorEntityDescription(
key="ION_POLARITY",
translation_key="ion_polarity",
device_class=SensorDeviceClass.ENUM,
options=list(ION_POLARITY_LABELS),
value_fn=decode_ion_polarity,
supported_fn=is_ionization_present,
),
"PH_PUMP_STATUS": NeoPoolSensorEntityDescription(
key="PH_PUMP_STATUS",
translation_key="ph_pump_status",
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
options_fn=ph_pump_options,
value_fn=decode_ph_pump_status,
supported_fn=is_ph_module_present,
),
"FILTRATION_SPEED": NeoPoolSensorEntityDescription(
key="FILTRATION_SPEED",
translation_key="filtration_speed",
device_class=SensorDeviceClass.ENUM,
options=list(FILTRATION_SPEED_STATE_LABELS),
value_fn=lambda data: data.get("filtration_speed_state"),
supported_fn=has_variable_speed_pump,
),
"MBF_PAR_INTELLIGENT_INTERVALS": NeoPoolSensorEntityDescription(
key="MBF_PAR_INTELLIGENT_INTERVALS",
translation_key="intelligent_intervals",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
supported_fn=lambda data: (
has_heating_relay(data) and is_temperature_active(data)
),
),
"MBF_PAR_INTELLIGENT_TT_NEXT_INTERVAL": NeoPoolSensorEntityDescription(
key="MBF_PAR_INTELLIGENT_TT_NEXT_INTERVAL",
translation_key="intelligent_tt_next_interval",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: calculate_next_interval_time(
data.get("MBF_PAR_INTELLIGENT_TT_NEXT_INTERVAL")
),
supported_fn=lambda data: (
has_heating_relay(data) and is_temperature_active(data)
),
),
"MBF_PAR_FILTVALVE_REMAINING": NeoPoolSensorEntityDescription(
key="MBF_PAR_FILTVALVE_REMAINING",
translation_key="filtvalve_remaining",
native_unit_of_measurement=UnitOfTime.SECONDS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
supported_fn=has_filtvalve,
),
"CELL_RUNTIME_TOTAL": NeoPoolSensorEntityDescription(
key="CELL_RUNTIME_TOTAL",
translation_key="cell_runtime_total",
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=0,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=is_hydrolysis_present,
),
"CELL_RUNTIME_PART": NeoPoolSensorEntityDescription(
key="CELL_RUNTIME_PART",
translation_key="cell_runtime_part",
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=0,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=is_hydrolysis_present,
),
"CELL_RUNTIME_POLA": NeoPoolSensorEntityDescription(
key="CELL_RUNTIME_POLA",
translation_key="cell_runtime_pola",
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=0,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=is_hydrolysis_present,
),
"CELL_RUNTIME_POLB": NeoPoolSensorEntityDescription(
key="CELL_RUNTIME_POLB",
translation_key="cell_runtime_polb",
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=0,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=is_hydrolysis_present,
),
"CELL_RUNTIME_POL_CHANGES": NeoPoolSensorEntityDescription(
key="CELL_RUNTIME_POL_CHANGES",
translation_key="cell_runtime_pol_changes",
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=0,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=is_hydrolysis_present,
),
}
async def async_setup_entry(
hass: HomeAssistant,
entry: NeoPoolConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up NeoPool sensors from a config entry."""
coordinator = entry.runtime_data
async_add_entities(
NeoPoolSensor(coordinator, key, desc)
for key, desc in SENSOR_DESCRIPTIONS.items()
if desc.supported_fn is None or desc.supported_fn(coordinator.data)
)
_PRODUCTION_KEYS_REQUIRING_FILTRATION = frozenset(
{
"MBF_HIDRO_CURRENT",
"MBF_HIDRO_VOLTAGE",
"MBF_ION_CURRENT",
}
)
_MEASURE_KEYS_REQUIRING_FILTRATION = frozenset(
{
"MBF_MEASURE_TEMPERATURE",
"MBF_MEASURE_PH",
"MBF_MEASURE_RX",
"MBF_MEASURE_CL",
"MBF_MEASURE_CONDUCTIVITY",
}
)
class NeoPoolSensor(NeoPoolEntity, SensorEntity):
"""Representation of a NeoPool sensor."""
entity_description: NeoPoolSensorEntityDescription
def __init__(
self,
coordinator: NeoPoolCoordinator,
key: str,
description: NeoPoolSensorEntityDescription,
) -> None:
"""Initialize the NeoPool sensor entity."""
super().__init__(coordinator)
self.entity_description = description
self._key = key
self._attr_unique_id = (
f"{self.coordinator.config_entry.unique_id}_{key.lower()}"
)
@property
@override
def suggested_display_precision(self) -> int | None:
"""Return the suggested display precision for the sensor value."""
if (precision_fn := self.entity_description.precision_fn) is not None:
return precision_fn(self.coordinator.data)
return super().suggested_display_precision
@property
@override
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement for the sensor value."""
if (unit_fn := self.entity_description.unit_fn) is not None:
return unit_fn(self.coordinator.data)
return super().native_unit_of_measurement
def _filtration_off(self) -> bool:
"""Return True when the filtration pump is off."""
return self.coordinator.data.get("Filtration Pump") is False
def _is_measurement_suppressed(self) -> bool:
"""Return True if a measurement sensor should report None."""
if self._key not in _MEASURE_KEYS_REQUIRING_FILTRATION:
return False
return self._filtration_off()
def _is_production_suppressed(self) -> bool:
"""Return True if a production sensor should report 0."""
if self._key not in _PRODUCTION_KEYS_REQUIRING_FILTRATION:
return False
return self._filtration_off()
@property
@override
def native_value(self) -> float | int | str | datetime | None:
"""Return the actual sensor value from coordinator data."""
if self._is_measurement_suppressed():
return None
if self._is_production_suppressed():
return 0
if (value_fn := self.entity_description.value_fn) is not None:
value: float | int | str | datetime | None = value_fn(self.coordinator.data)
return value
return self.coordinator.data.get(self._key)
@property
@override
def options(self) -> list[str] | None:
"""Return the list of options for the sensor."""
if (options_fn := self.entity_description.options_fn) is not None:
return options_fn(self.coordinator.data)
return super().options