1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-19 18:38:58 +00:00

Pooldose: Add select platform (#159240)

This commit is contained in:
Lukas
2025-12-19 00:13:26 +01:00
committed by GitHub
parent 9afb4a9eb8
commit 74baf44c83
10 changed files with 990 additions and 13 deletions

View File

@@ -20,6 +20,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
]

View File

@@ -7,15 +7,16 @@ from homeassistant.const import UnitOfTemperature, UnitOfVolume, UnitOfVolumeFlo
DOMAIN = "pooldose"
MANUFACTURER = "SEKO"
# Mapping of device units (upper case) to Home Assistant units
# Unit mappings for select entities (water meter and flow rate)
# Keys match API values exactly: lowercase for m3/m3/h, uppercase L for L/L/s
UNIT_MAPPING: dict[str, str] = {
# Temperature units
"°C": UnitOfTemperature.CELSIUS,
"°F": UnitOfTemperature.FAHRENHEIT,
# Volume flow rate units
"M3/H": UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
"L/S": UnitOfVolumeFlowRate.LITERS_PER_SECOND,
"m3/h": UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
"L/s": UnitOfVolumeFlowRate.LITERS_PER_SECOND,
# Volume units
"L": UnitOfVolume.LITERS,
"M3": UnitOfVolume.CUBIC_METERS,
"m3": UnitOfVolume.CUBIC_METERS,
}

View File

