mirror of
https://github.com/home-assistant/core.git
synced 2026-07-14 18:14:35 +01:00
381 lines
14 KiB
Python
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
|