Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion homeassistant/components/vistapool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

_LOGGER = logging.getLogger(__name__)

PLATFORMS: list[Platform] = [Platform.SENSOR]
PLATFORMS: list[Platform] = [Platform.LIGHT, Platform.SENSOR]


@dataclass
Expand Down
73 changes: 73 additions & 0 deletions homeassistant/components/vistapool/light.py
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")
Comment thread
fdebrus marked this conversation as resolved.
Comment thread
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
8 changes: 3 additions & 5 deletions homeassistant/components/vistapool/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ rules:
# Bronze
action-setup:
status: exempt
comment: No service actions in initial sensor-only platform
comment: No integration-specific service actions; entities use platform-standard actions only
appropriate-polling: done
brands: done
common-modules: done
Expand All @@ -11,7 +11,7 @@ rules:
dependency-transparency: done
docs-actions:
status: exempt
comment: No service actions in initial sensor-only platform
comment: No integration-specific service actions to document
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
Expand All @@ -24,9 +24,7 @@ rules:
unique-config-entry: done

# Silver
action-exceptions:
status: exempt
comment: No user actions (sensor-only platform)
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/vistapool/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
}
},
"entity": {
"light": {
"pool_light": {
"name": "Light"
}
},
Comment on lines +28 to +32
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The device name is already prepended, so how much does Pool add to the entity name?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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"
Expand Down Expand Up @@ -59,6 +64,9 @@
"no_pools": {
"message": "No pools were found on this account."
},
"set_failed": {
"message": "Failed to set {entity}."
},
Comment thread
fdebrus marked this conversation as resolved.
"update_failed": {
"message": "Error fetching data from Vistapool."
}
Expand Down
60 changes: 60 additions & 0 deletions tests/components/vistapool/snapshots/test_light.ambr
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',
})
# ---
138 changes: 138 additions & 0 deletions tests/components/vistapool/test_light.py
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
Loading