From ac7b063c2cd60378accaae55e83dcd6ca9ccc21c Mon Sep 17 00:00:00 2001 From: Lukas <12813107+lmaertin@users.noreply.github.com> Date: Fri, 21 Nov 2025 23:50:30 +0100 Subject: [PATCH] Add binary_sensor platform to pooldose integration (#156894) --- homeassistant/components/pooldose/__init__.py | 2 +- .../components/pooldose/binary_sensor.py | 129 +++++ homeassistant/components/pooldose/entity.py | 5 + homeassistant/components/pooldose/icons.json | 74 ++- .../components/pooldose/strings.json | 35 ++ .../pooldose/fixtures/instantvalues.json | 14 +- .../snapshots/test_binary_sensor.ambr | 540 ++++++++++++++++++ .../components/pooldose/test_binary_sensor.py | 193 +++++++ 8 files changed, 987 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/pooldose/binary_sensor.py create mode 100644 tests/components/pooldose/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/pooldose/test_binary_sensor.py diff --git a/homeassistant/components/pooldose/__init__.py b/homeassistant/components/pooldose/__init__.py index 3b0b1edff44..5c74e670696 100644 --- a/homeassistant/components/pooldose/__init__.py +++ b/homeassistant/components/pooldose/__init__.py @@ -17,7 +17,7 @@ from .coordinator import PooldoseConfigEntry, PooldoseCoordinator _LOGGER = logging.getLogger(__name__) -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_migrate_entry(hass: HomeAssistant, entry: PooldoseConfigEntry) -> bool: diff --git a/homeassistant/components/pooldose/binary_sensor.py b/homeassistant/components/pooldose/binary_sensor.py new file mode 100644 index 00000000000..9fea647cd3c --- /dev/null +++ b/homeassistant/components/pooldose/binary_sensor.py @@ -0,0 +1,129 @@ +"""Binary sensors for the Seko PoolDose integration.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, cast + +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 . import PooldoseConfigEntry +from .entity import PooldoseEntity + +_LOGGER = logging.getLogger(__name__) + + +BINARY_SENSOR_DESCRIPTIONS: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key="pump_alarm", + translation_key="pump_alarm", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + ), + BinarySensorEntityDescription( + key="ph_level_alarm", + translation_key="ph_level_alarm", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + ), + BinarySensorEntityDescription( + key="orp_level_alarm", + translation_key="orp_level_alarm", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + ), + BinarySensorEntityDescription( + key="flow_rate_alarm", + translation_key="flow_rate_alarm", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + ), + BinarySensorEntityDescription( + key="alarm_ofa_ph", + translation_key="alarm_ofa_ph", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + ), + BinarySensorEntityDescription( + key="alarm_ofa_orp", + translation_key="alarm_ofa_orp", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + ), + BinarySensorEntityDescription( + key="alarm_ofa_cl", + translation_key="alarm_ofa_cl", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + ), + BinarySensorEntityDescription( + key="relay_alarm", + translation_key="relay_alarm", + device_class=BinarySensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + ), + BinarySensorEntityDescription( + key="relay_aux1", + translation_key="relay_aux1", + device_class=BinarySensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + BinarySensorEntityDescription( + key="relay_aux2", + translation_key="relay_aux2", + device_class=BinarySensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + BinarySensorEntityDescription( + key="relay_aux3", + translation_key="relay_aux3", + device_class=BinarySensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: PooldoseConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up PoolDose binary sensor entities from a config entry.""" + if TYPE_CHECKING: + assert config_entry.unique_id is not None + + coordinator = config_entry.runtime_data + binary_sensor_data = coordinator.data["binary_sensor"] + serial_number = config_entry.unique_id + + async_add_entities( + PooldoseBinarySensor( + coordinator, + serial_number, + coordinator.device_info, + description, + "binary_sensor", + ) + for description in BINARY_SENSOR_DESCRIPTIONS + if description.key in binary_sensor_data + ) + + +class PooldoseBinarySensor(PooldoseEntity, BinarySensorEntity): + """Binary sensor entity for the Seko PoolDose Python API.""" + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + data = cast(dict, self.get_data()) + return cast(bool, data["value"]) diff --git a/homeassistant/components/pooldose/entity.py b/homeassistant/components/pooldose/entity.py index 19976c1d115..22c80d58b7a 100644 --- a/homeassistant/components/pooldose/entity.py +++ b/homeassistant/components/pooldose/entity.py @@ -68,6 +68,11 @@ class PooldoseEntity(CoordinatorEntity[PooldoseCoordinator]): coordinator.config_entry.data.get(CONF_MAC), ) + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and self.get_data() is not None + def get_data(self) -> ValueDict | None: """Get data for this entity, only if available.""" platform_data = self.coordinator.data[self.platform_name] diff --git a/homeassistant/components/pooldose/icons.json b/homeassistant/components/pooldose/icons.json index bb6da13822e..f114ce0e078 100644 --- a/homeassistant/components/pooldose/icons.json +++ b/homeassistant/components/pooldose/icons.json @@ -1,11 +1,79 @@ { "entity": { + "binary_sensor": { + "alarm_ofa_cl": { + "default": "mdi:clock-alert-outline", + "state": { + "on": "mdi:clock-alert" + } + }, + "alarm_ofa_orp": { + "default": "mdi:clock-alert-outline", + "state": { + "on": "mdi:clock-alert" + } + }, + "alarm_ofa_ph": { + "default": "mdi:clock-alert-outline", + "state": { + "on": "mdi:clock-alert" + } + }, + "flow_rate_alarm": { + "default": "mdi:autorenew", + "state": { + "on": "mdi:autorenew-off" + } + }, + "orp_level_alarm": { + "default": "mdi:flask", + "state": { + "on": "mdi:flask-empty" + } + }, + "ph_level_alarm": { + "default": "mdi:flask", + "state": { + "on": "mdi:flask-empty" + } + }, + "pump_alarm": { + "default": "mdi:pump", + "state": { + "on": "mdi:pump-off" + } + }, + "relay_alarm": { + "default": "mdi:electric-switch-closed", + "state": { + "on": "mdi:electric-switch" + } + }, + "relay_aux1": { + "default": "mdi:electric-switch-closed", + "state": { + "on": "mdi:electric-switch" + } + }, + "relay_aux2": { + "default": "mdi:electric-switch-closed", + "state": { + "on": "mdi:electric-switch" + } + }, + "relay_aux3": { + "default": "mdi:electric-switch-closed", + "state": { + "on": "mdi:electric-switch" + } + } + }, "sensor": { "cl": { "default": "mdi:pool" }, "cl_type_dosing": { - "default": "mdi:flask" + "default": "mdi:beaker" }, "flow_rate": { "default": "mdi:pipe-valve" @@ -29,7 +97,7 @@ "default": "mdi:form-select" }, "orp_type_dosing": { - "default": "mdi:flask" + "default": "mdi:beaker" }, "peristaltic_cl_dosing": { "default": "mdi:pump" @@ -50,7 +118,7 @@ "default": "mdi:form-select" }, "ph_type_dosing": { - "default": "mdi:flask" + "default": "mdi:beaker" } } } diff --git a/homeassistant/components/pooldose/strings.json b/homeassistant/components/pooldose/strings.json index 28105f401e8..cd510566c7e 100644 --- a/homeassistant/components/pooldose/strings.json +++ b/homeassistant/components/pooldose/strings.json @@ -33,6 +33,41 @@ } }, "entity": { + "binary_sensor": { + "alarm_ofa_cl": { + "name": "Chlorine tank level" + }, + "alarm_ofa_orp": { + "name": "ORP overfeed" + }, + "alarm_ofa_ph": { + "name": "pH overfeed" + }, + "flow_rate_alarm": { + "name": "Flow rate" + }, + "orp_level_alarm": { + "name": "ORP tank level" + }, + "ph_level_alarm": { + "name": "pH tank level" + }, + "pump_alarm": { + "name": "Recirculation" + }, + "relay_alarm": { + "name": "Alarm relay status" + }, + "relay_aux1": { + "name": "Auxiliary relay 1 status" + }, + "relay_aux2": { + "name": "Auxiliary relay 2 status" + }, + "relay_aux3": { + "name": "Auxiliary relay 3 status" + } + }, "sensor": { "cl": { "name": "Chlorine" diff --git a/tests/components/pooldose/fixtures/instantvalues.json b/tests/components/pooldose/fixtures/instantvalues.json index 8cf027fd28d..85849410a39 100644 --- a/tests/components/pooldose/fixtures/instantvalues.json +++ b/tests/components/pooldose/fixtures/instantvalues.json @@ -90,7 +90,7 @@ "flow_rate_alarm": { "value": false }, - "alarm_relay": { + "relay_alarm": { "value": true }, "relay_aux1": { @@ -98,6 +98,18 @@ }, "relay_aux2": { "value": false + }, + "relay_aux3": { + "value": false + }, + "alarm_ofa_ph": { + "value": false + }, + "alarm_ofa_orp": { + "value": false + }, + "alarm_ofa_cl": { + "value": false } }, "number": { diff --git a/tests/components/pooldose/snapshots/test_binary_sensor.ambr b/tests/components/pooldose/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..c02ac675f8c --- /dev/null +++ b/tests/components/pooldose/snapshots/test_binary_sensor.ambr @@ -0,0 +1,540 @@ +# serializer version: 1 +# name: test_all_binary_sensors[binary_sensor.pool_device_alarm_relay_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.pool_device_alarm_relay_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Alarm relay status', + 'platform': 'pooldose', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_alarm', + 'unique_id': 'TEST123456789_relay_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_alarm_relay_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Pool Device Alarm relay status', + }), + 'context': , + 'entity_id': 'binary_sensor.pool_device_alarm_relay_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_auxiliary_relay_1_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.pool_device_auxiliary_relay_1_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Auxiliary relay 1 status', + 'platform': 'pooldose', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_aux1', + 'unique_id': 'TEST123456789_relay_aux1', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_auxiliary_relay_1_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Pool Device Auxiliary relay 1 status', + }), + 'context': , + 'entity_id': 'binary_sensor.pool_device_auxiliary_relay_1_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_auxiliary_relay_2_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.pool_device_auxiliary_relay_2_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Auxiliary relay 2 status', + 'platform': 'pooldose', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_aux2', + 'unique_id': 'TEST123456789_relay_aux2', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_auxiliary_relay_2_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Pool Device Auxiliary relay 2 status', + }), + 'context': , + 'entity_id': 'binary_sensor.pool_device_auxiliary_relay_2_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_auxiliary_relay_3_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.pool_device_auxiliary_relay_3_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Auxiliary relay 3 status', + 'platform': 'pooldose', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_aux3', + 'unique_id': 'TEST123456789_relay_aux3', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_auxiliary_relay_3_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Pool Device Auxiliary relay 3 status', + }), + 'context': , + 'entity_id': 'binary_sensor.pool_device_auxiliary_relay_3_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_chlorine_tank_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.pool_device_chlorine_tank_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Chlorine tank level', + 'platform': 'pooldose', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_ofa_cl', + 'unique_id': 'TEST123456789_alarm_ofa_cl', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_chlorine_tank_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Pool Device Chlorine tank level', + }), + 'context': , + 'entity_id': 'binary_sensor.pool_device_chlorine_tank_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_flow_rate-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.pool_device_flow_rate', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Flow rate', + 'platform': 'pooldose', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'flow_rate_alarm', + 'unique_id': 'TEST123456789_flow_rate_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_flow_rate-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Pool Device Flow rate', + }), + 'context': , + 'entity_id': 'binary_sensor.pool_device_flow_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_orp_overfeed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.pool_device_orp_overfeed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ORP overfeed', + 'platform': 'pooldose', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_ofa_orp', + 'unique_id': 'TEST123456789_alarm_ofa_orp', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_orp_overfeed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Pool Device ORP overfeed', + }), + 'context': , + 'entity_id': 'binary_sensor.pool_device_orp_overfeed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_orp_tank_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.pool_device_orp_tank_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ORP tank level', + 'platform': 'pooldose', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'orp_level_alarm', + 'unique_id': 'TEST123456789_orp_level_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_orp_tank_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Pool Device ORP tank level', + }), + 'context': , + 'entity_id': 'binary_sensor.pool_device_orp_tank_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_ph_overfeed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.pool_device_ph_overfeed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'pH overfeed', + 'platform': 'pooldose', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_ofa_ph', + 'unique_id': 'TEST123456789_alarm_ofa_ph', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_ph_overfeed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Pool Device pH overfeed', + }), + 'context': , + 'entity_id': 'binary_sensor.pool_device_ph_overfeed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_ph_tank_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.pool_device_ph_tank_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'pH tank level', + 'platform': 'pooldose', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ph_level_alarm', + 'unique_id': 'TEST123456789_ph_level_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_ph_tank_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Pool Device pH tank level', + }), + 'context': , + 'entity_id': 'binary_sensor.pool_device_ph_tank_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_recirculation-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.pool_device_recirculation', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Recirculation', + 'platform': 'pooldose', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'pump_alarm', + 'unique_id': 'TEST123456789_pump_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_binary_sensors[binary_sensor.pool_device_recirculation-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Pool Device Recirculation', + }), + 'context': , + 'entity_id': 'binary_sensor.pool_device_recirculation', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/pooldose/test_binary_sensor.py b/tests/components/pooldose/test_binary_sensor.py new file mode 100644 index 00000000000..78738065e9d --- /dev/null +++ b/tests/components/pooldose/test_binary_sensor.py @@ -0,0 +1,193 @@ +"""Test the PoolDose binary sensor platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +from pooldose.request_status import RequestStatus +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_binary_sensors( + hass: HomeAssistant, + mock_pooldose_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the Pooldose binary sensors.""" + with patch("homeassistant.components.pooldose.PLATFORMS", [Platform.BINARY_SENSOR]): + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize("exception", [TimeoutError, ConnectionError, OSError]) +async def test_exception_raising( + hass: HomeAssistant, + mock_pooldose_client: AsyncMock, + mock_config_entry: MockConfigEntry, + exception: Exception, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the Pooldose binary sensors.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.pool_device_recirculation").state == STATE_ON + + mock_pooldose_client.instant_values_structured.side_effect = exception + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert ( + hass.states.get("binary_sensor.pool_device_recirculation").state + == STATE_UNAVAILABLE + ) + + +async def test_no_data( + hass: HomeAssistant, + mock_pooldose_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the Pooldose binary sensors.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.pool_device_recirculation").state == STATE_ON + + mock_pooldose_client.instant_values_structured.return_value = ( + RequestStatus.SUCCESS, + None, + ) + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert ( + hass.states.get("binary_sensor.pool_device_recirculation").state + == STATE_UNAVAILABLE + ) + + +async def test_binary_sensor_entity_unavailable_no_coordinator_data( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_pooldose_client: AsyncMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test binary sensor entity becomes unavailable when coordinator has no data.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # Verify initial working state + pump_state = hass.states.get("binary_sensor.pool_device_recirculation") + assert pump_state.state == STATE_ON + + # Set coordinator data to None by making API return empty + mock_pooldose_client.instant_values_structured.return_value = ( + RequestStatus.HOST_UNREACHABLE, + None, + ) + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Check binary sensor becomes unavailable + pump_state = hass.states.get("binary_sensor.pool_device_recirculation") + assert pump_state.state == STATE_UNAVAILABLE + + +async def test_binary_sensor_state_changes( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_pooldose_client: AsyncMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test binary sensor state changes.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # Verify initial states + pump_state = hass.states.get("binary_sensor.pool_device_recirculation") + assert pump_state.state == STATE_ON + + ph_level_state = hass.states.get("binary_sensor.pool_device_ph_tank_level") + assert ph_level_state.state == STATE_OFF + + # Update data with changed values + current_data = mock_pooldose_client.instant_values_structured.return_value[1] + updated_data = current_data.copy() + updated_data["binary_sensor"]["pump_alarm"]["value"] = False + updated_data["binary_sensor"]["ph_level_alarm"]["value"] = True + + mock_pooldose_client.instant_values_structured.return_value = ( + RequestStatus.SUCCESS, + updated_data, + ) + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Check states have changed + pump_state = hass.states.get("binary_sensor.pool_device_recirculation") + assert pump_state.state == STATE_OFF + + ph_level_state = hass.states.get("binary_sensor.pool_device_ph_tank_level") + assert ph_level_state.state == STATE_ON + + +async def test_binary_sensor_missing_from_data( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_pooldose_client: AsyncMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test binary sensor becomes unavailable when missing from coordinator data.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # Verify initial working state + flow_alarm_state = hass.states.get("binary_sensor.pool_device_flow_rate") + assert flow_alarm_state.state == STATE_OFF + + # Update data with missing sensor + current_data = mock_pooldose_client.instant_values_structured.return_value[1] + updated_data = current_data.copy() + del updated_data["binary_sensor"]["flow_rate_alarm"] + + mock_pooldose_client.instant_values_structured.return_value = ( + RequestStatus.SUCCESS, + updated_data, + ) + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Check sensor becomes unavailable when not in coordinator data + flow_alarm_state = hass.states.get("binary_sensor.pool_device_flow_rate") + assert flow_alarm_state.state == STATE_UNAVAILABLE