mirror of
https://github.com/home-assistant/core.git
synced 2026-11-05 13:11:29 -04:00
Add pool light scheduling to Vistapool (#180686)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""Tests for the Vistapool select platform."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
@@ -13,12 +14,17 @@ from homeassistant.components.select import (
|
||||
DOMAIN as SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
EVENT_STATE_CHANGED,
|
||||
STATE_UNKNOWN,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.common import MockConfigEntry, async_capture_events, snapshot_platform
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -215,3 +221,304 @@ async def test_select_option_raises_on_api_error(
|
||||
blocking=True,
|
||||
)
|
||||
assert excinfo.value.translation_key == "set_failed"
|
||||
|
||||
|
||||
_LIGHT_SCHEDULE_DATA = {
|
||||
"main": {"version": 1},
|
||||
"light": {"mode": 1, "status": 0, "freq": 86400, "from": 79200, "to": 3600},
|
||||
}
|
||||
|
||||
|
||||
async def test_light_selects_not_created_without_scheduling(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
mock_pool_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test controllers without light scheduling do not get the light selects."""
|
||||
mock_vistapool_client.fetch_pool_data.return_value = mock_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("select.my_pool_light_mode") is None
|
||||
assert hass.states.get("select.my_pool_light_schedule_frequency") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mode", "status", "expected"),
|
||||
[
|
||||
pytest.param(1, 0, "auto", id="schedule_armed"),
|
||||
pytest.param(1, 1, "auto", id="schedule_armed_while_on"),
|
||||
pytest.param(0, 1, "on", id="manual_on"),
|
||||
pytest.param(0, 0, "off", id="manual_off"),
|
||||
],
|
||||
)
|
||||
async def test_light_mode_current_option(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
mode: int,
|
||||
status: int,
|
||||
expected: str,
|
||||
) -> None:
|
||||
"""Test the armed schedule wins over the on/off state."""
|
||||
data = deepcopy(_LIGHT_SCHEDULE_DATA)
|
||||
data["light"]["mode"] = mode
|
||||
data["light"]["status"] = status
|
||||
mock_vistapool_client.fetch_pool_data.return_value = 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 == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("option", "expected_updates"),
|
||||
[
|
||||
pytest.param("off", {"light.mode": 0, "light.status": 0}, id="off"),
|
||||
pytest.param("on", {"light.mode": 0, "light.status": 1}, id="on"),
|
||||
pytest.param("auto", {"light.mode": 1}, id="auto"),
|
||||
],
|
||||
)
|
||||
async def test_light_mode_select_writes_one_command(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
option: str,
|
||||
expected_updates: dict[str, int],
|
||||
) -> None:
|
||||
"""Test each option lands as a single multi-field command.
|
||||
|
||||
Writing the fields separately would leave the controller half-applied
|
||||
between the two commands.
|
||||
"""
|
||||
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()
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: "select.my_pool_light_mode", ATTR_OPTION: option},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_vistapool_client.set_values.assert_awaited_once_with(
|
||||
"ABCDEF1234567890", expected_updates
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raw", "expected"),
|
||||
[
|
||||
pytest.param(86400, "daily", id="daily"),
|
||||
pytest.param(604800, "weekly", id="weekly"),
|
||||
pytest.param(12345, None, id="unknown_value"),
|
||||
],
|
||||
)
|
||||
async def test_light_schedule_frequency_maps_raw_values(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
raw: int,
|
||||
expected: str | None,
|
||||
) -> None:
|
||||
"""Test the frequency maps by raw seconds, not by option index."""
|
||||
data = deepcopy(_LIGHT_SCHEDULE_DATA)
|
||||
data["light"]["freq"] = raw
|
||||
mock_vistapool_client.fetch_pool_data.return_value = 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()
|
||||
|
||||
state = hass.states.get("select.my_pool_light_schedule_frequency").state
|
||||
assert state == (expected or STATE_UNKNOWN)
|
||||
|
||||
|
||||
async def test_light_schedule_frequency_writes_raw_value(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test selecting a frequency writes its raw seconds value."""
|
||||
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()
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{
|
||||
ATTR_ENTITY_ID: "select.my_pool_light_schedule_frequency",
|
||||
ATTR_OPTION: "weekly",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_vistapool_client.set_value.assert_awaited_once_with(
|
||||
"ABCDEF1234567890", "light.freq", 604800
|
||||
)
|
||||
|
||||
|
||||
async def test_select_reflects_choice_before_push(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
mock_pool_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test a select shows the chosen option without waiting for the push."""
|
||||
mock_vistapool_client.fetch_pool_data.return_value = mock_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()
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: "select.my_pool_pump_speed", ATTR_OPTION: "high"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert hass.states.get("select.my_pool_pump_speed").state == "high"
|
||||
|
||||
|
||||
async def test_light_mode_reflects_choice_before_push(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the light mode select applies every field of the chosen option."""
|
||||
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"
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: "select.my_pool_light_mode", ATTR_OPTION: "on"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Reads back as on only if both light.mode and light.status were applied.
|
||||
assert hass.states.get("select.my_pool_light_mode").state == "on"
|
||||
|
||||
|
||||
async def test_light_schedule_frequency_reflects_choice_before_push(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the frequency select shows the chosen option immediately."""
|
||||
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()
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{
|
||||
ATTR_ENTITY_ID: "select.my_pool_light_schedule_frequency",
|
||||
ATTR_OPTION: "weekly",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert hass.states.get("select.my_pool_light_schedule_frequency").state == "weekly"
|
||||
|
||||
|
||||
async def test_light_mode_never_publishes_partial_state(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test leaving auto does not briefly read as another option.
|
||||
|
||||
light.mode and light.status both feed current_option, so applying them
|
||||
one at a time would publish an off state between the two writes.
|
||||
"""
|
||||
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()
|
||||
|
||||
events = async_capture_events(hass, EVENT_STATE_CHANGED)
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: "select.my_pool_light_mode", ATTR_OPTION: "on"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
states = [
|
||||
event.data["new_state"].state
|
||||
for event in events
|
||||
if event.data["entity_id"] == "select.my_pool_light_mode"
|
||||
]
|
||||
assert states == ["on"]
|
||||
|
||||
|
||||
async def test_light_mode_raises_on_api_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test a failed multi-field write raises and leaves the state alone."""
|
||||
mock_vistapool_client.fetch_pool_data.return_value = deepcopy(_LIGHT_SCHEDULE_DATA)
|
||||
mock_vistapool_client.set_values.side_effect = AquariteError("boom")
|
||||
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"
|
||||
|
||||
with pytest.raises(HomeAssistantError) as excinfo:
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: "select.my_pool_light_mode", ATTR_OPTION: "on"},
|
||||
blocking=True,
|
||||
)
|
||||
assert excinfo.value.translation_key == "set_failed"
|
||||
|
||||
# The write never reached the controller, so nothing may be applied.
|
||||
assert hass.states.get("select.my_pool_light_mode").state == "auto"
|
||||
|
||||
|
||||
async def test_light_schedule_frequency_created_for_zero_value(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test a reported zero still creates the entity.
|
||||
|
||||
Zero is a value the controller reports, not a missing field, so it must
|
||||
surface as an unknown option rather than silently dropping the entity.
|
||||
"""
|
||||
data = deepcopy(_LIGHT_SCHEDULE_DATA)
|
||||
data["light"]["freq"] = 0
|
||||
mock_vistapool_client.fetch_pool_data.return_value = 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()
|
||||
|
||||
state = hass.states.get("select.my_pool_light_schedule_frequency")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests for the Vistapool time platform."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
@@ -177,3 +178,87 @@ async def test_time_set_value_raises_on_api_error(
|
||||
blocking=True,
|
||||
)
|
||||
assert excinfo.value.translation_key == "set_failed"
|
||||
|
||||
|
||||
_LIGHT_SCHEDULE_DATA = {
|
||||
"main": {"version": 1},
|
||||
"light": {"mode": 1, "status": 0, "from": 79200, "to": 3600},
|
||||
}
|
||||
|
||||
|
||||
async def test_light_schedule_times_not_created_without_scheduling(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
mock_pool_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test controllers without light scheduling do not get the light times."""
|
||||
mock_vistapool_client.fetch_pool_data.return_value = mock_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("time.my_pool_light_schedule_start") is None
|
||||
assert hass.states.get("time.my_pool_light_schedule_end") is None
|
||||
|
||||
|
||||
async def test_light_schedule_times_decode_seconds(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the light schedule bounds decode from seconds since midnight."""
|
||||
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()
|
||||
|
||||
# 79200 is 22:00, 3600 is 01:00 the next morning.
|
||||
assert hass.states.get("time.my_pool_light_schedule_start").state == "22:00:00"
|
||||
assert hass.states.get("time.my_pool_light_schedule_end").state == "01:00:00"
|
||||
|
||||
|
||||
async def test_light_schedule_time_set_value(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test setting a light schedule bound writes seconds since midnight."""
|
||||
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()
|
||||
|
||||
await hass.services.async_call(
|
||||
TIME_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: "time.my_pool_light_schedule_start", ATTR_TIME: "21:30:00"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_vistapool_client.set_value.assert_awaited_once_with(
|
||||
"ABCDEF1234567890", "light.from", 77400
|
||||
)
|
||||
|
||||
|
||||
async def test_light_schedule_time_created_for_midnight(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test a schedule bound of zero seconds still creates the entity.
|
||||
|
||||
Zero is midnight, a legitimate schedule bound, not a missing field.
|
||||
"""
|
||||
data = deepcopy(_LIGHT_SCHEDULE_DATA)
|
||||
data["light"]["from"] = 0
|
||||
mock_vistapool_client.fetch_pool_data.return_value = 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("time.my_pool_light_schedule_start").state == "00:00:00"
|
||||
|
||||
Reference in New Issue
Block a user