mirror of
https://github.com/home-assistant/core.git
synced 2026-09-13 20:20:10 +01:00
391 lines
15 KiB
Python
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)
|