Files

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