mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 01:11:51 -04:00
Bring every Vistapool module above 95% test coverage (#182482)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
b6fb2f7e90
commit
4a6b3a2d7e
@@ -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}"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user