Add switch platform to LetPot integration (#136383)
* Add switch platform to LetPot integration * deviceclient -> device_client * Remove coordinator data None check * Add exception handling + testpull/136936/head^2
parent
bb61e31298
commit
012f7112d7
|
@ -23,7 +23,7 @@ from .const import (
|
||||||
)
|
)
|
||||||
from .coordinator import LetPotDeviceCoordinator
|
from .coordinator import LetPotDeviceCoordinator
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.TIME]
|
PLATFORMS: list[Platform] = [Platform.SWITCH, Platform.TIME]
|
||||||
|
|
||||||
type LetPotConfigEntry = ConfigEntry[list[LetPotDeviceCoordinator]]
|
type LetPotConfigEntry = ConfigEntry[list[LetPotDeviceCoordinator]]
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"switch": {
|
||||||
|
"alarm_sound": {
|
||||||
|
"default": "mdi:bell-ring",
|
||||||
|
"state": {
|
||||||
|
"off": "mdi:bell-off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auto_mode": {
|
||||||
|
"default": "mdi:water-pump",
|
||||||
|
"state": {
|
||||||
|
"off": "mdi:water-pump-off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pump_cycling": {
|
||||||
|
"default": "mdi:pump",
|
||||||
|
"state": {
|
||||||
|
"off": "mdi:pump-off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -64,7 +64,7 @@ rules:
|
||||||
entity-disabled-by-default: todo
|
entity-disabled-by-default: todo
|
||||||
entity-translations: done
|
entity-translations: done
|
||||||
exception-translations: done
|
exception-translations: done
|
||||||
icon-translations: todo
|
icon-translations: done
|
||||||
reconfiguration-flow: todo
|
reconfiguration-flow: todo
|
||||||
repair-issues: todo
|
repair-issues: todo
|
||||||
stale-devices: todo
|
stale-devices: todo
|
||||||
|
|
|
@ -32,6 +32,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
|
"switch": {
|
||||||
|
"alarm_sound": {
|
||||||
|
"name": "Alarm sound"
|
||||||
|
},
|
||||||
|
"auto_mode": {
|
||||||
|
"name": "Auto mode"
|
||||||
|
},
|
||||||
|
"power": {
|
||||||
|
"name": "Power"
|
||||||
|
},
|
||||||
|
"pump_cycling": {
|
||||||
|
"name": "Pump cycling"
|
||||||
|
}
|
||||||
|
},
|
||||||
"time": {
|
"time": {
|
||||||
"light_schedule_end": {
|
"light_schedule_end": {
|
||||||
"name": "Light off"
|
"name": "Light off"
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
"""Support for LetPot switch entities."""
|
||||||
|
|
||||||
|
from collections.abc import Callable, Coroutine
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from letpot.deviceclient import LetPotDeviceClient
|
||||||
|
from letpot.models import DeviceFeature, LetPotDeviceStatus
|
||||||
|
|
||||||
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
|
from homeassistant.const import EntityCategory
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import LetPotConfigEntry
|
||||||
|
from .coordinator import LetPotDeviceCoordinator
|
||||||
|
from .entity import LetPotEntity, exception_handler
|
||||||
|
|
||||||
|
# Each change pushes a 'full' device status with the change. The library will cache
|
||||||
|
# pending changes to avoid overwriting, but try to avoid a lot of parallelism.
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class LetPotSwitchEntityDescription(SwitchEntityDescription):
|
||||||
|
"""Describes a LetPot switch entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[LetPotDeviceStatus], bool | None]
|
||||||
|
set_value_fn: Callable[[LetPotDeviceClient, bool], Coroutine[Any, Any, None]]
|
||||||
|
|
||||||
|
|
||||||
|
BASE_SWITCHES: tuple[LetPotSwitchEntityDescription, ...] = (
|
||||||
|
LetPotSwitchEntityDescription(
|
||||||
|
key="power",
|
||||||
|
translation_key="power",
|
||||||
|
value_fn=lambda status: status.system_on,
|
||||||
|
set_value_fn=lambda device_client, value: device_client.set_power(value),
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
),
|
||||||
|
LetPotSwitchEntityDescription(
|
||||||
|
key="pump_cycling",
|
||||||
|
translation_key="pump_cycling",
|
||||||
|
value_fn=lambda status: status.pump_mode == 1,
|
||||||
|
set_value_fn=lambda device_client, value: device_client.set_pump_mode(value),
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
ALARM_SWITCH: LetPotSwitchEntityDescription = LetPotSwitchEntityDescription(
|
||||||
|
key="alarm_sound",
|
||||||
|
translation_key="alarm_sound",
|
||||||
|
value_fn=lambda status: status.system_sound,
|
||||||
|
set_value_fn=lambda device_client, value: device_client.set_sound(value),
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
)
|
||||||
|
AUTO_MODE_SWITCH: LetPotSwitchEntityDescription = LetPotSwitchEntityDescription(
|
||||||
|
key="auto_mode",
|
||||||
|
translation_key="auto_mode",
|
||||||
|
value_fn=lambda status: status.water_mode == 1,
|
||||||
|
set_value_fn=lambda device_client, value: device_client.set_water_mode(value),
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: LetPotConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up LetPot switch entities based on a config entry and device status/features."""
|
||||||
|
coordinators = entry.runtime_data
|
||||||
|
entities: list[SwitchEntity] = [
|
||||||
|
LetPotSwitchEntity(coordinator, description)
|
||||||
|
for description in BASE_SWITCHES
|
||||||
|
for coordinator in coordinators
|
||||||
|
]
|
||||||
|
entities.extend(
|
||||||
|
LetPotSwitchEntity(coordinator, ALARM_SWITCH)
|
||||||
|
for coordinator in coordinators
|
||||||
|
if coordinator.data.system_sound is not None
|
||||||
|
)
|
||||||
|
entities.extend(
|
||||||
|
LetPotSwitchEntity(coordinator, AUTO_MODE_SWITCH)
|
||||||
|
for coordinator in coordinators
|
||||||
|
if DeviceFeature.PUMP_AUTO in coordinator.device_client.device_features
|
||||||
|
)
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class LetPotSwitchEntity(LetPotEntity, SwitchEntity):
|
||||||
|
"""Defines a LetPot switch entity."""
|
||||||
|
|
||||||
|
entity_description: LetPotSwitchEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: LetPotDeviceCoordinator,
|
||||||
|
description: LetPotSwitchEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize LetPot switch entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{coordinator.device.serial_number}_{description.key}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool | None:
|
||||||
|
"""Return if the entity is on."""
|
||||||
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
|
|
||||||
|
@exception_handler
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the entity on."""
|
||||||
|
await self.entity_description.set_value_fn(self.coordinator.device_client, True)
|
||||||
|
|
||||||
|
@exception_handler
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the entity off."""
|
||||||
|
await self.entity_description.set_value_fn(
|
||||||
|
self.coordinator.device_client, False
|
||||||
|
)
|
|
@ -1,6 +1,6 @@
|
||||||
"""Common fixtures for the LetPot tests."""
|
"""Common fixtures for the LetPot tests."""
|
||||||
|
|
||||||
from collections.abc import Generator
|
from collections.abc import Callable, Generator
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
from letpot.models import LetPotDevice
|
from letpot.models import LetPotDevice
|
||||||
|
@ -67,8 +67,23 @@ def mock_device_client() -> Generator[AsyncMock]:
|
||||||
device_client = mock_device_client.return_value
|
device_client = mock_device_client.return_value
|
||||||
device_client.device_model_code = "LPH21"
|
device_client.device_model_code = "LPH21"
|
||||||
device_client.device_model_name = "LetPot Air"
|
device_client.device_model_name = "LetPot Air"
|
||||||
|
|
||||||
|
subscribe_callbacks: list[Callable] = []
|
||||||
|
|
||||||
|
def subscribe_side_effect(callback: Callable) -> None:
|
||||||
|
subscribe_callbacks.append(callback)
|
||||||
|
|
||||||
|
def status_side_effect() -> None:
|
||||||
|
# Deliver a status update to any subscribers, like the real client
|
||||||
|
for callback in subscribe_callbacks:
|
||||||
|
callback(STATUS)
|
||||||
|
|
||||||
|
device_client.get_current_status.side_effect = status_side_effect
|
||||||
device_client.get_current_status.return_value = STATUS
|
device_client.get_current_status.return_value = STATUS
|
||||||
device_client.last_status.return_value = STATUS
|
device_client.last_status.return_value = STATUS
|
||||||
|
device_client.request_status_update.side_effect = status_side_effect
|
||||||
|
device_client.subscribe.side_effect = subscribe_side_effect
|
||||||
|
|
||||||
yield device_client
|
yield device_client
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
"""Test switch entities for the LetPot integration."""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from letpot.exceptions import LetPotConnectionException, LetPotException
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("service", "exception", "user_error"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
LetPotConnectionException("Connection failed"),
|
||||||
|
"An error occurred while communicating with the LetPot device: Connection failed",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
LetPotException("Random thing failed"),
|
||||||
|
"An unknown error occurred while communicating with the LetPot device: Random thing failed",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_switch_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_client: MagicMock,
|
||||||
|
mock_device_client: MagicMock,
|
||||||
|
service: str,
|
||||||
|
exception: Exception,
|
||||||
|
user_error: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test switch entity exception handling."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
mock_device_client.set_power.side_effect = exception
|
||||||
|
|
||||||
|
assert hass.states.get("switch.garden_power") is not None
|
||||||
|
with pytest.raises(HomeAssistantError, match=user_error):
|
||||||
|
await hass.services.async_call(
|
||||||
|
"switch",
|
||||||
|
service,
|
||||||
|
blocking=True,
|
||||||
|
target={"entity_id": "switch.garden_power"},
|
||||||
|
)
|
Loading…
Reference in New Issue