Skip to content
Merged
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
13 changes: 8 additions & 5 deletions homeassistant/components/letpot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import asyncio

from letpot.client import LetPotClient
from letpot.converters import CONVERTERS
from letpot.converters import GARDEN_CONVERTERS
from letpot.deviceclient import LetPotDeviceClient
from letpot.exceptions import LetPotAuthenticationException, LetPotException
from letpot.models import AuthenticationInfo
from letpot.models import AuthenticationInfo, LetPotGardenStatus

from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, Platform
from homeassistant.core import HomeAssistant
Expand Down Expand Up @@ -71,10 +71,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: LetPotConfigEntry) -> bo

device_client = LetPotDeviceClient(auth)

coordinators: list[LetPotDeviceCoordinator] = [
LetPotDeviceCoordinator(hass, entry, device, device_client)
coordinators: list[LetPotDeviceCoordinator[LetPotGardenStatus]] = [
LetPotDeviceCoordinator[LetPotGardenStatus](hass, entry, device, device_client)
for device in devices
if any(converter.supports_type(device.device_type) for converter in CONVERTERS)
if any(
converter.supports_type(device.device_type)
for converter in GARDEN_CONVERTERS
)
]

await asyncio.gather(
Expand Down
30 changes: 16 additions & 14 deletions homeassistant/components/letpot/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,24 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .coordinator import LetPotConfigEntry, LetPotDeviceCoordinator
from .coordinator import LetPotConfigEntry, LetPotDeviceCoordinator, LetPotGardenStatus
from .entity import LetPotEntity, LetPotEntityDescription

# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0


@dataclass(frozen=True, kw_only=True)
class LetPotBinarySensorEntityDescription(
class LetPotBinarySensorEntityDescription[_DataT: LetPotDeviceStatus](
LetPotEntityDescription, BinarySensorEntityDescription
):
"""Describes a LetPot binary sensor entity."""

Comment thread
jpelgrom marked this conversation as resolved.
is_on_fn: Callable[[LetPotDeviceStatus], bool]
is_on_fn: Callable[[_DataT], bool]


BINARY_SENSORS: tuple[LetPotBinarySensorEntityDescription, ...] = (
LetPotBinarySensorEntityDescription(
BINARY_SENSORS: tuple[LetPotBinarySensorEntityDescription[LetPotGardenStatus], ...] = (
LetPotBinarySensorEntityDescription[LetPotGardenStatus](
key="low_nutrients",
translation_key="low_nutrients",
is_on_fn=lambda status: bool(status.errors.low_nutrients),
Expand All @@ -42,7 +42,7 @@ class LetPotBinarySensorEntityDescription(
lambda coordinator: coordinator.data.errors.low_nutrients is not None
),
),
LetPotBinarySensorEntityDescription(
LetPotBinarySensorEntityDescription[LetPotGardenStatus](
key="low_water",
translation_key="low_water",
is_on_fn=lambda status: bool(status.errors.low_water),
Expand All @@ -51,7 +51,7 @@ class LetPotBinarySensorEntityDescription(
device_class=BinarySensorDeviceClass.PROBLEM,
supported_fn=lambda coordinator: coordinator.data.errors.low_water is not None,
),
LetPotBinarySensorEntityDescription(
LetPotBinarySensorEntityDescription[LetPotGardenStatus](
key="pump",
translation_key="pump",
is_on_fn=lambda status: status.pump_status == 1,
Expand All @@ -65,7 +65,7 @@ class LetPotBinarySensorEntityDescription(
)
),
),
LetPotBinarySensorEntityDescription(
LetPotBinarySensorEntityDescription[LetPotGardenStatus](
key="pump_error",
translation_key="pump_error",
is_on_fn=lambda status: bool(status.errors.pump_malfunction),
Expand All @@ -76,7 +76,7 @@ class LetPotBinarySensorEntityDescription(
lambda coordinator: coordinator.data.errors.pump_malfunction is not None
),
),
LetPotBinarySensorEntityDescription(
LetPotBinarySensorEntityDescription[LetPotGardenStatus](
key="refill_error",
translation_key="refill_error",
is_on_fn=lambda status: bool(status.errors.refill_error),
Expand All @@ -98,22 +98,24 @@ async def async_setup_entry(
"""Set up LetPot binary sensor entities based on a config entry and device status/features."""
coordinators = entry.runtime_data
async_add_entities(
LetPotBinarySensorEntity(coordinator, description)
LetPotBinarySensorEntity[LetPotGardenStatus](coordinator, description)
for description in BINARY_SENSORS
for coordinator in coordinators
if description.supported_fn(coordinator)
)


class LetPotBinarySensorEntity(LetPotEntity, BinarySensorEntity):
class LetPotBinarySensorEntity[_DataT: LetPotDeviceStatus](
LetPotEntity[_DataT], BinarySensorEntity
):
"""Defines a LetPot binary sensor entity."""

entity_description: LetPotBinarySensorEntityDescription
entity_description: LetPotBinarySensorEntityDescription[_DataT]

def __init__(
self,
coordinator: LetPotDeviceCoordinator,
description: LetPotBinarySensorEntityDescription,
coordinator: LetPotDeviceCoordinator[_DataT],
description: LetPotBinarySensorEntityDescription[_DataT],
) -> None:
"""Initialize LetPot binary sensor entity."""
super().__init__(coordinator)
Expand Down
19 changes: 12 additions & 7 deletions homeassistant/components/letpot/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"""Coordinator for the LetPot integration."""

import asyncio
from collections.abc import Callable
from datetime import timedelta
import logging
from typing import cast

from letpot.deviceclient import LetPotDeviceClient
from letpot.exceptions import LetPotAuthenticationException, LetPotException
from letpot.models import LetPotDevice, LetPotDeviceStatus
from letpot.models import LetPotDevice, LetPotDeviceStatus, LetPotGardenStatus

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
Expand All @@ -17,11 +19,13 @@

_LOGGER = logging.getLogger(__name__)

type LetPotConfigEntry = ConfigEntry[list[LetPotDeviceCoordinator]]
type LetPotConfigEntry = ConfigEntry[list[LetPotDeviceCoordinator[LetPotGardenStatus]]]


class LetPotDeviceCoordinator(DataUpdateCoordinator[LetPotDeviceStatus]):
"""Class to handle data updates for a specific garden."""
class LetPotDeviceCoordinator[_DataT: LetPotDeviceStatus](
DataUpdateCoordinator[_DataT]
):
"""Class to handle data updates for a specific device."""

config_entry: LetPotConfigEntry

Expand All @@ -46,20 +50,21 @@ def __init__(
self.device = device
self.device_client = device_client

def _handle_status_update(self, status: LetPotDeviceStatus) -> None:
def _handle_status_update(self, status: _DataT) -> None:
"""Distribute status update to entities."""
self.async_set_updated_data(data=status)

async def _async_setup(self) -> None:
"""Set up subscription for coordinator."""
try:
await self.device_client.subscribe(
self.device.serial_number, self._handle_status_update
self.device.serial_number,
cast(Callable[[LetPotDeviceStatus], None], self._handle_status_update),
)
except LetPotAuthenticationException as exc:
raise ConfigEntryAuthFailed from exc

async def _async_update_data(self) -> LetPotDeviceStatus:
async def _async_update_data(self) -> _DataT:
"""Request an update from the device and wait for a status update or timeout."""
try:
async with asyncio.timeout(REQUEST_UPDATE_TIMEOUT):
Expand Down
7 changes: 5 additions & 2 deletions homeassistant/components/letpot/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Any, Concatenate

from letpot.exceptions import LetPotConnectionException, LetPotException
from letpot.models import LetPotDeviceStatus

from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
Expand All @@ -22,12 +23,14 @@ class LetPotEntityDescription(EntityDescription):
supported_fn: Callable[[LetPotDeviceCoordinator], bool] = lambda _: True


class LetPotEntity(CoordinatorEntity[LetPotDeviceCoordinator]):
class LetPotEntity[_DataT: LetPotDeviceStatus](
CoordinatorEntity[LetPotDeviceCoordinator[_DataT]]
):
Comment on lines +26 to +28
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not sure how to address this. Changing the exception handler to use LetPotEntity[LetPotDeviceStatus] will create new mypy errors because the individual entities use a subclass of the status model. Changing the exception handler to use LetPotEntity[Any] works but that still results in losing type safety. The type isn't used in the exception handler so Any might be fine if there's no alternative.

"""Defines a base LetPot entity."""

_attr_has_entity_name = True

def __init__(self, coordinator: LetPotDeviceCoordinator) -> None:
def __init__(self, coordinator: LetPotDeviceCoordinator[_DataT]) -> None:
"""Initialize a LetPot entity."""
super().__init__(coordinator)
info = coordinator.device_client.device_info(coordinator.device.serial_number)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/letpot/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["letpot"],
"quality_scale": "silver",
"requirements": ["letpot==0.6.4"]
"requirements": ["letpot==0.7.0"]
Comment thread
jpelgrom marked this conversation as resolved.
}
28 changes: 16 additions & 12 deletions homeassistant/components/letpot/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import Any

from letpot.deviceclient import LetPotDeviceClient
from letpot.models import DeviceFeature
from letpot.models import DeviceFeature, LetPotDeviceStatus, LetPotGardenStatus

from homeassistant.components.number import (
NumberEntity,
Expand All @@ -25,16 +25,18 @@


@dataclass(frozen=True, kw_only=True)
class LetPotNumberEntityDescription(LetPotEntityDescription, NumberEntityDescription):
class LetPotNumberEntityDescription[_DataT: LetPotDeviceStatus](
LetPotEntityDescription, NumberEntityDescription
):
"""Describes a LetPot number entity."""
Comment thread
jpelgrom marked this conversation as resolved.

max_value_fn: Callable[[LetPotDeviceCoordinator], float]
value_fn: Callable[[LetPotDeviceCoordinator], float | None]
max_value_fn: Callable[[LetPotDeviceCoordinator[_DataT]], float]
value_fn: Callable[[LetPotDeviceCoordinator[_DataT]], float | None]
set_value_fn: Callable[[LetPotDeviceClient, str, float], Coroutine[Any, Any, None]]


NUMBERS: tuple[LetPotNumberEntityDescription, ...] = (
LetPotNumberEntityDescription(
NUMBERS: tuple[LetPotNumberEntityDescription[LetPotGardenStatus], ...] = (
LetPotNumberEntityDescription[LetPotGardenStatus](
key="light_brightness_levels",
translation_key="light_brightness",
value_fn=(
Expand Down Expand Up @@ -73,7 +75,7 @@ class LetPotNumberEntityDescription(LetPotEntityDescription, NumberEntityDescrip
mode=NumberMode.SLIDER,
entity_category=EntityCategory.CONFIG,
),
LetPotNumberEntityDescription(
LetPotNumberEntityDescription[LetPotGardenStatus](
key="plant_days",
translation_key="plant_days",
native_unit_of_measurement=UnitOfTime.DAYS,
Expand All @@ -99,22 +101,24 @@ async def async_setup_entry(
"""Set up LetPot number entities based on a config entry and device status/features."""
coordinators = entry.runtime_data
async_add_entities(
LetPotNumberEntity(coordinator, description)
LetPotNumberEntity[LetPotGardenStatus](coordinator, description)
for description in NUMBERS
for coordinator in coordinators
if description.supported_fn(coordinator)
)


class LetPotNumberEntity(LetPotEntity, NumberEntity):
class LetPotNumberEntity[_DataT: LetPotDeviceStatus](
LetPotEntity[_DataT], NumberEntity
):
"""Defines a LetPot number entity."""

entity_description: LetPotNumberEntityDescription
entity_description: LetPotNumberEntityDescription[_DataT]

def __init__(
self,
coordinator: LetPotDeviceCoordinator,
description: LetPotNumberEntityDescription,
coordinator: LetPotDeviceCoordinator[_DataT],
description: LetPotNumberEntityDescription[_DataT],
) -> None:
"""Initialize LetPot number entity."""
super().__init__(coordinator)
Expand Down
34 changes: 22 additions & 12 deletions homeassistant/components/letpot/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
from typing import Any

from letpot.deviceclient import LetPotDeviceClient
from letpot.models import DeviceFeature, LightMode, TemperatureUnit
from letpot.models import (
DeviceFeature,
LetPotDeviceStatus,
LetPotGardenStatus,
LightMode,
TemperatureUnit,
)

from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
Expand Down Expand Up @@ -52,15 +58,17 @@ async def _set_brightness_low_high_value(


@dataclass(frozen=True, kw_only=True)
class LetPotSelectEntityDescription(LetPotEntityDescription, SelectEntityDescription):
class LetPotSelectEntityDescription[_DataT: LetPotDeviceStatus](
LetPotEntityDescription, SelectEntityDescription
):
"""Describes a LetPot select entity."""

Comment thread
jpelgrom marked this conversation as resolved.
value_fn: Callable[[LetPotDeviceCoordinator], str | None]
value_fn: Callable[[LetPotDeviceCoordinator[_DataT]], str | None]
set_value_fn: Callable[[LetPotDeviceClient, str, str], Coroutine[Any, Any, None]]


SELECTORS: tuple[LetPotSelectEntityDescription, ...] = (
LetPotSelectEntityDescription(
SELECTORS: tuple[LetPotSelectEntityDescription[LetPotGardenStatus], ...] = (
LetPotSelectEntityDescription[LetPotGardenStatus](
key="display_temperature_unit",
translation_key="display_temperature_unit",
options=[x.name.lower() for x in TemperatureUnit],
Expand All @@ -86,7 +94,7 @@ class LetPotSelectEntityDescription(LetPotEntityDescription, SelectEntityDescrip
),
entity_category=EntityCategory.CONFIG,
),
LetPotSelectEntityDescription(
LetPotSelectEntityDescription[LetPotGardenStatus](
key="light_brightness_low_high",
translation_key="light_brightness",
options=[
Expand All @@ -105,7 +113,7 @@ class LetPotSelectEntityDescription(LetPotEntityDescription, SelectEntityDescrip
),
entity_category=EntityCategory.CONFIG,
),
LetPotSelectEntityDescription(
LetPotSelectEntityDescription[LetPotGardenStatus](
key="light_mode",
translation_key="light_mode",
options=[x.name.lower() for x in LightMode],
Expand Down Expand Up @@ -134,22 +142,24 @@ async def async_setup_entry(
"""Set up LetPot select entities based on a config entry and device status/features."""
coordinators = entry.runtime_data
async_add_entities(
LetPotSelectEntity(coordinator, description)
LetPotSelectEntity[LetPotGardenStatus](coordinator, description)
for description in SELECTORS
for coordinator in coordinators
if description.supported_fn(coordinator)
)


class LetPotSelectEntity(LetPotEntity, SelectEntity):
class LetPotSelectEntity[_DataT: LetPotDeviceStatus](
LetPotEntity[_DataT], SelectEntity
):
"""Defines a LetPot select entity."""

entity_description: LetPotSelectEntityDescription
entity_description: LetPotSelectEntityDescription[_DataT]

def __init__(
self,
coordinator: LetPotDeviceCoordinator,
description: LetPotSelectEntityDescription,
coordinator: LetPotDeviceCoordinator[_DataT],
description: LetPotSelectEntityDescription[_DataT],
) -> None:
"""Initialize LetPot select entity."""
super().__init__(coordinator)
Expand Down
Loading
Loading