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
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.TIME]
|
||||
PLATFORMS: list[Platform] = [Platform.SWITCH, Platform.TIME]
|
||||
|
||||
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-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
|
|
@ -32,6 +32,20 @@
|
|||
}
|
||||
},
|
||||
"entity": {
|
||||
"switch": {
|
||||
"alarm_sound": {
|
||||
"name": "Alarm sound"
|
||||
},
|
||||
"auto_mode": {
|
||||
"name": "Auto mode"
|
||||
},
|
||||
"power": {
|
||||
"name": "Power"
|
||||
},
|
||||
"pump_cycling": {
|
||||
"name": "Pump cycling"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"light_schedule_end": {
|
||||
"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."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Callable, Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from letpot.models import LetPotDevice
|
||||
|
@ -67,8 +67,23 @@ def mock_device_client() -> Generator[AsyncMock]:
|
|||
device_client = mock_device_client.return_value
|
||||
device_client.device_model_code = "LPH21"
|
||||
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.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
|
||||
|
||||
|
||||
|
|
|
@ -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