@@ -97,6 +97,32 @@
"default": "mdi:ph"
}
},
"select": {
"cl_type_dosing_method": {
"default": "mdi:beaker"
},
"cl_type_dosing_set": {
"default": "mdi:pool"
},
"flow_rate_unit": {
"default": "mdi:pipe-valve"
},
"orp_type_dosing_method": {
"default": "mdi:beaker"
},
"orp_type_dosing_set": {
"default": "mdi:water-check"
},
"ph_type_dosing_method": {
"default": "mdi:beaker"
},
"ph_type_dosing_set": {
"default": "mdi:ph"
},
"water_meter_unit": {
"default": "mdi:water"
}
},
"sensor": {
"cl": {
"default": "mdi:pool"

View File

@@ -0,0 +1,160 @@
"""Select entities for the Seko PoolDose integration."""
from __future__ import annotations
from dataclasses import dataclass
import logging
from typing import TYPE_CHECKING, Any, cast
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory, UnitOfVolume, UnitOfVolumeFlowRate
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import PooldoseConfigEntry
from .const import UNIT_MAPPING
from .entity import PooldoseEntity
if TYPE_CHECKING:
from .coordinator import PooldoseCoordinator
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True, kw_only=True)
class PooldoseSelectEntityDescription(SelectEntityDescription):
"""Describes PoolDose select entity."""
use_unit_conversion: bool = False
SELECT_DESCRIPTIONS: tuple[PooldoseSelectEntityDescription, ...] = (
PooldoseSelectEntityDescription(
key="water_meter_unit",
translation_key="water_meter_unit",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
options=[UnitOfVolume.LITERS, UnitOfVolume.CUBIC_METERS],
use_unit_conversion=True,
),
PooldoseSelectEntityDescription(
key="flow_rate_unit",
translation_key="flow_rate_unit",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
options=[
UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
UnitOfVolumeFlowRate.LITERS_PER_SECOND,
],
use_unit_conversion=True,
),
PooldoseSelectEntityDescription(
key="ph_type_dosing_set",
translation_key="ph_type_dosing_set",
entity_category=EntityCategory.CONFIG,
options=["alcalyne", "acid"],
),
PooldoseSelectEntityDescription(
key="ph_type_dosing_method",
translation_key="ph_type_dosing_method",
entity_category=EntityCategory.CONFIG,
options=["off", "proportional", "on_off", "timed"],
entity_registry_enabled_default=False,
),
PooldoseSelectEntityDescription(
key="orp_type_dosing_set",
translation_key="orp_type_dosing_set",
entity_category=EntityCategory.CONFIG,
options=["low", "high"],
entity_registry_enabled_default=False,
),
PooldoseSelectEntityDescription(
key="orp_type_dosing_method",
translation_key="orp_type_dosing_method",
entity_category=EntityCategory.CONFIG,
options=["off", "proportional", "on_off", "timed"],
entity_registry_enabled_default=False,
),
PooldoseSelectEntityDescription(
key="cl_type_dosing_set",
translation_key="cl_type_dosing_set",
entity_category=EntityCategory.CONFIG,
options=["low", "high"],
entity_registry_enabled_default=False,
),
PooldoseSelectEntityDescription(
key="cl_type_dosing_method",
translation_key="cl_type_dosing_method",
entity_category=EntityCategory.CONFIG,
options=["off", "proportional", "on_off", "timed"],
entity_registry_enabled_default=False,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: PooldoseConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up PoolDose select entities from a config entry."""
if TYPE_CHECKING:
assert config_entry.unique_id is not None
coordinator = config_entry.runtime_data
select_data = coordinator.data["select"]
serial_number = config_entry.unique_id
async_add_entities(
PooldoseSelect(coordinator, serial_number, coordinator.device_info, description)
for description in SELECT_DESCRIPTIONS
if description.key in select_data
)
class PooldoseSelect(PooldoseEntity, SelectEntity):
"""Select entity for the Seko PoolDose Python API."""
entity_description: PooldoseSelectEntityDescription
def __init__(
self,
coordinator: PooldoseCoordinator,
serial_number: str,
device_info: Any,
description: PooldoseSelectEntityDescription,
) -> None:
"""Initialize the select."""
super().__init__(coordinator, serial_number, device_info, description, "select")
self._async_update_attrs()
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._async_update_attrs()
super()._handle_coordinator_update()
def _async_update_attrs(self) -> None:
"""Update select attributes."""
data = cast(dict, self.get_data())
api_value = cast(str, data["value"])
# Convert API value to Home Assistant unit if unit conversion is enabled
if self.entity_description.use_unit_conversion:
# Map API value (e.g., "m3") to HA unit (e.g., "m³")
self._attr_current_option = UNIT_MAPPING.get(api_value, api_value)
else:
self._attr_current_option = api_value
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
# Convert Home Assistant unit to API value if unit conversion is enabled
if self.entity_description.use_unit_conversion:
# Invert UNIT_MAPPING to get API value from HA unit
reverse_map = {v: k for k, v in UNIT_MAPPING.items()}
api_value = reverse_map.get(option, option)
else:
api_value = option
await self.coordinator.client.set_select(self.entity_description.key, api_value)
self._attr_current_option = option
self.async_write_ha_state()

View File

@@ -32,14 +32,14 @@ _LOGGER = logging.getLogger(__name__)
class PooldoseSensorEntityDescription(SensorEntityDescription):
"""Describes PoolDose sensor entity."""
use_dynamic_unit: bool = False
use_unit_conversion: bool = False
SENSOR_DESCRIPTIONS: tuple[PooldoseSensorEntityDescription, ...] = (
PooldoseSensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
use_dynamic_unit=True,
use_unit_conversion=True,
),
PooldoseSensorEntityDescription(key="ph", device_class=SensorDeviceClass.PH),
PooldoseSensorEntityDescription(
@@ -57,14 +57,14 @@ SENSOR_DESCRIPTIONS: tuple[PooldoseSensorEntityDescription, ...] = (
key="flow_rate",
translation_key="flow_rate",
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
use_dynamic_unit=True,
use_unit_conversion=True,
),
PooldoseSensorEntityDescription(
key="water_meter_total_permanent",
translation_key="water_meter_total_permanent",
device_class=SensorDeviceClass.VOLUME,
state_class=SensorStateClass.TOTAL_INCREASING,
use_dynamic_unit=True,
use_unit_conversion=True,
),
PooldoseSensorEntityDescription(
key="ph_type_dosing",
@@ -227,12 +227,12 @@ class PooldoseSensor(PooldoseEntity, SensorEntity):
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement."""
if (
self.entity_description.use_dynamic_unit
self.entity_description.use_unit_conversion
and (data := self.get_data()) is not None
and (device_unit := data.get("unit"))
):
# Map device unit (upper case) to Home Assistant unit, return None if unknown
return UNIT_MAPPING.get(device_unit.upper())
# Map device unit to Home Assistant unit, return None if unknown
return UNIT_MAPPING.get(device_unit)
# Fall back to static unit from entity description
return super().native_unit_of_measurement

View File

@@ -97,6 +97,62 @@
"name": "pH target"
}
},
"select": {
"cl_type_dosing_method": {
"name": "Chlorine dosing method",
"state": {
"off": "[%key:common::state::off%]",
"on_off": "[%key:component::pooldose::entity::sensor::peristaltic_ph_dosing::state::on_off%]",
"proportional": "[%key:component::pooldose::entity::sensor::peristaltic_ph_dosing::state::proportional%]",
"timed": "[%key:component::pooldose::entity::sensor::peristaltic_ph_dosing::state::timed%]"
}
},
"cl_type_dosing_set": {
"name": "Chlorine dosing set",
"state": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]"
}
},
"flow_rate_unit": {
"name": "Flow rate unit"
},
"orp_type_dosing_method": {
"name": "ORP dosing method",
"state": {
"off": "[%key:common::state::off%]",
"on_off": "[%key:component::pooldose::entity::sensor::peristaltic_ph_dosing::state::on_off%]",
"proportional": "[%key:component::pooldose::entity::sensor::peristaltic_ph_dosing::state::proportional%]",
"timed": "[%key:component::pooldose::entity::sensor::peristaltic_ph_dosing::state::timed%]"
}
},
"orp_type_dosing_set": {
"name": "ORP dosing set",
"state": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]"
}
},
"ph_type_dosing_method": {
"name": "pH dosing method",
"state": {
"off": "[%key:common::state::off%]",
"on_off": "[%key:component::pooldose::entity::sensor::peristaltic_ph_dosing::state::on_off%]",
"proportional": "[%key:component::pooldose::entity::sensor::peristaltic_ph_dosing::state::proportional%]",
"timed": "[%key:component::pooldose::entity::sensor::peristaltic_ph_dosing::state::timed%]"
}
},
"ph_type_dosing_set": {
"name": "pH dosing set",
"state": {
"acid": "Acid (pH-)",
"alcalyne": "Alkaline (pH+)"
}
},
"water_meter_unit": {
"name": "Water meter unit"
}
},
"sensor": {
"cl": {
"name": "Chlorine"

View File

@@ -60,6 +60,7 @@ def mock_pooldose_client(device_info: dict[str, Any]) -> Generator[MagicMock]:
)
client.set_switch = AsyncMock(return_value=RequestStatus.SUCCESS)
client.set_select = AsyncMock(return_value=RequestStatus.SUCCESS)
client.is_connected = True
yield client

View File

@@ -18,7 +18,7 @@
},
"flow_rate": {
"value": 150,
"unit": "l/s"
"unit": "L/s"
},
"ph_type_dosing": {
"value": "alcalyne",
@@ -198,7 +198,28 @@
},
"select": {
"water_meter_unit": {
"value": "m³"
"value": "m3"
},
"flow_rate_unit": {
"value": "L/s"
},
"ph_type_dosing_set": {
"value": "acid"
},
"ph_type_dosing_method": {
"value": "proportional"
},
"orp_type_dosing_set": {
"value": "low"
},
"orp_type_dosing_method": {
"value": "on_off"
},
"cl_type_dosing_set": {
"value": "high"
},
"cl_type_dosing_method": {
"value": "timed"
}
}
}

