Bring every Vistapool module above 95% test coverage (#182482)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
fdebrus
2026-09-19 13:21:23 +02:00
committed by GitHub
co-authored by Claude Fable 5.1
parent b6fb2f7e90
commit 4a6b3a2d7e
5 changed files with 81 additions and 37 deletions
@@ -32,16 +32,6 @@ class VistapoolEntity(CoordinatorEntity[VistapoolDataUpdateCoordinator]):
"""Return if entity is available."""
return super().available and self.coordinator.push_connected
@property
def pool_id(self) -> str:
"""Return the pool ID for the entity."""
return self.coordinator.pool_id
@property
def pool_name(self) -> str:
"""Return the friendly pool name for the entity."""
return self.coordinator.pool_name
def build_unique_id(self, suffix: str) -> str:
"""Return a consistent unique ID for the entity."""
return f"{self.coordinator.pool_id}-{suffix}"
+6 -9
View File
@@ -44,13 +44,12 @@ class VistapoolNumberEntityDescription(NumberEntityDescription):
def _max_electrolysis(coordinator: VistapoolDataUpdateCoordinator) -> float:
"""Read the cell's hardware max, falling back to a safe default."""
# The path is typed in the library's coercion map, so an unparsable value
# already comes back as None rather than reaching float().
raw = coordinator.get_value("hidro.maxAllowedValue")
if raw is None:
return 50.0
try:
return float(raw) / 10
except TypeError, ValueError:
return 50.0
return float(raw) / 10
NUMBER_DESCRIPTIONS: tuple[VistapoolNumberEntityDescription, ...] = (
@@ -231,14 +230,12 @@ class VistapoolNumber(VistapoolEntity, NumberEntity):
@override
def native_value(self) -> float | None:
"""Return the scaled current value."""
# Every number path is typed in the library's coercion map, so an
# unparsable value already comes back as None rather than reaching float().
raw = self.coordinator.get_value(self.entity_description.value_path)
if raw is None:
return None
try:
value = float(raw)
except TypeError, ValueError:
return None
return value / self.entity_description.scale
return float(raw) / self.entity_description.scale
@override
async def async_set_native_value(self, value: float) -> None:
+8 -17
View File
@@ -41,10 +41,8 @@ class VistapoolSelectEntityDescription(SelectEntityDescription):
"""Describes a Vistapool select entity."""
value_path: str
# A capability flag that must be set, such as main.hasPH.
exists_path: str | tuple[str, ...] | None = None
# A field the controller only reports when it supports the feature. Unlike
# exists_path this is a presence check, so a valid zero still counts.
# A field the controller only reports when it supports the feature. This
# is a presence check, so a valid zero still counts.
presence_path: str | None = None
value_map: dict[str, int] | None = None
@@ -93,14 +91,6 @@ def _build_select_entities(
"""Build the select entities for a single pool."""
entities: list[SelectEntity] = []
for description in SELECT_DESCRIPTIONS:
if description.exists_path is not None:
required = (
(description.exists_path,)
if isinstance(description.exists_path, str)
else description.exists_path
)
if not all(coordinator.get_value(path) for path in required):
continue
if (
description.presence_path is not None
and coordinator.get_value(description.presence_path) is None
@@ -135,13 +125,14 @@ async def async_setup_entry(
def _to_index(raw: Any) -> int | None:
"""Convert a coordinator value into an options-list index, or None if not possible."""
"""Convert a coordinator value into an options-list index, or None if missing.
Every select path is typed in the library's coercion map, so get_value
already returns an int or None; an unparsable value never reaches here.
"""
if raw is None:
return None
try:
return int(raw)
except TypeError, ValueError:
return None
return int(raw)
class VistapoolSelect(VistapoolEntity, SelectEntity):
+39 -1
View File
@@ -13,7 +13,7 @@ from homeassistant.components.number import (
DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
@@ -113,6 +113,44 @@ async def test_number_electrolysis_max_fallback(
assert state.attributes["max"] == 50.0
@pytest.mark.parametrize(
("entity_id", "pool_data"),
[
pytest.param(
"number.my_pool_ph_maximum",
{
"main": {"hasPH": 1, "version": 1},
"modules": {"ph": {"status": {"high_value": "garbage"}}},
},
id="ph_maximum",
),
pytest.param(
"number.my_pool_redox_setpoint",
{
"main": {"hasRX": 1, "version": 1},
"modules": {"rx": {"status": {"value": "garbage"}}},
},
id="redox_setpoint",
),
],
)
async def test_number_unknown_when_unparsable(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_vistapool_client: AsyncMock,
entity_id: str,
pool_data: dict[str, Any],
) -> None:
"""Test an unparsable raw setpoint reads as unknown rather than raising."""
mock_vistapool_client.fetch_pool_data.return_value = pool_data
mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_UNKNOWN
async def test_number_hydrolysis_setpoint_branch(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
+28
View File
@@ -276,6 +276,34 @@ async def test_light_mode_current_option(
assert hass.states.get("select.my_pool_light_mode").state == expected
@pytest.mark.parametrize(
"light_data",
[
pytest.param({"status": 0}, id="mode_missing"),
pytest.param({"mode": 0}, id="status_missing"),
],
)
async def test_light_mode_unknown_after_partial_push(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_vistapool_client: AsyncMock,
light_data: dict[str, Any],
) -> None:
"""Test the light mode reports unknown when a push drops a field it derives from."""
mock_vistapool_client.fetch_pool_data.return_value = deepcopy(_LIGHT_SCHEDULE_DATA)
mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get("select.my_pool_light_mode").state == "auto"
on_data = mock_vistapool_client.subscribe_pool_resilient.call_args.args[1]
on_data({"main": {"version": 1}, "light": light_data})
await hass.async_block_till_done()
assert hass.states.get("select.my_pool_light_mode").state == STATE_UNKNOWN
@pytest.mark.parametrize(
("option", "expected_updates"),
[