-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
19 changed files
with
732 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
"""The LetPot integration.""" | ||
|
||
from __future__ import annotations | ||
|
||
import asyncio | ||
|
||
from letpot.client import LetPotClient | ||
from letpot.converters import CONVERTERS | ||
from letpot.exceptions import LetPotAuthenticationException, LetPotException | ||
from letpot.models import AuthenticationInfo | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, Platform | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
|
||
from .const import ( | ||
CONF_ACCESS_TOKEN_EXPIRES, | ||
CONF_REFRESH_TOKEN, | ||
CONF_REFRESH_TOKEN_EXPIRES, | ||
CONF_USER_ID, | ||
) | ||
from .coordinator import LetPotDeviceCoordinator | ||
|
||
PLATFORMS: list[Platform] = [Platform.TIME] | ||
|
||
type LetPotConfigEntry = ConfigEntry[list[LetPotDeviceCoordinator]] | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: LetPotConfigEntry) -> bool: | ||
"""Set up LetPot from a config entry.""" | ||
|
||
auth = AuthenticationInfo( | ||
access_token=entry.data[CONF_ACCESS_TOKEN], | ||
access_token_expires=entry.data[CONF_ACCESS_TOKEN_EXPIRES], | ||
refresh_token=entry.data[CONF_REFRESH_TOKEN], | ||
refresh_token_expires=entry.data[CONF_REFRESH_TOKEN_EXPIRES], | ||
user_id=entry.data[CONF_USER_ID], | ||
email=entry.data[CONF_EMAIL], | ||
) | ||
websession = async_get_clientsession(hass) | ||
client = LetPotClient(websession, auth) | ||
|
||
if not auth.is_valid: | ||
try: | ||
auth = await client.refresh_token() | ||
hass.config_entries.async_update_entry( | ||
entry, | ||
data={ | ||
CONF_ACCESS_TOKEN: auth.access_token, | ||
CONF_ACCESS_TOKEN_EXPIRES: auth.access_token_expires, | ||
CONF_REFRESH_TOKEN: auth.refresh_token, | ||
CONF_REFRESH_TOKEN_EXPIRES: auth.refresh_token_expires, | ||
CONF_USER_ID: auth.user_id, | ||
CONF_EMAIL: auth.email, | ||
}, | ||
) | ||
except LetPotAuthenticationException as exc: | ||
raise ConfigEntryError from exc | ||
|
||
try: | ||
devices = await client.get_devices() | ||
except LetPotAuthenticationException as exc: | ||
raise ConfigEntryError from exc | ||
except LetPotException as exc: | ||
raise ConfigEntryNotReady from exc | ||
|
||
coordinators: list[LetPotDeviceCoordinator] = [ | ||
LetPotDeviceCoordinator(hass, auth, device) | ||
for device in devices | ||
if any(converter.supports_type(device.device_type) for converter in CONVERTERS) | ||
] | ||
|
||
await asyncio.gather( | ||
*[ | ||
coordinator.async_config_entry_first_refresh() | ||
for coordinator in coordinators | ||
] | ||
) | ||
|
||
entry.runtime_data = coordinators | ||
|
||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
|
||
return True | ||
|
||
|
||
async def async_unload_entry(hass: HomeAssistant, entry: LetPotConfigEntry) -> bool: | ||
"""Unload a config entry.""" | ||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): | ||
for coordinator in entry.runtime_data: | ||
coordinator.device_client.disconnect() | ||
return unload_ok |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
"""Config flow for the LetPot integration.""" | ||
|
||
from __future__ import annotations | ||
|
||
import logging | ||
from typing import Any | ||
|
||
from letpot.client import LetPotClient | ||
from letpot.exceptions import LetPotAuthenticationException, LetPotConnectionException | ||
import voluptuous as vol | ||
|
||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult | ||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
from homeassistant.helpers.selector import ( | ||
TextSelector, | ||
TextSelectorConfig, | ||
TextSelectorType, | ||
) | ||
|
||
from .const import ( | ||
CONF_ACCESS_TOKEN_EXPIRES, | ||
CONF_REFRESH_TOKEN, | ||
CONF_REFRESH_TOKEN_EXPIRES, | ||
CONF_USER_ID, | ||
DOMAIN, | ||
) | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
STEP_USER_DATA_SCHEMA = vol.Schema( | ||
{ | ||
vol.Required(CONF_EMAIL): TextSelector( | ||
TextSelectorConfig( | ||
type=TextSelectorType.EMAIL, | ||
), | ||
), | ||
vol.Required(CONF_PASSWORD): TextSelector( | ||
TextSelectorConfig( | ||
type=TextSelectorType.PASSWORD, | ||
), | ||
), | ||
} | ||
) | ||
|
||
|
||
class LetPotConfigFlow(ConfigFlow, domain=DOMAIN): | ||
"""Handle a config flow for LetPot.""" | ||
|
||
VERSION = 1 | ||
|
||
async def _async_validate_credentials( | ||
self, email: str, password: str | ||
) -> dict[str, Any]: | ||
websession = async_get_clientsession(self.hass) | ||
client = LetPotClient(websession) | ||
auth = await client.login(email, password) | ||
return { | ||
CONF_ACCESS_TOKEN: auth.access_token, | ||
CONF_ACCESS_TOKEN_EXPIRES: auth.access_token_expires, | ||
CONF_REFRESH_TOKEN: auth.refresh_token, | ||
CONF_REFRESH_TOKEN_EXPIRES: auth.refresh_token_expires, | ||
CONF_USER_ID: auth.user_id, | ||
CONF_EMAIL: auth.email, | ||
} | ||
|
||
async def async_step_user( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> ConfigFlowResult: | ||
"""Handle a flow initialized by the user.""" | ||
errors: dict[str, str] = {} | ||
if user_input is not None: | ||
try: | ||
data_dict = await self._async_validate_credentials( | ||
user_input[CONF_EMAIL], user_input[CONF_PASSWORD] | ||
) | ||
except LetPotConnectionException: | ||
errors["base"] = "cannot_connect" | ||
except LetPotAuthenticationException: | ||
errors["base"] = "invalid_auth" | ||
except Exception: # noqa: BLE001 | ||
_LOGGER.exception("Unexpected exception") | ||
errors["base"] = "unknown" | ||
else: | ||
await self.async_set_unique_id(data_dict[CONF_USER_ID]) | ||
self._abort_if_unique_id_configured() | ||
return self.async_create_entry( | ||
title=data_dict[CONF_EMAIL], data=data_dict | ||
) | ||
return self.async_show_form( | ||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
"""Constants for the LetPot integration.""" | ||
|
||
DOMAIN = "letpot" | ||
|
||
CONF_ACCESS_TOKEN_EXPIRES = "access_token_expires" | ||
CONF_REFRESH_TOKEN = "refresh_token" | ||
CONF_REFRESH_TOKEN_EXPIRES = "refresh_token_expires" | ||
CONF_USER_ID = "user_id" | ||
|
||
REQUEST_UPDATE_TIMEOUT = 10 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
"""Coordinator for the LetPot integration.""" | ||
|
||
from __future__ import annotations | ||
|
||
import asyncio | ||
import logging | ||
from typing import TYPE_CHECKING | ||
|
||
from letpot.deviceclient import LetPotDeviceClient | ||
from letpot.exceptions import LetPotAuthenticationException, LetPotException | ||
from letpot.models import AuthenticationInfo, LetPotDevice, LetPotDeviceStatus | ||
|
||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryError | ||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||
|
||
from .const import REQUEST_UPDATE_TIMEOUT | ||
|
||
if TYPE_CHECKING: | ||
from . import LetPotConfigEntry | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class LetPotDeviceCoordinator(DataUpdateCoordinator[LetPotDeviceStatus]): | ||
"""Class to handle data updates for a specific garden.""" | ||
|
||
config_entry: LetPotConfigEntry | ||
|
||
device: LetPotDevice | ||
device_client: LetPotDeviceClient | ||
|
||
def __init__( | ||
self, hass: HomeAssistant, info: AuthenticationInfo, device: LetPotDevice | ||
) -> None: | ||
"""Initialize coordinator.""" | ||
super().__init__( | ||
hass, | ||
_LOGGER, | ||
name=f"LetPot {device.serial_number}", | ||
) | ||
self._info = info | ||
self.device = device | ||
self.device_client = LetPotDeviceClient(info, device.serial_number) | ||
|
||
def _handle_status_update(self, status: LetPotDeviceStatus) -> 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._handle_status_update) | ||
except LetPotAuthenticationException as exc: | ||
raise ConfigEntryError from exc | ||
|
||
async def _async_update_data(self) -> LetPotDeviceStatus: | ||
"""Request an update from the device and wait for a status update or timeout.""" | ||
try: | ||
async with asyncio.timeout(REQUEST_UPDATE_TIMEOUT): | ||
await self.device_client.get_current_status() | ||
except LetPotException as exc: | ||
raise UpdateFailed(exc) from exc | ||
|
||
# The subscription task will have updated coordinator.data, so return that data. | ||
# If we don't return anything here, coordinator.data will be set to None. | ||
return self.data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
"""Base class for LetPot entities.""" | ||
|
||
from homeassistant.helpers.device_registry import DeviceInfo | ||
from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||
|
||
from .const import DOMAIN | ||
from .coordinator import LetPotDeviceCoordinator | ||
|
||
|
||
class LetPotEntity(CoordinatorEntity[LetPotDeviceCoordinator]): | ||
"""Defines a base LetPot entity.""" | ||
|
||
_attr_has_entity_name = True | ||
|
||
def __init__(self, coordinator: LetPotDeviceCoordinator) -> None: | ||
"""Initialize a LetPot entity.""" | ||
super().__init__(coordinator) | ||
self._attr_device_info = DeviceInfo( | ||
identifiers={(DOMAIN, coordinator.device.serial_number)}, | ||
name=coordinator.device.name, | ||
manufacturer="LetPot", | ||
model=coordinator.device_client.device_model_name, | ||
model_id=coordinator.device_client.device_model_code, | ||
serial_number=coordinator.device.serial_number, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"domain": "letpot", | ||
"name": "LetPot", | ||
"codeowners": ["@jpelgrom"], | ||
"config_flow": true, | ||
"documentation": "https://www.home-assistant.io/integrations/letpot", | ||
"integration_type": "hub", | ||
"iot_class": "cloud_push", | ||
"quality_scale": "bronze", | ||
"requirements": ["letpot==0.2.0"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
rules: | ||
# Bronze | ||
action-setup: | ||
status: exempt | ||
comment: | | ||
This integration does not provide additional actions. | ||
appropriate-polling: | ||
status: exempt | ||
comment: | | ||
This integration only receives push-based updates. | ||
brands: done | ||
common-modules: done | ||
config-flow-test-coverage: done | ||
config-flow: done | ||
dependency-transparency: done | ||
docs-actions: | ||
status: exempt | ||
comment: | | ||
This integration does not provide additional actions. | ||
docs-high-level-description: done | ||
docs-installation-instructions: done | ||
docs-removal-instructions: done | ||
entity-event-setup: done | ||
entity-unique-id: done | ||
has-entity-name: done | ||
runtime-data: done | ||
test-before-configure: done | ||
test-before-setup: done | ||
unique-config-entry: done | ||
|
||
# Silver | ||
action-exceptions: todo | ||
config-entry-unloading: | ||
status: done | ||
comment: | | ||
Push connection connects in coordinator _async_setup, disconnects in init async_unload_entry. | ||
docs-configuration-parameters: | ||
status: exempt | ||
comment: | | ||
The integration does not have configuration options. | ||
docs-installation-parameters: done | ||
entity-unavailable: todo | ||
integration-owner: done | ||
log-when-unavailable: todo | ||
parallel-updates: done | ||
reauthentication-flow: todo | ||
test-coverage: todo | ||
|
||
# Gold | ||
devices: done | ||
diagnostics: todo | ||
discovery-update-info: todo | ||
discovery: todo | ||
docs-data-update: done | ||
docs-examples: todo | ||
docs-known-limitations: todo | ||
docs-supported-devices: done | ||
docs-supported-functions: todo | ||
docs-troubleshooting: todo | ||
docs-use-cases: todo | ||
dynamic-devices: todo | ||
entity-category: todo | ||
entity-device-class: todo | ||
entity-disabled-by-default: todo | ||
entity-translations: done | ||
exception-translations: todo | ||
icon-translations: todo | ||
reconfiguration-flow: todo | ||
repair-issues: todo | ||
stale-devices: todo | ||
|
||
# Platinum | ||
async-dependency: done | ||
inject-websession: done | ||
strict-typing: done |
Oops, something went wrong.