Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions homeassistant/components/pooldose/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
]
Expand Down
9 changes: 5 additions & 4 deletions homeassistant/components/pooldose/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@
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,
}
26 changes: 26 additions & 0 deletions homeassistant/components/pooldose/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
160 changes: 160 additions & 0 deletions homeassistant/components/pooldose/select.py
Original file line number Diff line number Diff line change
@@ -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()
Comment thread
lmaertin marked this conversation as resolved.
14 changes: 7 additions & 7 deletions homeassistant/components/pooldose/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@
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(
Expand All @@ -57,14 +57,14 @@ class PooldoseSensorEntityDescription(SensorEntityDescription):
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",
Expand Down Expand Up @@ -227,12 +227,12 @@ def native_value(self) -> float | int | str | None:
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
56 changes: 56 additions & 0 deletions homeassistant/components/pooldose/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions tests/components/pooldose/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
25 changes: 23 additions & 2 deletions tests/components/pooldose/fixtures/instantvalues.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"flow_rate": {
"value": 150,
"unit": "l/s"
"unit": "L/s"
},
"ph_type_dosing": {
"value": "alcalyne",
Expand Down Expand Up @@ -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"
}
}
}
Loading
Loading