From e3b90ea3f794256159d1405fd4c391c78446e1db Mon Sep 17 00:00:00 2001 From: Eugene Prystupa Date: Tue, 23 Jun 2020 23:40:11 -0400 Subject: [PATCH] Add Plum Lightpad config flow (#36802) * add support for config flow for Plum Lightpad integration * add support for config flow for Plum Lightpad integration (remove unintended change to requirements_test_all.txt) * add support for config flow for Plum Lightpad integration (fix lint issues) * add support for config flow for Plum Lightpad integration (PR feedback) * add support for config flow for Plum Lightpad integration (fix lint) * Update homeassistant/components/plum_lightpad/__init__.py use debug instead of info for logging Co-authored-by: Paulus Schoutsen * Update homeassistant/components/plum_lightpad/strings.json switch to use generated references instead of hard-coded strings Co-authored-by: Paulus Schoutsen * Update homeassistant/components/plum_lightpad/strings.json switch to use references instead of hard-coded string Co-authored-by: Paulus Schoutsen * Update homeassistant/components/plum_lightpad/strings.json removing translated title per suggestion Co-authored-by: Paulus Schoutsen * Update homeassistant/components/plum_lightpad/strings.json removing per suggestion Co-authored-by: Paulus Schoutsen * remove unnecessary deepcopy * remove unnecessary logging warning, since ignoring is expected for configuration.yaml scenario * switch to hass.loop.create_task per PR feedback * show login errors when configuring integration via UI (PR feedback) * disable wrongly flag pylint violation * add except handler to handle connection errors when setting up config flow entry * address PR feedback regarding exception handling * Update homeassistant/components/plum_lightpad/config_flow.py use helper instead of custom code/message-id Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 2 +- .../components/plum_lightpad/__init__.py | 89 +++++++++---------- .../components/plum_lightpad/config_flow.py | 62 +++++++++++++ .../components/plum_lightpad/light.py | 78 +++++++++++++--- .../components/plum_lightpad/manifest.json | 10 ++- .../components/plum_lightpad/strings.json | 18 ++++ .../plum_lightpad/translations/en.json | 20 +++++ .../components/plum_lightpad/utils.py | 14 +++ homeassistant/generated/config_flows.py | 1 + 9 files changed, 232 insertions(+), 62 deletions(-) create mode 100644 homeassistant/components/plum_lightpad/config_flow.py create mode 100644 homeassistant/components/plum_lightpad/strings.json create mode 100644 homeassistant/components/plum_lightpad/translations/en.json create mode 100644 homeassistant/components/plum_lightpad/utils.py diff --git a/CODEOWNERS b/CODEOWNERS index c3226e65bc3..d860a0f57ed 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -316,7 +316,7 @@ homeassistant/components/plaato/* @JohNan homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plex/* @jjlawren homeassistant/components/plugwise/* @CoMPaTech @bouwew -homeassistant/components/plum_lightpad/* @ColinHarrington +homeassistant/components/plum_lightpad/* @ColinHarrington @prystupa homeassistant/components/point/* @fredrike homeassistant/components/powerwall/* @bdraco @jrester homeassistant/components/prometheus/* @knyar diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index a995d1a816a..8e7596bd7e0 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -1,16 +1,18 @@ """Support for Plum Lightpad devices.""" -import asyncio import logging -from plumlightpad import Plum +from aiohttp import ContentTypeError +from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers import discovery -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from .const import DOMAIN +from .utils import load_plum _LOGGER = logging.getLogger(__name__) @@ -26,56 +28,53 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +PLATFORMS = ["light"] -async def async_setup(hass, config): + +async def async_setup(hass: HomeAssistant, config: dict): """Plum Lightpad Platform initialization.""" + if DOMAIN not in config: + return True conf = config[DOMAIN] - plum = Plum(conf[CONF_USERNAME], conf[CONF_PASSWORD]) - hass.data[DOMAIN] = plum + _LOGGER.info("Found Plum Lightpad configuration in config, importing...") + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Plum Lightpad from a config entry.""" + _LOGGER.debug("Setting up config entry with ID = %s", entry.unique_id) + + username = entry.data.get(CONF_USERNAME) + password = entry.data.get(CONF_PASSWORD) + + try: + plum = await load_plum(username, password, hass) + except ContentTypeError as ex: + _LOGGER.error("Unable to authenticate to Plum cloud: %s", ex) + return False + except (ConnectTimeout, HTTPError) as ex: + _LOGGER.error("Unable to connect to Plum cloud: %s", ex) + raise ConfigEntryNotReady + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = plum + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) def cleanup(event): """Clean up resources.""" plum.cleanup() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) - - cloud_web_sesison = async_get_clientsession(hass, verify_ssl=True) - await plum.loadCloudData(cloud_web_sesison) - - async def new_load(device): - """Load light and sensor platforms when LogicalLoad is detected.""" - await asyncio.wait( - [ - hass.async_create_task( - discovery.async_load_platform( - hass, "light", DOMAIN, discovered=device, hass_config=conf - ) - ) - ] - ) - - async def new_lightpad(device): - """Load light and binary sensor platforms when Lightpad detected.""" - await asyncio.wait( - [ - hass.async_create_task( - discovery.async_load_platform( - hass, "light", DOMAIN, discovered=device, hass_config=conf - ) - ) - ] - ) - - device_web_session = async_get_clientsession(hass, verify_ssl=False) - hass.async_create_task( - plum.discover( - hass.loop, - loadListener=new_load, - lightpadListener=new_lightpad, - websession=device_web_session, - ) - ) - return True diff --git a/homeassistant/components/plum_lightpad/config_flow.py b/homeassistant/components/plum_lightpad/config_flow.py new file mode 100644 index 00000000000..acf9380bf71 --- /dev/null +++ b/homeassistant/components/plum_lightpad/config_flow.py @@ -0,0 +1,62 @@ +"""Config flow for Plum Lightpad.""" +import logging +from typing import Any, Dict, Optional + +from aiohttp import ContentTypeError +from requests.exceptions import ConnectTimeout, HTTPError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import ConfigType + +from .const import DOMAIN # pylint: disable=unused-import +from .utils import load_plum + +_LOGGER = logging.getLogger(__name__) + + +class PlumLightpadConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Plum Lightpad integration.""" + + VERSION = 1 + + def _show_form(self, errors=None): + schema = { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + + return self.async_show_form( + step_id="user", data_schema=vol.Schema(schema), errors=errors or {}, + ) + + async def async_step_user( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle a flow initialized by the user or redirected to by import.""" + if not user_input: + return self._show_form() + + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + + # load Plum just so we know username/password work + try: + await load_plum(username, password, self.hass) + except (ContentTypeError, ConnectTimeout, HTTPError) as ex: + _LOGGER.error("Unable to connect/authenticate to Plum cloud: %s", str(ex)) + return self._show_form({"base": "cannot_connect"}) + + await self.async_set_unique_id(username) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=username, data={CONF_USERNAME: username, CONF_PASSWORD: password} + ) + + async def async_step_import( + self, import_config: Optional[ConfigType] + ) -> Dict[str, Any]: + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py index 0dffa4c966c..4a02e83de76 100644 --- a/homeassistant/components/plum_lightpad/light.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -1,4 +1,9 @@ """Support for Plum Lightpad lights.""" +import logging +from typing import Callable, List + +from plumlightpad import Plum + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, @@ -6,30 +11,55 @@ from homeassistant.components.light import ( SUPPORT_COLOR, LightEntity, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import Entity import homeassistant.util.color as color_util from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Initialize the Plum Lightpad Light and GlowRing.""" - if discovery_info is None: - return - plum = hass.data[DOMAIN] +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity]], None], +) -> None: + """Set up Plum Lightpad dimmer lights and glow rings.""" - entities = [] + plum: Plum = hass.data[DOMAIN][entry.entry_id] - if "lpid" in discovery_info: - lightpad = plum.get_lightpad(discovery_info["lpid"]) - entities.append(GlowRing(lightpad=lightpad)) + def setup_entities(device) -> None: + entities = [] - if "llid" in discovery_info: - logical_load = plum.get_load(discovery_info["llid"]) - entities.append(PlumLight(load=logical_load)) + if "lpid" in device: + lightpad = plum.get_lightpad(device["lpid"]) + entities.append(GlowRing(lightpad=lightpad)) - if entities: - async_add_entities(entities) + if "llid" in device: + logical_load = plum.get_load(device["llid"]) + entities.append(PlumLight(load=logical_load)) + + if entities: + async_add_entities(entities) + + async def new_load(device): + setup_entities(device) + + async def new_lightpad(device): + setup_entities(device) + + device_web_session = async_get_clientsession(hass, verify_ssl=False) + hass.loop.create_task( + plum.discover( + hass.loop, + loadListener=new_load, + lightpadListener=new_lightpad, + websession=device_web_session, + ) + ) class PlumLight(LightEntity): @@ -64,6 +94,16 @@ class PlumLight(LightEntity): """Return the name of the switch if any.""" return self._load.name + @property + def device_info(self): + """Return the device info.""" + return { + "name": self.name, + "identifiers": {(DOMAIN, self.unique_id)}, + "model": "Dimmer", + "manufacturer": "Plum", + } + @property def brightness(self) -> int: """Return the brightness of this switch between 0..255.""" @@ -145,6 +185,16 @@ class GlowRing(LightEntity): """Return the name of the switch if any.""" return self._name + @property + def device_info(self): + """Return the device info.""" + return { + "name": self.name, + "identifiers": {(DOMAIN, self.unique_id)}, + "model": "Glow Ring", + "manufacturer": "Plum", + } + @property def brightness(self) -> int: """Return the brightness of this switch between 0..255.""" diff --git a/homeassistant/components/plum_lightpad/manifest.json b/homeassistant/components/plum_lightpad/manifest.json index 5c846d41ad1..ed9bb9c2eb4 100644 --- a/homeassistant/components/plum_lightpad/manifest.json +++ b/homeassistant/components/plum_lightpad/manifest.json @@ -2,6 +2,12 @@ "domain": "plum_lightpad", "name": "Plum Lightpad", "documentation": "https://www.home-assistant.io/integrations/plum_lightpad", - "requirements": ["plumlightpad==0.0.11"], - "codeowners": ["@ColinHarrington"] + "requirements": [ + "plumlightpad==0.0.11" + ], + "codeowners": [ + "@ColinHarrington", + "@prystupa" + ], + "config_flow": true } diff --git a/homeassistant/components/plum_lightpad/strings.json b/homeassistant/components/plum_lightpad/strings.json new file mode 100644 index 00000000000..935e1614696 --- /dev/null +++ b/homeassistant/components/plum_lightpad/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + } + } +} diff --git a/homeassistant/components/plum_lightpad/translations/en.json b/homeassistant/components/plum_lightpad/translations/en.json new file mode 100644 index 00000000000..95cafaa7313 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_per_username_allowed": "Only one config entry per unique username is supported" + }, + "error": { + "cannot_connect": "Unable to connect to Plum Cloud." + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Email" + }, + "title": "Fill in your Plum Cloud login information" + } + } + }, + "title": "Plum Lightpad" +} diff --git a/homeassistant/components/plum_lightpad/utils.py b/homeassistant/components/plum_lightpad/utils.py new file mode 100644 index 00000000000..6704b443d72 --- /dev/null +++ b/homeassistant/components/plum_lightpad/utils.py @@ -0,0 +1,14 @@ +"""Reusable utilities for the Plum Lightpad component.""" + +from plumlightpad import Plum + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + + +async def load_plum(username: str, password: str, hass: HomeAssistant) -> Plum: + """Initialize Plum Lightpad API and load metadata stored in the cloud.""" + plum = Plum(username, password) + cloud_web_session = async_get_clientsession(hass, verify_ssl=True) + await plum.loadCloudData(cloud_web_session) + return plum diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 54678007eb7..977be4bae87 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -122,6 +122,7 @@ FLOWS = [ "plaato", "plex", "plugwise", + "plum_lightpad", "point", "powerwall", "ps4",