mirror of
https://github.com/home-assistant/core.git
synced 2026-10-07 14:58:11 -04:00
Bump aioaquarite to 0.13.0 and drop the Vistapool optimistic layer (#184132)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
774da58f54
commit
81e7dd9fad
@@ -1,6 +1,5 @@
|
||||
"""Vistapool Button entities."""
|
||||
|
||||
import asyncio
|
||||
from typing import override
|
||||
|
||||
from aioaquarite import AquariteError
|
||||
@@ -60,7 +59,8 @@ class VistapoolLEDPulseButton(VistapoolEntity, ButtonEntity):
|
||||
Mirrors the "Next" button under LED Color in the Vistapool app's
|
||||
Illumination screen. If the light is on, sends light.status=0, waits a
|
||||
moment, then light.status=1; the physical LED fixture advances to the
|
||||
next color on power-on. If the light is off, just turns it on.
|
||||
next color on power-on. If the light is off, just turns it on. The
|
||||
library runs the sequence, so the light never shows the intermediate off.
|
||||
"""
|
||||
|
||||
_attr_translation_key = "led_pulse"
|
||||
@@ -74,18 +74,21 @@ class VistapoolLEDPulseButton(VistapoolEntity, ButtonEntity):
|
||||
async def async_press(self) -> None:
|
||||
"""Send a color-advance pulse to the pool LED fixture."""
|
||||
try:
|
||||
if self.coordinator.get_value(_LIGHT_STATUS_PATH) in (True, "1"):
|
||||
if self.coordinator.get_value(_LIGHT_STATUS_PATH) == 1:
|
||||
await self.coordinator.api.pulse(
|
||||
self.coordinator.pool_id,
|
||||
_LIGHT_STATUS_PATH,
|
||||
0,
|
||||
1,
|
||||
_LED_PULSE_DELAY_SECONDS,
|
||||
)
|
||||
else:
|
||||
await self.coordinator.api.set_value(
|
||||
self.coordinator.pool_id, _LIGHT_STATUS_PATH, 0
|
||||
self.coordinator.pool_id, _LIGHT_STATUS_PATH, 1
|
||||
)
|
||||
await asyncio.sleep(_LED_PULSE_DELAY_SECONDS)
|
||||
await self.coordinator.api.set_value(
|
||||
self.coordinator.pool_id, _LIGHT_STATUS_PATH, 1
|
||||
)
|
||||
except AquariteError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_failed",
|
||||
translation_placeholders={"entity": self.entity_id},
|
||||
) from err
|
||||
self.coordinator.apply_optimistic(_LIGHT_STATUS_PATH, 1)
|
||||
|
||||
@@ -68,41 +68,38 @@ class VistapoolDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
return self._push_connected
|
||||
|
||||
async def subscribe(self) -> None:
|
||||
"""Subscribe to Firestore real-time updates via the library."""
|
||||
|
||||
def _on_data(data: dict[str, Any]) -> None:
|
||||
"""Callback from the Firestore thread; push data to the HA loop."""
|
||||
self.hass.loop.call_soon_threadsafe(self._async_handle_push, data)
|
||||
"""Subscribe to Firestore real-time updates via the library.
|
||||
|
||||
The library invokes the data callback on the event loop, with
|
||||
acknowledged writes already reflected, so deliveries are published
|
||||
as they come.
|
||||
"""
|
||||
self.subscription = await self.api.subscribe_pool_resilient(
|
||||
self.pool_id, _on_data, on_health=self._async_on_subscription_health
|
||||
self.pool_id,
|
||||
self.async_set_updated_data,
|
||||
on_health=self._async_on_subscription_health,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_handle_push(self, data: dict[str, Any]) -> None:
|
||||
"""Apply a snapshot; its arrival is what proves the connection is up."""
|
||||
if not self._push_connected:
|
||||
self._push_connected = True
|
||||
_LOGGER.info("Reconnected to %s, entities are available again", self.name)
|
||||
self.async_set_updated_data(data)
|
||||
|
||||
@callback
|
||||
def _async_on_subscription_health(self, healthy: bool) -> None:
|
||||
"""Mark entities unavailable while the push connection is down.
|
||||
"""Mirror the push connection state into entity availability.
|
||||
|
||||
Tracked separately from last_update_success: an optimistic update
|
||||
Tracked separately from last_update_success: an acknowledged write
|
||||
or a manual refresh sets that flag back to True while the
|
||||
subscription is still down, and the health callback only fires on
|
||||
transitions, so it would not correct it. Only an incoming snapshot
|
||||
clears this.
|
||||
subscription is still down. The library reports healthy only once a
|
||||
reconnected stream has delivered a consistent snapshot, so data
|
||||
arriving in between must not fake availability either.
|
||||
"""
|
||||
if healthy or not self._push_connected:
|
||||
if healthy == self._push_connected:
|
||||
return
|
||||
self._push_connected = False
|
||||
_LOGGER.warning(
|
||||
"Lost the connection to %s, entities are unavailable until it recovers",
|
||||
self.name,
|
||||
)
|
||||
self._push_connected = healthy
|
||||
if healthy:
|
||||
_LOGGER.info("Reconnected to %s, entities are available again", self.name)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Lost the connection to %s, entities are unavailable until it recovers",
|
||||
self.name,
|
||||
)
|
||||
self.async_update_listeners()
|
||||
|
||||
@override
|
||||
@@ -116,32 +113,3 @@ class VistapoolDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
def get_value(self, path: str, default: Any = None) -> Any:
|
||||
"""Get nested data using dot-notation path."""
|
||||
return AquariteClient.get_value(self.data, path, default)
|
||||
|
||||
def apply_optimistic(self, value_path: str, value: Any) -> None:
|
||||
"""Reflect a just-written value before the Firestore push round-trips.
|
||||
|
||||
Hayward's cloud takes several seconds to acknowledge a write back
|
||||
through Firestore, which would make the UI feel laggy. Writing into
|
||||
coordinator.data after a successful REST call gives entities instant
|
||||
feedback; the next snapshot from Firestore overwrites it harmlessly.
|
||||
"""
|
||||
self.apply_optimistic_values({value_path: value})
|
||||
|
||||
def apply_optimistic_values(self, updates: dict[str, Any]) -> None:
|
||||
"""Reflect several just-written values as a single update.
|
||||
|
||||
Applying them one at a time would publish a state where only part
|
||||
of the write has landed, which entities derived from more than one
|
||||
path briefly read as a different value.
|
||||
"""
|
||||
for value_path, value in updates.items():
|
||||
keys = value_path.split(".")
|
||||
target: dict[str, Any] = self.data
|
||||
for key in keys[:-1]:
|
||||
child = target.get(key)
|
||||
if not isinstance(child, dict):
|
||||
child = {}
|
||||
target[key] = child
|
||||
target = child
|
||||
target[keys[-1]] = value
|
||||
self.async_set_updated_data(self.data)
|
||||
|
||||
@@ -86,4 +86,3 @@ class VistapoolLight(VistapoolEntity, LightEntity):
|
||||
translation_key="set_failed",
|
||||
translation_placeholders={"entity": self.entity_id},
|
||||
) from err
|
||||
self.coordinator.apply_optimistic(_VALUE_PATH, value)
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioaquarite"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["aioaquarite==0.11.0"]
|
||||
"requirements": ["aioaquarite==0.13.0"]
|
||||
}
|
||||
|
||||
@@ -260,4 +260,3 @@ class VistapoolNumber(VistapoolEntity, NumberEntity):
|
||||
translation_key="set_failed",
|
||||
translation_placeholders={"entity": self.entity_id},
|
||||
) from err
|
||||
self.coordinator.apply_optimistic(self.entity_description.value_path, raw)
|
||||
|
||||
@@ -77,12 +77,6 @@ rules:
|
||||
stale-devices: done
|
||||
|
||||
# Platinum
|
||||
async-dependency:
|
||||
status: todo
|
||||
comment: >-
|
||||
Commands and Firestore reads are native async, but the real-time
|
||||
listener runs the synchronous google-cloud-firestore client in a
|
||||
thread: AsyncDocumentReference.on_snapshot raises NotImplementedError
|
||||
upstream, so the push subscription cannot be native async today.
|
||||
async-dependency: todo
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
|
||||
@@ -186,7 +186,6 @@ class VistapoolSelect(VistapoolEntity, SelectEntity):
|
||||
translation_key="set_failed",
|
||||
translation_placeholders={"entity": self.entity_id},
|
||||
) from err
|
||||
self.coordinator.apply_optimistic(self.entity_description.value_path, value)
|
||||
|
||||
|
||||
class VistapoolLightModeSelect(VistapoolEntity, SelectEntity):
|
||||
@@ -231,4 +230,3 @@ class VistapoolLightModeSelect(VistapoolEntity, SelectEntity):
|
||||
translation_key="set_failed",
|
||||
translation_placeholders={"entity": self.entity_id},
|
||||
) from err
|
||||
self.coordinator.apply_optimistic_values(updates)
|
||||
|
||||
@@ -170,4 +170,3 @@ class VistapoolSwitch(VistapoolEntity, SwitchEntity):
|
||||
translation_key="set_failed",
|
||||
translation_placeholders={"entity": self.entity_id},
|
||||
) from err
|
||||
self.coordinator.apply_optimistic(self.entity_description.value_path, value)
|
||||
|
||||
@@ -146,4 +146,3 @@ class VistapoolTime(VistapoolEntity, TimeEntity):
|
||||
translation_key="set_failed",
|
||||
translation_placeholders={"entity": self.entity_id},
|
||||
) from err
|
||||
self.coordinator.apply_optimistic(self.entity_description.value_path, seconds)
|
||||
|
||||
Generated
+1
-1
@@ -203,7 +203,7 @@ aioapcaccess==1.0.0
|
||||
aioaquacell==1.0.0
|
||||
|
||||
# homeassistant.components.vistapool
|
||||
aioaquarite==0.11.0
|
||||
aioaquarite==0.13.0
|
||||
|
||||
# homeassistant.components.aseko_pool_live
|
||||
aioaseko==1.0.0
|
||||
|
||||
@@ -73,9 +73,6 @@ def mock_vistapool_client(
|
||||
# default to an empty dict so always-on sensors come up with
|
||||
# `native_value=None` and module-gated sensors are skipped.
|
||||
client.fetch_pool_data = AsyncMock(return_value={})
|
||||
# The token-refresh loop awaits `auth.get_client()` and expects
|
||||
# `(client, refreshed)`.
|
||||
mock_vistapool_auth.get_client = AsyncMock(return_value=(client, False))
|
||||
with (
|
||||
patch("homeassistant.components.vistapool.AquariteClient", return_value=client),
|
||||
patch(
|
||||
|
||||
@@ -10,6 +10,7 @@ import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
||||
from homeassistant.components.vistapool.button import _LED_PULSE_DELAY_SECONDS
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -28,13 +29,6 @@ def _only_button_platform() -> Generator[None]:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _skip_pulse_delay() -> Generator[None]:
|
||||
"""Skip the LED pulse delay so tests don't actually sleep."""
|
||||
with patch("homeassistant.components.vistapool.button._LED_PULSE_DELAY_SECONDS", 0):
|
||||
yield
|
||||
|
||||
|
||||
async def test_all_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
@@ -97,7 +91,7 @@ async def test_button_press_when_light_on(
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test pressing the button when the light is on power-cycles it."""
|
||||
"""Test pressing the button when the light is on runs the library pulse."""
|
||||
mock_vistapool_client.fetch_pool_data.return_value = {
|
||||
"main": {"hasLED": 1, "version": 1},
|
||||
"light": {"status": 1},
|
||||
@@ -114,17 +108,10 @@ async def test_button_press_when_light_on(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_vistapool_client.set_value.await_count == 2
|
||||
assert mock_vistapool_client.set_value.await_args_list[0].args == (
|
||||
"ABCDEF1234567890",
|
||||
"light.status",
|
||||
0,
|
||||
)
|
||||
assert mock_vistapool_client.set_value.await_args_list[1].args == (
|
||||
"ABCDEF1234567890",
|
||||
"light.status",
|
||||
1,
|
||||
mock_vistapool_client.pulse.assert_awaited_once_with(
|
||||
"ABCDEF1234567890", "light.status", 0, 1, _LED_PULSE_DELAY_SECONDS
|
||||
)
|
||||
mock_vistapool_client.set_value.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_button_press_rapid_repeat_after_off(
|
||||
@@ -132,11 +119,11 @@ async def test_button_press_rapid_repeat_after_off(
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test a second press lands the off/on pulse instead of repeating turn-on.
|
||||
"""Test a second press lands the pulse instead of repeating turn-on.
|
||||
|
||||
Without the optimistic update, the second press would read the stale
|
||||
off-state (the Firestore push hasn't round-tripped yet) and send another
|
||||
bare light.status=1 — a no-op on the wire that doesn't advance the color.
|
||||
The library delivers the acknowledged turn-on through the data callback
|
||||
before the Firestore push round-trips, so the second press reads the
|
||||
light as on and pulses it instead of sending another bare on.
|
||||
"""
|
||||
mock_vistapool_client.fetch_pool_data.return_value = deepcopy(_LED_DATA)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
@@ -144,12 +131,21 @@ async def test_button_press_rapid_repeat_after_off(
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
on_data = mock_vistapool_client.subscribe_pool_resilient.call_args.args[1]
|
||||
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{ATTR_ENTITY_ID: _BUTTON},
|
||||
blocking=True,
|
||||
)
|
||||
mock_vistapool_client.set_value.assert_awaited_once_with(
|
||||
"ABCDEF1234567890", "light.status", 1
|
||||
)
|
||||
|
||||
on_data({"main": {"hasLED": 1, "version": 1}, "light": {"status": 1}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
@@ -157,32 +153,32 @@ async def test_button_press_rapid_repeat_after_off(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_vistapool_client.set_value.await_count == 3
|
||||
assert mock_vistapool_client.set_value.await_args_list[0].args == (
|
||||
"ABCDEF1234567890",
|
||||
"light.status",
|
||||
1,
|
||||
)
|
||||
assert mock_vistapool_client.set_value.await_args_list[1].args == (
|
||||
"ABCDEF1234567890",
|
||||
"light.status",
|
||||
0,
|
||||
)
|
||||
assert mock_vistapool_client.set_value.await_args_list[2].args == (
|
||||
"ABCDEF1234567890",
|
||||
"light.status",
|
||||
1,
|
||||
mock_vistapool_client.pulse.assert_awaited_once_with(
|
||||
"ABCDEF1234567890", "light.status", 0, 1, _LED_PULSE_DELAY_SECONDS
|
||||
)
|
||||
assert mock_vistapool_client.set_value.await_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("light_status", "failing_method"),
|
||||
[
|
||||
pytest.param(0, "set_value", id="turn_on_fails"),
|
||||
pytest.param(1, "pulse", id="pulse_fails"),
|
||||
],
|
||||
)
|
||||
async def test_button_press_raises_on_api_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
light_status: int,
|
||||
failing_method: str,
|
||||
) -> None:
|
||||
"""Test the button re-raises HomeAssistantError when the library fails."""
|
||||
mock_vistapool_client.fetch_pool_data.return_value = deepcopy(_LED_DATA)
|
||||
mock_vistapool_client.set_value.side_effect = AquariteError("boom")
|
||||
mock_vistapool_client.fetch_pool_data.return_value = {
|
||||
"main": {"hasLED": 1, "version": 1},
|
||||
"light": {"status": light_status},
|
||||
}
|
||||
getattr(mock_vistapool_client, failing_method).side_effect = AquariteError("boom")
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
@@ -298,26 +298,6 @@ async def test_setup_prunes_devices_removed_while_offline(
|
||||
)
|
||||
|
||||
|
||||
async def test_apply_optimistic_creates_missing_intermediate_dicts(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test apply_optimistic walks through and creates missing intermediate dicts."""
|
||||
mock_vistapool_client.fetch_pool_data.return_value = {"existing": "scalar"}
|
||||
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()
|
||||
|
||||
coordinator = next(iter(mock_config_entry.runtime_data.coordinators.values()))
|
||||
coordinator.apply_optimistic("filtration.intel.temp", 27)
|
||||
coordinator.apply_optimistic("existing.nested.key", 1)
|
||||
|
||||
assert coordinator.data["filtration"]["intel"]["temp"] == 27
|
||||
assert coordinator.data["existing"] == {"nested": {"key": 1}}
|
||||
|
||||
|
||||
async def test_entities_unavailable_while_push_connection_is_down(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
@@ -326,7 +306,10 @@ async def test_entities_unavailable_while_push_connection_is_down(
|
||||
"""Test entities go unavailable when the Firestore subscription drops.
|
||||
|
||||
The integration has no polling interval, so without this the last
|
||||
snapshot would stay on display as if it were still current.
|
||||
snapshot would stay on display as if it were still current. Only the
|
||||
library's health transition restores availability: data it delivers in
|
||||
between (an acknowledged write, a reconcile fetch) is not proof the
|
||||
stream is back.
|
||||
"""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
@@ -343,10 +326,14 @@ async def test_entities_unavailable_while_push_connection_is_down(
|
||||
|
||||
assert hass.states.get(_TEMPERATURE_ENTITY).state == STATE_UNAVAILABLE
|
||||
|
||||
# Only an incoming snapshot proves the connection is back.
|
||||
on_data({"main": {"temperature": 25}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(_TEMPERATURE_ENTITY).state == STATE_UNAVAILABLE
|
||||
|
||||
on_health(True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(_TEMPERATURE_ENTITY).state == "25.0"
|
||||
|
||||
|
||||
@@ -357,7 +344,7 @@ async def test_entities_stay_unavailable_on_local_updates_during_outage(
|
||||
) -> None:
|
||||
"""Test updates that are not push snapshots do not fake availability.
|
||||
|
||||
Both an optimistic write and a manual refresh set the coordinator's
|
||||
Both an acknowledged write and a manual refresh set the coordinator's
|
||||
success flag, so availability cannot ride on that flag alone.
|
||||
"""
|
||||
mock_vistapool_client.fetch_pool_data.return_value = {"light": {"status": 0}}
|
||||
@@ -366,20 +353,22 @@ async def test_entities_stay_unavailable_on_local_updates_during_outage(
|
||||
await hass.async_block_till_done()
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
|
||||
on_health = mock_vistapool_client.subscribe_pool_resilient.call_args.kwargs[
|
||||
"on_health"
|
||||
]
|
||||
call = mock_vistapool_client.subscribe_pool_resilient.call_args
|
||||
on_data = call.args[1]
|
||||
on_health = call.kwargs["on_health"]
|
||||
on_health(False)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(_LIGHT_ENTITY).state == STATE_UNAVAILABLE
|
||||
|
||||
# An optimistic write updates coordinator data while the push is down.
|
||||
# The library delivers an acknowledged write through the data callback
|
||||
# while the push is down.
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: _LIGHT_ENTITY},
|
||||
blocking=True,
|
||||
)
|
||||
on_data({"light": {"status": 1}})
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(_LIGHT_ENTITY).state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@@ -98,43 +98,6 @@ async def test_light_set_value(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "initial_status", "initial_state", "expected_state"),
|
||||
[
|
||||
pytest.param(SERVICE_TURN_ON, 0, STATE_OFF, STATE_ON, id="turn_on"),
|
||||
pytest.param(SERVICE_TURN_OFF, 1, STATE_ON, STATE_OFF, id="turn_off"),
|
||||
],
|
||||
)
|
||||
async def test_light_optimistic_state(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_vistapool_client: AsyncMock,
|
||||
mock_pool_data: dict[str, Any],
|
||||
service: str,
|
||||
initial_status: int,
|
||||
initial_state: str,
|
||||
expected_state: str,
|
||||
) -> None:
|
||||
"""Test the entity state reflects the just-written value before the Firestore push."""
|
||||
mock_pool_data["light"] = {"status": initial_status}
|
||||
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("light.my_pool_light").state == initial_state
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: "light.my_pool_light"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert hass.states.get("light.my_pool_light").state == expected_state
|
||||
|
||||
|
||||
async def test_light_set_value_raises_on_api_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
|
||||
@@ -270,7 +270,6 @@ async def test_number_set_value(
|
||||
mock_vistapool_client.set_value.assert_awaited_once_with(
|
||||
"ABCDEF1234567890", expected_path, expected_raw
|
||||
)
|
||||
assert hass.states.get(entity_id).state == str(float(user_value))
|
||||
value_arg = mock_vistapool_client.set_value.await_args.args[2]
|
||||
assert isinstance(value_arg, int)
|
||||
|
||||
|
||||
@@ -14,17 +14,12 @@ from homeassistant.components.select import (
|
||||
DOMAIN as SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
EVENT_STATE_CHANGED,
|
||||
STATE_UNKNOWN,
|
||||
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
|
||||
|
||||
from tests.common import MockConfigEntry, async_capture_events, snapshot_platform
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -420,88 +415,6 @@ async def test_select_reflects_choice_before_push(
|
||||
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,
|
||||
|
||||
@@ -150,7 +150,6 @@ async def test_time_set_value(
|
||||
mock_vistapool_client.set_value.assert_awaited_once_with(
|
||||
"ABCDEF1234567890", expected_path, expected_seconds
|
||||
)
|
||||
assert hass.states.get(entity_id).state == time_value
|
||||
|
||||
|
||||
async def test_time_set_value_raises_on_api_error(
|
||||
|
||||
Reference in New Issue
Block a user