forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add laundrify integration (home-assistant#65090)
* First version of laundrify integration * Code cleanup * Code cleanup after review #2 * Move coordinator to its own file * Save devices as dict and implement available prop as fn * Validate token on init, abort if already configured * Some more cleanup after review * Add strict type hints * Minor changes after code review * Remove OptionsFlow (use default poll interval instead) * Fix CODEOWNERS to pass hassfest job * Fix formatting to pass prettier job * Fix mypy typing error * Update internal device property after fetching data * Call parental update handler and remove obsolete code * Add coordinator tests and fix some config flow tests * Refactor tests * Refactor fixtures * Device unavailable if polling fails
- Loading branch information
Showing
22 changed files
with
709 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,52 @@ | ||
"""The laundrify integration.""" | ||
from __future__ import annotations | ||
|
||
from laundrify_aio import LaundrifyAPI | ||
from laundrify_aio.exceptions import ApiConnectionException, UnauthorizedException | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import CONF_ACCESS_TOKEN, Platform | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
|
||
from .const import DEFAULT_POLL_INTERVAL, DOMAIN | ||
from .coordinator import LaundrifyUpdateCoordinator | ||
|
||
PLATFORMS = [Platform.BINARY_SENSOR] | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Set up laundrify from a config entry.""" | ||
|
||
session = async_get_clientsession(hass) | ||
api_client = LaundrifyAPI(entry.data[CONF_ACCESS_TOKEN], session) | ||
|
||
try: | ||
await api_client.validate_token() | ||
except UnauthorizedException as err: | ||
raise ConfigEntryAuthFailed("Invalid authentication") from err | ||
except ApiConnectionException as err: | ||
raise ConfigEntryNotReady("Cannot reach laundrify API") from err | ||
|
||
coordinator = LaundrifyUpdateCoordinator(hass, api_client, DEFAULT_POLL_INTERVAL) | ||
|
||
await coordinator.async_config_entry_first_refresh() | ||
|
||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { | ||
"api": api_client, | ||
"coordinator": coordinator, | ||
} | ||
|
||
hass.config_entries.async_setup_platforms(entry, PLATFORMS) | ||
|
||
return True | ||
|
||
|
||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Unload a config entry.""" | ||
|
||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): | ||
hass.data[DOMAIN].pop(entry.entry_id) | ||
|
||
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,84 @@ | ||
"""Platform for binary sensor integration.""" | ||
from __future__ import annotations | ||
|
||
import logging | ||
|
||
from homeassistant.components.binary_sensor import ( | ||
BinarySensorDeviceClass, | ||
BinarySensorEntity, | ||
) | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.core import HomeAssistant, callback | ||
from homeassistant.helpers.entity import DeviceInfo | ||
from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||
from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||
|
||
from .const import DOMAIN, MANUFACTURER, MODEL | ||
from .coordinator import LaundrifyUpdateCoordinator | ||
from .model import LaundrifyDevice | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
async def async_setup_entry( | ||
hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback | ||
) -> None: | ||
"""Set up sensors from a config entry created in the integrations UI.""" | ||
|
||
coordinator = hass.data[DOMAIN][config.entry_id]["coordinator"] | ||
|
||
async_add_entities( | ||
LaundrifyPowerPlug(coordinator, device) for device in coordinator.data.values() | ||
) | ||
|
||
|
||
class LaundrifyPowerPlug( | ||
CoordinatorEntity[LaundrifyUpdateCoordinator], BinarySensorEntity | ||
): | ||
"""Representation of a laundrify Power Plug.""" | ||
|
||
_attr_device_class = BinarySensorDeviceClass.RUNNING | ||
_attr_icon = "mdi:washing-machine" | ||
|
||
def __init__( | ||
self, coordinator: LaundrifyUpdateCoordinator, device: LaundrifyDevice | ||
) -> None: | ||
"""Pass coordinator to CoordinatorEntity.""" | ||
super().__init__(coordinator) | ||
self._device = device | ||
self._attr_unique_id = device["_id"] | ||
|
||
@property | ||
def device_info(self) -> DeviceInfo: | ||
"""Configure the Device of this Entity.""" | ||
return DeviceInfo( | ||
identifiers={(DOMAIN, self._device["_id"])}, | ||
name=self.name, | ||
manufacturer=MANUFACTURER, | ||
model=MODEL, | ||
sw_version=self._device["firmwareVersion"], | ||
) | ||
|
||
@property | ||
def available(self) -> bool: | ||
"""Check if the device is available.""" | ||
return ( | ||
self.unique_id in self.coordinator.data | ||
and self.coordinator.last_update_success | ||
) | ||
|
||
@property | ||
def name(self) -> str: | ||
"""Name of the entity.""" | ||
return self._device["name"] | ||
|
||
@property | ||
def is_on(self) -> bool: | ||
"""Return entity state.""" | ||
return self._device["status"] == "ON" | ||
|
||
@callback | ||
def _handle_coordinator_update(self) -> None: | ||
"""Handle updated data from the coordinator.""" | ||
self._device = self.coordinator.data[self.unique_id] | ||
super()._handle_coordinator_update() |
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 @@ | ||
"""Config flow for laundrify integration.""" | ||
from __future__ import annotations | ||
|
||
import logging | ||
from typing import Any | ||
|
||
from laundrify_aio import LaundrifyAPI | ||
from laundrify_aio.exceptions import ( | ||
ApiConnectionException, | ||
InvalidFormat, | ||
UnknownAuthCode, | ||
) | ||
from voluptuous import Required, Schema | ||
|
||
from homeassistant.config_entries import ConfigFlow | ||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CODE | ||
from homeassistant.data_entry_flow import FlowResult | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
|
||
from .const import DOMAIN | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
CONFIG_SCHEMA = Schema({Required(CONF_CODE): str}) | ||
|
||
|
||
class LaundrifyConfigFlow(ConfigFlow, domain=DOMAIN): | ||
"""Handle a config flow for laundrify.""" | ||
|
||
VERSION = 1 | ||
|
||
async def async_step_user( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> FlowResult: | ||
"""Handle a flow initialized by the user.""" | ||
return await self.async_step_init(user_input) | ||
|
||
async def async_step_init( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> FlowResult: | ||
"""Handle the initial step.""" | ||
if user_input is None: | ||
return self.async_show_form(step_id="init", data_schema=CONFIG_SCHEMA) | ||
|
||
errors = {} | ||
|
||
try: | ||
access_token = await LaundrifyAPI.exchange_auth_code(user_input[CONF_CODE]) | ||
|
||
session = async_get_clientsession(self.hass) | ||
api_client = LaundrifyAPI(access_token, session) | ||
|
||
account_id = await api_client.get_account_id() | ||
except InvalidFormat: | ||
errors[CONF_CODE] = "invalid_format" | ||
except UnknownAuthCode: | ||
errors[CONF_CODE] = "invalid_auth" | ||
except ApiConnectionException: | ||
errors["base"] = "cannot_connect" | ||
except Exception: # pylint: disable=broad-except | ||
_LOGGER.exception("Unexpected exception") | ||
errors["base"] = "unknown" | ||
else: | ||
entry_data = {CONF_ACCESS_TOKEN: access_token} | ||
|
||
await self.async_set_unique_id(account_id) | ||
self._abort_if_unique_id_configured() | ||
|
||
# Create a new entry if it doesn't exist | ||
return self.async_create_entry( | ||
title=DOMAIN, | ||
data=entry_data, | ||
) | ||
|
||
return self.async_show_form( | ||
step_id="init", data_schema=CONFIG_SCHEMA, errors=errors | ||
) | ||
|
||
async def async_step_reauth( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> FlowResult: | ||
"""Perform reauth upon an API authentication error.""" | ||
return await self.async_step_reauth_confirm() | ||
|
||
async def async_step_reauth_confirm( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> FlowResult: | ||
"""Dialog that informs the user that reauth is required.""" | ||
if user_input is None: | ||
return self.async_show_form( | ||
step_id="reauth_confirm", | ||
data_schema=Schema({}), | ||
) | ||
return await self.async_step_init() |
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 laundrify integration.""" | ||
|
||
DOMAIN = "laundrify" | ||
|
||
MANUFACTURER = "laundrify" | ||
MODEL = "WLAN-Adapter (SU02)" | ||
|
||
DEFAULT_POLL_INTERVAL = 60 | ||
|
||
REQUEST_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,46 @@ | ||
"""Custom DataUpdateCoordinator for the laundrify integration.""" | ||
from datetime import timedelta | ||
import logging | ||
|
||
import async_timeout | ||
from laundrify_aio import LaundrifyAPI | ||
from laundrify_aio.exceptions import ApiConnectionException, UnauthorizedException | ||
|
||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryAuthFailed | ||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||
|
||
from .const import DOMAIN, REQUEST_TIMEOUT | ||
from .model import LaundrifyDevice | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class LaundrifyUpdateCoordinator(DataUpdateCoordinator): | ||
"""Class to manage fetching laundrify API data.""" | ||
|
||
def __init__( | ||
self, hass: HomeAssistant, laundrify_api: LaundrifyAPI, poll_interval: int | ||
) -> None: | ||
"""Initialize laundrify coordinator.""" | ||
super().__init__( | ||
hass, | ||
_LOGGER, | ||
name=DOMAIN, | ||
update_interval=timedelta(seconds=poll_interval), | ||
) | ||
self.laundrify_api = laundrify_api | ||
|
||
async def _async_update_data(self) -> dict[str, LaundrifyDevice]: | ||
"""Fetch data from laundrify API.""" | ||
try: | ||
# Note: asyncio.TimeoutError and aiohttp.ClientError are already | ||
# handled by the data update coordinator. | ||
async with async_timeout.timeout(REQUEST_TIMEOUT): | ||
return {m["_id"]: m for m in await self.laundrify_api.get_machines()} | ||
except UnauthorizedException as err: | ||
# Raising ConfigEntryAuthFailed will cancel future updates | ||
# and start a config flow with SOURCE_REAUTH (async_step_reauth) | ||
raise ConfigEntryAuthFailed from err | ||
except ApiConnectionException as err: | ||
raise UpdateFailed(f"Error communicating with API: {err}") from err |
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,9 @@ | ||
{ | ||
"domain": "laundrify", | ||
"name": "laundrify", | ||
"config_flow": true, | ||
"documentation": "https://www.home-assistant.io/integrations/laundrify", | ||
"requirements": ["laundrify_aio==1.1.1"], | ||
"codeowners": ["@xLarry"], | ||
"iot_class": "cloud_polling" | ||
} |
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,13 @@ | ||
"""Models for laundrify platform.""" | ||
from __future__ import annotations | ||
|
||
from typing import TypedDict | ||
|
||
|
||
class LaundrifyDevice(TypedDict): | ||
"""laundrify Power Plug.""" | ||
|
||
_id: str | ||
name: str | ||
status: str | ||
firmwareVersion: str |
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 @@ | ||
{ | ||
"config": { | ||
"step": { | ||
"init": { | ||
"description": "Please enter your personal Auth Code that is shown in the laundrify-App.", | ||
"data": { | ||
"code": "Auth Code (xxx-xxx)" | ||
} | ||
}, | ||
"reauth_confirm": { | ||
"title": "[%key:common::config_flow::title::reauth%]", | ||
"description": "The laundrify integration needs to re-authenticate." | ||
} | ||
}, | ||
"error": { | ||
"invalid_format": "Invalid format. Please specify as xxx-xxx.", | ||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||
"unknown": "[%key:common::config_flow::error::unknown%]" | ||
}, | ||
"abort": { | ||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" | ||
} | ||
} | ||
} |
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 @@ | ||
{ | ||
"config": { | ||
"abort": { | ||
"single_instance_allowed": "Already configured. Only a single configuration possible." | ||
}, | ||
"error": { | ||
"cannot_connect": "Failed to connect", | ||
"invalid_auth": "Invalid authentication", | ||
"invalid_format": "Invalid format. Please specify as xxx-xxx.", | ||
"unknown": "Unexpected error" | ||
}, | ||
"step": { | ||
"init": { | ||
"data": { | ||
"code": "Auth Code (xxx-xxx)" | ||
}, | ||
"description": "Please enter your personal AuthCode that is shown in the laundrify-App." | ||
}, | ||
"reauth_confirm": { | ||
"description": "The laundrify integration needs to re-authenticate.", | ||
"title": "Reauthenticate Integration" | ||
} | ||
} | ||
} | ||
} |
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 |
---|---|---|
|
@@ -187,6 +187,7 @@ | |
"kraken", | ||
"kulersky", | ||
"launch_library", | ||
"laundrify", | ||
"life360", | ||
"lifx", | ||
"litejet", | ||
|
Oops, something went wrong.