View File

@@ -0,0 +1,469 @@
# serializer version: 1
# name: test_all_selects[select.pool_device_chlorine_dosing_method-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'off',
'proportional',
'on_off',
'timed',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.pool_device_chlorine_dosing_method',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Chlorine dosing method',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'cl_type_dosing_method',
'unique_id': 'TEST123456789_cl_type_dosing_method',
'unit_of_measurement': None,
})
# ---
# name: test_all_selects[select.pool_device_chlorine_dosing_method-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Pool Device Chlorine dosing method',
'options': list([
'off',
'proportional',
'on_off',
'timed',
]),
}),
'context': <ANY>,
'entity_id': 'select.pool_device_chlorine_dosing_method',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'timed',
})
# ---
# name: test_all_selects[select.pool_device_chlorine_dosing_set-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'low',
'high',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.pool_device_chlorine_dosing_set',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Chlorine dosing set',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'cl_type_dosing_set',
'unique_id': 'TEST123456789_cl_type_dosing_set',
'unit_of_measurement': None,
})
# ---
# name: test_all_selects[select.pool_device_chlorine_dosing_set-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Pool Device Chlorine dosing set',
'options': list([
'low',
'high',
]),
}),
'context': <ANY>,
'entity_id': 'select.pool_device_chlorine_dosing_set',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'high',
})
# ---
# name: test_all_selects[select.pool_device_flow_rate_unit-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
<UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR: 'm³/h'>,
<UnitOfVolumeFlowRate.LITERS_PER_SECOND: 'L/s'>,
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.pool_device_flow_rate_unit',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Flow rate unit',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'flow_rate_unit',
'unique_id': 'TEST123456789_flow_rate_unit',
'unit_of_measurement': None,
})
# ---
# name: test_all_selects[select.pool_device_flow_rate_unit-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Pool Device Flow rate unit',
'options': list([
<UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR: 'm³/h'>,
<UnitOfVolumeFlowRate.LITERS_PER_SECOND: 'L/s'>,
]),
}),
'context': <ANY>,
'entity_id': 'select.pool_device_flow_rate_unit',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'L/s',
})
# ---
# name: test_all_selects[select.pool_device_orp_dosing_method-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'off',
'proportional',
'on_off',
'timed',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.pool_device_orp_dosing_method',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'ORP dosing method',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'orp_type_dosing_method',
'unique_id': 'TEST123456789_orp_type_dosing_method',
'unit_of_measurement': None,
})
# ---
# name: test_all_selects[select.pool_device_orp_dosing_method-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Pool Device ORP dosing method',
'options': list([
'off',
'proportional',
'on_off',
'timed',
]),
}),
'context': <ANY>,
'entity_id': 'select.pool_device_orp_dosing_method',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on_off',
})
# ---
# name: test_all_selects[select.pool_device_orp_dosing_set-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'low',
'high',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.pool_device_orp_dosing_set',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'ORP dosing set',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'orp_type_dosing_set',
'unique_id': 'TEST123456789_orp_type_dosing_set',
'unit_of_measurement': None,
})
# ---
# name: test_all_selects[select.pool_device_orp_dosing_set-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Pool Device ORP dosing set',
'options': list([
'low',
'high',
]),
}),
'context': <ANY>,
'entity_id': 'select.pool_device_orp_dosing_set',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'low',
})
# ---
# name: test_all_selects[select.pool_device_ph_dosing_method-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'off',
'proportional',
'on_off',
'timed',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.pool_device_ph_dosing_method',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'pH dosing method',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'ph_type_dosing_method',
'unique_id': 'TEST123456789_ph_type_dosing_method',
'unit_of_measurement': None,
})
# ---
# name: test_all_selects[select.pool_device_ph_dosing_method-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Pool Device pH dosing method',
'options': list([
'off',
'proportional',
'on_off',
'timed',
]),
}),
'context': <ANY>,
'entity_id': 'select.pool_device_ph_dosing_method',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'proportional',
})
# ---
# name: test_all_selects[select.pool_device_ph_dosing_set-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'alcalyne',
'acid',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.pool_device_ph_dosing_set',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'pH dosing set',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'ph_type_dosing_set',
'unique_id': 'TEST123456789_ph_type_dosing_set',
'unit_of_measurement': None,
})
# ---
# name: test_all_selects[select.pool_device_ph_dosing_set-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Pool Device pH dosing set',
'options': list([
'alcalyne',
'acid',
]),
}),
'context': <ANY>,
'entity_id': 'select.pool_device_ph_dosing_set',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'acid',
})
# ---
# name: test_all_selects[select.pool_device_water_meter_unit-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
<UnitOfVolume.LITERS: 'L'>,
<UnitOfVolume.CUBIC_METERS: 'm³'>,
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.pool_device_water_meter_unit',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Water meter unit',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'water_meter_unit',
'unique_id': 'TEST123456789_water_meter_unit',
'unit_of_measurement': None,
})
# ---
# name: test_all_selects[select.pool_device_water_meter_unit-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Pool Device Water meter unit',
'options': list([
<UnitOfVolume.LITERS: 'L'>,
<UnitOfVolume.CUBIC_METERS: 'm³'>,
]),
}),
'context': <ANY>,
'entity_id': 'select.pool_device_water_meter_unit',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'm³',
})
# ---

