Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Plum Lightpad config flow #36802

Merged
merged 18 commits into from
Jun 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ae29b47
add support for config flow for Plum Lightpad integration
prystupa Jun 14, 2020
e97c8e2
add support for config flow for Plum Lightpad integration (remove uni…
prystupa Jun 14, 2020
27212ce
add support for config flow for Plum Lightpad integration (fix lint i…
prystupa Jun 15, 2020
6475327
add support for config flow for Plum Lightpad integration (PR feedback)
prystupa Jun 16, 2020
c9a75b7
add support for config flow for Plum Lightpad integration (fix lint)
prystupa Jun 16, 2020
1f293dd
Update homeassistant/components/plum_lightpad/__init__.py
prystupa Jun 23, 2020
1258f9d
Update homeassistant/components/plum_lightpad/strings.json
prystupa Jun 23, 2020
0579ace
Update homeassistant/components/plum_lightpad/strings.json
prystupa Jun 23, 2020
dab5b74
Update homeassistant/components/plum_lightpad/strings.json
prystupa Jun 23, 2020
846fd88
Update homeassistant/components/plum_lightpad/strings.json
prystupa Jun 23, 2020
eac0b39
remove unnecessary deepcopy
prystupa Jun 23, 2020
2599a04
remove unnecessary logging warning, since ignoring is expected for co…
prystupa Jun 23, 2020
f6b7a57
switch to hass.loop.create_task per PR feedback
prystupa Jun 23, 2020
2cbc91e
show login errors when configuring integration via UI (PR feedback)
prystupa Jun 23, 2020
562a86e
disable wrongly flag pylint violation
prystupa Jun 23, 2020
a01c430
add except handler to handle connection errors when setting up config…
prystupa Jun 23, 2020
6bf820c
address PR feedback regarding exception handling
prystupa Jun 23, 2020
1d2d685
Update homeassistant/components/plum_lightpad/config_flow.py
prystupa Jun 23, 2020
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
2 changes: 1 addition & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,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
prystupa marked this conversation as resolved.
Show resolved Hide resolved
homeassistant/components/point/* @fredrike
homeassistant/components/powerwall/* @bdraco @jrester
homeassistant/components/prometheus/* @knyar
Expand Down
87 changes: 43 additions & 44 deletions homeassistant/components/plum_lightpad/__init__.py
Original file line number Diff line number Diff line change
@@ -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__)

Expand All @@ -26,56 +28,53 @@
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
)
)

def cleanup(event):
"""Clean up resources."""
plum.cleanup()
return True

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

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
)
)
]
)
username = entry.data.get(CONF_USERNAME)
password = entry.data.get(CONF_PASSWORD)

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,
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()
prystupa marked this conversation as resolved.
Show resolved Hide resolved

hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)
prystupa marked this conversation as resolved.
Show resolved Hide resolved
return True
62 changes: 62 additions & 0 deletions homeassistant/components/plum_lightpad/config_flow.py
Original file line number Diff line number Diff line change
@@ -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)
78 changes: 64 additions & 14 deletions homeassistant/components/plum_lightpad/light.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,65 @@
"""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,
SUPPORT_BRIGHTNESS,
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_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: Callable[[List[Entity]], None],
) -> None:
"""Set up Plum Lightpad dimmer lights and glow rings."""

plum: Plum = hass.data[DOMAIN][entry.entry_id]

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
def setup_entities(device) -> None:
entities = []

plum = hass.data[DOMAIN]
if "lpid" in device:
lightpad = plum.get_lightpad(device["lpid"])
entities.append(GlowRing(lightpad=lightpad))

entities = []
if "llid" in device:
logical_load = plum.get_load(device["llid"])
entities.append(PlumLight(load=logical_load))

if "lpid" in discovery_info:
lightpad = plum.get_lightpad(discovery_info["lpid"])
entities.append(GlowRing(lightpad=lightpad))
if entities:
async_add_entities(entities)

if "llid" in discovery_info:
logical_load = plum.get_load(discovery_info["llid"])
entities.append(PlumLight(load=logical_load))
async def new_load(device):
setup_entities(device)

if entities:
async_add_entities(entities)
async def new_lightpad(device):
setup_entities(device)

device_web_session = async_get_clientsession(hass, verify_ssl=False)
hass.loop.create_task(
prystupa marked this conversation as resolved.
Show resolved Hide resolved
plum.discover(
hass.loop,
loadListener=new_load,
lightpadListener=new_lightpad,
websession=device_web_session,
)
)
prystupa marked this conversation as resolved.
Show resolved Hide resolved


class PlumLight(LightEntity):
Expand Down Expand Up @@ -64,6 +94,16 @@ def name(self):
"""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:
prystupa marked this conversation as resolved.
Show resolved Hide resolved
"""Return the brightness of this switch between 0..255."""
Expand Down Expand Up @@ -145,6 +185,16 @@ def name(self):
"""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."""
Expand Down
10 changes: 8 additions & 2 deletions homeassistant/components/plum_lightpad/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
18 changes: 18 additions & 0 deletions homeassistant/components/plum_lightpad/strings.json
Original file line number Diff line number Diff line change
@@ -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%]"
}
}
}
20 changes: 20 additions & 0 deletions homeassistant/components/plum_lightpad/translations/en.json
Original file line number Diff line number Diff line change
@@ -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"
}
14 changes: 14 additions & 0 deletions homeassistant/components/plum_lightpad/utils.py
Original file line number Diff line number Diff line change
@@ -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:
prystupa marked this conversation as resolved.
Show resolved Hide resolved
"""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
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
"plaato",
"plex",
"plugwise",
"plum_lightpad",
"point",
"powerwall",
"ps4",
Expand Down