Files

391 lines
15 KiB
Python

"""Binary sensor platform for the NeoPool integration."""
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, override
from neopool_modbus.capabilities import (
has_heating_relay,
is_chlorine_module_present,
is_conductivity_module_present,
is_hydrolysis_present,
is_ionization_present,
is_ph_module_present,
is_redox_module_present,
)
from neopool_modbus.registers import is_valid_relay_gpio
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
CONF_USE_AUX1,
CONF_USE_AUX2,
CONF_USE_AUX3,
CONF_USE_AUX4,
CONF_USE_COVER_SENSOR,
CONF_USE_LIGHT,
)
from .coordinator import NeoPoolConfigEntry, NeoPoolCoordinator
from .entity import NeoPoolEntity
PARALLEL_UPDATES = 0
type _SupportedFn = Callable[[dict[str, Any]], bool]
@dataclass(frozen=True, kw_only=True)
class NeoPoolBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes a NeoPool binary sensor entity."""
supported_fn: _SupportedFn | None = None
value_fn: Callable[[dict[str, Any], HomeAssistant], bool | None] | None = None
def _gpio_ok(gpio_key: str) -> _SupportedFn:
"""Return a supported_fn that checks a relay GPIO key is valid."""
return lambda data: gpio_key not in data or is_valid_relay_gpio(data[gpio_key] or 0)
def _pool_cover_open(data: dict[str, Any], hass: HomeAssistant) -> bool | None:
"""Invert the raw cover state for the OPENING device class.
The cover bit is only valid while filtration runs; otherwise report unknown.
"""
if data.get("Filtration Pump") is not True:
return None
value = data.get("Pool Cover")
if value is None:
return None
return not bool(value)
BINARY_SENSOR_DESCRIPTIONS: dict[str, NeoPoolBinarySensorEntityDescription] = {
"pH Acid Pump": NeoPoolBinarySensorEntityDescription(
key="pH Acid Pump",
translation_key="ph_acid_pump",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
supported_fn=_gpio_ok("MBF_PAR_PH_ACID_RELAY_GPIO"),
),
"Filtration Pump": NeoPoolBinarySensorEntityDescription(
key="Filtration Pump",
translation_key="filtration_pump",
device_class=BinarySensorDeviceClass.RUNNING,
supported_fn=_gpio_ok("MBF_PAR_FILT_GPIO"),
),
"Pool Light": NeoPoolBinarySensorEntityDescription(
key="Pool Light",
translation_key="pool_light",
device_class=BinarySensorDeviceClass.LIGHT,
supported_fn=_gpio_ok("MBF_PAR_LIGHTING_GPIO"),
),
"AUX1": NeoPoolBinarySensorEntityDescription(
key="AUX1",
translation_key="aux",
translation_placeholders={"number": "1"},
device_class=BinarySensorDeviceClass.POWER,
),
"AUX2": NeoPoolBinarySensorEntityDescription(
key="AUX2",
translation_key="aux",
translation_placeholders={"number": "2"},
device_class=BinarySensorDeviceClass.POWER,
),
"AUX3": NeoPoolBinarySensorEntityDescription(
key="AUX3",
translation_key="aux",
translation_placeholders={"number": "3"},
device_class=BinarySensorDeviceClass.POWER,
),
"AUX4": NeoPoolBinarySensorEntityDescription(
key="AUX4",
translation_key="aux",
translation_placeholders={"number": "4"},
device_class=BinarySensorDeviceClass.POWER,
),
"pH module control status": NeoPoolBinarySensorEntityDescription(
key="pH module control status",
translation_key="ph_module_control_status",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=is_ph_module_present,
),
"pH control module": NeoPoolBinarySensorEntityDescription(
key="pH control module",
translation_key="ph_control_module",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=is_ph_module_present,
),
"pH measurement active": NeoPoolBinarySensorEntityDescription(
key="pH measurement active",
translation_key="ph_measurement_active",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=is_ph_module_present,
),
"Redox pump active": NeoPoolBinarySensorEntityDescription(
key="Redox pump active",
translation_key="redox_pump_active",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=lambda data: (
is_redox_module_present(data)
and (
"MBF_PAR_RX_RELAY_GPIO" not in data
or is_valid_relay_gpio(data["MBF_PAR_RX_RELAY_GPIO"] or 0)
)
),
),
"Redox control module": NeoPoolBinarySensorEntityDescription(
key="Redox control module",
translation_key="redox_control_module",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=is_redox_module_present,
),
"Redox measurement active": NeoPoolBinarySensorEntityDescription(
key="Redox measurement active",
translation_key="redox_measurement_active",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=is_redox_module_present,
),
"Chlorine flow sensor problem": NeoPoolBinarySensorEntityDescription(
key="Chlorine flow sensor problem",
translation_key="chlorine_flow_sensor_problem",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
supported_fn=is_chlorine_module_present,
),
"Chlorine pump active": NeoPoolBinarySensorEntityDescription(
key="Chlorine pump active",
translation_key="chlorine_pump_active",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=lambda data: (
is_chlorine_module_present(data)
and (
"MBF_PAR_CL_RELAY_GPIO" not in data
or is_valid_relay_gpio(data["MBF_PAR_CL_RELAY_GPIO"] or 0)
)
),
),
"Chlorine control module": NeoPoolBinarySensorEntityDescription(
key="Chlorine control module",
translation_key="chlorine_control_module",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=is_chlorine_module_present,
),
"Chlorine measurement active": NeoPoolBinarySensorEntityDescription(
key="Chlorine measurement active",
translation_key="chlorine_measurement_active",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=is_chlorine_module_present,
),
"Conductivity pump active": NeoPoolBinarySensorEntityDescription(
key="Conductivity pump active",
translation_key="conductivity_pump_active",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=lambda data: (
is_conductivity_module_present(data)
and (
"MBF_PAR_CD_RELAY_GPIO" not in data
or is_valid_relay_gpio(data["MBF_PAR_CD_RELAY_GPIO"] or 0)
)
),
),
"Conductivity control module": NeoPoolBinarySensorEntityDescription(
key="Conductivity control module",
translation_key="conductivity_control_module",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=is_conductivity_module_present,
),
"Conductivity measurement active": NeoPoolBinarySensorEntityDescription(
key="Conductivity measurement active",
translation_key="conductivity_measurement_active",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=is_conductivity_module_present,
),
"ION On Target": NeoPoolBinarySensorEntityDescription(
key="ION On Target",
translation_key="ion_on_target",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=is_ionization_present, # pragma: no cover
),
"ION Low": NeoPoolBinarySensorEntityDescription(
key="ION Low",
translation_key="ion_low",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
supported_fn=is_ionization_present, # pragma: no cover
),
"ION Program time exceeded": NeoPoolBinarySensorEntityDescription(
key="ION Program time exceeded",
translation_key="ion_program_time_exceeded",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
supported_fn=is_ionization_present, # pragma: no cover
),
"HIDRO Low": NeoPoolBinarySensorEntityDescription(
key="HIDRO Low",
translation_key="hidro_low",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
supported_fn=is_hydrolysis_present,
),
"Pool Cover": NeoPoolBinarySensorEntityDescription(
key="Pool Cover",
translation_key="pool_cover",
device_class=BinarySensorDeviceClass.OPENING,
value_fn=_pool_cover_open,
),
"HIDRO Module active": NeoPoolBinarySensorEntityDescription(
key="HIDRO Module active",
translation_key="hidro_module_active",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
supported_fn=is_hydrolysis_present,
),
"HIDRO Module regulated": NeoPoolBinarySensorEntityDescription(
key="HIDRO Module regulated",
translation_key="hidro_module_regulated",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
supported_fn=is_hydrolysis_present,
),
"HIDRO Activated by the RX module": NeoPoolBinarySensorEntityDescription(
key="HIDRO Activated by the RX module",
translation_key="hidro_activated_by_the_rx_module",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
supported_fn=lambda data: (
is_hydrolysis_present(data) and is_redox_module_present(data)
), # pragma: no cover
),
"HIDRO Chlorine shock mode": NeoPoolBinarySensorEntityDescription(
key="HIDRO Chlorine shock mode",
translation_key="hidro_chlorine_shock_mode",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
supported_fn=is_hydrolysis_present,
),
"HIDRO Activated by the CL module": NeoPoolBinarySensorEntityDescription(
key="HIDRO Activated by the CL module",
translation_key="hidro_activated_by_the_cl_module",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
supported_fn=lambda data: (
is_hydrolysis_present(data) and is_chlorine_module_present(data)
),
),
"Heating": NeoPoolBinarySensorEntityDescription(
key="Heating",
translation_key="heating",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
supported_fn=has_heating_relay,
),
"UV Lamp": NeoPoolBinarySensorEntityDescription(
key="UV Lamp",
translation_key="uv_lamp",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
supported_fn=lambda data: (
"MBF_PAR_UV_RELAY_GPIO" not in data
or is_valid_relay_gpio(data["MBF_PAR_UV_RELAY_GPIO"] or 0)
),
),
}
# Entities gated on a config-entry option (in addition to their supported_fn).
# The controller cannot detect what is physically wired to the light or aux
# relays, nor whether a cover sensor is present, so these entities are opt-in
# per config entry rather than surfaced from a device capability bit.
_ENTITY_OPTION_KEY: dict[str, str] = {
"Pool Light": CONF_USE_LIGHT,
"AUX1": CONF_USE_AUX1,
"AUX2": CONF_USE_AUX2,
"AUX3": CONF_USE_AUX3,
"AUX4": CONF_USE_AUX4,
"Pool Cover": CONF_USE_COVER_SENSOR,
}
async def async_setup_entry(
hass: HomeAssistant,
entry: NeoPoolConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up NeoPool binary sensors from a config entry."""
coordinator = entry.runtime_data
options = entry.options
async_add_entities(
NeoPoolBinarySensor(coordinator, key, desc)
for key, desc in BINARY_SENSOR_DESCRIPTIONS.items()
if (
(option_key := _ENTITY_OPTION_KEY.get(key)) is None
or bool(options.get(option_key))
)
and (desc.supported_fn is None or desc.supported_fn(coordinator.data))
)
class NeoPoolBinarySensor(NeoPoolEntity, BinarySensorEntity):
"""Representation of a NeoPool binary sensor."""
_winter_mode_active = False
entity_description: NeoPoolBinarySensorEntityDescription
def __init__(
self,
coordinator: NeoPoolCoordinator,
key: str,
description: NeoPoolBinarySensorEntityDescription,
) -> None:
"""Initialize the binary sensor."""
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 is_on(self) -> bool | None:
"""Return True if the binary sensor is on."""
if (value_fn := self.entity_description.value_fn) is not None:
value: bool | None = value_fn(self.coordinator.data, self.hass)
return value
value = self.coordinator.data.get(self._key)
return None if value is None else bool(value)