-
-
Notifications
You must be signed in to change notification settings - Fork 37.6k
Add button platform to Vistapool #172550
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+333
−6
Merged
Add button platform to Vistapool #172550
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
d02248c
Add button platform to Vistapool
claude a1f7c14
Vistapool: assert translation_key on the button press error
claude f0fbd4d
Vistapool: drop redundant 1 from button on-value tuple
claude 956ab05
Vistapool: patch the LED pulse delay constant to 0 in tests
claude 1009e78
Vistapool: use American "color" instead of "colour" for LED button
claude b6b4258
Vistapool: optimistically update light.status after LED button press
claude ab5148c
Vistapool: bump aioaquarite to 0.5.1
claude 2367274
Revert "Vistapool: bump aioaquarite to 0.5.1"
claude 2c69814
Vistapool: collapse patch() call to one line per ruff format
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| """Vistapool Button entities.""" | ||
|
|
||
| import asyncio | ||
|
|
||
| from aioaquarite import AquariteError | ||
|
|
||
| from homeassistant.components.button import ButtonEntity | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.exceptions import HomeAssistantError | ||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||
|
|
||
| from . import VistapoolConfigEntry | ||
| from .const import DOMAIN | ||
| from .coordinator import VistapoolDataUpdateCoordinator | ||
| from .entity import VistapoolEntity | ||
|
|
||
| PARALLEL_UPDATES = 1 | ||
|
|
||
| _HASLED_PATH = "main.hasLED" | ||
| _LIGHT_STATUS_PATH = "light.status" | ||
| _LED_PULSE_DELAY_SECONDS = 1.0 | ||
|
|
||
|
|
||
| async def async_setup_entry( | ||
| hass: HomeAssistant, | ||
| entry: VistapoolConfigEntry, | ||
| async_add_entities: AddConfigEntryEntitiesCallback, | ||
| ) -> None: | ||
| """Set up Vistapool buttons for every pool that has an LED fixture.""" | ||
| async_add_entities( | ||
| VistapoolLEDPulseButton(coordinator) | ||
| for coordinator in entry.runtime_data.coordinators.values() | ||
| if coordinator.get_value(_HASLED_PATH) | ||
| ) | ||
|
|
||
|
|
||
| class VistapoolLEDPulseButton(VistapoolEntity, ButtonEntity): | ||
| """Power-cycle the pool light to advance the LED fixture's color. | ||
|
|
||
| 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. | ||
| """ | ||
|
|
||
| _attr_translation_key = "led_pulse" | ||
|
|
||
| def __init__(self, coordinator: VistapoolDataUpdateCoordinator) -> None: | ||
| """Initialize the LED pulse button.""" | ||
| super().__init__(coordinator) | ||
| self._attr_unique_id = self.build_unique_id("led_pulse") | ||
|
|
||
| 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"): | ||
|
fdebrus marked this conversation as resolved.
fdebrus marked this conversation as resolved.
|
||
| await self.coordinator.api.set_value( | ||
| self.coordinator.pool_id, _LIGHT_STATUS_PATH, 0 | ||
| ) | ||
| await asyncio.sleep(_LED_PULSE_DELAY_SECONDS) | ||
|
fdebrus marked this conversation as resolved.
|
||
| await self.coordinator.api.set_value( | ||
| self.coordinator.pool_id, _LIGHT_STATUS_PATH, 1 | ||
| ) | ||
|
fdebrus marked this conversation as resolved.
fdebrus marked this conversation as resolved.
|
||
| except AquariteError as err: | ||
|
fdebrus marked this conversation as resolved.
|
||
| raise HomeAssistantError( | ||
| translation_domain=DOMAIN, | ||
| translation_key="set_failed", | ||
| translation_placeholders={"entity": self.entity_id}, | ||
| ) from err | ||
|
fdebrus marked this conversation as resolved.
|
||
| # Optimistically reflect the just-written value so a rapid second press | ||
| # doesn't read the stale off-state before the Firestore push round-trips. | ||
| self.coordinator.data.setdefault("light", {})["status"] = 1 | ||
| self.coordinator.async_set_updated_data(self.coordinator.data) | ||
|
fdebrus marked this conversation as resolved.
fdebrus marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # serializer version: 1 | ||
| # name: test_all_entities[button.my_pool_led_next_color-entry] | ||
| EntityRegistryEntrySnapshot({ | ||
| 'aliases': list([ | ||
| None, | ||
| ]), | ||
| 'area_id': None, | ||
| 'capabilities': None, | ||
| 'config_entry_id': <ANY>, | ||
| 'config_subentry_id': <ANY>, | ||
| 'device_class': None, | ||
| 'device_id': <ANY>, | ||
| 'disabled_by': None, | ||
| 'domain': 'button', | ||
| 'entity_category': None, | ||
| 'entity_id': 'button.my_pool_led_next_color', | ||
| 'has_entity_name': True, | ||
| 'hidden_by': None, | ||
| 'icon': None, | ||
| 'id': <ANY>, | ||
| 'labels': set({ | ||
| }), | ||
| 'name': None, | ||
| 'object_id_base': 'LED next color', | ||
| 'options': dict({ | ||
| }), | ||
| 'original_device_class': None, | ||
| 'original_icon': None, | ||
| 'original_name': 'LED next color', | ||
| 'platform': 'vistapool', | ||
| 'previous_unique_id': None, | ||
| 'suggested_object_id': None, | ||
| 'supported_features': 0, | ||
| 'translation_key': 'led_pulse', | ||
| 'unique_id': 'ABCDEF1234567890-led_pulse', | ||
| 'unit_of_measurement': None, | ||
| }) | ||
| # --- | ||
| # name: test_all_entities[button.my_pool_led_next_color-state] | ||
| StateSnapshot({ | ||
| 'attributes': ReadOnlyDict({ | ||
| 'friendly_name': 'My Pool LED next color', | ||
| }), | ||
| 'context': <ANY>, | ||
| 'entity_id': 'button.my_pool_led_next_color', | ||
| 'last_changed': <ANY>, | ||
| 'last_reported': <ANY>, | ||
| 'last_updated': <ANY>, | ||
| 'state': 'unknown', | ||
| }) | ||
| # --- |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| """Tests for the Vistapool button platform.""" | ||
|
|
||
| from collections.abc import Generator | ||
| from typing import Any | ||
| from unittest.mock import AsyncMock, patch | ||
|
|
||
| from aioaquarite import AquariteError | ||
| import pytest | ||
| from syrupy.assertion import SnapshotAssertion | ||
|
|
||
| from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS | ||
| from homeassistant.const import ATTR_ENTITY_ID, 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 | ||
|
|
||
| _BUTTON = "button.my_pool_led_next_color" | ||
| _LED_DATA = {"main": {"hasLED": 1, "version": 1}, "light": {"status": 0}} | ||
|
|
||
|
|
||
| @pytest.fixture(autouse=True) | ||
| def _only_button_platform() -> Generator[None]: | ||
|
fdebrus marked this conversation as resolved.
|
||
| """Restrict integration setup to the button platform for these tests.""" | ||
| with patch("homeassistant.components.vistapool.PLATFORMS", [Platform.BUTTON]): | ||
| yield | ||
|
|
||
|
|
||
| @pytest.fixture(autouse=True) | ||
| def _skip_pulse_delay() -> Generator[None]: | ||
|
fdebrus marked this conversation as resolved.
|
||
| """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( | ||
|
fdebrus marked this conversation as resolved.
|
||
| hass: HomeAssistant, | ||
| snapshot: SnapshotAssertion, | ||
| entity_registry: er.EntityRegistry, | ||
| mock_config_entry: MockConfigEntry, | ||
| mock_vistapool_client: AsyncMock, | ||
| ) -> None: | ||
| """Test the LED-pulse button when hasLED is set.""" | ||
| mock_vistapool_client.fetch_pool_data.return_value = _LED_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 snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) | ||
|
|
||
|
|
||
| async def test_button_not_created_without_led( | ||
| hass: HomeAssistant, | ||
| mock_config_entry: MockConfigEntry, | ||
| mock_vistapool_client: AsyncMock, | ||
| mock_pool_data: dict[str, Any], | ||
| ) -> None: | ||
| """Test the LED-pulse button is not created when hasLED is 0.""" | ||
| 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(_BUTTON) is None | ||
|
|
||
|
|
||
| async def test_button_press_when_light_off( | ||
| hass: HomeAssistant, | ||
| mock_config_entry: MockConfigEntry, | ||
| mock_vistapool_client: AsyncMock, | ||
| ) -> None: | ||
| """Test pressing the button when the light is off just turns it on.""" | ||
| mock_vistapool_client.fetch_pool_data.return_value = _LED_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( | ||
| BUTTON_DOMAIN, | ||
| SERVICE_PRESS, | ||
| {ATTR_ENTITY_ID: _BUTTON}, | ||
| blocking=True, | ||
| ) | ||
|
|
||
| mock_vistapool_client.set_value.assert_awaited_once_with( | ||
| "ABCDEF1234567890", "light.status", 1 | ||
| ) | ||
|
|
||
|
|
||
| async def test_button_press_when_light_on( | ||
| hass: HomeAssistant, | ||
| mock_config_entry: MockConfigEntry, | ||
| mock_vistapool_client: AsyncMock, | ||
| ) -> None: | ||
| """Test pressing the button when the light is on power-cycles it.""" | ||
| mock_vistapool_client.fetch_pool_data.return_value = { | ||
| "main": {"hasLED": 1, "version": 1}, | ||
| "light": {"status": 1}, | ||
| } | ||
| 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( | ||
| BUTTON_DOMAIN, | ||
| SERVICE_PRESS, | ||
| {ATTR_ENTITY_ID: _BUTTON}, | ||
| 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, | ||
| ) | ||
|
|
||
|
|
||
| async def test_button_press_rapid_repeat_after_off( | ||
| hass: HomeAssistant, | ||
| mock_config_entry: MockConfigEntry, | ||
| mock_vistapool_client: AsyncMock, | ||
| ) -> None: | ||
| """Test a second press lands the off/on 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. | ||
| """ | ||
| mock_vistapool_client.fetch_pool_data.return_value = _LED_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( | ||
| BUTTON_DOMAIN, | ||
| SERVICE_PRESS, | ||
| {ATTR_ENTITY_ID: _BUTTON}, | ||
| blocking=True, | ||
| ) | ||
| await hass.services.async_call( | ||
| BUTTON_DOMAIN, | ||
| SERVICE_PRESS, | ||
| {ATTR_ENTITY_ID: _BUTTON}, | ||
| 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, | ||
| ) | ||
|
|
||
|
|
||
| async def test_button_press_raises_on_api_error( | ||
| hass: HomeAssistant, | ||
| mock_config_entry: MockConfigEntry, | ||
| mock_vistapool_client: AsyncMock, | ||
| ) -> None: | ||
| """Test the button re-raises HomeAssistantError when the library fails.""" | ||
| mock_vistapool_client.fetch_pool_data.return_value = _LED_DATA | ||
| mock_vistapool_client.set_value.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() | ||
|
|
||
| with pytest.raises(HomeAssistantError) as excinfo: | ||
| await hass.services.async_call( | ||
| BUTTON_DOMAIN, | ||
| SERVICE_PRESS, | ||
| {ATTR_ENTITY_ID: _BUTTON}, | ||
| blocking=True, | ||
| ) | ||
| assert excinfo.value.translation_key == "set_failed" | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.