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 <paulus@home-assistant.io>

* Update homeassistant/components/plum_lightpad/strings.json

switch to use generated references instead of hard-coded strings

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Update homeassistant/components/plum_lightpad/strings.json

switch to use references instead of hard-coded string

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Update homeassistant/components/plum_lightpad/strings.json

removing translated title per suggestion

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Update homeassistant/components/plum_lightpad/strings.json

removing per suggestion

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* 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 <paulus@home-assistant.io>

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
Eugene Prystupa 2020-06-23 23:40:11 -04:00 committed by GitHub
parent 6c7355785a
commit e3b90ea3f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 232 additions and 62 deletions

View File

@ -316,7 +316,7 @@ homeassistant/components/plaato/* @JohNan
homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plant/* @ChristianKuehnel
homeassistant/components/plex/* @jjlawren homeassistant/components/plex/* @jjlawren
homeassistant/components/plugwise/* @CoMPaTech @bouwew homeassistant/components/plugwise/* @CoMPaTech @bouwew
homeassistant/components/plum_lightpad/* @ColinHarrington homeassistant/components/plum_lightpad/* @ColinHarrington @prystupa
homeassistant/components/point/* @fredrike homeassistant/components/point/* @fredrike
homeassistant/components/powerwall/* @bdraco @jrester homeassistant/components/powerwall/* @bdraco @jrester
homeassistant/components/prometheus/* @knyar homeassistant/components/prometheus/* @knyar

View File

@ -1,16 +1,18 @@
"""Support for Plum Lightpad devices.""" """Support for Plum Lightpad devices."""
import asyncio
import logging import logging
from plumlightpad import Plum from aiohttp import ContentTypeError
from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol 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.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers import discovery from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from .const import DOMAIN from .const import DOMAIN
from .utils import load_plum
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -26,56 +28,53 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
PLATFORMS = ["light"]
async def async_setup(hass, config):
async def async_setup(hass: HomeAssistant, config: dict):
"""Plum Lightpad Platform initialization.""" """Plum Lightpad Platform initialization."""
if DOMAIN not in config:
return True
conf = config[DOMAIN] 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): def cleanup(event):
"""Clean up resources.""" """Clean up resources."""
plum.cleanup() plum.cleanup()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, 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 return True

View File

@ -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)

View File

@ -1,4 +1,9 @@
"""Support for Plum Lightpad lights.""" """Support for Plum Lightpad lights."""
import logging
from typing import Callable, List
from plumlightpad import Plum
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_HS_COLOR, ATTR_HS_COLOR,
@ -6,30 +11,55 @@ from homeassistant.components.light import (
SUPPORT_COLOR, SUPPORT_COLOR,
LightEntity, 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 import homeassistant.util.color as color_util
from .const import DOMAIN 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: def setup_entities(device) -> None:
lightpad = plum.get_lightpad(discovery_info["lpid"]) entities = []
entities.append(GlowRing(lightpad=lightpad))
if "llid" in discovery_info: if "lpid" in device:
logical_load = plum.get_load(discovery_info["llid"]) lightpad = plum.get_lightpad(device["lpid"])
entities.append(PlumLight(load=logical_load)) entities.append(GlowRing(lightpad=lightpad))
if entities: if "llid" in device:
async_add_entities(entities) 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): class PlumLight(LightEntity):
@ -64,6 +94,16 @@ class PlumLight(LightEntity):
"""Return the name of the switch if any.""" """Return the name of the switch if any."""
return self._load.name 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 @property
def brightness(self) -> int: def brightness(self) -> int:
"""Return the brightness of this switch between 0..255.""" """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 the name of the switch if any."""
return self._name 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 @property
def brightness(self) -> int: def brightness(self) -> int:
"""Return the brightness of this switch between 0..255.""" """Return the brightness of this switch between 0..255."""

View File

@ -2,6 +2,12 @@
"domain": "plum_lightpad", "domain": "plum_lightpad",
"name": "Plum Lightpad", "name": "Plum Lightpad",
"documentation": "https://www.home-assistant.io/integrations/plum_lightpad", "documentation": "https://www.home-assistant.io/integrations/plum_lightpad",
"requirements": ["plumlightpad==0.0.11"], "requirements": [
"codeowners": ["@ColinHarrington"] "plumlightpad==0.0.11"
],
"codeowners": [
"@ColinHarrington",
"@prystupa"
],
"config_flow": true
} }

View File

@ -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%]"
}
}
}

View File

@ -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"
}

View File

@ -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

View File

@ -122,6 +122,7 @@ FLOWS = [
"plaato", "plaato",
"plex", "plex",
"plugwise", "plugwise",
"plum_lightpad",
"point", "point",
"powerwall", "powerwall",
"ps4", "ps4",