View File

@@ -0,0 +1,242 @@
"""Tests for the Seko PoolDose select platform."""
from datetime import timedelta
from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory
from pooldose.request_status import RequestStatus
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_OPTION,
Platform,
UnitOfVolume,
UnitOfVolumeFlowRate,
)
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.fixture
def platforms() -> list[Platform]:
"""Fixture to specify platforms to test."""
return [Platform.SELECT]
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_all_selects(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the Pooldose select entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_select_entity_unavailable_no_coordinator_data(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_pooldose_client: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test select entity becomes unavailable when coordinator has no data."""
# Verify entity has a state initially
water_meter_state = hass.states.get("select.pool_device_water_meter_unit")
assert water_meter_state.state == UnitOfVolume.CUBIC_METERS
# Update coordinator data to None
mock_pooldose_client.instant_values_structured.return_value = (None, None)
freezer.tick(timedelta(minutes=5))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Check entity becomes unavailable
water_meter_state = hass.states.get("select.pool_device_water_meter_unit")
assert water_meter_state.state == "unavailable"
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_select_state_changes(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_pooldose_client: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test select state changes when coordinator updates."""
# Initial state
ph_method_state = hass.states.get("select.pool_device_ph_dosing_method")
assert ph_method_state.state == "proportional"
# Update coordinator data with select value changed
current_data = mock_pooldose_client.instant_values_structured.return_value[1]
updated_data = current_data.copy()
updated_data["select"]["ph_type_dosing_method"]["value"] = "timed"
mock_pooldose_client.instant_values_structured.return_value = (
RequestStatus.SUCCESS,
updated_data,
)
freezer.tick(timedelta(minutes=5))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Check state changed
ph_method_state = hass.states.get("select.pool_device_ph_dosing_method")
assert ph_method_state.state == "timed"
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_select_option_unit_conversion(
hass: HomeAssistant,
mock_pooldose_client: AsyncMock,
) -> None:
"""Test selecting an option with unit conversion (HA unit -> API value)."""
# Verify initial state is m³ (displayed as Unicode)
water_meter_state = hass.states.get("select.pool_device_water_meter_unit")
assert water_meter_state.state == UnitOfVolume.CUBIC_METERS
# Select Liters option
await hass.services.async_call(
SELECT_DOMAIN,
"select_option",
{
ATTR_ENTITY_ID: "select.pool_device_water_meter_unit",
ATTR_OPTION: UnitOfVolume.LITERS,
},
blocking=True,
)
# Verify API was called with "L" (not Unicode)
mock_pooldose_client.set_select.assert_called_once_with("water_meter_unit", "L")
# Verify state updated to L (Unicode)
water_meter_state = hass.states.get("select.pool_device_water_meter_unit")
assert water_meter_state.state == UnitOfVolume.LITERS
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_select_option_flow_rate_unit_conversion(
hass: HomeAssistant,
mock_pooldose_client: AsyncMock,
) -> None:
"""Test selecting flow rate unit with conversion."""
# Verify initial state
flow_rate_state = hass.states.get("select.pool_device_flow_rate_unit")
assert flow_rate_state.state == UnitOfVolumeFlowRate.LITERS_PER_SECOND
# Select cubic meters per hour
await hass.services.async_call(
SELECT_DOMAIN,
"select_option",
{
ATTR_ENTITY_ID: "select.pool_device_flow_rate_unit",
ATTR_OPTION: UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
},
blocking=True,
)
# Verify API was called with "m3/h" (not Unicode m³/h)
mock_pooldose_client.set_select.assert_called_once_with("flow_rate_unit", "m3/h")
# Verify state updated to m³/h (with Unicode)
flow_rate_state = hass.states.get("select.pool_device_flow_rate_unit")
assert flow_rate_state.state == UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR
@pytest.mark.usefixtures("init_integration")
async def test_select_option_no_conversion(
hass: HomeAssistant,
mock_pooldose_client: AsyncMock,
) -> None:
"""Test selecting an option without unit conversion."""
# Verify initial state
ph_set_state = hass.states.get("select.pool_device_ph_dosing_set")
assert ph_set_state.state == "acid"
# Select alkaline option
await hass.services.async_call(
SELECT_DOMAIN,
"select_option",
{
ATTR_ENTITY_ID: "select.pool_device_ph_dosing_set",
ATTR_OPTION: "alcalyne",
},
blocking=True,
)
# Verify API was called with exact value
mock_pooldose_client.set_select.assert_called_once_with(
"ph_type_dosing_set", "alcalyne"
)
# Verify state updated
ph_set_state = hass.states.get("select.pool_device_ph_dosing_set")
assert ph_set_state.state == "alcalyne"
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_select_dosing_method_options(
hass: HomeAssistant,
mock_pooldose_client: AsyncMock,
) -> None:
"""Test selecting different dosing method options."""
# Test ORP dosing method
orp_method_state = hass.states.get("select.pool_device_orp_dosing_method")
assert orp_method_state.state == "on_off"
# Change to proportional
await hass.services.async_call(
SELECT_DOMAIN,
"select_option",
{
ATTR_ENTITY_ID: "select.pool_device_orp_dosing_method",
ATTR_OPTION: "proportional",
},
blocking=True,
)
# Verify API call
mock_pooldose_client.set_select.assert_called_once_with(
"orp_type_dosing_method", "proportional"
)
# Verify state
orp_method_state = hass.states.get("select.pool_device_orp_dosing_method")
assert orp_method_state.state == "proportional"
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_select_dosing_set_high_low(
hass: HomeAssistant,
mock_pooldose_client: AsyncMock,
) -> None:
"""Test selecting high/low dosing intensity."""
# Chlorine dosing set starts as high in fixture
cl_set_state = hass.states.get("select.pool_device_chlorine_dosing_set")
assert cl_set_state.state == "high"
# Change to low
await hass.services.async_call(
SELECT_DOMAIN,
"select_option",
{
ATTR_ENTITY_ID: "select.pool_device_chlorine_dosing_set",
ATTR_OPTION: "low",
},
blocking=True,
)
# Verify API call
mock_pooldose_client.set_select.assert_called_once_with("cl_type_dosing_set", "low")
# Verify state
cl_set_state = hass.states.get("select.pool_device_chlorine_dosing_set")
assert cl_set_state.state == "low"