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:
@@ -20,6 +20,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
160
homeassistant/components/pooldose/select.py
Normal file
160
homeassistant/components/pooldose/select.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
469
tests/components/pooldose/snapshots/test_select.ambr
Normal file
469
tests/components/pooldose/snapshots/test_select.ambr
Normal 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³',
|
||||
})
|
||||
# ---
|
||||
242
tests/components/pooldose/test_select.py
Normal file
242
tests/components/pooldose/test_select.py
Normal 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"
|
||||
Reference in New Issue
Block a user