-
-
Notifications
You must be signed in to change notification settings - Fork 37.6k
Add light platform to Vistapool #172549
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
base: dev
Are you sure you want to change the base?
Add light platform to Vistapool #172549
Changes from all commits
8e8d5dd
bc4dbb8
c6e7f4e
70821bb
f10080a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| """Vistapool Light entities.""" | ||
|
|
||
| from typing import Any | ||
|
|
||
| from aioaquarite import AquariteError | ||
|
|
||
| from homeassistant.components.light import ColorMode, LightEntity | ||
| 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 | ||
|
|
||
| _VALUE_PATH = "light.status" | ||
|
|
||
|
|
||
| async def async_setup_entry( | ||
| hass: HomeAssistant, | ||
| entry: VistapoolConfigEntry, | ||
| async_add_entities: AddConfigEntryEntitiesCallback, | ||
| ) -> None: | ||
| """Set up the Vistapool light for every pool on the account.""" | ||
| async_add_entities( | ||
| VistapoolLight(coordinator) | ||
| for coordinator in entry.runtime_data.coordinators.values() | ||
| ) | ||
|
|
||
|
|
||
| class VistapoolLight(VistapoolEntity, LightEntity): | ||
| """Representation of a Vistapool pool light.""" | ||
|
|
||
| _attr_translation_key = "pool_light" | ||
| _attr_color_mode = ColorMode.ONOFF | ||
| _attr_supported_color_modes = {ColorMode.ONOFF} | ||
|
|
||
| def __init__(self, coordinator: VistapoolDataUpdateCoordinator) -> None: | ||
| """Initialize the light entity.""" | ||
| super().__init__(coordinator) | ||
| self._attr_unique_id = self.build_unique_id("pool_light") | ||
|
|
||
| @property | ||
| def is_on(self) -> bool | None: | ||
| """Return true if the light is on.""" | ||
| value = self.coordinator.get_value(_VALUE_PATH) | ||
| if value is None: | ||
| return None | ||
| return value in (True, "1") | ||
|
fdebrus marked this conversation as resolved.
Comment on lines
+47
to
+52
|
||
|
|
||
| async def async_turn_on(self, **kwargs: Any) -> None: | ||
| """Turn the light on.""" | ||
| await self._async_set_value(1) | ||
|
|
||
| async def async_turn_off(self, **kwargs: Any) -> None: | ||
| """Turn the light off.""" | ||
| await self._async_set_value(0) | ||
|
|
||
| async def _async_set_value(self, value: int) -> None: | ||
| """Send a value update via the Vistapool cloud API.""" | ||
| try: | ||
| await self.coordinator.api.set_value( | ||
| self.coordinator.pool_id, _VALUE_PATH, value | ||
| ) | ||
|
Comment on lines
+62
to
+67
|
||
| except AquariteError as err: | ||
| raise HomeAssistantError( | ||
| translation_domain=DOMAIN, | ||
| translation_key="set_failed", | ||
| translation_placeholders={"entity": self.entity_id}, | ||
| ) from err | ||
|
Comment on lines
+69
to
+73
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,11 @@ | |
| } | ||
| }, | ||
| "entity": { | ||
| "light": { | ||
| "pool_light": { | ||
| "name": "Light" | ||
| } | ||
| }, | ||
|
Comment on lines
+28
to
+32
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The device name is already prepended, so how much does
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch ! Dropping to "Light" so the friendly name renders as "My Pool Light" instead of "My Pool Pool light". There's only ever one light fixture per controller, so the unqualified name is unambiguous |
||
| "sensor": { | ||
| "chlorine": { | ||
| "name": "Chlorine" | ||
|
|
@@ -59,6 +64,9 @@ | |
| "no_pools": { | ||
| "message": "No pools were found on this account." | ||
| }, | ||
| "set_failed": { | ||
| "message": "Failed to set {entity}." | ||
| }, | ||
|
fdebrus marked this conversation as resolved.
|
||
| "update_failed": { | ||
| "message": "Error fetching data from Vistapool." | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # serializer version: 1 | ||
| # name: test_all_entities[light.my_pool_light-entry] | ||
| EntityRegistryEntrySnapshot({ | ||
| 'aliases': list([ | ||
| None, | ||
| ]), | ||
| 'area_id': None, | ||
| 'capabilities': dict({ | ||
| 'supported_color_modes': list([ | ||
| <ColorMode.ONOFF: 'onoff'>, | ||
| ]), | ||
| }), | ||
| 'config_entry_id': <ANY>, | ||
| 'config_subentry_id': <ANY>, | ||
| 'device_class': None, | ||
| 'device_id': <ANY>, | ||
| 'disabled_by': None, | ||
| 'domain': 'light', | ||
| 'entity_category': None, | ||
| 'entity_id': 'light.my_pool_light', | ||
| 'has_entity_name': True, | ||
| 'hidden_by': None, | ||
| 'icon': None, | ||
| 'id': <ANY>, | ||
| 'labels': set({ | ||
| }), | ||
| 'name': None, | ||
| 'object_id_base': 'Light', | ||
| 'options': dict({ | ||
| }), | ||
| 'original_device_class': None, | ||
| 'original_icon': None, | ||
| 'original_name': 'Light', | ||
| 'platform': 'vistapool', | ||
| 'previous_unique_id': None, | ||
| 'suggested_object_id': None, | ||
| 'supported_features': 0, | ||
| 'translation_key': 'pool_light', | ||
| 'unique_id': 'ABCDEF1234567890-pool_light', | ||
| 'unit_of_measurement': None, | ||
| }) | ||
| # --- | ||
| # name: test_all_entities[light.my_pool_light-state] | ||
| StateSnapshot({ | ||
| 'attributes': ReadOnlyDict({ | ||
| 'color_mode': None, | ||
| 'friendly_name': 'My Pool Light', | ||
| 'supported_color_modes': list([ | ||
| <ColorMode.ONOFF: 'onoff'>, | ||
| ]), | ||
| 'supported_features': <LightEntityFeature: 0>, | ||
| }), | ||
| 'context': <ANY>, | ||
| 'entity_id': 'light.my_pool_light', | ||
| 'last_changed': <ANY>, | ||
| 'last_reported': <ANY>, | ||
| 'last_updated': <ANY>, | ||
| 'state': 'off', | ||
| }) | ||
| # --- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| """Tests for the Vistapool light 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.light import ( | ||
| DOMAIN as LIGHT_DOMAIN, | ||
| SERVICE_TURN_OFF, | ||
| SERVICE_TURN_ON, | ||
| ) | ||
| from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, 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 | ||
|
|
||
|
|
||
| @pytest.fixture(autouse=True) | ||
| def _only_light_platform() -> Generator[None]: | ||
| """Restrict integration setup to the light platform for these tests.""" | ||
| with patch("homeassistant.components.vistapool.PLATFORMS", [Platform.LIGHT]): | ||
| yield | ||
|
|
||
|
|
||
| async def test_all_entities( | ||
| hass: HomeAssistant, | ||
| snapshot: SnapshotAssertion, | ||
| entity_registry: er.EntityRegistry, | ||
| mock_config_entry: MockConfigEntry, | ||
| mock_vistapool_client: AsyncMock, | ||
| mock_pool_data: dict[str, Any], | ||
| ) -> None: | ||
| """Test light entities for the default fixture.""" | ||
| 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 snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) | ||
|
|
||
|
|
||
| async def test_light_string_value( | ||
| hass: HomeAssistant, | ||
| mock_config_entry: MockConfigEntry, | ||
| mock_vistapool_client: AsyncMock, | ||
| ) -> None: | ||
| """Test the light coerces a numeric-as-string status to bool.""" | ||
| mock_vistapool_client.fetch_pool_data.return_value = { | ||
| "main": {"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() | ||
|
|
||
| assert hass.states.get("light.my_pool_light").state == STATE_ON | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ("service", "expected_value"), | ||
| [ | ||
| pytest.param(SERVICE_TURN_ON, 1, id="turn_on"), | ||
| pytest.param(SERVICE_TURN_OFF, 0, id="turn_off"), | ||
| ], | ||
| ) | ||
| async def test_light_set_value( | ||
| hass: HomeAssistant, | ||
| mock_config_entry: MockConfigEntry, | ||
| mock_vistapool_client: AsyncMock, | ||
| mock_pool_data: dict[str, Any], | ||
| service: str, | ||
| expected_value: int, | ||
| ) -> None: | ||
| """Test turn_on / turn_off write light.status via set_value.""" | ||
| 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( | ||
| LIGHT_DOMAIN, | ||
| service, | ||
| {ATTR_ENTITY_ID: "light.my_pool_light"}, | ||
| blocking=True, | ||
| ) | ||
|
|
||
| mock_vistapool_client.set_value.assert_awaited_once_with( | ||
| "ABCDEF1234567890", "light.status", expected_value | ||
| ) | ||
|
|
||
|
|
||
| async def test_light_set_value_raises_on_api_error( | ||
| hass: HomeAssistant, | ||
| mock_config_entry: MockConfigEntry, | ||
| mock_vistapool_client: AsyncMock, | ||
| mock_pool_data: dict[str, Any], | ||
| ) -> None: | ||
| """Test action raises HomeAssistantError when the library fails.""" | ||
| mock_vistapool_client.fetch_pool_data.return_value = mock_pool_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( | ||
| LIGHT_DOMAIN, | ||
| SERVICE_TURN_ON, | ||
| {ATTR_ENTITY_ID: "light.my_pool_light"}, | ||
| blocking=True, | ||
| ) | ||
| assert excinfo.value.translation_key == "set_failed" | ||
|
|
||
|
|
||
| async def test_light_default_fixture_state( | ||
| hass: HomeAssistant, | ||
| mock_config_entry: MockConfigEntry, | ||
| mock_vistapool_client: AsyncMock, | ||
| mock_pool_data: dict[str, Any], | ||
| ) -> None: | ||
| """Test the light reports off in the default fixture (light.status=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("light.my_pool_light").state == STATE_OFF |
Uh oh!
There was an error while loading. Please reload this page.