1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-25 05:26:47 +00:00

Pooldose: add number platform (#157787)

This commit is contained in:
Lukas
2025-12-02 23:31:44 +01:00
committed by GitHub
parent 5112742b71
commit 90dc3a8fdf
7 changed files with 888 additions and 1 deletions

View File

@@ -17,7 +17,12 @@ from .coordinator import PooldoseConfigEntry, PooldoseCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.NUMBER,
Platform.SENSOR,
Platform.SWITCH,
]
async def async_migrate_entry(hass: HomeAssistant, entry: PooldoseConfigEntry) -> bool:

View File

@@ -68,6 +68,35 @@
}
}
},
"number": {
"cl_target": {
"default": "mdi:pool"
},
"ofa_cl_lower": {
"default": "mdi:arrow-down-bold"
},
"ofa_cl_upper": {
"default": "mdi:arrow-up-bold"
},
"ofa_orp_lower": {
"default": "mdi:arrow-down-bold"
},
"ofa_orp_upper": {
"default": "mdi:arrow-up-bold"
},
"ofa_ph_lower": {
"default": "mdi:arrow-down-bold"
},
"ofa_ph_upper": {
"default": "mdi:arrow-up-bold"
},
"orp_target": {
"default": "mdi:water-check"
},
"ph_target": {
"default": "mdi:ph"
}
},
"sensor": {
"cl": {
"default": "mdi:pool"

View File

@@ -0,0 +1,142 @@
"""Number entities for the Seko PoolDose integration."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any, cast
from homeassistant.components.number import (
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
EntityCategory,
UnitOfElectricPotential,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import PooldoseConfigEntry
from .entity import PooldoseEntity
if TYPE_CHECKING:
from .coordinator import PooldoseCoordinator
_LOGGER = logging.getLogger(__name__)
NUMBER_DESCRIPTIONS: tuple[NumberEntityDescription, ...] = (
NumberEntityDescription(
key="ph_target",
translation_key="ph_target",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.PH,
),
NumberEntityDescription(
key="orp_target",
translation_key="orp_target",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
),
NumberEntityDescription(
key="cl_target",
translation_key="cl_target",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
NumberEntityDescription(
key="ofa_ph_lower",
translation_key="ofa_ph_lower",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.PH,
),
NumberEntityDescription(
key="ofa_ph_upper",
translation_key="ofa_ph_upper",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.PH,
),
NumberEntityDescription(
key="ofa_orp_lower",
translation_key="ofa_orp_lower",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
),
NumberEntityDescription(
key="ofa_orp_upper",
translation_key="ofa_orp_upper",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
),
NumberEntityDescription(
key="ofa_cl_lower",
translation_key="ofa_cl_lower",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
NumberEntityDescription(
key="ofa_cl_upper",
translation_key="ofa_cl_upper",
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: PooldoseConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up PoolDose number entities from a config entry."""
if TYPE_CHECKING:
assert config_entry.unique_id is not None
coordinator = config_entry.runtime_data
number_data = coordinator.data.get("number", {})
serial_number = config_entry.unique_id
async_add_entities(
PooldoseNumber(coordinator, serial_number, coordinator.device_info, description)
for description in NUMBER_DESCRIPTIONS
if description.key in number_data
)
class PooldoseNumber(PooldoseEntity, NumberEntity):
"""Number entity for the Seko PoolDose Python API."""
def __init__(
self,
coordinator: PooldoseCoordinator,
serial_number: str,
device_info: Any,
description: NumberEntityDescription,
) -> None:
"""Initialize the number."""
super().__init__(coordinator, serial_number, device_info, description, "number")
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 number attributes."""
data = cast(dict, self.get_data())
self._attr_native_value = data["value"]
self._attr_native_min_value = data["min"]
self._attr_native_max_value = data["max"]
self._attr_native_step = data["step"]
async def async_set_native_value(self, value: float) -> None:
"""Set new value."""
await self.coordinator.client.set_number(self.entity_description.key, value)
self._attr_native_value = value
self.async_write_ha_state()

View File

@@ -68,6 +68,35 @@
"name": "Auxiliary relay 3 status"
}
},
"number": {
"cl_target": {
"name": "Chlorine target"
},
"ofa_cl_lower": {
"name": "Chlorine overfeed alarm lower limit"
},
"ofa_cl_upper": {
"name": "Chlorine overfeed alarm upper limit"
},
"ofa_orp_lower": {
"name": "ORP overfeed alarm lower limit"
},
"ofa_orp_upper": {
"name": "ORP overfeed alarm upper limit"
},
"ofa_ph_lower": {
"name": "pH overfeed alarm lower limit"
},
"ofa_ph_upper": {
"name": "pH overfeed alarm upper limit"
},
"orp_target": {
"name": "ORP target"
},
"ph_target": {
"name": "pH target"
}
},
"sensor": {
"cl": {
"name": "Chlorine"

View File

@@ -141,6 +141,48 @@
"min": 0,
"max": 65535,
"step": 0.01
},
"ofa_ph_lower": {
"value": 6,
"unit": null,
"min": 0,
"max": 14,
"step": 0.1
},
"ofa_ph_upper": {
"value": 8,
"unit": null,
"min": 0,
"max": 14,
"step": 0.1
},
"ofa_orp_lower": {
"value": 600,
"unit": "mV",
"min": 0,
"max": 1000,
"step": 1
},
"ofa_orp_upper": {
"value": 800,
"unit": "mV",
"min": 0,
"max": 1000,
"step": 1
},
"ofa_cl_lower": {
"value": 0.2,
"unit": "ppm",
"min": 0,
"max": 10,
"step": 0.1
},
"ofa_cl_upper": {
"value": 0.9,
"unit": "ppm",
"min": 0,
"max": 10,
"step": 0.1
}
},
"switch": {

View File

@@ -0,0 +1,526 @@
# serializer version: 1
# name: test_all_numbers[number.pool_device_chlorine_overfeed_alarm_lower_limit-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 10,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.pool_device_chlorine_overfeed_alarm_lower_limit',
'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 overfeed alarm lower limit',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'ofa_cl_lower',
'unique_id': 'TEST123456789_ofa_cl_lower',
'unit_of_measurement': 'ppm',
})
# ---
# name: test_all_numbers[number.pool_device_chlorine_overfeed_alarm_lower_limit-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Pool Device Chlorine overfeed alarm lower limit',
'max': 10,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
'unit_of_measurement': 'ppm',
}),
'context': <ANY>,
'entity_id': 'number.pool_device_chlorine_overfeed_alarm_lower_limit',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.2',
})
# ---
# name: test_all_numbers[number.pool_device_chlorine_overfeed_alarm_upper_limit-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 10,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.pool_device_chlorine_overfeed_alarm_upper_limit',
'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 overfeed alarm upper limit',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'ofa_cl_upper',
'unique_id': 'TEST123456789_ofa_cl_upper',
'unit_of_measurement': 'ppm',
})
# ---
# name: test_all_numbers[number.pool_device_chlorine_overfeed_alarm_upper_limit-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Pool Device Chlorine overfeed alarm upper limit',
'max': 10,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
'unit_of_measurement': 'ppm',
}),
'context': <ANY>,
'entity_id': 'number.pool_device_chlorine_overfeed_alarm_upper_limit',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.9',
})
# ---
# name: test_all_numbers[number.pool_device_chlorine_target-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 65535,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.01,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.pool_device_chlorine_target',
'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 target',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'cl_target',
'unique_id': 'TEST123456789_cl_target',
'unit_of_measurement': 'ppm',
})
# ---
# name: test_all_numbers[number.pool_device_chlorine_target-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Pool Device Chlorine target',
'max': 65535,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.01,
'unit_of_measurement': 'ppm',
}),
'context': <ANY>,
'entity_id': 'number.pool_device_chlorine_target',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1',
})
# ---
# name: test_all_numbers[number.pool_device_orp_overfeed_alarm_lower_limit-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 1000,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.pool_device_orp_overfeed_alarm_lower_limit',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.VOLTAGE: 'voltage'>,
'original_icon': None,
'original_name': 'ORP overfeed alarm lower limit',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'ofa_orp_lower',
'unique_id': 'TEST123456789_ofa_orp_lower',
'unit_of_measurement': <UnitOfElectricPotential.MILLIVOLT: 'mV'>,
})
# ---
# name: test_all_numbers[number.pool_device_orp_overfeed_alarm_lower_limit-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'voltage',
'friendly_name': 'Pool Device ORP overfeed alarm lower limit',
'max': 1000,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': <UnitOfElectricPotential.MILLIVOLT: 'mV'>,
}),
'context': <ANY>,
'entity_id': 'number.pool_device_orp_overfeed_alarm_lower_limit',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '600',
})
# ---
# name: test_all_numbers[number.pool_device_orp_overfeed_alarm_upper_limit-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 1000,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.pool_device_orp_overfeed_alarm_upper_limit',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.VOLTAGE: 'voltage'>,
'original_icon': None,
'original_name': 'ORP overfeed alarm upper limit',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'ofa_orp_upper',
'unique_id': 'TEST123456789_ofa_orp_upper',
'unit_of_measurement': <UnitOfElectricPotential.MILLIVOLT: 'mV'>,
})
# ---
# name: test_all_numbers[number.pool_device_orp_overfeed_alarm_upper_limit-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'voltage',
'friendly_name': 'Pool Device ORP overfeed alarm upper limit',
'max': 1000,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': <UnitOfElectricPotential.MILLIVOLT: 'mV'>,
}),
'context': <ANY>,
'entity_id': 'number.pool_device_orp_overfeed_alarm_upper_limit',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '800',
})
# ---
# name: test_all_numbers[number.pool_device_orp_target-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 850,
'min': 400,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.pool_device_orp_target',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.VOLTAGE: 'voltage'>,
'original_icon': None,
'original_name': 'ORP target',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'orp_target',
'unique_id': 'TEST123456789_orp_target',
'unit_of_measurement': <UnitOfElectricPotential.MILLIVOLT: 'mV'>,
})
# ---
# name: test_all_numbers[number.pool_device_orp_target-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'voltage',
'friendly_name': 'Pool Device ORP target',
'max': 850,
'min': 400,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': <UnitOfElectricPotential.MILLIVOLT: 'mV'>,
}),
'context': <ANY>,
'entity_id': 'number.pool_device_orp_target',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '680',
})
# ---
# name: test_all_numbers[number.pool_device_ph_overfeed_alarm_lower_limit-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 14,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.pool_device_ph_overfeed_alarm_lower_limit',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.PH: 'ph'>,
'original_icon': None,
'original_name': 'pH overfeed alarm lower limit',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'ofa_ph_lower',
'unique_id': 'TEST123456789_ofa_ph_lower',
'unit_of_measurement': None,
})
# ---
# name: test_all_numbers[number.pool_device_ph_overfeed_alarm_lower_limit-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'ph',
'friendly_name': 'Pool Device pH overfeed alarm lower limit',
'max': 14,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
}),
'context': <ANY>,
'entity_id': 'number.pool_device_ph_overfeed_alarm_lower_limit',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '6',
})
# ---
# name: test_all_numbers[number.pool_device_ph_overfeed_alarm_upper_limit-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 14,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.pool_device_ph_overfeed_alarm_upper_limit',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.PH: 'ph'>,
'original_icon': None,
'original_name': 'pH overfeed alarm upper limit',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'ofa_ph_upper',
'unique_id': 'TEST123456789_ofa_ph_upper',
'unit_of_measurement': None,
})
# ---
# name: test_all_numbers[number.pool_device_ph_overfeed_alarm_upper_limit-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'ph',
'friendly_name': 'Pool Device pH overfeed alarm upper limit',
'max': 14,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
}),
'context': <ANY>,
'entity_id': 'number.pool_device_ph_overfeed_alarm_upper_limit',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '8',
})
# ---
# name: test_all_numbers[number.pool_device_ph_target-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 8,
'min': 6,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.pool_device_ph_target',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.PH: 'ph'>,
'original_icon': None,
'original_name': 'pH target',
'platform': 'pooldose',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'ph_target',
'unique_id': 'TEST123456789_ph_target',
'unit_of_measurement': None,
})
# ---
# name: test_all_numbers[number.pool_device_ph_target-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'ph',
'friendly_name': 'Pool Device pH target',
'max': 8,
'min': 6,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
}),
'context': <ANY>,
'entity_id': 'number.pool_device_ph_target',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '6.5',
})
# ---

View File

@@ -0,0 +1,114 @@
"""Tests for the Seko PoolDose number platform."""
from copy import deepcopy
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.number import DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE
from homeassistant.const import ATTR_ENTITY_ID, 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.fixture
def platforms() -> list[Platform]:
"""Fixture to specify platforms to test."""
return [Platform.NUMBER]
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_all_numbers(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the Pooldose numbers."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.usefixtures("init_integration")
async def test_number_entity_unavailable_no_coordinator_data(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_pooldose_client: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test number entity becomes unavailable when coordinator has no data."""
# Verify entity has a state initially
ph_target_state = hass.states.get("number.pool_device_ph_target")
assert ph_target_state.state == "6.5"
# 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
ph_target_state = hass.states.get("number.pool_device_ph_target")
assert ph_target_state.state == "unavailable"
@pytest.mark.usefixtures("init_integration")
async def test_number_state_changes(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_pooldose_client: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test number state changes when coordinator updates."""
# Initial state
ph_target_state = hass.states.get("number.pool_device_ph_target")
assert ph_target_state.state == "6.5"
# Update coordinator data with number value changed
current_data = mock_pooldose_client.instant_values_structured.return_value[1]
updated_data = deepcopy(current_data)
updated_data["number"]["ph_target"]["value"] = 7.2
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_target_state = hass.states.get("number.pool_device_ph_target")
assert ph_target_state.state == "7.2"
@pytest.mark.usefixtures("init_integration")
async def test_set_number_value(
hass: HomeAssistant,
mock_pooldose_client: AsyncMock,
) -> None:
"""Test setting a number value."""
# Verify initial state
ph_target_state = hass.states.get("number.pool_device_ph_target")
assert ph_target_state.state == "6.5"
# Set new value
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: "number.pool_device_ph_target", "value": 7.0},
blocking=True,
)
# Verify API was called
mock_pooldose_client.set_number.assert_called_once_with("ph_target", 7.0)
# Verify state updated immediately (optimistic update)
ph_target_state = hass.states.get("number.pool_device_ph_target")
assert ph_target_state.state == "7.0"