Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
158 changes: 158 additions & 0 deletions homeassistant/components/pooldose/select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"""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,
options=[UnitOfVolume.LITERS, UnitOfVolume.CUBIC_METERS],
use_unit_conversion=True,
),
PooldoseSelectEntityDescription(
key="flow_rate_unit",
translation_key="flow_rate_unit",
entity_category=EntityCategory.CONFIG,
options=[
UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
UnitOfVolumeFlowRate.LITERS_PER_SECOND,
],
use_unit_conversion=True,
),
Comment thread
lmaertin marked this conversation as resolved.
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